blob: 45b0965281b5d5c8cf8e86998a31fd4b998dfacd [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,
iomodo443b20a2025-07-28 15:24:05 +0400110 m.prProvider,
111 m.config.GitHub.Owner,
112 m.config.GitHub.Repo,
113 m.cloneManager,
iomodod9ff8da2025-07-28 11:42:22 +0400114 )
115
116 return nil
117}
118
user5a7d60d2025-07-27 21:22:04 +0400119// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +0400120func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +0400121 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +0400122 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +0400123 if err != nil {
124 return nil, fmt.Errorf("failed to load system prompt: %w", err)
125 }
126
127 // Create LLM provider
128 llmConfig := llm.Config{
iomodof1ddefe2025-07-28 09:02:05 +0400129 Provider: llm.ProviderFake, // Use fake provider for testing
iomodo50598c62025-07-27 22:06:32 +0400130 APIKey: m.config.OpenAI.APIKey,
131 BaseURL: m.config.OpenAI.BaseURL,
132 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +0400133 }
iomodo50598c62025-07-27 22:06:32 +0400134
user5a7d60d2025-07-27 21:22:04 +0400135 provider, err := llm.CreateProvider(llmConfig)
136 if err != nil {
137 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
138 }
139
iomodo50598c62025-07-27 22:06:32 +0400140 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400141 Name: agentConfig.Name,
142 Role: agentConfig.Role,
143 Model: agentConfig.Model,
144 SystemPrompt: systemPrompt,
145 Provider: provider,
146 MaxTokens: agentConfig.MaxTokens,
147 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400148 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400149 }
150
151 return agent, nil
152}
153
154// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400155func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400156 content, err := os.ReadFile(filePath)
157 if err != nil {
158 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
159 }
160 return string(content), nil
161}
162
163// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400164func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
165 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400166 if !exists {
167 return fmt.Errorf("agent %s not found", agentName)
168 }
169
iomodo50598c62025-07-27 22:06:32 +0400170 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400171 return fmt.Errorf("agent %s is already running", agentName)
172 }
173
174 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400175 m.stopChannels[agentName] = stopChan
176 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400177
iomodo50598c62025-07-27 22:06:32 +0400178 go m.runAgentLoop(agent, loopInterval, stopChan)
179
user5a7d60d2025-07-27 21:22:04 +0400180 log.Printf("Started agent %s (%s) with %s model", agentName, agent.Role, agent.Model)
181 return nil
182}
183
184// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400185func (m *Manager) StopAgent(agentName string) error {
186 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400187 return fmt.Errorf("agent %s is not running", agentName)
188 }
189
iomodo50598c62025-07-27 22:06:32 +0400190 close(m.stopChannels[agentName])
191 delete(m.stopChannels, agentName)
192 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400193
194 log.Printf("Stopped agent %s", agentName)
195 return nil
196}
197
198// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400199func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400200 ticker := time.NewTicker(interval)
201 defer ticker.Stop()
202
203 for {
204 select {
205 case <-stopChan:
206 log.Printf("Agent %s stopping", agent.Name)
207 return
208 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400209 if err := m.processAgentTasks(agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400210 log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
211 }
212 }
213 }
214}
215
216// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400217func (m *Manager) processAgentTasks(agent *Agent) error {
218 if agent.CurrentTask != nil {
219 return nil
220 }
221
user5a7d60d2025-07-27 21:22:04 +0400222 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400223 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400224 if err != nil {
225 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
226 }
227
iomodo50598c62025-07-27 22:06:32 +0400228 log.Printf("Processing %d tasks for agent %s", len(tasks), agent.Name)
229
user5a7d60d2025-07-27 21:22:04 +0400230 for _, task := range tasks {
iomodo50598c62025-07-27 22:06:32 +0400231 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
232 if err := m.processTask(agent, task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400233 log.Printf("Error processing task %s: %v", task.ID, err)
234 // Mark task as failed
235 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400236 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400237 log.Printf("Error updating failed task %s: %v", task.ID, err)
238 }
iomodo50598c62025-07-27 22:06:32 +0400239 agent.Stats.TasksFailed++
240 } else {
241 agent.Stats.TasksCompleted++
242 }
243 // Update success rate
244 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
245 if total > 0 {
246 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400247 }
248 }
249 }
250
251 return nil
252}
253
254// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400255func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400256 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400257 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400258
259 log.Printf("Agent %s processing task %s: %s", agent.Name, task.ID, task.Title)
260
261 // Mark task as in progress
262 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400263 agent.CurrentTask = &task.ID
264 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400265 return fmt.Errorf("failed to update task status: %w", err)
266 }
267
iomodo5c99a442025-07-28 14:23:52 +0400268 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400269 if m.shouldGenerateSubtasks(task) {
iomodo5c99a442025-07-28 14:23:52 +0400270 log.Printf("LLM determined task %s should generate subtasks", task.ID)
iomodod9ff8da2025-07-28 11:42:22 +0400271 if err := m.generateSubtasksForTask(ctx, task); err != nil {
272 log.Printf("Warning: Failed to generate subtasks for task %s: %v", task.ID, err)
273 // Continue with normal processing if subtask generation fails
274 } else {
275 // Task has been converted to subtask management, mark as completed
276 task.Status = tm.StatusCompleted
iomodo5c99a442025-07-28 14:23:52 +0400277 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 +0400278 completedAt := time.Now()
279 task.CompletedAt = &completedAt
280 agent.CurrentTask = nil
281
282 if err := m.taskManager.UpdateTask(task); err != nil {
283 return fmt.Errorf("failed to update task with subtasks: %w", err)
284 }
285
iomodo5c99a442025-07-28 14:23:52 +0400286 log.Printf("Task %s converted to subtasks by agent %s using LLM analysis", task.ID, agent.Name)
iomodod9ff8da2025-07-28 11:42:22 +0400287 return nil
288 }
289 }
290
user5a7d60d2025-07-27 21:22:04 +0400291 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400292 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400293 if err != nil {
294 return fmt.Errorf("failed to generate solution: %w", err)
295 }
296
297 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400298 branchName := m.generateBranchName(task)
299 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400300 return fmt.Errorf("failed to commit solution: %w", err)
301 }
302
303 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400304 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400305 if err != nil {
306 return fmt.Errorf("failed to create pull request: %w", err)
307 }
308
309 // Update task as completed
310 task.Status = tm.StatusCompleted
311 task.Solution = solution
312 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400313 completedAt := time.Now()
314 task.CompletedAt = &completedAt
315 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400316
iomodo50598c62025-07-27 22:06:32 +0400317 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400318 return fmt.Errorf("failed to update completed task: %w", err)
319 }
320
iomodo50598c62025-07-27 22:06:32 +0400321 // Update agent stats
322 duration := time.Since(startTime)
323 if agent.Stats.AvgTime == 0 {
324 agent.Stats.AvgTime = duration.Milliseconds()
325 } else {
326 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
327 }
328
329 log.Printf("Task %s completed by agent %s in %v. PR: %s", task.ID, agent.Name, duration, prURL)
user5a7d60d2025-07-27 21:22:04 +0400330 return nil
331}
332
333// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400334func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
335 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400336
337 req := llm.ChatCompletionRequest{
338 Model: agent.Model,
339 Messages: []llm.Message{
340 {
341 Role: llm.RoleSystem,
342 Content: agent.SystemPrompt,
343 },
344 {
345 Role: llm.RoleUser,
346 Content: prompt,
347 },
348 },
349 MaxTokens: agent.MaxTokens,
350 Temperature: agent.Temperature,
351 }
352
353 resp, err := agent.Provider.ChatCompletion(ctx, req)
354 if err != nil {
355 return "", fmt.Errorf("LLM request failed: %w", err)
356 }
357
358 if len(resp.Choices) == 0 {
359 return "", fmt.Errorf("no response from LLM")
360 }
361
362 return resp.Choices[0].Message.Content, nil
363}
364
365// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400366func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400367 return fmt.Sprintf(`Task: %s
368
369Priority: %s
370Description: %s
371
372Please provide a complete solution for this task. Include:
3731. Detailed implementation plan
3742. Code changes needed (if applicable)
3753. Files to be created or modified
3764. Testing considerations
3775. Any dependencies or prerequisites
378
379Your response should be comprehensive and actionable.`,
380 task.Title,
381 task.Priority,
382 task.Description)
383}
384
385// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400386func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400387 // Clean title for use in branch name
388 cleanTitle := strings.ToLower(task.Title)
389 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
390 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
391 // Remove special characters
392 var result strings.Builder
393 for _, r := range cleanTitle {
394 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
395 result.WriteRune(r)
396 }
397 }
398 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400399
user5a7d60d2025-07-27 21:22:04 +0400400 // Limit length
401 if len(cleanTitle) > 40 {
402 cleanTitle = cleanTitle[:40]
403 }
iomodo50598c62025-07-27 22:06:32 +0400404
405 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400406}
407
408// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400409func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400410 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400411
user5a7d60d2025-07-27 21:22:04 +0400412 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400413 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400414 if err != nil {
415 return fmt.Errorf("failed to get agent clone: %w", err)
416 }
iomodo50598c62025-07-27 22:06:32 +0400417
user5a7d60d2025-07-27 21:22:04 +0400418 log.Printf("Agent %s working in clone: %s", agent.Name, clonePath)
419
420 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400421 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400422 log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
423 }
424
425 // All Git operations use the agent's clone directory
426 gitCmd := func(args ...string) *exec.Cmd {
427 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
428 }
429
430 // Ensure we're on main branch before creating new branch
431 cmd := gitCmd("checkout", "main")
432 if err := cmd.Run(); err != nil {
433 // Try master branch if main doesn't exist
434 cmd = gitCmd("checkout", "master")
435 if err := cmd.Run(); err != nil {
436 return fmt.Errorf("failed to checkout main/master branch: %w", err)
437 }
438 }
439
440 // Create branch
441 cmd = gitCmd("checkout", "-b", branchName)
442 if err := cmd.Run(); err != nil {
443 return fmt.Errorf("failed to create branch: %w", err)
444 }
445
446 // Create solution file in agent's clone
447 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
448 if err := os.MkdirAll(solutionDir, 0755); err != nil {
449 return fmt.Errorf("failed to create solution directory: %w", err)
450 }
451
452 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
453 solutionContent := fmt.Sprintf(`# Solution for Task: %s
454
455**Agent:** %s (%s)
456**Model:** %s
457**Completed:** %s
458
459## Task Description
460%s
461
462## Solution
463%s
464
465---
466*Generated by Staff AI Agent System*
467`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
468
469 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
470 return fmt.Errorf("failed to write solution file: %w", err)
471 }
472
473 // Stage files
474 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
475 cmd = gitCmd("add", relativeSolutionFile)
476 if err := cmd.Run(); err != nil {
477 return fmt.Errorf("failed to stage files: %w", err)
478 }
479
480 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400481 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400482 cmd = gitCmd("commit", "-m", commitMsg)
483 if err := cmd.Run(); err != nil {
484 return fmt.Errorf("failed to commit: %w", err)
485 }
486
487 // Push branch
488 cmd = gitCmd("push", "-u", "origin", branchName)
489 if err := cmd.Run(); err != nil {
490 return fmt.Errorf("failed to push branch: %w", err)
491 }
492
493 log.Printf("Agent %s successfully pushed branch %s", agent.Name, branchName)
494 return nil
495}
496
497// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400498func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
499 template := m.config.Git.CommitMessageTemplate
500
user5a7d60d2025-07-27 21:22:04 +0400501 replacements := map[string]string{
502 "{task_id}": task.ID,
503 "{task_title}": task.Title,
504 "{agent_name}": agent.Name,
505 "{solution}": "See solution file for details",
506 }
507
508 result := template
509 for placeholder, value := range replacements {
510 result = strings.ReplaceAll(result, placeholder, value)
511 }
512
513 return result
514}
515
516// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400517func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400518 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400519
user5a7d60d2025-07-27 21:22:04 +0400520 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400521 description := m.buildPRDescription(task, solution, agent)
522
user5a7d60d2025-07-27 21:22:04 +0400523 options := git.PullRequestOptions{
524 Title: title,
525 Description: description,
526 HeadBranch: branchName,
527 BaseBranch: "main",
528 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
529 Draft: false,
530 }
531
iomodo50598c62025-07-27 22:06:32 +0400532 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400533 if err != nil {
534 return "", fmt.Errorf("failed to create PR: %w", err)
535 }
536
iomodo50598c62025-07-27 22:06:32 +0400537 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 +0400538}
539
540// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400541func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
542 template := m.config.Git.PRTemplate
543
user5a7d60d2025-07-27 21:22:04 +0400544 // Truncate solution for PR if too long
545 truncatedSolution := solution
546 if len(solution) > 1000 {
547 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
548 }
iomodo50598c62025-07-27 22:06:32 +0400549
user5a7d60d2025-07-27 21:22:04 +0400550 replacements := map[string]string{
551 "{task_id}": task.ID,
552 "{task_title}": task.Title,
553 "{task_description}": task.Description,
554 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
555 "{priority}": string(task.Priority),
556 "{solution}": truncatedSolution,
557 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
558 }
559
560 result := template
561 for placeholder, value := range replacements {
562 result = strings.ReplaceAll(result, placeholder, value)
563 }
564
565 return result
566}
567
568// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400569func (m *Manager) AutoAssignTask(taskID string) error {
570 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400571 if err != nil {
572 return fmt.Errorf("failed to get task: %w", err)
573 }
574
iomodo50598c62025-07-27 22:06:32 +0400575 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400576 if err != nil {
577 return fmt.Errorf("failed to auto-assign task: %w", err)
578 }
579
580 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400581 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400582 return fmt.Errorf("failed to update task assignment: %w", err)
583 }
584
iomodo50598c62025-07-27 22:06:32 +0400585 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
user5a7d60d2025-07-27 21:22:04 +0400586 log.Printf("Auto-assigned task %s to %s: %s", taskID, agentName, explanation)
587
588 return nil
589}
590
591// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400592func (m *Manager) GetAgentStatus() map[string]AgentInfo {
593 status := make(map[string]AgentInfo)
594
595 for name, agent := range m.agents {
596 agentStatus := StatusIdle
597 if m.isRunning[name] {
598 if agent.CurrentTask != nil {
599 agentStatus = StatusRunning
600 }
601 } else {
602 agentStatus = StatusStopped
603 }
604
605 status[name] = AgentInfo{
606 Name: agent.Name,
607 Role: agent.Role,
608 Model: agent.Model,
609 Status: agentStatus,
610 CurrentTask: agent.CurrentTask,
611 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400612 }
613 }
iomodo50598c62025-07-27 22:06:32 +0400614
user5a7d60d2025-07-27 21:22:04 +0400615 return status
616}
617
iomodo5c99a442025-07-28 14:23:52 +0400618// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400619func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
620 // Don't generate subtasks for subtasks
621 if task.ParentTaskID != "" {
622 return false
623 }
624
iomodo5c99a442025-07-28 14:23:52 +0400625 // Don't generate if already evaluated
626 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400627 return false
628 }
629
iomodo5c99a442025-07-28 14:23:52 +0400630 // Ask LLM to decide
631 ctx := context.Background()
632 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
633 if err != nil {
634 log.Printf("Warning: Failed to get LLM subtask decision for task %s: %v", task.ID, err)
635 // Fallback to simple heuristics
636 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400637 }
638
iomodo5c99a442025-07-28 14:23:52 +0400639 // Update task to mark as evaluated
640 task.SubtasksEvaluated = true
641 if err := m.taskManager.UpdateTask(task); err != nil {
642 log.Printf("Warning: Failed to update task evaluation status: %v", err)
643 }
644
645 log.Printf("LLM subtask decision for task %s: needs_subtasks=%v, complexity=%d, reasoning=%s",
646 task.ID, decision.NeedsSubtasks, decision.ComplexityScore, decision.Reasoning)
647
648 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400649}
650
651// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
652func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
653 if m.subtaskService == nil {
654 return fmt.Errorf("subtask service not initialized")
655 }
656
657 // Analyze the task for subtasks
658 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
659 if err != nil {
660 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
661 }
662
663 // Generate a PR with the subtask proposals
664 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
665 if err != nil {
666 return fmt.Errorf("failed to generate subtask PR: %w", err)
667 }
668
669 // Update the task with subtask information
670 task.SubtasksPRURL = prURL
671 task.SubtasksGenerated = true
672
673 log.Printf("Generated subtask PR for task %s: %s", task.ID, prURL)
iomodo5c99a442025-07-28 14:23:52 +0400674 log.Printf("Proposed %d subtasks and %d new agents for task %s", len(analysis.Subtasks), len(analysis.AgentCreations), task.ID)
675
676 // Log proposed new agents if any
677 if len(analysis.AgentCreations) > 0 {
678 for _, agent := range analysis.AgentCreations {
679 log.Printf("Proposed new agent: %s with skills: %v", agent.Role, agent.Skills)
680 }
681 }
iomodod9ff8da2025-07-28 11:42:22 +0400682
683 return nil
684}
685
user5a7d60d2025-07-27 21:22:04 +0400686// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400687func (m *Manager) IsAgentRunning(agentName string) bool {
688 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400689}
690
691// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400692func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400693 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400694 for agentName := range m.isRunning {
695 if m.isRunning[agentName] {
696 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400697 }
698 }
699
700 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400701 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400702 if err := agent.Provider.Close(); err != nil {
703 log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
704 }
705 }
706
707 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400708 if err := m.cloneManager.CleanupAllClones(); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400709 log.Printf("Error cleaning up agent clones: %v", err)
710 }
711
iomodod9ff8da2025-07-28 11:42:22 +0400712 // Cleanup subtask service
713 if m.subtaskService != nil {
714 if err := m.subtaskService.Close(); err != nil {
715 log.Printf("Error closing subtask service: %v", err)
716 }
717 }
718
user5a7d60d2025-07-27 21:22:04 +0400719 return nil
iomodo50598c62025-07-27 22:06:32 +0400720}