| package agent |
| |
| import ( |
| "context" |
| "fmt" |
| "log/slog" |
| "os" |
| "time" |
| |
| "github.com/iomodo/staff/config" |
| "github.com/iomodo/staff/llm" |
| "github.com/iomodo/staff/llm/provider" |
| "github.com/iomodo/staff/tm" |
| ) |
| |
| type Agent struct { |
| // Identity |
| Name string |
| Role string |
| |
| // LLM Configuration |
| Model string |
| SystemPrompt string |
| MaxTokens *int |
| Temperature *float64 |
| |
| // Runtime |
| Provider llm.LLMProvider |
| CurrentTask *string // Task ID currently being processed |
| |
| IsRunning bool |
| StopChan chan struct{} |
| |
| logger *slog.Logger |
| |
| taskManager tm.TaskManager |
| thinker *Thinker |
| } |
| |
| func NewAgent(agentConfig config.AgentConfig, llmConfig llm.Config, taskManager tm.TaskManager, agentRoles []string, logger *slog.Logger) (*Agent, error) { |
| // Load system prompt |
| systemPrompt, err := loadSystemPrompt(agentConfig.SystemPromptFile) |
| if err != nil { |
| return nil, fmt.Errorf("failed to load system prompt: %w", err) |
| } |
| |
| prov := provider.CreateProvider(llmConfig) |
| thinker := NewThinker(prov, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger) |
| |
| agent := &Agent{ |
| Name: agentConfig.Name, |
| Role: agentConfig.Role, |
| Model: agentConfig.Model, |
| SystemPrompt: systemPrompt, |
| Provider: prov, |
| MaxTokens: agentConfig.MaxTokens, |
| Temperature: agentConfig.Temperature, |
| taskManager: taskManager, |
| logger: logger, |
| thinker: thinker, |
| } |
| |
| return agent, nil |
| } |
| |
| // Start starts an agent to process tasks in a loop |
| func (a *Agent) Start(loopInterval time.Duration) error { |
| if a.IsRunning { |
| return fmt.Errorf("agent %s is already running", a.Name) |
| } |
| |
| a.IsRunning = true |
| a.StopChan = make(chan struct{}) |
| |
| go a.runLoop(loopInterval) |
| |
| a.logger.Info("Started agent", |
| slog.String("name", a.Name), |
| slog.String("role", a.Role), |
| slog.String("model", a.Model)) |
| return nil |
| } |
| |
| func (a *Agent) Stop() { |
| close(a.StopChan) |
| a.IsRunning = false |
| } |
| |
| func (a *Agent) runLoop(interval time.Duration) { |
| ticker := time.NewTicker(interval) |
| defer ticker.Stop() |
| |
| for { |
| select { |
| case <-a.StopChan: |
| a.logger.Info("Agent stopping", slog.String("name", a.Name)) |
| return |
| case <-ticker.C: |
| if err := a.processTasks(); err != nil { |
| a.logger.Error("Error processing tasks for agent", |
| slog.String("agent", a.Name), |
| slog.String("error", err.Error())) |
| } |
| } |
| } |
| } |
| |
| // processAgentTasks processes all assigned tasks for an agent |
| func (a *Agent) processTasks() error { |
| if a.CurrentTask != nil { |
| return nil |
| } |
| |
| // Get tasks assigned to this agent |
| tasks, err := a.taskManager.GetTasksByAssignee(a.Name) |
| if err != nil { |
| return fmt.Errorf("failed to get tasks for agent %s: %w", a.Name, err) |
| } |
| |
| a.logger.Info("Processing tasks for agent", |
| slog.Int("task_count", len(tasks)), |
| slog.String("agent", a.Name)) |
| |
| for _, task := range tasks { |
| if task.Status == tm.StatusToDo { |
| err2 := a.processTask(task) |
| if err2 == nil { |
| return nil |
| } |
| a.logger.Error("Error processing task", |
| slog.String("task_id", task.ID), |
| slog.String("error", err2.Error())) |
| |
| } |
| } |
| |
| return nil |
| } |
| |
| // processTask processes a single task with an agent |
| func (a *Agent) processTask(task *tm.Task) error { |
| ctx := context.Background() |
| startTime := time.Now() |
| |
| a.logger.Info("Agent processing task", |
| slog.String("agent", a.Name), |
| slog.String("task_id", task.ID), |
| slog.String("title", task.Title)) |
| |
| // Mark task as in progress |
| task.Status = tm.StatusInProgress |
| a.CurrentTask = &task.ID |
| defer func() { |
| if r := recover(); r != nil { |
| a.logger.Error("Task processing panicked, clearing agent state", |
| slog.String("task_id", task.ID), |
| slog.String("agent", a.Name)) |
| a.CurrentTask = nil |
| panic(r) |
| } |
| }() |
| |
| // Check if this task should generate subtasks (with LLM decision) |
| if a.thinker.ShouldGenerateSubtasks(task) { |
| err := a.processSubtask(ctx, task) |
| if err == nil { |
| a.logger.Info("Task converted to subtasks by agent using LLM analysis", |
| slog.String("task_id", task.ID), |
| slog.String("agent", a.Name)) |
| return nil |
| } |
| a.logger.Error("Subtask processing failed, cleared agent state", |
| slog.String("task_id", task.ID), |
| slog.String("agent", a.Name), |
| slog.String("error", err.Error())) |
| } |
| |
| err := a.processSolution(ctx, task) |
| if err != nil { |
| return fmt.Errorf("failed to process solution for task: %w", err) |
| } |
| duration := time.Since(startTime) |
| a.logger.Info("Task completed by agent", |
| slog.String("task_id", task.ID), |
| slog.String("agent", a.Name), |
| slog.Duration("duration", duration)) |
| return nil |
| } |
| |
| func (a *Agent) processSubtask(ctx context.Context, task *tm.Task) error { |
| a.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID)) |
| analysis, err := a.thinker.GenerateSubtasksForTask(ctx, task) |
| if err != nil { |
| return fmt.Errorf("failed to generate subtasks for task: %w", err) |
| } |
| |
| solutionURL, err2 := a.taskManager.ProposeSubTasks(ctx, task, analysis, a.Name) |
| if err2 != nil { |
| return fmt.Errorf("failed to propose subtasks for task: %w", err2) |
| } |
| task.SolutionURL = solutionURL |
| |
| a.logger.Info("Generated subtask Solution for task", |
| slog.String("task_id", task.ID), |
| slog.String("solution_url", solutionURL)) |
| a.logger.Info("Proposed subtasks and new agents for task", |
| slog.String("task_id", task.ID), |
| slog.Int("subtask_count", len(analysis.Subtasks)), |
| slog.Int("new_agent_count", len(analysis.AgentCreations))) |
| |
| // Log proposed new agents if any |
| if len(analysis.AgentCreations) > 0 { |
| for _, agent := range analysis.AgentCreations { |
| a.logger.Info("Proposed new agent", |
| slog.String("role", agent.Role), |
| slog.Any("skills", agent.Skills)) |
| } |
| } |
| |
| return nil |
| } |
| |
| func (a *Agent) processSolution(ctx context.Context, task *tm.Task) error { |
| solution, err := a.thinker.GenerateSolution(ctx, task) |
| if err != nil { |
| return fmt.Errorf("failed to generate solution: %w", err) |
| } |
| |
| solutionURL, err := a.taskManager.ProposeSolution(ctx, task, solution, a.Name) |
| if err != nil { |
| return fmt.Errorf("failed to propose solution: %w", err) |
| } |
| task.SolutionURL = solutionURL |
| |
| a.logger.Info("Generated Solution for task", |
| slog.String("task_id", task.ID), |
| slog.String("agent", a.Name), |
| slog.String("solution_url", solutionURL)) |
| return nil |
| } |
| |
| // loadSystemPrompt loads the system prompt from file |
| func loadSystemPrompt(filePath string) (string, error) { |
| content, err := os.ReadFile(filePath) |
| if err != nil { |
| return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err) |
| } |
| return string(content), nil |
| } |