blob: ab625bf1323182b4a4f931cd19d887763ac5ef9b [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package agent
2
3import (
4 "context"
5 "fmt"
6 "log"
7 "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{}
user5a7d60d2025-07-27 21:22:04 +040033}
34
iomodo50598c62025-07-27 22:06:32 +040035// NewManager creates a new agent manager
36func NewManager(cfg *config.Config, taskManager tm.TaskManager) (*Manager, error) {
user5a7d60d2025-07-27 21:22:04 +040037 // Create auto-assigner
38 autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
39
40 // Create GitHub PR provider
41 githubConfig := git.GitHubConfig{
42 Token: cfg.GitHub.Token,
43 }
44 prProvider := git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
45
46 // Create clone manager for per-agent Git repositories
47 repoURL := fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
48 workspacePath := filepath.Join(".", "workspace")
49 cloneManager := git.NewCloneManager(repoURL, workspacePath)
50
iomodo50598c62025-07-27 22:06:32 +040051 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040052 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040053 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040054 taskManager: taskManager,
55 autoAssigner: autoAssigner,
56 prProvider: prProvider,
57 cloneManager: cloneManager,
58 isRunning: make(map[string]bool),
59 stopChannels: make(map[string]chan struct{}),
60 }
61
62 // Initialize agents
63 if err := manager.initializeAgents(); err != nil {
64 return nil, fmt.Errorf("failed to initialize agents: %w", err)
65 }
66
iomodod9ff8da2025-07-28 11:42:22 +040067 // Initialize subtask service after agents are created
68 if err := manager.initializeSubtaskService(); err != nil {
69 return nil, fmt.Errorf("failed to initialize subtask service: %w", err)
70 }
71
user5a7d60d2025-07-27 21:22:04 +040072 return manager, nil
73}
74
75// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +040076func (m *Manager) initializeAgents() error {
77 for _, agentConfig := range m.config.Agents {
78 agent, err := m.createAgent(agentConfig)
user5a7d60d2025-07-27 21:22:04 +040079 if err != nil {
80 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
81 }
iomodo50598c62025-07-27 22:06:32 +040082 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +040083 }
84 return nil
85}
86
iomodod9ff8da2025-07-28 11:42:22 +040087// initializeSubtaskService creates the subtask service with available agent roles
88func (m *Manager) initializeSubtaskService() error {
89 // Get agent roles from configuration
90 agentRoles := make([]string, 0, len(m.config.Agents))
91 for _, agentConfig := range m.config.Agents {
92 agentRoles = append(agentRoles, agentConfig.Name)
93 }
94
95 // Use the first agent's LLM provider for subtask analysis
96 if len(m.agents) == 0 {
97 return fmt.Errorf("no agents available for subtask service")
98 }
99
100 var firstAgent *Agent
101 for _, agent := range m.agents {
102 firstAgent = agent
103 break
104 }
105
106 m.subtaskService = subtasks.NewSubtaskService(
107 firstAgent.Provider,
108 m.taskManager,
109 agentRoles,
110 )
111
112 return nil
113}
114
user5a7d60d2025-07-27 21:22:04 +0400115// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +0400116func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +0400117 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +0400118 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +0400119 if err != nil {
120 return nil, fmt.Errorf("failed to load system prompt: %w", err)
121 }
122
123 // Create LLM provider
124 llmConfig := llm.Config{
iomodof1ddefe2025-07-28 09:02:05 +0400125 Provider: llm.ProviderFake, // Use fake provider for testing
iomodo50598c62025-07-27 22:06:32 +0400126 APIKey: m.config.OpenAI.APIKey,
127 BaseURL: m.config.OpenAI.BaseURL,
128 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +0400129 }
iomodo50598c62025-07-27 22:06:32 +0400130
user5a7d60d2025-07-27 21:22:04 +0400131 provider, err := llm.CreateProvider(llmConfig)
132 if err != nil {
133 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
134 }
135
iomodo50598c62025-07-27 22:06:32 +0400136 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400137 Name: agentConfig.Name,
138 Role: agentConfig.Role,
139 Model: agentConfig.Model,
140 SystemPrompt: systemPrompt,
141 Provider: provider,
142 MaxTokens: agentConfig.MaxTokens,
143 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400144 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400145 }
146
147 return agent, nil
148}
149
150// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400151func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400152 content, err := os.ReadFile(filePath)
153 if err != nil {
154 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
155 }
156 return string(content), nil
157}
158
159// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400160func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
161 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400162 if !exists {
163 return fmt.Errorf("agent %s not found", agentName)
164 }
165
iomodo50598c62025-07-27 22:06:32 +0400166 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400167 return fmt.Errorf("agent %s is already running", agentName)
168 }
169
170 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400171 m.stopChannels[agentName] = stopChan
172 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400173
iomodo50598c62025-07-27 22:06:32 +0400174 go m.runAgentLoop(agent, loopInterval, stopChan)
175
user5a7d60d2025-07-27 21:22:04 +0400176 log.Printf("Started agent %s (%s) with %s model", agentName, agent.Role, agent.Model)
177 return nil
178}
179
180// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400181func (m *Manager) StopAgent(agentName string) error {
182 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400183 return fmt.Errorf("agent %s is not running", agentName)
184 }
185
iomodo50598c62025-07-27 22:06:32 +0400186 close(m.stopChannels[agentName])
187 delete(m.stopChannels, agentName)
188 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400189
190 log.Printf("Stopped agent %s", agentName)
191 return nil
192}
193
194// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400195func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400196 ticker := time.NewTicker(interval)
197 defer ticker.Stop()
198
199 for {
200 select {
201 case <-stopChan:
202 log.Printf("Agent %s stopping", agent.Name)
203 return
204 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400205 if err := m.processAgentTasks(agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400206 log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
207 }
208 }
209 }
210}
211
212// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400213func (m *Manager) processAgentTasks(agent *Agent) error {
214 if agent.CurrentTask != nil {
215 return nil
216 }
217
user5a7d60d2025-07-27 21:22:04 +0400218 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400219 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400220 if err != nil {
221 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
222 }
223
iomodo50598c62025-07-27 22:06:32 +0400224 log.Printf("Processing %d tasks for agent %s", len(tasks), agent.Name)
225
user5a7d60d2025-07-27 21:22:04 +0400226 for _, task := range tasks {
iomodo50598c62025-07-27 22:06:32 +0400227 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
228 if err := m.processTask(agent, task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400229 log.Printf("Error processing task %s: %v", task.ID, err)
230 // Mark task as failed
231 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400232 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400233 log.Printf("Error updating failed task %s: %v", task.ID, err)
234 }
iomodo50598c62025-07-27 22:06:32 +0400235 agent.Stats.TasksFailed++
236 } else {
237 agent.Stats.TasksCompleted++
238 }
239 // Update success rate
240 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
241 if total > 0 {
242 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400243 }
244 }
245 }
246
247 return nil
248}
249
250// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400251func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400252 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400253 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400254
255 log.Printf("Agent %s processing task %s: %s", agent.Name, task.ID, task.Title)
256
257 // Mark task as in progress
258 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400259 agent.CurrentTask = &task.ID
260 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400261 return fmt.Errorf("failed to update task status: %w", err)
262 }
263
iomodo5c99a442025-07-28 14:23:52 +0400264 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400265 if m.shouldGenerateSubtasks(task) {
iomodo5c99a442025-07-28 14:23:52 +0400266 log.Printf("LLM determined task %s should generate subtasks", task.ID)
iomodod9ff8da2025-07-28 11:42:22 +0400267 if err := m.generateSubtasksForTask(ctx, task); err != nil {
268 log.Printf("Warning: Failed to generate subtasks for task %s: %v", task.ID, err)
269 // Continue with normal processing if subtask generation fails
270 } else {
271 // Task has been converted to subtask management, mark as completed
272 task.Status = tm.StatusCompleted
iomodo5c99a442025-07-28 14:23:52 +0400273 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 +0400274 completedAt := time.Now()
275 task.CompletedAt = &completedAt
276 agent.CurrentTask = nil
277
278 if err := m.taskManager.UpdateTask(task); err != nil {
279 return fmt.Errorf("failed to update task with subtasks: %w", err)
280 }
281
iomodo5c99a442025-07-28 14:23:52 +0400282 log.Printf("Task %s converted to subtasks by agent %s using LLM analysis", task.ID, agent.Name)
iomodod9ff8da2025-07-28 11:42:22 +0400283 return nil
284 }
285 }
286
user5a7d60d2025-07-27 21:22:04 +0400287 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400288 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400289 if err != nil {
290 return fmt.Errorf("failed to generate solution: %w", err)
291 }
292
293 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400294 branchName := m.generateBranchName(task)
295 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400296 return fmt.Errorf("failed to commit solution: %w", err)
297 }
298
299 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400300 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400301 if err != nil {
302 return fmt.Errorf("failed to create pull request: %w", err)
303 }
304
305 // Update task as completed
306 task.Status = tm.StatusCompleted
307 task.Solution = solution
308 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400309 completedAt := time.Now()
310 task.CompletedAt = &completedAt
311 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400312
iomodo50598c62025-07-27 22:06:32 +0400313 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400314 return fmt.Errorf("failed to update completed task: %w", err)
315 }
316
iomodo50598c62025-07-27 22:06:32 +0400317 // Update agent stats
318 duration := time.Since(startTime)
319 if agent.Stats.AvgTime == 0 {
320 agent.Stats.AvgTime = duration.Milliseconds()
321 } else {
322 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
323 }
324
325 log.Printf("Task %s completed by agent %s in %v. PR: %s", task.ID, agent.Name, duration, prURL)
user5a7d60d2025-07-27 21:22:04 +0400326 return nil
327}
328
329// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400330func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
331 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400332
333 req := llm.ChatCompletionRequest{
334 Model: agent.Model,
335 Messages: []llm.Message{
336 {
337 Role: llm.RoleSystem,
338 Content: agent.SystemPrompt,
339 },
340 {
341 Role: llm.RoleUser,
342 Content: prompt,
343 },
344 },
345 MaxTokens: agent.MaxTokens,
346 Temperature: agent.Temperature,
347 }
348
349 resp, err := agent.Provider.ChatCompletion(ctx, req)
350 if err != nil {
351 return "", fmt.Errorf("LLM request failed: %w", err)
352 }
353
354 if len(resp.Choices) == 0 {
355 return "", fmt.Errorf("no response from LLM")
356 }
357
358 return resp.Choices[0].Message.Content, nil
359}
360
361// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400362func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400363 return fmt.Sprintf(`Task: %s
364
365Priority: %s
366Description: %s
367
368Please provide a complete solution for this task. Include:
3691. Detailed implementation plan
3702. Code changes needed (if applicable)
3713. Files to be created or modified
3724. Testing considerations
3735. Any dependencies or prerequisites
374
375Your response should be comprehensive and actionable.`,
376 task.Title,
377 task.Priority,
378 task.Description)
379}
380
381// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400382func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400383 // Clean title for use in branch name
384 cleanTitle := strings.ToLower(task.Title)
385 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
386 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
387 // Remove special characters
388 var result strings.Builder
389 for _, r := range cleanTitle {
390 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
391 result.WriteRune(r)
392 }
393 }
394 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400395
user5a7d60d2025-07-27 21:22:04 +0400396 // Limit length
397 if len(cleanTitle) > 40 {
398 cleanTitle = cleanTitle[:40]
399 }
iomodo50598c62025-07-27 22:06:32 +0400400
401 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400402}
403
404// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400405func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400406 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400407
user5a7d60d2025-07-27 21:22:04 +0400408 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400409 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400410 if err != nil {
411 return fmt.Errorf("failed to get agent clone: %w", err)
412 }
iomodo50598c62025-07-27 22:06:32 +0400413
user5a7d60d2025-07-27 21:22:04 +0400414 log.Printf("Agent %s working in clone: %s", agent.Name, clonePath)
415
416 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400417 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400418 log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
419 }
420
421 // All Git operations use the agent's clone directory
422 gitCmd := func(args ...string) *exec.Cmd {
423 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
424 }
425
426 // Ensure we're on main branch before creating new branch
427 cmd := gitCmd("checkout", "main")
428 if err := cmd.Run(); err != nil {
429 // Try master branch if main doesn't exist
430 cmd = gitCmd("checkout", "master")
431 if err := cmd.Run(); err != nil {
432 return fmt.Errorf("failed to checkout main/master branch: %w", err)
433 }
434 }
435
436 // Create branch
437 cmd = gitCmd("checkout", "-b", branchName)
438 if err := cmd.Run(); err != nil {
439 return fmt.Errorf("failed to create branch: %w", err)
440 }
441
442 // Create solution file in agent's clone
443 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
444 if err := os.MkdirAll(solutionDir, 0755); err != nil {
445 return fmt.Errorf("failed to create solution directory: %w", err)
446 }
447
448 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
449 solutionContent := fmt.Sprintf(`# Solution for Task: %s
450
451**Agent:** %s (%s)
452**Model:** %s
453**Completed:** %s
454
455## Task Description
456%s
457
458## Solution
459%s
460
461---
462*Generated by Staff AI Agent System*
463`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
464
465 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
466 return fmt.Errorf("failed to write solution file: %w", err)
467 }
468
469 // Stage files
470 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
471 cmd = gitCmd("add", relativeSolutionFile)
472 if err := cmd.Run(); err != nil {
473 return fmt.Errorf("failed to stage files: %w", err)
474 }
475
476 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400477 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400478 cmd = gitCmd("commit", "-m", commitMsg)
479 if err := cmd.Run(); err != nil {
480 return fmt.Errorf("failed to commit: %w", err)
481 }
482
483 // Push branch
484 cmd = gitCmd("push", "-u", "origin", branchName)
485 if err := cmd.Run(); err != nil {
486 return fmt.Errorf("failed to push branch: %w", err)
487 }
488
489 log.Printf("Agent %s successfully pushed branch %s", agent.Name, branchName)
490 return nil
491}
492
493// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400494func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
495 template := m.config.Git.CommitMessageTemplate
496
user5a7d60d2025-07-27 21:22:04 +0400497 replacements := map[string]string{
498 "{task_id}": task.ID,
499 "{task_title}": task.Title,
500 "{agent_name}": agent.Name,
501 "{solution}": "See solution file for details",
502 }
503
504 result := template
505 for placeholder, value := range replacements {
506 result = strings.ReplaceAll(result, placeholder, value)
507 }
508
509 return result
510}
511
512// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400513func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400514 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400515
user5a7d60d2025-07-27 21:22:04 +0400516 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400517 description := m.buildPRDescription(task, solution, agent)
518
user5a7d60d2025-07-27 21:22:04 +0400519 options := git.PullRequestOptions{
520 Title: title,
521 Description: description,
522 HeadBranch: branchName,
523 BaseBranch: "main",
524 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
525 Draft: false,
526 }
527
iomodo50598c62025-07-27 22:06:32 +0400528 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400529 if err != nil {
530 return "", fmt.Errorf("failed to create PR: %w", err)
531 }
532
iomodo50598c62025-07-27 22:06:32 +0400533 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 +0400534}
535
536// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400537func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
538 template := m.config.Git.PRTemplate
539
user5a7d60d2025-07-27 21:22:04 +0400540 // Truncate solution for PR if too long
541 truncatedSolution := solution
542 if len(solution) > 1000 {
543 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
544 }
iomodo50598c62025-07-27 22:06:32 +0400545
user5a7d60d2025-07-27 21:22:04 +0400546 replacements := map[string]string{
547 "{task_id}": task.ID,
548 "{task_title}": task.Title,
549 "{task_description}": task.Description,
550 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
551 "{priority}": string(task.Priority),
552 "{solution}": truncatedSolution,
553 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
554 }
555
556 result := template
557 for placeholder, value := range replacements {
558 result = strings.ReplaceAll(result, placeholder, value)
559 }
560
561 return result
562}
563
564// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400565func (m *Manager) AutoAssignTask(taskID string) error {
566 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400567 if err != nil {
568 return fmt.Errorf("failed to get task: %w", err)
569 }
570
iomodo50598c62025-07-27 22:06:32 +0400571 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400572 if err != nil {
573 return fmt.Errorf("failed to auto-assign task: %w", err)
574 }
575
576 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400577 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400578 return fmt.Errorf("failed to update task assignment: %w", err)
579 }
580
iomodo50598c62025-07-27 22:06:32 +0400581 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
user5a7d60d2025-07-27 21:22:04 +0400582 log.Printf("Auto-assigned task %s to %s: %s", taskID, agentName, explanation)
583
584 return nil
585}
586
587// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400588func (m *Manager) GetAgentStatus() map[string]AgentInfo {
589 status := make(map[string]AgentInfo)
590
591 for name, agent := range m.agents {
592 agentStatus := StatusIdle
593 if m.isRunning[name] {
594 if agent.CurrentTask != nil {
595 agentStatus = StatusRunning
596 }
597 } else {
598 agentStatus = StatusStopped
599 }
600
601 status[name] = AgentInfo{
602 Name: agent.Name,
603 Role: agent.Role,
604 Model: agent.Model,
605 Status: agentStatus,
606 CurrentTask: agent.CurrentTask,
607 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400608 }
609 }
iomodo50598c62025-07-27 22:06:32 +0400610
user5a7d60d2025-07-27 21:22:04 +0400611 return status
612}
613
iomodo5c99a442025-07-28 14:23:52 +0400614// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400615func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
616 // Don't generate subtasks for subtasks
617 if task.ParentTaskID != "" {
618 return false
619 }
620
iomodo5c99a442025-07-28 14:23:52 +0400621 // Don't generate if already evaluated
622 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400623 return false
624 }
625
iomodo5c99a442025-07-28 14:23:52 +0400626 // Ask LLM to decide
627 ctx := context.Background()
628 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
629 if err != nil {
630 log.Printf("Warning: Failed to get LLM subtask decision for task %s: %v", task.ID, err)
631 // Fallback to simple heuristics
632 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400633 }
634
iomodo5c99a442025-07-28 14:23:52 +0400635 // Update task to mark as evaluated
636 task.SubtasksEvaluated = true
637 if err := m.taskManager.UpdateTask(task); err != nil {
638 log.Printf("Warning: Failed to update task evaluation status: %v", err)
639 }
640
641 log.Printf("LLM subtask decision for task %s: needs_subtasks=%v, complexity=%d, reasoning=%s",
642 task.ID, decision.NeedsSubtasks, decision.ComplexityScore, decision.Reasoning)
643
644 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400645}
646
647// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
648func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
649 if m.subtaskService == nil {
650 return fmt.Errorf("subtask service not initialized")
651 }
652
653 // Analyze the task for subtasks
654 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
655 if err != nil {
656 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
657 }
658
659 // Generate a PR with the subtask proposals
660 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
661 if err != nil {
662 return fmt.Errorf("failed to generate subtask PR: %w", err)
663 }
664
665 // Update the task with subtask information
666 task.SubtasksPRURL = prURL
667 task.SubtasksGenerated = true
668
669 log.Printf("Generated subtask PR for task %s: %s", task.ID, prURL)
iomodo5c99a442025-07-28 14:23:52 +0400670 log.Printf("Proposed %d subtasks and %d new agents for task %s", len(analysis.Subtasks), len(analysis.AgentCreations), task.ID)
671
672 // Log proposed new agents if any
673 if len(analysis.AgentCreations) > 0 {
674 for _, agent := range analysis.AgentCreations {
675 log.Printf("Proposed new agent: %s with skills: %v", agent.Role, agent.Skills)
676 }
677 }
iomodod9ff8da2025-07-28 11:42:22 +0400678
679 return nil
680}
681
user5a7d60d2025-07-27 21:22:04 +0400682// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400683func (m *Manager) IsAgentRunning(agentName string) bool {
684 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400685}
686
687// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400688func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400689 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400690 for agentName := range m.isRunning {
691 if m.isRunning[agentName] {
692 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400693 }
694 }
695
696 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400697 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400698 if err := agent.Provider.Close(); err != nil {
699 log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
700 }
701 }
702
703 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400704 if err := m.cloneManager.CleanupAllClones(); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400705 log.Printf("Error cleaning up agent clones: %v", err)
706 }
707
iomodod9ff8da2025-07-28 11:42:22 +0400708 // Cleanup subtask service
709 if m.subtaskService != nil {
710 if err := m.subtaskService.Close(); err != nil {
711 log.Printf("Error closing subtask service: %v", err)
712 }
713 }
714
user5a7d60d2025-07-27 21:22:04 +0400715 return nil
iomodo50598c62025-07-27 22:06:32 +0400716}