Refactor everything
Change-Id: Ic3a37c38cfecba943c91f6ae545ce1c5b551c0d5
diff --git a/server/agent/agent.go b/server/agent/agent.go
new file mode 100644
index 0000000..fba80b7
--- /dev/null
+++ b/server/agent/agent.go
@@ -0,0 +1,239 @@
+package agent
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "time"
+
+ "github.com/iomodo/staff/config"
+ "github.com/iomodo/staff/llm"
+ "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)
+ }
+
+ provider, err := llm.CreateProvider(llmConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create LLM provider: %w", err)
+ }
+
+ thinker := NewThinker(provider, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger)
+
+ agent := &Agent{
+ Name: agentConfig.Name,
+ Role: agentConfig.Role,
+ Model: agentConfig.Model,
+ SystemPrompt: systemPrompt,
+ Provider: provider,
+ 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 {
+ if err := a.processTask(task); err != nil {
+ a.logger.Error("Error processing task",
+ slog.String("task_id", task.ID),
+ slog.String("error", err.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
+
+ // 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("Error processing subtask",
+ slog.String("task_id", task.ID),
+ 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)
+ 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
+}