blob: fba80b737105aba7b68c2706c0a678979dae5de5 [file] [log] [blame]
iomodoa53240a2025-07-30 17:33:35 +04001package agent
2
3import (
4 "context"
5 "fmt"
6 "log/slog"
7 "os"
8 "time"
9
10 "github.com/iomodo/staff/config"
11 "github.com/iomodo/staff/llm"
12 "github.com/iomodo/staff/tm"
13)
14
15type Agent struct {
16 // Identity
17 Name string
18 Role string
19
20 // LLM Configuration
21 Model string
22 SystemPrompt string
23 MaxTokens *int
24 Temperature *float64
25
26 // Runtime
27 Provider llm.LLMProvider
28 CurrentTask *string // Task ID currently being processed
29
30 IsRunning bool
31 StopChan chan struct{}
32
33 logger *slog.Logger
34
35 taskManager tm.TaskManager
36 thinker *Thinker
37}
38
39func NewAgent(agentConfig config.AgentConfig, llmConfig llm.Config, taskManager tm.TaskManager, agentRoles []string, logger *slog.Logger) (*Agent, error) {
40 // Load system prompt
41 systemPrompt, err := loadSystemPrompt(agentConfig.SystemPromptFile)
42 if err != nil {
43 return nil, fmt.Errorf("failed to load system prompt: %w", err)
44 }
45
46 provider, err := llm.CreateProvider(llmConfig)
47 if err != nil {
48 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
49 }
50
51 thinker := NewThinker(provider, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger)
52
53 agent := &Agent{
54 Name: agentConfig.Name,
55 Role: agentConfig.Role,
56 Model: agentConfig.Model,
57 SystemPrompt: systemPrompt,
58 Provider: provider,
59 MaxTokens: agentConfig.MaxTokens,
60 Temperature: agentConfig.Temperature,
61 taskManager: taskManager,
62 logger: logger,
63 thinker: thinker,
64 }
65
66 return agent, nil
67}
68
69// Start starts an agent to process tasks in a loop
70func (a *Agent) Start(loopInterval time.Duration) error {
71 if a.IsRunning {
72 return fmt.Errorf("agent %s is already running", a.Name)
73 }
74
75 a.IsRunning = true
76 a.StopChan = make(chan struct{})
77
78 go a.runLoop(loopInterval)
79
80 a.logger.Info("Started agent",
81 slog.String("name", a.Name),
82 slog.String("role", a.Role),
83 slog.String("model", a.Model))
84 return nil
85}
86
87func (a *Agent) Stop() {
88 close(a.StopChan)
89 a.IsRunning = false
90}
91
92func (a *Agent) runLoop(interval time.Duration) {
93 ticker := time.NewTicker(interval)
94 defer ticker.Stop()
95
96 for {
97 select {
98 case <-a.StopChan:
99 a.logger.Info("Agent stopping", slog.String("name", a.Name))
100 return
101 case <-ticker.C:
102 if err := a.processTasks(); err != nil {
103 a.logger.Error("Error processing tasks for agent",
104 slog.String("agent", a.Name),
105 slog.String("error", err.Error()))
106 }
107 }
108 }
109}
110
111// processAgentTasks processes all assigned tasks for an agent
112func (a *Agent) processTasks() error {
113 if a.CurrentTask != nil {
114 return nil
115 }
116
117 // Get tasks assigned to this agent
118 tasks, err := a.taskManager.GetTasksByAssignee(a.Name)
119 if err != nil {
120 return fmt.Errorf("failed to get tasks for agent %s: %w", a.Name, err)
121 }
122
123 a.logger.Info("Processing tasks for agent",
124 slog.Int("task_count", len(tasks)),
125 slog.String("agent", a.Name))
126
127 for _, task := range tasks {
128 if task.Status == tm.StatusToDo {
129 if err := a.processTask(task); err != nil {
130 a.logger.Error("Error processing task",
131 slog.String("task_id", task.ID),
132 slog.String("error", err.Error()))
133 }
134 }
135 }
136
137 return nil
138}
139
140// processTask processes a single task with an agent
141func (a *Agent) processTask(task *tm.Task) error {
142 ctx := context.Background()
143 startTime := time.Now()
144
145 a.logger.Info("Agent processing task",
146 slog.String("agent", a.Name),
147 slog.String("task_id", task.ID),
148 slog.String("title", task.Title))
149
150 // Mark task as in progress
151 task.Status = tm.StatusInProgress
152 a.CurrentTask = &task.ID
153
154 // Check if this task should generate subtasks (with LLM decision)
155 if a.thinker.ShouldGenerateSubtasks(task) {
156 err := a.processSubtask(ctx, task)
157 if err == nil {
158 a.logger.Info("Task converted to subtasks by agent using LLM analysis",
159 slog.String("task_id", task.ID),
160 slog.String("agent", a.Name))
161 return nil
162 }
163 a.logger.Error("Error processing subtask",
164 slog.String("task_id", task.ID),
165 slog.String("error", err.Error()))
166 }
167
168 err := a.processSolution(ctx, task)
169 if err != nil {
170 return fmt.Errorf("failed to process solution for task: %w", err)
171 }
172 duration := time.Since(startTime)
173 a.logger.Info("Task completed by agent",
174 slog.String("task_id", task.ID),
175 slog.String("agent", a.Name),
176 slog.Duration("duration", duration))
177 return nil
178}
179
180func (a *Agent) processSubtask(ctx context.Context, task *tm.Task) error {
181 a.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
182 analysis, err := a.thinker.GenerateSubtasksForTask(ctx, task)
183 if err != nil {
184 return fmt.Errorf("failed to generate subtasks for task: %w", err)
185 }
186
187 solutionURL, err2 := a.taskManager.ProposeSubTasks(ctx, task, analysis)
188 if err2 != nil {
189 return fmt.Errorf("failed to propose subtasks for task: %w", err2)
190 }
191 task.SolutionURL = solutionURL
192
193 a.logger.Info("Generated subtask Solution for task",
194 slog.String("task_id", task.ID),
195 slog.String("solution_url", solutionURL))
196 a.logger.Info("Proposed subtasks and new agents for task",
197 slog.String("task_id", task.ID),
198 slog.Int("subtask_count", len(analysis.Subtasks)),
199 slog.Int("new_agent_count", len(analysis.AgentCreations)))
200
201 // Log proposed new agents if any
202 if len(analysis.AgentCreations) > 0 {
203 for _, agent := range analysis.AgentCreations {
204 a.logger.Info("Proposed new agent",
205 slog.String("role", agent.Role),
206 slog.Any("skills", agent.Skills))
207 }
208 }
209
210 return nil
211}
212
213func (a *Agent) processSolution(ctx context.Context, task *tm.Task) error {
214 solution, err := a.thinker.GenerateSolution(ctx, task)
215 if err != nil {
216 return fmt.Errorf("failed to generate solution: %w", err)
217 }
218
219 solutionURL, err := a.taskManager.ProposeSolution(ctx, task, solution, a.Name)
220 if err != nil {
221 return fmt.Errorf("failed to propose solution: %w", err)
222 }
223 task.SolutionURL = solutionURL
224
225 a.logger.Info("Generated Solution for task",
226 slog.String("task_id", task.ID),
227 slog.String("agent", a.Name),
228 slog.String("solution_url", solutionURL))
229 return nil
230}
231
232// loadSystemPrompt loads the system prompt from file
233func loadSystemPrompt(filePath string) (string, error) {
234 content, err := os.ReadFile(filePath)
235 if err != nil {
236 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
237 }
238 return string(content), nil
239}