blob: 3d8277f20ca9fe2b82bbb1fbb7935a2817f32415 [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
iomodod9ff8da2025-07-28 11:42:22 +0400264 // Check if this task should generate subtasks
265 if m.shouldGenerateSubtasks(task) {
266 log.Printf("Analyzing task %s for subtask generation", task.ID)
267 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
273 task.Solution = "Task broken down into subtasks. See subtasks PR for details."
274 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
282 log.Printf("Task %s converted to subtasks by agent %s", task.ID, agent.Name)
283 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
iomodod9ff8da2025-07-28 11:42:22 +0400614// shouldGenerateSubtasks determines if a task should be broken down into subtasks
615func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
616 // Don't generate subtasks for subtasks
617 if task.ParentTaskID != "" {
618 return false
619 }
620
621 // Don't generate if already generated
622 if task.SubtasksGenerated {
623 return false
624 }
625
626 // Only generate for high priority tasks or complex descriptions
627 if task.Priority == tm.PriorityHigh || len(task.Description) > 200 {
628 return true
629 }
630
631 return false
632}
633
634// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
635func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
636 if m.subtaskService == nil {
637 return fmt.Errorf("subtask service not initialized")
638 }
639
640 // Analyze the task for subtasks
641 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
642 if err != nil {
643 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
644 }
645
646 // Generate a PR with the subtask proposals
647 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
648 if err != nil {
649 return fmt.Errorf("failed to generate subtask PR: %w", err)
650 }
651
652 // Update the task with subtask information
653 task.SubtasksPRURL = prURL
654 task.SubtasksGenerated = true
655
656 log.Printf("Generated subtask PR for task %s: %s", task.ID, prURL)
657 log.Printf("Proposed %d subtasks for task %s", len(analysis.Subtasks), task.ID)
658
659 return nil
660}
661
user5a7d60d2025-07-27 21:22:04 +0400662// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400663func (m *Manager) IsAgentRunning(agentName string) bool {
664 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400665}
666
667// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400668func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400669 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400670 for agentName := range m.isRunning {
671 if m.isRunning[agentName] {
672 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400673 }
674 }
675
676 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400677 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400678 if err := agent.Provider.Close(); err != nil {
679 log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
680 }
681 }
682
683 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400684 if err := m.cloneManager.CleanupAllClones(); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400685 log.Printf("Error cleaning up agent clones: %v", err)
686 }
687
iomodod9ff8da2025-07-28 11:42:22 +0400688 // Cleanup subtask service
689 if m.subtaskService != nil {
690 if err := m.subtaskService.Close(); err != nil {
691 log.Printf("Error closing subtask service: %v", err)
692 }
693 }
694
user5a7d60d2025-07-27 21:22:04 +0400695 return nil
iomodo50598c62025-07-27 22:06:32 +0400696}