blob: 2d31b87b4a88d989b339e5ea3c78edec086108bb [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
user5a7d60d2025-07-27 21:22:04 +040018 "github.com/iomodo/staff/tm"
19)
20
iomodo50598c62025-07-27 22:06:32 +040021// Manager manages multiple AI agents with Git operations and task processing
22type Manager struct {
user5a7d60d2025-07-27 21:22:04 +040023 config *config.Config
iomodo50598c62025-07-27 22:06:32 +040024 agents map[string]*Agent
user5a7d60d2025-07-27 21:22:04 +040025 taskManager tm.TaskManager
26 autoAssigner *assignment.AutoAssigner
27 prProvider git.PullRequestProvider
28 cloneManager *git.CloneManager
29 isRunning map[string]bool
30 stopChannels map[string]chan struct{}
31}
32
iomodo50598c62025-07-27 22:06:32 +040033// NewManager creates a new agent manager
34func NewManager(cfg *config.Config, taskManager tm.TaskManager) (*Manager, error) {
user5a7d60d2025-07-27 21:22:04 +040035 // Create auto-assigner
36 autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
37
38 // Create GitHub PR provider
39 githubConfig := git.GitHubConfig{
40 Token: cfg.GitHub.Token,
41 }
42 prProvider := git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
43
44 // Create clone manager for per-agent Git repositories
45 repoURL := fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
46 workspacePath := filepath.Join(".", "workspace")
47 cloneManager := git.NewCloneManager(repoURL, workspacePath)
48
iomodo50598c62025-07-27 22:06:32 +040049 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040050 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040051 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040052 taskManager: taskManager,
53 autoAssigner: autoAssigner,
54 prProvider: prProvider,
55 cloneManager: cloneManager,
56 isRunning: make(map[string]bool),
57 stopChannels: make(map[string]chan struct{}),
58 }
59
60 // Initialize agents
61 if err := manager.initializeAgents(); err != nil {
62 return nil, fmt.Errorf("failed to initialize agents: %w", err)
63 }
64
65 return manager, nil
66}
67
68// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +040069func (m *Manager) initializeAgents() error {
70 for _, agentConfig := range m.config.Agents {
71 agent, err := m.createAgent(agentConfig)
user5a7d60d2025-07-27 21:22:04 +040072 if err != nil {
73 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
74 }
iomodo50598c62025-07-27 22:06:32 +040075 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +040076 }
77 return nil
78}
79
80// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +040081func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +040082 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +040083 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +040084 if err != nil {
85 return nil, fmt.Errorf("failed to load system prompt: %w", err)
86 }
87
88 // Create LLM provider
89 llmConfig := llm.Config{
90 Provider: llm.ProviderOpenAI,
iomodo50598c62025-07-27 22:06:32 +040091 APIKey: m.config.OpenAI.APIKey,
92 BaseURL: m.config.OpenAI.BaseURL,
93 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +040094 }
iomodo50598c62025-07-27 22:06:32 +040095
user5a7d60d2025-07-27 21:22:04 +040096 provider, err := llm.CreateProvider(llmConfig)
97 if err != nil {
98 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
99 }
100
iomodo50598c62025-07-27 22:06:32 +0400101 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400102 Name: agentConfig.Name,
103 Role: agentConfig.Role,
104 Model: agentConfig.Model,
105 SystemPrompt: systemPrompt,
106 Provider: provider,
107 MaxTokens: agentConfig.MaxTokens,
108 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400109 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400110 }
111
112 return agent, nil
113}
114
115// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400116func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400117 content, err := os.ReadFile(filePath)
118 if err != nil {
119 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
120 }
121 return string(content), nil
122}
123
124// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400125func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
126 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400127 if !exists {
128 return fmt.Errorf("agent %s not found", agentName)
129 }
130
iomodo50598c62025-07-27 22:06:32 +0400131 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400132 return fmt.Errorf("agent %s is already running", agentName)
133 }
134
135 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400136 m.stopChannels[agentName] = stopChan
137 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400138
iomodo50598c62025-07-27 22:06:32 +0400139 go m.runAgentLoop(agent, loopInterval, stopChan)
140
user5a7d60d2025-07-27 21:22:04 +0400141 log.Printf("Started agent %s (%s) with %s model", agentName, agent.Role, agent.Model)
142 return nil
143}
144
145// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400146func (m *Manager) StopAgent(agentName string) error {
147 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400148 return fmt.Errorf("agent %s is not running", agentName)
149 }
150
iomodo50598c62025-07-27 22:06:32 +0400151 close(m.stopChannels[agentName])
152 delete(m.stopChannels, agentName)
153 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400154
155 log.Printf("Stopped agent %s", agentName)
156 return nil
157}
158
159// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400160func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400161 ticker := time.NewTicker(interval)
162 defer ticker.Stop()
163
164 for {
165 select {
166 case <-stopChan:
167 log.Printf("Agent %s stopping", agent.Name)
168 return
169 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400170 if err := m.processAgentTasks(agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400171 log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
172 }
173 }
174 }
175}
176
177// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400178func (m *Manager) processAgentTasks(agent *Agent) error {
179 if agent.CurrentTask != nil {
180 return nil
181 }
182
user5a7d60d2025-07-27 21:22:04 +0400183 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400184 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400185 if err != nil {
186 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
187 }
188
iomodo50598c62025-07-27 22:06:32 +0400189 log.Printf("Processing %d tasks for agent %s", len(tasks), agent.Name)
190
user5a7d60d2025-07-27 21:22:04 +0400191 for _, task := range tasks {
iomodo50598c62025-07-27 22:06:32 +0400192 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
193 if err := m.processTask(agent, task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400194 log.Printf("Error processing task %s: %v", task.ID, err)
195 // Mark task as failed
196 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400197 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400198 log.Printf("Error updating failed task %s: %v", task.ID, err)
199 }
iomodo50598c62025-07-27 22:06:32 +0400200 agent.Stats.TasksFailed++
201 } else {
202 agent.Stats.TasksCompleted++
203 }
204 // Update success rate
205 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
206 if total > 0 {
207 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400208 }
209 }
210 }
211
212 return nil
213}
214
215// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400216func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400217 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400218 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400219
220 log.Printf("Agent %s processing task %s: %s", agent.Name, task.ID, task.Title)
221
222 // Mark task as in progress
223 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400224 agent.CurrentTask = &task.ID
225 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400226 return fmt.Errorf("failed to update task status: %w", err)
227 }
228
229 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400230 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400231 if err != nil {
232 return fmt.Errorf("failed to generate solution: %w", err)
233 }
234
235 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400236 branchName := m.generateBranchName(task)
237 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400238 return fmt.Errorf("failed to commit solution: %w", err)
239 }
240
241 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400242 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400243 if err != nil {
244 return fmt.Errorf("failed to create pull request: %w", err)
245 }
246
247 // Update task as completed
248 task.Status = tm.StatusCompleted
249 task.Solution = solution
250 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400251 completedAt := time.Now()
252 task.CompletedAt = &completedAt
253 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400254
iomodo50598c62025-07-27 22:06:32 +0400255 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400256 return fmt.Errorf("failed to update completed task: %w", err)
257 }
258
iomodo50598c62025-07-27 22:06:32 +0400259 // Update agent stats
260 duration := time.Since(startTime)
261 if agent.Stats.AvgTime == 0 {
262 agent.Stats.AvgTime = duration.Milliseconds()
263 } else {
264 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
265 }
266
267 log.Printf("Task %s completed by agent %s in %v. PR: %s", task.ID, agent.Name, duration, prURL)
user5a7d60d2025-07-27 21:22:04 +0400268 return nil
269}
270
271// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400272func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
273 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400274
275 req := llm.ChatCompletionRequest{
276 Model: agent.Model,
277 Messages: []llm.Message{
278 {
279 Role: llm.RoleSystem,
280 Content: agent.SystemPrompt,
281 },
282 {
283 Role: llm.RoleUser,
284 Content: prompt,
285 },
286 },
287 MaxTokens: agent.MaxTokens,
288 Temperature: agent.Temperature,
289 }
290
291 resp, err := agent.Provider.ChatCompletion(ctx, req)
292 if err != nil {
293 return "", fmt.Errorf("LLM request failed: %w", err)
294 }
295
296 if len(resp.Choices) == 0 {
297 return "", fmt.Errorf("no response from LLM")
298 }
299
300 return resp.Choices[0].Message.Content, nil
301}
302
303// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400304func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400305 return fmt.Sprintf(`Task: %s
306
307Priority: %s
308Description: %s
309
310Please provide a complete solution for this task. Include:
3111. Detailed implementation plan
3122. Code changes needed (if applicable)
3133. Files to be created or modified
3144. Testing considerations
3155. Any dependencies or prerequisites
316
317Your response should be comprehensive and actionable.`,
318 task.Title,
319 task.Priority,
320 task.Description)
321}
322
323// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400324func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400325 // Clean title for use in branch name
326 cleanTitle := strings.ToLower(task.Title)
327 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
328 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
329 // Remove special characters
330 var result strings.Builder
331 for _, r := range cleanTitle {
332 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
333 result.WriteRune(r)
334 }
335 }
336 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400337
user5a7d60d2025-07-27 21:22:04 +0400338 // Limit length
339 if len(cleanTitle) > 40 {
340 cleanTitle = cleanTitle[:40]
341 }
iomodo50598c62025-07-27 22:06:32 +0400342
343 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400344}
345
346// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400347func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400348 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400349
user5a7d60d2025-07-27 21:22:04 +0400350 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400351 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400352 if err != nil {
353 return fmt.Errorf("failed to get agent clone: %w", err)
354 }
iomodo50598c62025-07-27 22:06:32 +0400355
user5a7d60d2025-07-27 21:22:04 +0400356 log.Printf("Agent %s working in clone: %s", agent.Name, clonePath)
357
358 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400359 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400360 log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
361 }
362
363 // All Git operations use the agent's clone directory
364 gitCmd := func(args ...string) *exec.Cmd {
365 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
366 }
367
368 // Ensure we're on main branch before creating new branch
369 cmd := gitCmd("checkout", "main")
370 if err := cmd.Run(); err != nil {
371 // Try master branch if main doesn't exist
372 cmd = gitCmd("checkout", "master")
373 if err := cmd.Run(); err != nil {
374 return fmt.Errorf("failed to checkout main/master branch: %w", err)
375 }
376 }
377
378 // Create branch
379 cmd = gitCmd("checkout", "-b", branchName)
380 if err := cmd.Run(); err != nil {
381 return fmt.Errorf("failed to create branch: %w", err)
382 }
383
384 // Create solution file in agent's clone
385 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
386 if err := os.MkdirAll(solutionDir, 0755); err != nil {
387 return fmt.Errorf("failed to create solution directory: %w", err)
388 }
389
390 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
391 solutionContent := fmt.Sprintf(`# Solution for Task: %s
392
393**Agent:** %s (%s)
394**Model:** %s
395**Completed:** %s
396
397## Task Description
398%s
399
400## Solution
401%s
402
403---
404*Generated by Staff AI Agent System*
405`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
406
407 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
408 return fmt.Errorf("failed to write solution file: %w", err)
409 }
410
411 // Stage files
412 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
413 cmd = gitCmd("add", relativeSolutionFile)
414 if err := cmd.Run(); err != nil {
415 return fmt.Errorf("failed to stage files: %w", err)
416 }
417
418 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400419 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400420 cmd = gitCmd("commit", "-m", commitMsg)
421 if err := cmd.Run(); err != nil {
422 return fmt.Errorf("failed to commit: %w", err)
423 }
424
425 // Push branch
426 cmd = gitCmd("push", "-u", "origin", branchName)
427 if err := cmd.Run(); err != nil {
428 return fmt.Errorf("failed to push branch: %w", err)
429 }
430
431 log.Printf("Agent %s successfully pushed branch %s", agent.Name, branchName)
432 return nil
433}
434
435// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400436func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
437 template := m.config.Git.CommitMessageTemplate
438
user5a7d60d2025-07-27 21:22:04 +0400439 replacements := map[string]string{
440 "{task_id}": task.ID,
441 "{task_title}": task.Title,
442 "{agent_name}": agent.Name,
443 "{solution}": "See solution file for details",
444 }
445
446 result := template
447 for placeholder, value := range replacements {
448 result = strings.ReplaceAll(result, placeholder, value)
449 }
450
451 return result
452}
453
454// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400455func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400456 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400457
user5a7d60d2025-07-27 21:22:04 +0400458 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400459 description := m.buildPRDescription(task, solution, agent)
460
user5a7d60d2025-07-27 21:22:04 +0400461 options := git.PullRequestOptions{
462 Title: title,
463 Description: description,
464 HeadBranch: branchName,
465 BaseBranch: "main",
466 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
467 Draft: false,
468 }
469
iomodo50598c62025-07-27 22:06:32 +0400470 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400471 if err != nil {
472 return "", fmt.Errorf("failed to create PR: %w", err)
473 }
474
iomodo50598c62025-07-27 22:06:32 +0400475 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 +0400476}
477
478// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400479func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
480 template := m.config.Git.PRTemplate
481
user5a7d60d2025-07-27 21:22:04 +0400482 // Truncate solution for PR if too long
483 truncatedSolution := solution
484 if len(solution) > 1000 {
485 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
486 }
iomodo50598c62025-07-27 22:06:32 +0400487
user5a7d60d2025-07-27 21:22:04 +0400488 replacements := map[string]string{
489 "{task_id}": task.ID,
490 "{task_title}": task.Title,
491 "{task_description}": task.Description,
492 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
493 "{priority}": string(task.Priority),
494 "{solution}": truncatedSolution,
495 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
496 }
497
498 result := template
499 for placeholder, value := range replacements {
500 result = strings.ReplaceAll(result, placeholder, value)
501 }
502
503 return result
504}
505
506// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400507func (m *Manager) AutoAssignTask(taskID string) error {
508 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400509 if err != nil {
510 return fmt.Errorf("failed to get task: %w", err)
511 }
512
iomodo50598c62025-07-27 22:06:32 +0400513 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400514 if err != nil {
515 return fmt.Errorf("failed to auto-assign task: %w", err)
516 }
517
518 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400519 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400520 return fmt.Errorf("failed to update task assignment: %w", err)
521 }
522
iomodo50598c62025-07-27 22:06:32 +0400523 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
user5a7d60d2025-07-27 21:22:04 +0400524 log.Printf("Auto-assigned task %s to %s: %s", taskID, agentName, explanation)
525
526 return nil
527}
528
529// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400530func (m *Manager) GetAgentStatus() map[string]AgentInfo {
531 status := make(map[string]AgentInfo)
532
533 for name, agent := range m.agents {
534 agentStatus := StatusIdle
535 if m.isRunning[name] {
536 if agent.CurrentTask != nil {
537 agentStatus = StatusRunning
538 }
539 } else {
540 agentStatus = StatusStopped
541 }
542
543 status[name] = AgentInfo{
544 Name: agent.Name,
545 Role: agent.Role,
546 Model: agent.Model,
547 Status: agentStatus,
548 CurrentTask: agent.CurrentTask,
549 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400550 }
551 }
iomodo50598c62025-07-27 22:06:32 +0400552
user5a7d60d2025-07-27 21:22:04 +0400553 return status
554}
555
user5a7d60d2025-07-27 21:22:04 +0400556// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400557func (m *Manager) IsAgentRunning(agentName string) bool {
558 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400559}
560
561// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400562func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400563 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400564 for agentName := range m.isRunning {
565 if m.isRunning[agentName] {
566 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400567 }
568 }
569
570 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400571 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400572 if err := agent.Provider.Close(); err != nil {
573 log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
574 }
575 }
576
577 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400578 if err := m.cloneManager.CleanupAllClones(); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400579 log.Printf("Error cleaning up agent clones: %v", err)
580 }
581
582 return nil
iomodo50598c62025-07-27 22:06:32 +0400583}