blob: c2066652cb1230a2ffff6ab6e4734a6b96879746 [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package agent
2
3import (
4 "context"
5 "fmt"
iomodo62da94a2025-07-28 19:01:55 +04006 "log/slog"
user5a7d60d2025-07-27 21:22:04 +04007 "os"
8 "os/exec"
9 "path/filepath"
10 "strings"
11 "time"
12
13 "github.com/iomodo/staff/assignment"
14 "github.com/iomodo/staff/config"
15 "github.com/iomodo/staff/git"
16 "github.com/iomodo/staff/llm"
iomodo50598c62025-07-27 22:06:32 +040017 _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
iomodod9ff8da2025-07-28 11:42:22 +040018 "github.com/iomodo/staff/subtasks"
user5a7d60d2025-07-27 21:22:04 +040019 "github.com/iomodo/staff/tm"
20)
21
iomodo50598c62025-07-27 22:06:32 +040022// Manager manages multiple AI agents with Git operations and task processing
23type Manager struct {
iomodod9ff8da2025-07-28 11:42:22 +040024 config *config.Config
25 agents map[string]*Agent
26 taskManager tm.TaskManager
27 autoAssigner *assignment.AutoAssigner
28 prProvider git.PullRequestProvider
29 cloneManager *git.CloneManager
30 subtaskService *subtasks.SubtaskService
31 isRunning map[string]bool
32 stopChannels map[string]chan struct{}
iomodo62da94a2025-07-28 19:01:55 +040033 logger *slog.Logger
user5a7d60d2025-07-27 21:22:04 +040034}
35
iomodo50598c62025-07-27 22:06:32 +040036// NewManager creates a new agent manager
iomodo62da94a2025-07-28 19:01:55 +040037func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
38 if logger == nil {
39 logger = slog.Default()
40 }
user5a7d60d2025-07-27 21:22:04 +040041 // Create auto-assigner
42 autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
43
iomodo578f5042025-07-28 20:46:02 +040044 // Create PR provider based on configuration
45 var prProvider git.PullRequestProvider
46 var repoURL string
47
48 switch cfg.GetPrimaryGitProvider() {
49 case "github":
50 githubConfig := git.GitHubConfig{
51 Token: cfg.GitHub.Token,
52 Logger: logger,
53 }
54 prProvider = git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
55 repoURL = fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
56 logger.Info("Using GitHub as pull request provider",
57 slog.String("owner", cfg.GitHub.Owner),
58 slog.String("repo", cfg.GitHub.Repo))
59 case "gerrit":
60 gerritConfig := git.GerritConfig{
61 Username: cfg.Gerrit.Username,
62 Password: cfg.Gerrit.Password,
63 BaseURL: cfg.Gerrit.BaseURL,
64 Logger: logger,
65 }
66 prProvider = git.NewGerritPullRequestProvider(cfg.Gerrit.Project, gerritConfig)
67 repoURL = fmt.Sprintf("%s/%s", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
68 logger.Info("Using Gerrit as pull request provider",
69 slog.String("base_url", cfg.Gerrit.BaseURL),
70 slog.String("project", cfg.Gerrit.Project))
71 default:
72 return nil, fmt.Errorf("no valid Git provider configured")
user5a7d60d2025-07-27 21:22:04 +040073 }
user5a7d60d2025-07-27 21:22:04 +040074
iomodo578f5042025-07-28 20:46:02 +040075 // Create clone manager for per-agent Git repositories
user5a7d60d2025-07-27 21:22:04 +040076 workspacePath := filepath.Join(".", "workspace")
77 cloneManager := git.NewCloneManager(repoURL, workspacePath)
78
iomodo50598c62025-07-27 22:06:32 +040079 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040080 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040081 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040082 taskManager: taskManager,
83 autoAssigner: autoAssigner,
84 prProvider: prProvider,
85 cloneManager: cloneManager,
86 isRunning: make(map[string]bool),
87 stopChannels: make(map[string]chan struct{}),
iomodo62da94a2025-07-28 19:01:55 +040088 logger: logger,
user5a7d60d2025-07-27 21:22:04 +040089 }
90
91 // Initialize agents
92 if err := manager.initializeAgents(); err != nil {
93 return nil, fmt.Errorf("failed to initialize agents: %w", err)
94 }
95
iomodod9ff8da2025-07-28 11:42:22 +040096 // Initialize subtask service after agents are created
97 if err := manager.initializeSubtaskService(); err != nil {
98 return nil, fmt.Errorf("failed to initialize subtask service: %w", err)
99 }
100
user5a7d60d2025-07-27 21:22:04 +0400101 return manager, nil
102}
103
104// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +0400105func (m *Manager) initializeAgents() error {
106 for _, agentConfig := range m.config.Agents {
107 agent, err := m.createAgent(agentConfig)
user5a7d60d2025-07-27 21:22:04 +0400108 if err != nil {
109 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
110 }
iomodo50598c62025-07-27 22:06:32 +0400111 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +0400112 }
113 return nil
114}
115
iomodod9ff8da2025-07-28 11:42:22 +0400116// initializeSubtaskService creates the subtask service with available agent roles
117func (m *Manager) initializeSubtaskService() error {
118 // Get agent roles from configuration
119 agentRoles := make([]string, 0, len(m.config.Agents))
120 for _, agentConfig := range m.config.Agents {
121 agentRoles = append(agentRoles, agentConfig.Name)
122 }
123
124 // Use the first agent's LLM provider for subtask analysis
125 if len(m.agents) == 0 {
126 return fmt.Errorf("no agents available for subtask service")
127 }
128
129 var firstAgent *Agent
130 for _, agent := range m.agents {
131 firstAgent = agent
132 break
133 }
134
iomodo578f5042025-07-28 20:46:02 +0400135 // Get owner and repo for subtask service based on provider
136 var owner, repo string
137 switch m.config.GetPrimaryGitProvider() {
138 case "github":
139 owner = m.config.GitHub.Owner
140 repo = m.config.GitHub.Repo
141 case "gerrit":
142 owner = m.config.Gerrit.Project
143 repo = m.config.Gerrit.Project
144 }
145
iomodod9ff8da2025-07-28 11:42:22 +0400146 m.subtaskService = subtasks.NewSubtaskService(
147 firstAgent.Provider,
148 m.taskManager,
149 agentRoles,
iomodo443b20a2025-07-28 15:24:05 +0400150 m.prProvider,
iomodo578f5042025-07-28 20:46:02 +0400151 owner,
152 repo,
iomodo443b20a2025-07-28 15:24:05 +0400153 m.cloneManager,
iomodo62da94a2025-07-28 19:01:55 +0400154 m.logger,
iomodod9ff8da2025-07-28 11:42:22 +0400155 )
156
157 return nil
158}
159
user5a7d60d2025-07-27 21:22:04 +0400160// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +0400161func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +0400162 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +0400163 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +0400164 if err != nil {
165 return nil, fmt.Errorf("failed to load system prompt: %w", err)
166 }
167
168 // Create LLM provider
169 llmConfig := llm.Config{
iomodof1ddefe2025-07-28 09:02:05 +0400170 Provider: llm.ProviderFake, // Use fake provider for testing
iomodo50598c62025-07-27 22:06:32 +0400171 APIKey: m.config.OpenAI.APIKey,
172 BaseURL: m.config.OpenAI.BaseURL,
173 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +0400174 }
iomodo50598c62025-07-27 22:06:32 +0400175
user5a7d60d2025-07-27 21:22:04 +0400176 provider, err := llm.CreateProvider(llmConfig)
177 if err != nil {
178 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
179 }
180
iomodo50598c62025-07-27 22:06:32 +0400181 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400182 Name: agentConfig.Name,
183 Role: agentConfig.Role,
184 Model: agentConfig.Model,
185 SystemPrompt: systemPrompt,
186 Provider: provider,
187 MaxTokens: agentConfig.MaxTokens,
188 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400189 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400190 }
191
192 return agent, nil
193}
194
195// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400196func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400197 content, err := os.ReadFile(filePath)
198 if err != nil {
199 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
200 }
201 return string(content), nil
202}
203
204// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400205func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
206 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400207 if !exists {
208 return fmt.Errorf("agent %s not found", agentName)
209 }
210
iomodo50598c62025-07-27 22:06:32 +0400211 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400212 return fmt.Errorf("agent %s is already running", agentName)
213 }
214
215 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400216 m.stopChannels[agentName] = stopChan
217 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400218
iomodo50598c62025-07-27 22:06:32 +0400219 go m.runAgentLoop(agent, loopInterval, stopChan)
220
iomodo62da94a2025-07-28 19:01:55 +0400221 m.logger.Info("Started agent",
222 slog.String("name", agentName),
223 slog.String("role", agent.Role),
224 slog.String("model", agent.Model))
user5a7d60d2025-07-27 21:22:04 +0400225 return nil
226}
227
228// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400229func (m *Manager) StopAgent(agentName string) error {
230 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400231 return fmt.Errorf("agent %s is not running", agentName)
232 }
233
iomodo50598c62025-07-27 22:06:32 +0400234 close(m.stopChannels[agentName])
235 delete(m.stopChannels, agentName)
236 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400237
iomodo62da94a2025-07-28 19:01:55 +0400238 m.logger.Info("Stopped agent", slog.String("name", agentName))
user5a7d60d2025-07-27 21:22:04 +0400239 return nil
240}
241
242// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400243func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400244 ticker := time.NewTicker(interval)
245 defer ticker.Stop()
246
247 for {
248 select {
249 case <-stopChan:
iomodo62da94a2025-07-28 19:01:55 +0400250 m.logger.Info("Agent stopping", slog.String("name", agent.Name))
user5a7d60d2025-07-27 21:22:04 +0400251 return
252 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400253 if err := m.processAgentTasks(agent); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400254 m.logger.Error("Error processing tasks for agent",
255 slog.String("agent", agent.Name),
256 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400257 }
258 }
259 }
260}
261
262// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400263func (m *Manager) processAgentTasks(agent *Agent) error {
264 if agent.CurrentTask != nil {
265 return nil
266 }
267
user5a7d60d2025-07-27 21:22:04 +0400268 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400269 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400270 if err != nil {
271 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
272 }
273
iomodo62da94a2025-07-28 19:01:55 +0400274 m.logger.Info("Processing tasks for agent",
275 slog.Int("task_count", len(tasks)),
276 slog.String("agent", agent.Name))
iomodo50598c62025-07-27 22:06:32 +0400277
user5a7d60d2025-07-27 21:22:04 +0400278 for _, task := range tasks {
iomodo50598c62025-07-27 22:06:32 +0400279 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
280 if err := m.processTask(agent, task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400281 m.logger.Error("Error processing task",
282 slog.String("task_id", task.ID),
283 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400284 // Mark task as failed
285 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400286 if err := m.taskManager.UpdateTask(task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400287 m.logger.Error("Error updating failed task",
288 slog.String("task_id", task.ID),
289 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400290 }
iomodo50598c62025-07-27 22:06:32 +0400291 agent.Stats.TasksFailed++
292 } else {
293 agent.Stats.TasksCompleted++
294 }
295 // Update success rate
296 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
297 if total > 0 {
298 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400299 }
300 }
301 }
302
303 return nil
304}
305
306// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400307func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400308 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400309 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400310
iomodo62da94a2025-07-28 19:01:55 +0400311 m.logger.Info("Agent processing task",
312 slog.String("agent", agent.Name),
313 slog.String("task_id", task.ID),
314 slog.String("title", task.Title))
user5a7d60d2025-07-27 21:22:04 +0400315
316 // Mark task as in progress
317 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400318 agent.CurrentTask = &task.ID
319 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400320 return fmt.Errorf("failed to update task status: %w", err)
321 }
322
iomodo5c99a442025-07-28 14:23:52 +0400323 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400324 if m.shouldGenerateSubtasks(task) {
iomodo62da94a2025-07-28 19:01:55 +0400325 m.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
iomodod9ff8da2025-07-28 11:42:22 +0400326 if err := m.generateSubtasksForTask(ctx, task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400327 m.logger.Warn("Failed to generate subtasks for task",
328 slog.String("task_id", task.ID),
329 slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400330 // Continue with normal processing if subtask generation fails
331 } else {
332 // Task has been converted to subtask management, mark as completed
333 task.Status = tm.StatusCompleted
iomodo5c99a442025-07-28 14:23:52 +0400334 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 +0400335 completedAt := time.Now()
336 task.CompletedAt = &completedAt
337 agent.CurrentTask = nil
338
339 if err := m.taskManager.UpdateTask(task); err != nil {
340 return fmt.Errorf("failed to update task with subtasks: %w", err)
341 }
342
iomodo62da94a2025-07-28 19:01:55 +0400343 m.logger.Info("Task converted to subtasks by agent using LLM analysis",
344 slog.String("task_id", task.ID),
345 slog.String("agent", agent.Name))
iomodod9ff8da2025-07-28 11:42:22 +0400346 return nil
347 }
348 }
349
user5a7d60d2025-07-27 21:22:04 +0400350 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400351 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400352 if err != nil {
353 return fmt.Errorf("failed to generate solution: %w", err)
354 }
355
356 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400357 branchName := m.generateBranchName(task)
358 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400359 return fmt.Errorf("failed to commit solution: %w", err)
360 }
361
362 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400363 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400364 if err != nil {
365 return fmt.Errorf("failed to create pull request: %w", err)
366 }
367
368 // Update task as completed
369 task.Status = tm.StatusCompleted
370 task.Solution = solution
371 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400372 completedAt := time.Now()
373 task.CompletedAt = &completedAt
374 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400375
iomodo50598c62025-07-27 22:06:32 +0400376 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400377 return fmt.Errorf("failed to update completed task: %w", err)
378 }
379
iomodo50598c62025-07-27 22:06:32 +0400380 // Update agent stats
381 duration := time.Since(startTime)
382 if agent.Stats.AvgTime == 0 {
383 agent.Stats.AvgTime = duration.Milliseconds()
384 } else {
385 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
386 }
387
iomodo62da94a2025-07-28 19:01:55 +0400388 m.logger.Info("Task completed by agent",
389 slog.String("task_id", task.ID),
390 slog.String("agent", agent.Name),
391 slog.Duration("duration", duration),
392 slog.String("pr_url", prURL))
user5a7d60d2025-07-27 21:22:04 +0400393 return nil
394}
395
396// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400397func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
398 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400399
400 req := llm.ChatCompletionRequest{
401 Model: agent.Model,
402 Messages: []llm.Message{
403 {
404 Role: llm.RoleSystem,
405 Content: agent.SystemPrompt,
406 },
407 {
408 Role: llm.RoleUser,
409 Content: prompt,
410 },
411 },
412 MaxTokens: agent.MaxTokens,
413 Temperature: agent.Temperature,
414 }
415
416 resp, err := agent.Provider.ChatCompletion(ctx, req)
417 if err != nil {
418 return "", fmt.Errorf("LLM request failed: %w", err)
419 }
420
421 if len(resp.Choices) == 0 {
422 return "", fmt.Errorf("no response from LLM")
423 }
424
425 return resp.Choices[0].Message.Content, nil
426}
427
428// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400429func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400430 return fmt.Sprintf(`Task: %s
431
432Priority: %s
433Description: %s
434
435Please provide a complete solution for this task. Include:
4361. Detailed implementation plan
4372. Code changes needed (if applicable)
4383. Files to be created or modified
4394. Testing considerations
4405. Any dependencies or prerequisites
441
442Your response should be comprehensive and actionable.`,
443 task.Title,
444 task.Priority,
445 task.Description)
446}
447
448// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400449func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400450 // Clean title for use in branch name
451 cleanTitle := strings.ToLower(task.Title)
452 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
453 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
454 // Remove special characters
455 var result strings.Builder
456 for _, r := range cleanTitle {
457 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
458 result.WriteRune(r)
459 }
460 }
461 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400462
user5a7d60d2025-07-27 21:22:04 +0400463 // Limit length
464 if len(cleanTitle) > 40 {
465 cleanTitle = cleanTitle[:40]
466 }
iomodo50598c62025-07-27 22:06:32 +0400467
468 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400469}
470
471// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400472func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400473 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400474
user5a7d60d2025-07-27 21:22:04 +0400475 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400476 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400477 if err != nil {
478 return fmt.Errorf("failed to get agent clone: %w", err)
479 }
iomodo50598c62025-07-27 22:06:32 +0400480
iomodo62da94a2025-07-28 19:01:55 +0400481 m.logger.Info("Agent working in clone",
482 slog.String("agent", agent.Name),
483 slog.String("clone_path", clonePath))
user5a7d60d2025-07-27 21:22:04 +0400484
485 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400486 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400487 m.logger.Warn("Failed to refresh clone for agent",
488 slog.String("agent", agent.Name),
489 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400490 }
491
492 // All Git operations use the agent's clone directory
493 gitCmd := func(args ...string) *exec.Cmd {
494 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
495 }
496
497 // Ensure we're on main branch before creating new branch
498 cmd := gitCmd("checkout", "main")
499 if err := cmd.Run(); err != nil {
500 // Try master branch if main doesn't exist
501 cmd = gitCmd("checkout", "master")
502 if err := cmd.Run(); err != nil {
503 return fmt.Errorf("failed to checkout main/master branch: %w", err)
504 }
505 }
506
507 // Create branch
508 cmd = gitCmd("checkout", "-b", branchName)
509 if err := cmd.Run(); err != nil {
510 return fmt.Errorf("failed to create branch: %w", err)
511 }
512
513 // Create solution file in agent's clone
514 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
515 if err := os.MkdirAll(solutionDir, 0755); err != nil {
516 return fmt.Errorf("failed to create solution directory: %w", err)
517 }
518
519 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
520 solutionContent := fmt.Sprintf(`# Solution for Task: %s
521
522**Agent:** %s (%s)
523**Model:** %s
524**Completed:** %s
525
526## Task Description
527%s
528
529## Solution
530%s
531
532---
533*Generated by Staff AI Agent System*
534`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
535
536 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
537 return fmt.Errorf("failed to write solution file: %w", err)
538 }
539
540 // Stage files
541 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
542 cmd = gitCmd("add", relativeSolutionFile)
543 if err := cmd.Run(); err != nil {
544 return fmt.Errorf("failed to stage files: %w", err)
545 }
546
547 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400548 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400549 cmd = gitCmd("commit", "-m", commitMsg)
550 if err := cmd.Run(); err != nil {
551 return fmt.Errorf("failed to commit: %w", err)
552 }
553
554 // Push branch
555 cmd = gitCmd("push", "-u", "origin", branchName)
556 if err := cmd.Run(); err != nil {
557 return fmt.Errorf("failed to push branch: %w", err)
558 }
559
iomodo62da94a2025-07-28 19:01:55 +0400560 m.logger.Info("Agent successfully pushed branch",
561 slog.String("agent", agent.Name),
562 slog.String("branch", branchName))
user5a7d60d2025-07-27 21:22:04 +0400563 return nil
564}
565
566// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400567func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
568 template := m.config.Git.CommitMessageTemplate
569
user5a7d60d2025-07-27 21:22:04 +0400570 replacements := map[string]string{
571 "{task_id}": task.ID,
572 "{task_title}": task.Title,
573 "{agent_name}": agent.Name,
574 "{solution}": "See solution file for details",
575 }
576
577 result := template
578 for placeholder, value := range replacements {
579 result = strings.ReplaceAll(result, placeholder, value)
580 }
581
582 return result
583}
584
585// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400586func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400587 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400588
user5a7d60d2025-07-27 21:22:04 +0400589 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400590 description := m.buildPRDescription(task, solution, agent)
591
user5a7d60d2025-07-27 21:22:04 +0400592 options := git.PullRequestOptions{
593 Title: title,
594 Description: description,
595 HeadBranch: branchName,
596 BaseBranch: "main",
597 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
598 Draft: false,
599 }
600
iomodo50598c62025-07-27 22:06:32 +0400601 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400602 if err != nil {
603 return "", fmt.Errorf("failed to create PR: %w", err)
604 }
605
iomodo578f5042025-07-28 20:46:02 +0400606 // Generate provider-specific PR URL
607 switch m.config.GetPrimaryGitProvider() {
608 case "github":
609 return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
610 case "gerrit":
611 return fmt.Sprintf("%s/c/%s/+/%d", m.config.Gerrit.BaseURL, m.config.Gerrit.Project, pr.Number), nil
612 default:
613 return "", fmt.Errorf("unknown git provider")
614 }
user5a7d60d2025-07-27 21:22:04 +0400615}
616
617// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400618func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
619 template := m.config.Git.PRTemplate
620
user5a7d60d2025-07-27 21:22:04 +0400621 // Truncate solution for PR if too long
622 truncatedSolution := solution
623 if len(solution) > 1000 {
624 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
625 }
iomodo50598c62025-07-27 22:06:32 +0400626
user5a7d60d2025-07-27 21:22:04 +0400627 replacements := map[string]string{
628 "{task_id}": task.ID,
629 "{task_title}": task.Title,
630 "{task_description}": task.Description,
631 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
632 "{priority}": string(task.Priority),
633 "{solution}": truncatedSolution,
634 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
635 }
636
637 result := template
638 for placeholder, value := range replacements {
639 result = strings.ReplaceAll(result, placeholder, value)
640 }
641
642 return result
643}
644
645// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400646func (m *Manager) AutoAssignTask(taskID string) error {
647 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400648 if err != nil {
649 return fmt.Errorf("failed to get task: %w", err)
650 }
651
iomodo50598c62025-07-27 22:06:32 +0400652 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400653 if err != nil {
654 return fmt.Errorf("failed to auto-assign task: %w", err)
655 }
656
657 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400658 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400659 return fmt.Errorf("failed to update task assignment: %w", err)
660 }
661
iomodo50598c62025-07-27 22:06:32 +0400662 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomodo62da94a2025-07-28 19:01:55 +0400663 m.logger.Info("Auto-assigned task to agent",
664 slog.String("task_id", taskID),
665 slog.String("agent", agentName),
666 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400667
668 return nil
669}
670
671// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400672func (m *Manager) GetAgentStatus() map[string]AgentInfo {
673 status := make(map[string]AgentInfo)
674
675 for name, agent := range m.agents {
676 agentStatus := StatusIdle
677 if m.isRunning[name] {
678 if agent.CurrentTask != nil {
679 agentStatus = StatusRunning
680 }
681 } else {
682 agentStatus = StatusStopped
683 }
684
685 status[name] = AgentInfo{
686 Name: agent.Name,
687 Role: agent.Role,
688 Model: agent.Model,
689 Status: agentStatus,
690 CurrentTask: agent.CurrentTask,
691 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400692 }
693 }
iomodo50598c62025-07-27 22:06:32 +0400694
user5a7d60d2025-07-27 21:22:04 +0400695 return status
696}
697
iomodo5c99a442025-07-28 14:23:52 +0400698// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400699func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
700 // Don't generate subtasks for subtasks
701 if task.ParentTaskID != "" {
702 return false
703 }
704
iomodo5c99a442025-07-28 14:23:52 +0400705 // Don't generate if already evaluated
706 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400707 return false
708 }
709
iomodo5c99a442025-07-28 14:23:52 +0400710 // Ask LLM to decide
711 ctx := context.Background()
712 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
713 if err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400714 m.logger.Warn("Failed to get LLM subtask decision for task",
715 slog.String("task_id", task.ID),
716 slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400717 // Fallback to simple heuristics
718 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400719 }
720
iomodo5c99a442025-07-28 14:23:52 +0400721 // Update task to mark as evaluated
722 task.SubtasksEvaluated = true
723 if err := m.taskManager.UpdateTask(task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400724 m.logger.Warn("Failed to update task evaluation status", slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400725 }
726
iomodo62da94a2025-07-28 19:01:55 +0400727 m.logger.Info("LLM subtask decision for task",
728 slog.String("task_id", task.ID),
729 slog.Bool("needs_subtasks", decision.NeedsSubtasks),
730 slog.Int("complexity_score", decision.ComplexityScore),
731 slog.String("reasoning", decision.Reasoning))
iomodo5c99a442025-07-28 14:23:52 +0400732
733 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400734}
735
736// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
737func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
738 if m.subtaskService == nil {
739 return fmt.Errorf("subtask service not initialized")
740 }
741
742 // Analyze the task for subtasks
743 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
744 if err != nil {
745 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
746 }
747
748 // Generate a PR with the subtask proposals
749 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
750 if err != nil {
751 return fmt.Errorf("failed to generate subtask PR: %w", err)
752 }
753
754 // Update the task with subtask information
755 task.SubtasksPRURL = prURL
756 task.SubtasksGenerated = true
757
iomodo62da94a2025-07-28 19:01:55 +0400758 m.logger.Info("Generated subtask PR for task",
759 slog.String("task_id", task.ID),
760 slog.String("pr_url", prURL))
761 m.logger.Info("Proposed subtasks and new agents for task",
762 slog.String("task_id", task.ID),
763 slog.Int("subtask_count", len(analysis.Subtasks)),
764 slog.Int("new_agent_count", len(analysis.AgentCreations)))
iomodo5c99a442025-07-28 14:23:52 +0400765
766 // Log proposed new agents if any
767 if len(analysis.AgentCreations) > 0 {
768 for _, agent := range analysis.AgentCreations {
iomodo62da94a2025-07-28 19:01:55 +0400769 m.logger.Info("Proposed new agent",
770 slog.String("role", agent.Role),
771 slog.Any("skills", agent.Skills))
iomodo5c99a442025-07-28 14:23:52 +0400772 }
773 }
iomodod9ff8da2025-07-28 11:42:22 +0400774
775 return nil
776}
777
user5a7d60d2025-07-27 21:22:04 +0400778// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400779func (m *Manager) IsAgentRunning(agentName string) bool {
780 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400781}
782
783// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400784func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400785 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400786 for agentName := range m.isRunning {
787 if m.isRunning[agentName] {
788 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400789 }
790 }
791
792 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400793 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400794 if err := agent.Provider.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400795 m.logger.Error("Error closing provider for agent",
796 slog.String("agent", agent.Name),
797 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400798 }
799 }
800
801 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400802 if err := m.cloneManager.CleanupAllClones(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400803 m.logger.Error("Error cleaning up agent clones", slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400804 }
805
iomodod9ff8da2025-07-28 11:42:22 +0400806 // Cleanup subtask service
807 if m.subtaskService != nil {
808 if err := m.subtaskService.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400809 m.logger.Error("Error closing subtask service", slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400810 }
811 }
812
user5a7d60d2025-07-27 21:22:04 +0400813 return nil
iomodo50598c62025-07-27 22:06:32 +0400814}