blob: a64ca1d9753ac96162d37219ae1fd9de39e44c38 [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"
iomodo75542322025-07-30 19:27:48 +040012 "github.com/iomodo/staff/llm/provider"
iomodoa53240a2025-07-30 17:33:35 +040013 "github.com/iomodo/staff/tm"
14)
15
16type Agent struct {
17 // Identity
18 Name string
19 Role string
20
21 // LLM Configuration
22 Model string
23 SystemPrompt string
24 MaxTokens *int
25 Temperature *float64
26
27 // Runtime
28 Provider llm.LLMProvider
29 CurrentTask *string // Task ID currently being processed
30
31 IsRunning bool
32 StopChan chan struct{}
33
34 logger *slog.Logger
35
36 taskManager tm.TaskManager
37 thinker *Thinker
38}
39
40func NewAgent(agentConfig config.AgentConfig, llmConfig llm.Config, taskManager tm.TaskManager, agentRoles []string, logger *slog.Logger) (*Agent, error) {
41 // Load system prompt
42 systemPrompt, err := loadSystemPrompt(agentConfig.SystemPromptFile)
43 if err != nil {
44 return nil, fmt.Errorf("failed to load system prompt: %w", err)
45 }
46
iomodo75542322025-07-30 19:27:48 +040047 prov := provider.CreateProvider(llmConfig)
48 thinker := NewThinker(prov, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger)
iomodoa53240a2025-07-30 17:33:35 +040049
50 agent := &Agent{
51 Name: agentConfig.Name,
52 Role: agentConfig.Role,
53 Model: agentConfig.Model,
54 SystemPrompt: systemPrompt,
iomodo75542322025-07-30 19:27:48 +040055 Provider: prov,
iomodoa53240a2025-07-30 17:33:35 +040056 MaxTokens: agentConfig.MaxTokens,
57 Temperature: agentConfig.Temperature,
58 taskManager: taskManager,
59 logger: logger,
60 thinker: thinker,
61 }
62
63 return agent, nil
64}
65
66// Start starts an agent to process tasks in a loop
67func (a *Agent) Start(loopInterval time.Duration) error {
68 if a.IsRunning {
69 return fmt.Errorf("agent %s is already running", a.Name)
70 }
71
72 a.IsRunning = true
73 a.StopChan = make(chan struct{})
74
75 go a.runLoop(loopInterval)
76
77 a.logger.Info("Started agent",
78 slog.String("name", a.Name),
79 slog.String("role", a.Role),
80 slog.String("model", a.Model))
81 return nil
82}
83
84func (a *Agent) Stop() {
85 close(a.StopChan)
86 a.IsRunning = false
87}
88
89func (a *Agent) runLoop(interval time.Duration) {
90 ticker := time.NewTicker(interval)
91 defer ticker.Stop()
92
93 for {
94 select {
95 case <-a.StopChan:
96 a.logger.Info("Agent stopping", slog.String("name", a.Name))
97 return
98 case <-ticker.C:
99 if err := a.processTasks(); err != nil {
100 a.logger.Error("Error processing tasks for agent",
101 slog.String("agent", a.Name),
102 slog.String("error", err.Error()))
103 }
104 }
105 }
106}
107
108// processAgentTasks processes all assigned tasks for an agent
109func (a *Agent) processTasks() error {
110 if a.CurrentTask != nil {
111 return nil
112 }
113
114 // Get tasks assigned to this agent
115 tasks, err := a.taskManager.GetTasksByAssignee(a.Name)
116 if err != nil {
117 return fmt.Errorf("failed to get tasks for agent %s: %w", a.Name, err)
118 }
119
120 a.logger.Info("Processing tasks for agent",
121 slog.Int("task_count", len(tasks)),
122 slog.String("agent", a.Name))
123
124 for _, task := range tasks {
125 if task.Status == tm.StatusToDo {
iomodo907b43d2025-07-31 19:43:42 +0400126 err2 := a.processTask(task)
127 if err2 == nil {
128 return nil
iomodoa53240a2025-07-30 17:33:35 +0400129 }
iomodo907b43d2025-07-31 19:43:42 +0400130 a.logger.Error("Error processing task",
131 slog.String("task_id", task.ID),
132 slog.String("error", err2.Error()))
133
iomodoa53240a2025-07-30 17:33:35 +0400134 }
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
iomodo907b43d2025-07-31 19:43:42 +0400153 defer func() {
154 if r := recover(); r != nil {
155 a.logger.Error("Task processing panicked, clearing agent state",
156 slog.String("task_id", task.ID),
157 slog.String("agent", a.Name))
158 a.CurrentTask = nil
159 panic(r)
160 }
161 }()
iomodoa53240a2025-07-30 17:33:35 +0400162
163 // Check if this task should generate subtasks (with LLM decision)
164 if a.thinker.ShouldGenerateSubtasks(task) {
165 err := a.processSubtask(ctx, task)
166 if err == nil {
167 a.logger.Info("Task converted to subtasks by agent using LLM analysis",
168 slog.String("task_id", task.ID),
169 slog.String("agent", a.Name))
170 return nil
171 }
iomodo907b43d2025-07-31 19:43:42 +0400172 a.logger.Error("Subtask processing failed, cleared agent state",
iomodoa53240a2025-07-30 17:33:35 +0400173 slog.String("task_id", task.ID),
iomodo907b43d2025-07-31 19:43:42 +0400174 slog.String("agent", a.Name),
iomodoa53240a2025-07-30 17:33:35 +0400175 slog.String("error", err.Error()))
176 }
177
178 err := a.processSolution(ctx, task)
179 if err != nil {
180 return fmt.Errorf("failed to process solution for task: %w", err)
181 }
182 duration := time.Since(startTime)
183 a.logger.Info("Task completed by agent",
184 slog.String("task_id", task.ID),
185 slog.String("agent", a.Name),
186 slog.Duration("duration", duration))
187 return nil
188}
189
190func (a *Agent) processSubtask(ctx context.Context, task *tm.Task) error {
191 a.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
192 analysis, err := a.thinker.GenerateSubtasksForTask(ctx, task)
193 if err != nil {
194 return fmt.Errorf("failed to generate subtasks for task: %w", err)
195 }
196
iomodo1c1c60d2025-07-30 17:54:10 +0400197 solutionURL, err2 := a.taskManager.ProposeSubTasks(ctx, task, analysis, a.Name)
iomodoa53240a2025-07-30 17:33:35 +0400198 if err2 != nil {
199 return fmt.Errorf("failed to propose subtasks for task: %w", err2)
200 }
201 task.SolutionURL = solutionURL
202
203 a.logger.Info("Generated subtask Solution for task",
204 slog.String("task_id", task.ID),
205 slog.String("solution_url", solutionURL))
206 a.logger.Info("Proposed subtasks and new agents for task",
207 slog.String("task_id", task.ID),
208 slog.Int("subtask_count", len(analysis.Subtasks)),
209 slog.Int("new_agent_count", len(analysis.AgentCreations)))
210
211 // Log proposed new agents if any
212 if len(analysis.AgentCreations) > 0 {
213 for _, agent := range analysis.AgentCreations {
214 a.logger.Info("Proposed new agent",
215 slog.String("role", agent.Role),
216 slog.Any("skills", agent.Skills))
217 }
218 }
219
220 return nil
221}
222
223func (a *Agent) processSolution(ctx context.Context, task *tm.Task) error {
224 solution, err := a.thinker.GenerateSolution(ctx, task)
225 if err != nil {
226 return fmt.Errorf("failed to generate solution: %w", err)
227 }
228
229 solutionURL, err := a.taskManager.ProposeSolution(ctx, task, solution, a.Name)
230 if err != nil {
231 return fmt.Errorf("failed to propose solution: %w", err)
232 }
233 task.SolutionURL = solutionURL
234
235 a.logger.Info("Generated Solution for task",
236 slog.String("task_id", task.ID),
237 slog.String("agent", a.Name),
238 slog.String("solution_url", solutionURL))
239 return nil
240}
241
242// loadSystemPrompt loads the system prompt from file
243func loadSystemPrompt(filePath string) (string, error) {
244 content, err := os.ReadFile(filePath)
245 if err != nil {
246 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
247 }
248 return string(content), nil
249}