blob: f18979411ad8bff8ac2fa1ddd6a95e0d48c47896 [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 {
126 if err := a.processTask(task); err != nil {
127 a.logger.Error("Error processing task",
128 slog.String("task_id", task.ID),
129 slog.String("error", err.Error()))
130 }
131 }
132 }
133
134 return nil
135}
136
137// processTask processes a single task with an agent
138func (a *Agent) processTask(task *tm.Task) error {
139 ctx := context.Background()
140 startTime := time.Now()
141
142 a.logger.Info("Agent processing task",
143 slog.String("agent", a.Name),
144 slog.String("task_id", task.ID),
145 slog.String("title", task.Title))
146
147 // Mark task as in progress
148 task.Status = tm.StatusInProgress
149 a.CurrentTask = &task.ID
150
151 // Check if this task should generate subtasks (with LLM decision)
152 if a.thinker.ShouldGenerateSubtasks(task) {
153 err := a.processSubtask(ctx, task)
154 if err == nil {
155 a.logger.Info("Task converted to subtasks by agent using LLM analysis",
156 slog.String("task_id", task.ID),
157 slog.String("agent", a.Name))
158 return nil
159 }
160 a.logger.Error("Error processing subtask",
161 slog.String("task_id", task.ID),
162 slog.String("error", err.Error()))
163 }
164
165 err := a.processSolution(ctx, task)
166 if err != nil {
167 return fmt.Errorf("failed to process solution for task: %w", err)
168 }
169 duration := time.Since(startTime)
170 a.logger.Info("Task completed by agent",
171 slog.String("task_id", task.ID),
172 slog.String("agent", a.Name),
173 slog.Duration("duration", duration))
174 return nil
175}
176
177func (a *Agent) processSubtask(ctx context.Context, task *tm.Task) error {
178 a.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
179 analysis, err := a.thinker.GenerateSubtasksForTask(ctx, task)
180 if err != nil {
181 return fmt.Errorf("failed to generate subtasks for task: %w", err)
182 }
183
iomodo1c1c60d2025-07-30 17:54:10 +0400184 solutionURL, err2 := a.taskManager.ProposeSubTasks(ctx, task, analysis, a.Name)
iomodoa53240a2025-07-30 17:33:35 +0400185 if err2 != nil {
186 return fmt.Errorf("failed to propose subtasks for task: %w", err2)
187 }
188 task.SolutionURL = solutionURL
189
190 a.logger.Info("Generated subtask Solution for task",
191 slog.String("task_id", task.ID),
192 slog.String("solution_url", solutionURL))
193 a.logger.Info("Proposed subtasks and new agents for task",
194 slog.String("task_id", task.ID),
195 slog.Int("subtask_count", len(analysis.Subtasks)),
196 slog.Int("new_agent_count", len(analysis.AgentCreations)))
197
198 // Log proposed new agents if any
199 if len(analysis.AgentCreations) > 0 {
200 for _, agent := range analysis.AgentCreations {
201 a.logger.Info("Proposed new agent",
202 slog.String("role", agent.Role),
203 slog.Any("skills", agent.Skills))
204 }
205 }
206
207 return nil
208}
209
210func (a *Agent) processSolution(ctx context.Context, task *tm.Task) error {
211 solution, err := a.thinker.GenerateSolution(ctx, task)
212 if err != nil {
213 return fmt.Errorf("failed to generate solution: %w", err)
214 }
215
216 solutionURL, err := a.taskManager.ProposeSolution(ctx, task, solution, a.Name)
217 if err != nil {
218 return fmt.Errorf("failed to propose solution: %w", err)
219 }
220 task.SolutionURL = solutionURL
221
222 a.logger.Info("Generated Solution for task",
223 slog.String("task_id", task.ID),
224 slog.String("agent", a.Name),
225 slog.String("solution_url", solutionURL))
226 return nil
227}
228
229// loadSystemPrompt loads the system prompt from file
230func loadSystemPrompt(filePath string) (string, error) {
231 content, err := os.ReadFile(filePath)
232 if err != nil {
233 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
234 }
235 return string(content), nil
236}