blob: 88bfe218b8d7b1019b0f2adff79372db5c044310 [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package agent
2
3import (
4 "context"
5 "fmt"
iomodo62da94a2025-07-28 19:01:55 +04006 "log/slog"
user5a7d60d2025-07-27 21:22:04 +04007 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "github.com/iomodo/staff/assignment"
14 "github.com/iomodo/staff/config"
15 "github.com/iomodo/staff/git"
16 "github.com/iomodo/staff/llm"
iomodo50598c62025-07-27 22:06:32 +040017 _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
iomodod9ff8da2025-07-28 11:42:22 +040018 "github.com/iomodo/staff/subtasks"
user5a7d60d2025-07-27 21:22:04 +040019 "github.com/iomodo/staff/tm"
20)
21
iomodo50598c62025-07-27 22:06:32 +040022// Manager manages multiple AI agents with Git operations and task processing
23type Manager struct {
iomodod9ff8da2025-07-28 11:42:22 +040024 config *config.Config
25 agents map[string]*Agent
26 taskManager tm.TaskManager
27 autoAssigner *assignment.AutoAssigner
28 prProvider git.PullRequestProvider
29 cloneManager *git.CloneManager
30 subtaskService *subtasks.SubtaskService
31 isRunning map[string]bool
32 stopChannels map[string]chan struct{}
iomodo62da94a2025-07-28 19:01:55 +040033 logger *slog.Logger
user5a7d60d2025-07-27 21:22:04 +040034}
35
iomodo50598c62025-07-27 22:06:32 +040036// NewManager creates a new agent manager
iomodo62da94a2025-07-28 19:01:55 +040037func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
38 if logger == nil {
39 logger = slog.Default()
40 }
user5a7d60d2025-07-27 21:22:04 +040041 // Create auto-assigner
42 autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
43
44 // Create GitHub PR provider
45 githubConfig := git.GitHubConfig{
iomodo62da94a2025-07-28 19:01:55 +040046 Token: cfg.GitHub.Token,
47 Logger: logger,
user5a7d60d2025-07-27 21:22:04 +040048 }
49 prProvider := git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
50
51 // Create clone manager for per-agent Git repositories
52 repoURL := fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
53 workspacePath := filepath.Join(".", "workspace")
54 cloneManager := git.NewCloneManager(repoURL, workspacePath)
55
iomodo50598c62025-07-27 22:06:32 +040056 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040057 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040058 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040059 taskManager: taskManager,
60 autoAssigner: autoAssigner,
61 prProvider: prProvider,
62 cloneManager: cloneManager,
63 isRunning: make(map[string]bool),
64 stopChannels: make(map[string]chan struct{}),
iomodo62da94a2025-07-28 19:01:55 +040065 logger: logger,
user5a7d60d2025-07-27 21:22:04 +040066 }
67
68 // Initialize agents
69 if err := manager.initializeAgents(); err != nil {
70 return nil, fmt.Errorf("failed to initialize agents: %w", err)
71 }
72
iomodod9ff8da2025-07-28 11:42:22 +040073 // Initialize subtask service after agents are created
74 if err := manager.initializeSubtaskService(); err != nil {
75 return nil, fmt.Errorf("failed to initialize subtask service: %w", err)
76 }
77
user5a7d60d2025-07-27 21:22:04 +040078 return manager, nil
79}
80
81// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +040082func (m *Manager) initializeAgents() error {
83 for _, agentConfig := range m.config.Agents {
84 agent, err := m.createAgent(agentConfig)
user5a7d60d2025-07-27 21:22:04 +040085 if err != nil {
86 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
87 }
iomodo50598c62025-07-27 22:06:32 +040088 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +040089 }
90 return nil
91}
92
iomodod9ff8da2025-07-28 11:42:22 +040093// initializeSubtaskService creates the subtask service with available agent roles
94func (m *Manager) initializeSubtaskService() error {
95 // Get agent roles from configuration
96 agentRoles := make([]string, 0, len(m.config.Agents))
97 for _, agentConfig := range m.config.Agents {
98 agentRoles = append(agentRoles, agentConfig.Name)
99 }
100
101 // Use the first agent's LLM provider for subtask analysis
102 if len(m.agents) == 0 {
103 return fmt.Errorf("no agents available for subtask service")
104 }
105
106 var firstAgent *Agent
107 for _, agent := range m.agents {
108 firstAgent = agent
109 break
110 }
111
112 m.subtaskService = subtasks.NewSubtaskService(
113 firstAgent.Provider,
114 m.taskManager,
115 agentRoles,
iomodo443b20a2025-07-28 15:24:05 +0400116 m.prProvider,
117 m.config.GitHub.Owner,
118 m.config.GitHub.Repo,
119 m.cloneManager,
iomodo62da94a2025-07-28 19:01:55 +0400120 m.logger,
iomodod9ff8da2025-07-28 11:42:22 +0400121 )
122
123 return nil
124}
125
user5a7d60d2025-07-27 21:22:04 +0400126// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +0400127func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +0400128 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +0400129 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +0400130 if err != nil {
131 return nil, fmt.Errorf("failed to load system prompt: %w", err)
132 }
133
134 // Create LLM provider
135 llmConfig := llm.Config{
iomodof1ddefe2025-07-28 09:02:05 +0400136 Provider: llm.ProviderFake, // Use fake provider for testing
iomodo50598c62025-07-27 22:06:32 +0400137 APIKey: m.config.OpenAI.APIKey,
138 BaseURL: m.config.OpenAI.BaseURL,
139 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +0400140 }
iomodo50598c62025-07-27 22:06:32 +0400141
user5a7d60d2025-07-27 21:22:04 +0400142 provider, err := llm.CreateProvider(llmConfig)
143 if err != nil {
144 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
145 }
146
iomodo50598c62025-07-27 22:06:32 +0400147 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400148 Name: agentConfig.Name,
149 Role: agentConfig.Role,
150 Model: agentConfig.Model,
151 SystemPrompt: systemPrompt,
152 Provider: provider,
153 MaxTokens: agentConfig.MaxTokens,
154 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400155 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400156 }
157
158 return agent, nil
159}
160
161// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400162func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400163 content, err := os.ReadFile(filePath)
164 if err != nil {
165 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
166 }
167 return string(content), nil
168}
169
170// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400171func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
172 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400173 if !exists {
174 return fmt.Errorf("agent %s not found", agentName)
175 }
176
iomodo50598c62025-07-27 22:06:32 +0400177 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400178 return fmt.Errorf("agent %s is already running", agentName)
179 }
180
181 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400182 m.stopChannels[agentName] = stopChan
183 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400184
iomodo50598c62025-07-27 22:06:32 +0400185 go m.runAgentLoop(agent, loopInterval, stopChan)
186
iomodo62da94a2025-07-28 19:01:55 +0400187 m.logger.Info("Started agent",
188 slog.String("name", agentName),
189 slog.String("role", agent.Role),
190 slog.String("model", agent.Model))
user5a7d60d2025-07-27 21:22:04 +0400191 return nil
192}
193
194// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400195func (m *Manager) StopAgent(agentName string) error {
196 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400197 return fmt.Errorf("agent %s is not running", agentName)
198 }
199
iomodo50598c62025-07-27 22:06:32 +0400200 close(m.stopChannels[agentName])
201 delete(m.stopChannels, agentName)
202 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400203
iomodo62da94a2025-07-28 19:01:55 +0400204 m.logger.Info("Stopped agent", slog.String("name", agentName))
user5a7d60d2025-07-27 21:22:04 +0400205 return nil
206}
207
208// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400209func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400210 ticker := time.NewTicker(interval)
211 defer ticker.Stop()
212
213 for {
214 select {
215 case <-stopChan:
iomodo62da94a2025-07-28 19:01:55 +0400216 m.logger.Info("Agent stopping", slog.String("name", agent.Name))
user5a7d60d2025-07-27 21:22:04 +0400217 return
218 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400219 if err := m.processAgentTasks(agent); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400220 m.logger.Error("Error processing tasks for agent",
221 slog.String("agent", agent.Name),
222 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400223 }
224 }
225 }
226}
227
228// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400229func (m *Manager) processAgentTasks(agent *Agent) error {
230 if agent.CurrentTask != nil {
231 return nil
232 }
233
user5a7d60d2025-07-27 21:22:04 +0400234 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400235 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400236 if err != nil {
237 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
238 }
239
iomodo62da94a2025-07-28 19:01:55 +0400240 m.logger.Info("Processing tasks for agent",
241 slog.Int("task_count", len(tasks)),
242 slog.String("agent", agent.Name))
iomodo50598c62025-07-27 22:06:32 +0400243
user5a7d60d2025-07-27 21:22:04 +0400244 for _, task := range tasks {
iomodo50598c62025-07-27 22:06:32 +0400245 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
246 if err := m.processTask(agent, task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400247 m.logger.Error("Error processing task",
248 slog.String("task_id", task.ID),
249 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400250 // Mark task as failed
251 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400252 if err := m.taskManager.UpdateTask(task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400253 m.logger.Error("Error updating failed task",
254 slog.String("task_id", task.ID),
255 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400256 }
iomodo50598c62025-07-27 22:06:32 +0400257 agent.Stats.TasksFailed++
258 } else {
259 agent.Stats.TasksCompleted++
260 }
261 // Update success rate
262 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
263 if total > 0 {
264 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400265 }
266 }
267 }
268
269 return nil
270}
271
272// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400273func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400274 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400275 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400276
iomodo62da94a2025-07-28 19:01:55 +0400277 m.logger.Info("Agent processing task",
278 slog.String("agent", agent.Name),
279 slog.String("task_id", task.ID),
280 slog.String("title", task.Title))
user5a7d60d2025-07-27 21:22:04 +0400281
282 // Mark task as in progress
283 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400284 agent.CurrentTask = &task.ID
285 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400286 return fmt.Errorf("failed to update task status: %w", err)
287 }
288
iomodo5c99a442025-07-28 14:23:52 +0400289 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400290 if m.shouldGenerateSubtasks(task) {
iomodo62da94a2025-07-28 19:01:55 +0400291 m.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
iomodod9ff8da2025-07-28 11:42:22 +0400292 if err := m.generateSubtasksForTask(ctx, task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400293 m.logger.Warn("Failed to generate subtasks for task",
294 slog.String("task_id", task.ID),
295 slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400296 // Continue with normal processing if subtask generation fails
297 } else {
298 // Task has been converted to subtask management, mark as completed
299 task.Status = tm.StatusCompleted
iomodo5c99a442025-07-28 14:23:52 +0400300 task.Solution = "Task analyzed by LLM and broken down into subtasks with potential new agent creation. See subtasks PR for details."
iomodod9ff8da2025-07-28 11:42:22 +0400301 completedAt := time.Now()
302 task.CompletedAt = &completedAt
303 agent.CurrentTask = nil
304
305 if err := m.taskManager.UpdateTask(task); err != nil {
306 return fmt.Errorf("failed to update task with subtasks: %w", err)
307 }
308
iomodo62da94a2025-07-28 19:01:55 +0400309 m.logger.Info("Task converted to subtasks by agent using LLM analysis",
310 slog.String("task_id", task.ID),
311 slog.String("agent", agent.Name))
iomodod9ff8da2025-07-28 11:42:22 +0400312 return nil
313 }
314 }
315
user5a7d60d2025-07-27 21:22:04 +0400316 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400317 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400318 if err != nil {
319 return fmt.Errorf("failed to generate solution: %w", err)
320 }
321
322 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400323 branchName := m.generateBranchName(task)
324 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400325 return fmt.Errorf("failed to commit solution: %w", err)
326 }
327
328 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400329 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400330 if err != nil {
331 return fmt.Errorf("failed to create pull request: %w", err)
332 }
333
334 // Update task as completed
335 task.Status = tm.StatusCompleted
336 task.Solution = solution
337 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400338 completedAt := time.Now()
339 task.CompletedAt = &completedAt
340 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400341
iomodo50598c62025-07-27 22:06:32 +0400342 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400343 return fmt.Errorf("failed to update completed task: %w", err)
344 }
345
iomodo50598c62025-07-27 22:06:32 +0400346 // Update agent stats
347 duration := time.Since(startTime)
348 if agent.Stats.AvgTime == 0 {
349 agent.Stats.AvgTime = duration.Milliseconds()
350 } else {
351 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
352 }
353
iomodo62da94a2025-07-28 19:01:55 +0400354 m.logger.Info("Task completed by agent",
355 slog.String("task_id", task.ID),
356 slog.String("agent", agent.Name),
357 slog.Duration("duration", duration),
358 slog.String("pr_url", prURL))
user5a7d60d2025-07-27 21:22:04 +0400359 return nil
360}
361
362// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400363func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
364 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400365
366 req := llm.ChatCompletionRequest{
367 Model: agent.Model,
368 Messages: []llm.Message{
369 {
370 Role: llm.RoleSystem,
371 Content: agent.SystemPrompt,
372 },
373 {
374 Role: llm.RoleUser,
375 Content: prompt,
376 },
377 },
378 MaxTokens: agent.MaxTokens,
379 Temperature: agent.Temperature,
380 }
381
382 resp, err := agent.Provider.ChatCompletion(ctx, req)
383 if err != nil {
384 return "", fmt.Errorf("LLM request failed: %w", err)
385 }
386
387 if len(resp.Choices) == 0 {
388 return "", fmt.Errorf("no response from LLM")
389 }
390
391 return resp.Choices[0].Message.Content, nil
392}
393
394// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400395func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400396 return fmt.Sprintf(`Task: %s
397
398Priority: %s
399Description: %s
400
401Please provide a complete solution for this task. Include:
4021. Detailed implementation plan
4032. Code changes needed (if applicable)
4043. Files to be created or modified
4054. Testing considerations
4065. Any dependencies or prerequisites
407
408Your response should be comprehensive and actionable.`,
409 task.Title,
410 task.Priority,
411 task.Description)
412}
413
414// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400415func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400416 // Clean title for use in branch name
417 cleanTitle := strings.ToLower(task.Title)
418 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
419 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
420 // Remove special characters
421 var result strings.Builder
422 for _, r := range cleanTitle {
423 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
424 result.WriteRune(r)
425 }
426 }
427 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400428
user5a7d60d2025-07-27 21:22:04 +0400429 // Limit length
430 if len(cleanTitle) > 40 {
431 cleanTitle = cleanTitle[:40]
432 }
iomodo50598c62025-07-27 22:06:32 +0400433
434 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400435}
436
437// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400438func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400439 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400440
user5a7d60d2025-07-27 21:22:04 +0400441 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400442 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400443 if err != nil {
444 return fmt.Errorf("failed to get agent clone: %w", err)
445 }
iomodo50598c62025-07-27 22:06:32 +0400446
iomodo62da94a2025-07-28 19:01:55 +0400447 m.logger.Info("Agent working in clone",
448 slog.String("agent", agent.Name),
449 slog.String("clone_path", clonePath))
user5a7d60d2025-07-27 21:22:04 +0400450
451 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400452 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400453 m.logger.Warn("Failed to refresh clone for agent",
454 slog.String("agent", agent.Name),
455 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400456 }
457
458 // All Git operations use the agent's clone directory
459 gitCmd := func(args ...string) *exec.Cmd {
460 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
461 }
462
463 // Ensure we're on main branch before creating new branch
464 cmd := gitCmd("checkout", "main")
465 if err := cmd.Run(); err != nil {
466 // Try master branch if main doesn't exist
467 cmd = gitCmd("checkout", "master")
468 if err := cmd.Run(); err != nil {
469 return fmt.Errorf("failed to checkout main/master branch: %w", err)
470 }
471 }
472
473 // Create branch
474 cmd = gitCmd("checkout", "-b", branchName)
475 if err := cmd.Run(); err != nil {
476 return fmt.Errorf("failed to create branch: %w", err)
477 }
478
479 // Create solution file in agent's clone
480 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
481 if err := os.MkdirAll(solutionDir, 0755); err != nil {
482 return fmt.Errorf("failed to create solution directory: %w", err)
483 }
484
485 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
486 solutionContent := fmt.Sprintf(`# Solution for Task: %s
487
488**Agent:** %s (%s)
489**Model:** %s
490**Completed:** %s
491
492## Task Description
493%s
494
495## Solution
496%s
497
498---
499*Generated by Staff AI Agent System*
500`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
501
502 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
503 return fmt.Errorf("failed to write solution file: %w", err)
504 }
505
506 // Stage files
507 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
508 cmd = gitCmd("add", relativeSolutionFile)
509 if err := cmd.Run(); err != nil {
510 return fmt.Errorf("failed to stage files: %w", err)
511 }
512
513 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400514 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400515 cmd = gitCmd("commit", "-m", commitMsg)
516 if err := cmd.Run(); err != nil {
517 return fmt.Errorf("failed to commit: %w", err)
518 }
519
520 // Push branch
521 cmd = gitCmd("push", "-u", "origin", branchName)
522 if err := cmd.Run(); err != nil {
523 return fmt.Errorf("failed to push branch: %w", err)
524 }
525
iomodo62da94a2025-07-28 19:01:55 +0400526 m.logger.Info("Agent successfully pushed branch",
527 slog.String("agent", agent.Name),
528 slog.String("branch", branchName))
user5a7d60d2025-07-27 21:22:04 +0400529 return nil
530}
531
532// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400533func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
534 template := m.config.Git.CommitMessageTemplate
535
user5a7d60d2025-07-27 21:22:04 +0400536 replacements := map[string]string{
537 "{task_id}": task.ID,
538 "{task_title}": task.Title,
539 "{agent_name}": agent.Name,
540 "{solution}": "See solution file for details",
541 }
542
543 result := template
544 for placeholder, value := range replacements {
545 result = strings.ReplaceAll(result, placeholder, value)
546 }
547
548 return result
549}
550
551// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400552func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400553 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400554
user5a7d60d2025-07-27 21:22:04 +0400555 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400556 description := m.buildPRDescription(task, solution, agent)
557
user5a7d60d2025-07-27 21:22:04 +0400558 options := git.PullRequestOptions{
559 Title: title,
560 Description: description,
561 HeadBranch: branchName,
562 BaseBranch: "main",
563 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
564 Draft: false,
565 }
566
iomodo50598c62025-07-27 22:06:32 +0400567 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400568 if err != nil {
569 return "", fmt.Errorf("failed to create PR: %w", err)
570 }
571
iomodo50598c62025-07-27 22:06:32 +0400572 return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
user5a7d60d2025-07-27 21:22:04 +0400573}
574
575// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400576func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
577 template := m.config.Git.PRTemplate
578
user5a7d60d2025-07-27 21:22:04 +0400579 // Truncate solution for PR if too long
580 truncatedSolution := solution
581 if len(solution) > 1000 {
582 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
583 }
iomodo50598c62025-07-27 22:06:32 +0400584
user5a7d60d2025-07-27 21:22:04 +0400585 replacements := map[string]string{
586 "{task_id}": task.ID,
587 "{task_title}": task.Title,
588 "{task_description}": task.Description,
589 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
590 "{priority}": string(task.Priority),
591 "{solution}": truncatedSolution,
592 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
593 }
594
595 result := template
596 for placeholder, value := range replacements {
597 result = strings.ReplaceAll(result, placeholder, value)
598 }
599
600 return result
601}
602
603// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400604func (m *Manager) AutoAssignTask(taskID string) error {
605 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400606 if err != nil {
607 return fmt.Errorf("failed to get task: %w", err)
608 }
609
iomodo50598c62025-07-27 22:06:32 +0400610 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400611 if err != nil {
612 return fmt.Errorf("failed to auto-assign task: %w", err)
613 }
614
615 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400616 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400617 return fmt.Errorf("failed to update task assignment: %w", err)
618 }
619
iomodo50598c62025-07-27 22:06:32 +0400620 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomodo62da94a2025-07-28 19:01:55 +0400621 m.logger.Info("Auto-assigned task to agent",
622 slog.String("task_id", taskID),
623 slog.String("agent", agentName),
624 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400625
626 return nil
627}
628
629// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400630func (m *Manager) GetAgentStatus() map[string]AgentInfo {
631 status := make(map[string]AgentInfo)
632
633 for name, agent := range m.agents {
634 agentStatus := StatusIdle
635 if m.isRunning[name] {
636 if agent.CurrentTask != nil {
637 agentStatus = StatusRunning
638 }
639 } else {
640 agentStatus = StatusStopped
641 }
642
643 status[name] = AgentInfo{
644 Name: agent.Name,
645 Role: agent.Role,
646 Model: agent.Model,
647 Status: agentStatus,
648 CurrentTask: agent.CurrentTask,
649 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400650 }
651 }
iomodo50598c62025-07-27 22:06:32 +0400652
user5a7d60d2025-07-27 21:22:04 +0400653 return status
654}
655
iomodo5c99a442025-07-28 14:23:52 +0400656// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400657func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
658 // Don't generate subtasks for subtasks
659 if task.ParentTaskID != "" {
660 return false
661 }
662
iomodo5c99a442025-07-28 14:23:52 +0400663 // Don't generate if already evaluated
664 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400665 return false
666 }
667
iomodo5c99a442025-07-28 14:23:52 +0400668 // Ask LLM to decide
669 ctx := context.Background()
670 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
671 if err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400672 m.logger.Warn("Failed to get LLM subtask decision for task",
673 slog.String("task_id", task.ID),
674 slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400675 // Fallback to simple heuristics
676 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400677 }
678
iomodo5c99a442025-07-28 14:23:52 +0400679 // Update task to mark as evaluated
680 task.SubtasksEvaluated = true
681 if err := m.taskManager.UpdateTask(task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400682 m.logger.Warn("Failed to update task evaluation status", slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400683 }
684
iomodo62da94a2025-07-28 19:01:55 +0400685 m.logger.Info("LLM subtask decision for task",
686 slog.String("task_id", task.ID),
687 slog.Bool("needs_subtasks", decision.NeedsSubtasks),
688 slog.Int("complexity_score", decision.ComplexityScore),
689 slog.String("reasoning", decision.Reasoning))
iomodo5c99a442025-07-28 14:23:52 +0400690
691 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400692}
693
694// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
695func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
696 if m.subtaskService == nil {
697 return fmt.Errorf("subtask service not initialized")
698 }
699
700 // Analyze the task for subtasks
701 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
702 if err != nil {
703 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
704 }
705
706 // Generate a PR with the subtask proposals
707 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
708 if err != nil {
709 return fmt.Errorf("failed to generate subtask PR: %w", err)
710 }
711
712 // Update the task with subtask information
713 task.SubtasksPRURL = prURL
714 task.SubtasksGenerated = true
715
iomodo62da94a2025-07-28 19:01:55 +0400716 m.logger.Info("Generated subtask PR for task",
717 slog.String("task_id", task.ID),
718 slog.String("pr_url", prURL))
719 m.logger.Info("Proposed subtasks and new agents for task",
720 slog.String("task_id", task.ID),
721 slog.Int("subtask_count", len(analysis.Subtasks)),
722 slog.Int("new_agent_count", len(analysis.AgentCreations)))
iomodo5c99a442025-07-28 14:23:52 +0400723
724 // Log proposed new agents if any
725 if len(analysis.AgentCreations) > 0 {
726 for _, agent := range analysis.AgentCreations {
iomodo62da94a2025-07-28 19:01:55 +0400727 m.logger.Info("Proposed new agent",
728 slog.String("role", agent.Role),
729 slog.Any("skills", agent.Skills))
iomodo5c99a442025-07-28 14:23:52 +0400730 }
731 }
iomodod9ff8da2025-07-28 11:42:22 +0400732
733 return nil
734}
735
user5a7d60d2025-07-27 21:22:04 +0400736// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400737func (m *Manager) IsAgentRunning(agentName string) bool {
738 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400739}
740
741// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400742func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400743 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400744 for agentName := range m.isRunning {
745 if m.isRunning[agentName] {
746 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400747 }
748 }
749
750 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400751 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400752 if err := agent.Provider.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400753 m.logger.Error("Error closing provider for agent",
754 slog.String("agent", agent.Name),
755 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400756 }
757 }
758
759 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400760 if err := m.cloneManager.CleanupAllClones(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400761 m.logger.Error("Error cleaning up agent clones", slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400762 }
763
iomodod9ff8da2025-07-28 11:42:22 +0400764 // Cleanup subtask service
765 if m.subtaskService != nil {
766 if err := m.subtaskService.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400767 m.logger.Error("Error closing subtask service", slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400768 }
769 }
770
user5a7d60d2025-07-27 21:22:04 +0400771 return nil
iomodo50598c62025-07-27 22:06:32 +0400772}