blob: 191d42e16da93f8d8f11f784f4e95abc132c15a7 [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
user5a7d60d2025-07-27 21:22:04 +040013 "github.com/iomodo/staff/config"
14 "github.com/iomodo/staff/git"
15 "github.com/iomodo/staff/llm"
iomodo50598c62025-07-27 22:06:32 +040016 _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
iomododea44b02025-07-29 12:55:25 +040017 "github.com/iomodo/staff/task"
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 {
iomododea44b02025-07-29 12:55:25 +040023 config *config.Config
24 agents map[string]*Agent
25 taskManager tm.TaskManager
26 autoAssigner *task.AutoAssigner
27 prProvider git.PullRequestProvider
28 cloneManager *git.CloneManager
29 subtaskService *task.SubtaskService
30 isRunning map[string]bool
31 stopChannels map[string]chan struct{}
32 logger *slog.Logger
user5a7d60d2025-07-27 21:22:04 +040033}
34
iomodo50598c62025-07-27 22:06:32 +040035// NewManager creates a new agent manager
iomodo62da94a2025-07-28 19:01:55 +040036func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
37 if logger == nil {
38 logger = slog.Default()
39 }
user5a7d60d2025-07-27 21:22:04 +040040 // Create auto-assigner
iomododea44b02025-07-29 12:55:25 +040041 autoAssigner := task.NewAutoAssigner(cfg.Agents)
user5a7d60d2025-07-27 21:22:04 +040042
iomodo578f5042025-07-28 20:46:02 +040043 // Create PR provider based on configuration
44 var prProvider git.PullRequestProvider
45 var repoURL string
iomododea44b02025-07-29 12:55:25 +040046
iomodo578f5042025-07-28 20:46:02 +040047 switch cfg.GetPrimaryGitProvider() {
48 case "github":
49 githubConfig := git.GitHubConfig{
50 Token: cfg.GitHub.Token,
51 Logger: logger,
52 }
53 prProvider = git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
54 repoURL = fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
iomododea44b02025-07-29 12:55:25 +040055 logger.Info("Using GitHub as pull request provider",
56 slog.String("owner", cfg.GitHub.Owner),
iomodo578f5042025-07-28 20:46:02 +040057 slog.String("repo", cfg.GitHub.Repo))
58 case "gerrit":
59 gerritConfig := git.GerritConfig{
60 Username: cfg.Gerrit.Username,
61 Password: cfg.Gerrit.Password,
62 BaseURL: cfg.Gerrit.BaseURL,
63 Logger: logger,
64 }
65 prProvider = git.NewGerritPullRequestProvider(cfg.Gerrit.Project, gerritConfig)
66 repoURL = fmt.Sprintf("%s/%s", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
iomododea44b02025-07-29 12:55:25 +040067 logger.Info("Using Gerrit as pull request provider",
68 slog.String("base_url", cfg.Gerrit.BaseURL),
iomodo578f5042025-07-28 20:46:02 +040069 slog.String("project", cfg.Gerrit.Project))
70 default:
71 return nil, fmt.Errorf("no valid Git provider configured")
user5a7d60d2025-07-27 21:22:04 +040072 }
user5a7d60d2025-07-27 21:22:04 +040073
iomododea44b02025-07-29 12:55:25 +040074 // Create clone manager for per-agent Git repositories
user5a7d60d2025-07-27 21:22:04 +040075 workspacePath := filepath.Join(".", "workspace")
76 cloneManager := git.NewCloneManager(repoURL, workspacePath)
77
iomodo50598c62025-07-27 22:06:32 +040078 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040079 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040080 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040081 taskManager: taskManager,
82 autoAssigner: autoAssigner,
83 prProvider: prProvider,
84 cloneManager: cloneManager,
85 isRunning: make(map[string]bool),
86 stopChannels: make(map[string]chan struct{}),
iomodo62da94a2025-07-28 19:01:55 +040087 logger: logger,
user5a7d60d2025-07-27 21:22:04 +040088 }
89
90 // Initialize agents
91 if err := manager.initializeAgents(); err != nil {
92 return nil, fmt.Errorf("failed to initialize agents: %w", err)
93 }
94
iomodod9ff8da2025-07-28 11:42:22 +040095 // Initialize subtask service after agents are created
96 if err := manager.initializeSubtaskService(); err != nil {
97 return nil, fmt.Errorf("failed to initialize subtask service: %w", err)
98 }
99
user5a7d60d2025-07-27 21:22:04 +0400100 return manager, nil
101}
102
103// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +0400104func (m *Manager) initializeAgents() error {
105 for _, agentConfig := range m.config.Agents {
106 agent, err := m.createAgent(agentConfig)
user5a7d60d2025-07-27 21:22:04 +0400107 if err != nil {
108 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
109 }
iomodo50598c62025-07-27 22:06:32 +0400110 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +0400111 }
112 return nil
113}
114
iomodod9ff8da2025-07-28 11:42:22 +0400115// initializeSubtaskService creates the subtask service with available agent roles
116func (m *Manager) initializeSubtaskService() error {
117 // Get agent roles from configuration
118 agentRoles := make([]string, 0, len(m.config.Agents))
119 for _, agentConfig := range m.config.Agents {
120 agentRoles = append(agentRoles, agentConfig.Name)
121 }
122
123 // Use the first agent's LLM provider for subtask analysis
124 if len(m.agents) == 0 {
125 return fmt.Errorf("no agents available for subtask service")
126 }
127
128 var firstAgent *Agent
129 for _, agent := range m.agents {
130 firstAgent = agent
131 break
132 }
133
iomodo578f5042025-07-28 20:46:02 +0400134 // Get owner and repo for subtask service based on provider
135 var owner, repo string
136 switch m.config.GetPrimaryGitProvider() {
137 case "github":
138 owner = m.config.GitHub.Owner
139 repo = m.config.GitHub.Repo
140 case "gerrit":
141 owner = m.config.Gerrit.Project
142 repo = m.config.Gerrit.Project
143 }
iomododea44b02025-07-29 12:55:25 +0400144
145 m.subtaskService = task.NewSubtaskService(
iomodod9ff8da2025-07-28 11:42:22 +0400146 firstAgent.Provider,
147 m.taskManager,
148 agentRoles,
iomodo443b20a2025-07-28 15:24:05 +0400149 m.prProvider,
iomodo578f5042025-07-28 20:46:02 +0400150 owner,
151 repo,
iomodo443b20a2025-07-28 15:24:05 +0400152 m.cloneManager,
iomodo62da94a2025-07-28 19:01:55 +0400153 m.logger,
iomodod9ff8da2025-07-28 11:42:22 +0400154 )
155
156 return nil
157}
158
user5a7d60d2025-07-27 21:22:04 +0400159// createAgent creates a single agent instance
iomodo50598c62025-07-27 22:06:32 +0400160func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
user5a7d60d2025-07-27 21:22:04 +0400161 // Load system prompt
iomodo50598c62025-07-27 22:06:32 +0400162 systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
user5a7d60d2025-07-27 21:22:04 +0400163 if err != nil {
164 return nil, fmt.Errorf("failed to load system prompt: %w", err)
165 }
166
167 // Create LLM provider
168 llmConfig := llm.Config{
iomodof1ddefe2025-07-28 09:02:05 +0400169 Provider: llm.ProviderFake, // Use fake provider for testing
iomodo50598c62025-07-27 22:06:32 +0400170 APIKey: m.config.OpenAI.APIKey,
171 BaseURL: m.config.OpenAI.BaseURL,
172 Timeout: m.config.OpenAI.Timeout,
user5a7d60d2025-07-27 21:22:04 +0400173 }
iomodo50598c62025-07-27 22:06:32 +0400174
user5a7d60d2025-07-27 21:22:04 +0400175 provider, err := llm.CreateProvider(llmConfig)
176 if err != nil {
177 return nil, fmt.Errorf("failed to create LLM provider: %w", err)
178 }
179
iomodo50598c62025-07-27 22:06:32 +0400180 agent := &Agent{
user5a7d60d2025-07-27 21:22:04 +0400181 Name: agentConfig.Name,
182 Role: agentConfig.Role,
183 Model: agentConfig.Model,
184 SystemPrompt: systemPrompt,
185 Provider: provider,
186 MaxTokens: agentConfig.MaxTokens,
187 Temperature: agentConfig.Temperature,
iomodo50598c62025-07-27 22:06:32 +0400188 Stats: AgentStats{},
user5a7d60d2025-07-27 21:22:04 +0400189 }
190
191 return agent, nil
192}
193
194// loadSystemPrompt loads the system prompt from file
iomodo50598c62025-07-27 22:06:32 +0400195func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400196 content, err := os.ReadFile(filePath)
197 if err != nil {
198 return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
199 }
200 return string(content), nil
201}
202
203// StartAgent starts an agent to process tasks in a loop
iomodo50598c62025-07-27 22:06:32 +0400204func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
205 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +0400206 if !exists {
207 return fmt.Errorf("agent %s not found", agentName)
208 }
209
iomodo50598c62025-07-27 22:06:32 +0400210 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400211 return fmt.Errorf("agent %s is already running", agentName)
212 }
213
214 stopChan := make(chan struct{})
iomodo50598c62025-07-27 22:06:32 +0400215 m.stopChannels[agentName] = stopChan
216 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400217
iomodo50598c62025-07-27 22:06:32 +0400218 go m.runAgentLoop(agent, loopInterval, stopChan)
219
iomododea44b02025-07-29 12:55:25 +0400220 m.logger.Info("Started agent",
221 slog.String("name", agentName),
222 slog.String("role", agent.Role),
iomodo62da94a2025-07-28 19:01:55 +0400223 slog.String("model", agent.Model))
user5a7d60d2025-07-27 21:22:04 +0400224 return nil
225}
226
227// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400228func (m *Manager) StopAgent(agentName string) error {
229 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400230 return fmt.Errorf("agent %s is not running", agentName)
231 }
232
iomodo50598c62025-07-27 22:06:32 +0400233 close(m.stopChannels[agentName])
234 delete(m.stopChannels, agentName)
235 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400236
iomodo62da94a2025-07-28 19:01:55 +0400237 m.logger.Info("Stopped agent", slog.String("name", agentName))
user5a7d60d2025-07-27 21:22:04 +0400238 return nil
239}
240
241// runAgentLoop runs the main processing loop for an agent
iomodo50598c62025-07-27 22:06:32 +0400242func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
user5a7d60d2025-07-27 21:22:04 +0400243 ticker := time.NewTicker(interval)
244 defer ticker.Stop()
245
246 for {
247 select {
248 case <-stopChan:
iomodo62da94a2025-07-28 19:01:55 +0400249 m.logger.Info("Agent stopping", slog.String("name", agent.Name))
user5a7d60d2025-07-27 21:22:04 +0400250 return
251 case <-ticker.C:
iomodo50598c62025-07-27 22:06:32 +0400252 if err := m.processAgentTasks(agent); err != nil {
iomododea44b02025-07-29 12:55:25 +0400253 m.logger.Error("Error processing tasks for agent",
254 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400255 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400256 }
257 }
258 }
259}
260
261// processAgentTasks processes all assigned tasks for an agent
iomodo50598c62025-07-27 22:06:32 +0400262func (m *Manager) processAgentTasks(agent *Agent) error {
263 if agent.CurrentTask != nil {
264 return nil
265 }
266
user5a7d60d2025-07-27 21:22:04 +0400267 // Get tasks assigned to this agent
iomodo50598c62025-07-27 22:06:32 +0400268 tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400269 if err != nil {
270 return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
271 }
272
iomododea44b02025-07-29 12:55:25 +0400273 m.logger.Info("Processing tasks for agent",
274 slog.Int("task_count", len(tasks)),
iomodo62da94a2025-07-28 19:01:55 +0400275 slog.String("agent", agent.Name))
iomodo50598c62025-07-27 22:06:32 +0400276
user5a7d60d2025-07-27 21:22:04 +0400277 for _, task := range tasks {
iomodod5796042025-07-29 17:04:51 +0400278 if task.Status == tm.StatusToDo {
iomodo50598c62025-07-27 22:06:32 +0400279 if err := m.processTask(agent, task); err != nil {
iomododea44b02025-07-29 12:55:25 +0400280 m.logger.Error("Error processing task",
281 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400282 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400283 // Mark task as failed
284 task.Status = tm.StatusFailed
iomodo50598c62025-07-27 22:06:32 +0400285 if err := m.taskManager.UpdateTask(task); err != nil {
iomododea44b02025-07-29 12:55:25 +0400286 m.logger.Error("Error updating failed task",
287 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400288 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400289 }
iomodo50598c62025-07-27 22:06:32 +0400290 agent.Stats.TasksFailed++
291 } else {
292 agent.Stats.TasksCompleted++
293 }
294 // Update success rate
295 total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
296 if total > 0 {
297 agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
user5a7d60d2025-07-27 21:22:04 +0400298 }
299 }
300 }
301
302 return nil
303}
304
305// processTask processes a single task with an agent
iomodo50598c62025-07-27 22:06:32 +0400306func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
user5a7d60d2025-07-27 21:22:04 +0400307 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400308 startTime := time.Now()
user5a7d60d2025-07-27 21:22:04 +0400309
iomododea44b02025-07-29 12:55:25 +0400310 m.logger.Info("Agent processing task",
311 slog.String("agent", agent.Name),
312 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400313 slog.String("title", task.Title))
user5a7d60d2025-07-27 21:22:04 +0400314
315 // Mark task as in progress
316 task.Status = tm.StatusInProgress
iomodo50598c62025-07-27 22:06:32 +0400317 agent.CurrentTask = &task.ID
user5a7d60d2025-07-27 21:22:04 +0400318
iomodo5c99a442025-07-28 14:23:52 +0400319 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400320 if m.shouldGenerateSubtasks(task) {
iomodo62da94a2025-07-28 19:01:55 +0400321 m.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
iomodod9ff8da2025-07-28 11:42:22 +0400322 if err := m.generateSubtasksForTask(ctx, task); err != nil {
iomododea44b02025-07-29 12:55:25 +0400323 m.logger.Warn("Failed to generate subtasks for task",
324 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400325 slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400326 } else {
iomododea44b02025-07-29 12:55:25 +0400327 m.logger.Info("Task converted to subtasks by agent using LLM analysis",
328 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400329 slog.String("agent", agent.Name))
iomodod9ff8da2025-07-28 11:42:22 +0400330 return nil
331 }
332 }
333
user5a7d60d2025-07-27 21:22:04 +0400334 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400335 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400336 if err != nil {
337 return fmt.Errorf("failed to generate solution: %w", err)
338 }
339
340 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400341 branchName := m.generateBranchName(task)
342 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400343 return fmt.Errorf("failed to commit solution: %w", err)
344 }
345
346 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400347 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400348 if err != nil {
349 return fmt.Errorf("failed to create pull request: %w", err)
350 }
351
iomodo50598c62025-07-27 22:06:32 +0400352 // Update agent stats
353 duration := time.Since(startTime)
354 if agent.Stats.AvgTime == 0 {
355 agent.Stats.AvgTime = duration.Milliseconds()
356 } else {
357 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
358 }
359
iomododea44b02025-07-29 12:55:25 +0400360 m.logger.Info("Task completed by agent",
361 slog.String("task_id", task.ID),
362 slog.String("agent", agent.Name),
363 slog.Duration("duration", duration),
iomodo62da94a2025-07-28 19:01:55 +0400364 slog.String("pr_url", prURL))
user5a7d60d2025-07-27 21:22:04 +0400365 return nil
366}
367
368// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400369func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
370 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400371
372 req := llm.ChatCompletionRequest{
373 Model: agent.Model,
374 Messages: []llm.Message{
375 {
376 Role: llm.RoleSystem,
377 Content: agent.SystemPrompt,
378 },
379 {
380 Role: llm.RoleUser,
381 Content: prompt,
382 },
383 },
384 MaxTokens: agent.MaxTokens,
385 Temperature: agent.Temperature,
386 }
387
388 resp, err := agent.Provider.ChatCompletion(ctx, req)
389 if err != nil {
390 return "", fmt.Errorf("LLM request failed: %w", err)
391 }
392
393 if len(resp.Choices) == 0 {
394 return "", fmt.Errorf("no response from LLM")
395 }
396
397 return resp.Choices[0].Message.Content, nil
398}
399
400// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400401func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400402 return fmt.Sprintf(`Task: %s
403
404Priority: %s
405Description: %s
406
407Please provide a complete solution for this task. Include:
4081. Detailed implementation plan
4092. Code changes needed (if applicable)
4103. Files to be created or modified
4114. Testing considerations
4125. Any dependencies or prerequisites
413
414Your response should be comprehensive and actionable.`,
415 task.Title,
416 task.Priority,
417 task.Description)
418}
419
420// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400421func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400422 // Clean title for use in branch name
423 cleanTitle := strings.ToLower(task.Title)
424 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
425 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
426 // Remove special characters
427 var result strings.Builder
428 for _, r := range cleanTitle {
429 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
430 result.WriteRune(r)
431 }
432 }
433 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400434
user5a7d60d2025-07-27 21:22:04 +0400435 // Limit length
436 if len(cleanTitle) > 40 {
437 cleanTitle = cleanTitle[:40]
438 }
iomodo50598c62025-07-27 22:06:32 +0400439
440 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400441}
442
443// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400444func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400445 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400446
user5a7d60d2025-07-27 21:22:04 +0400447 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400448 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400449 if err != nil {
450 return fmt.Errorf("failed to get agent clone: %w", err)
451 }
iomodo50598c62025-07-27 22:06:32 +0400452
iomododea44b02025-07-29 12:55:25 +0400453 m.logger.Info("Agent working in clone",
454 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400455 slog.String("clone_path", clonePath))
user5a7d60d2025-07-27 21:22:04 +0400456
457 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400458 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
iomododea44b02025-07-29 12:55:25 +0400459 m.logger.Warn("Failed to refresh clone for agent",
460 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400461 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400462 }
463
464 // All Git operations use the agent's clone directory
465 gitCmd := func(args ...string) *exec.Cmd {
466 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
467 }
468
469 // Ensure we're on main branch before creating new branch
470 cmd := gitCmd("checkout", "main")
471 if err := cmd.Run(); err != nil {
472 // Try master branch if main doesn't exist
473 cmd = gitCmd("checkout", "master")
474 if err := cmd.Run(); err != nil {
475 return fmt.Errorf("failed to checkout main/master branch: %w", err)
476 }
477 }
478
479 // Create branch
480 cmd = gitCmd("checkout", "-b", branchName)
481 if err := cmd.Run(); err != nil {
482 return fmt.Errorf("failed to create branch: %w", err)
483 }
484
485 // Create solution file in agent's clone
486 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
487 if err := os.MkdirAll(solutionDir, 0755); err != nil {
488 return fmt.Errorf("failed to create solution directory: %w", err)
489 }
490
491 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
492 solutionContent := fmt.Sprintf(`# Solution for Task: %s
493
494**Agent:** %s (%s)
495**Model:** %s
496**Completed:** %s
497
498## Task Description
499%s
500
501## Solution
502%s
503
504---
505*Generated by Staff AI Agent System*
506`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
507
508 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
509 return fmt.Errorf("failed to write solution file: %w", err)
510 }
511
512 // Stage files
513 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
514 cmd = gitCmd("add", relativeSolutionFile)
515 if err := cmd.Run(); err != nil {
516 return fmt.Errorf("failed to stage files: %w", err)
517 }
518
519 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400520 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400521 cmd = gitCmd("commit", "-m", commitMsg)
522 if err := cmd.Run(); err != nil {
523 return fmt.Errorf("failed to commit: %w", err)
524 }
525
526 // Push branch
527 cmd = gitCmd("push", "-u", "origin", branchName)
528 if err := cmd.Run(); err != nil {
529 return fmt.Errorf("failed to push branch: %w", err)
530 }
531
iomododea44b02025-07-29 12:55:25 +0400532 m.logger.Info("Agent successfully pushed branch",
533 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400534 slog.String("branch", branchName))
user5a7d60d2025-07-27 21:22:04 +0400535 return nil
536}
537
538// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400539func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
540 template := m.config.Git.CommitMessageTemplate
541
user5a7d60d2025-07-27 21:22:04 +0400542 replacements := map[string]string{
543 "{task_id}": task.ID,
544 "{task_title}": task.Title,
545 "{agent_name}": agent.Name,
546 "{solution}": "See solution file for details",
547 }
548
549 result := template
550 for placeholder, value := range replacements {
551 result = strings.ReplaceAll(result, placeholder, value)
552 }
553
554 return result
555}
556
557// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400558func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400559 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400560
user5a7d60d2025-07-27 21:22:04 +0400561 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400562 description := m.buildPRDescription(task, solution, agent)
563
user5a7d60d2025-07-27 21:22:04 +0400564 options := git.PullRequestOptions{
565 Title: title,
566 Description: description,
567 HeadBranch: branchName,
568 BaseBranch: "main",
569 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
570 Draft: false,
571 }
572
iomodo50598c62025-07-27 22:06:32 +0400573 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400574 if err != nil {
575 return "", fmt.Errorf("failed to create PR: %w", err)
576 }
577
iomodo578f5042025-07-28 20:46:02 +0400578 // Generate provider-specific PR URL
579 switch m.config.GetPrimaryGitProvider() {
580 case "github":
581 return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
582 case "gerrit":
583 return fmt.Sprintf("%s/c/%s/+/%d", m.config.Gerrit.BaseURL, m.config.Gerrit.Project, pr.Number), nil
584 default:
585 return "", fmt.Errorf("unknown git provider")
586 }
user5a7d60d2025-07-27 21:22:04 +0400587}
588
589// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400590func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
591 template := m.config.Git.PRTemplate
592
user5a7d60d2025-07-27 21:22:04 +0400593 // Truncate solution for PR if too long
594 truncatedSolution := solution
595 if len(solution) > 1000 {
596 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
597 }
iomodo50598c62025-07-27 22:06:32 +0400598
user5a7d60d2025-07-27 21:22:04 +0400599 replacements := map[string]string{
600 "{task_id}": task.ID,
601 "{task_title}": task.Title,
602 "{task_description}": task.Description,
603 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
604 "{priority}": string(task.Priority),
605 "{solution}": truncatedSolution,
606 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
607 }
608
609 result := template
610 for placeholder, value := range replacements {
611 result = strings.ReplaceAll(result, placeholder, value)
612 }
613
614 return result
615}
616
617// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400618func (m *Manager) AutoAssignTask(taskID string) error {
619 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400620 if err != nil {
621 return fmt.Errorf("failed to get task: %w", err)
622 }
623
iomodo50598c62025-07-27 22:06:32 +0400624 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400625 if err != nil {
626 return fmt.Errorf("failed to auto-assign task: %w", err)
627 }
628
629 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400630 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400631 return fmt.Errorf("failed to update task assignment: %w", err)
632 }
633
iomodo50598c62025-07-27 22:06:32 +0400634 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomododea44b02025-07-29 12:55:25 +0400635 m.logger.Info("Auto-assigned task to agent",
636 slog.String("task_id", taskID),
637 slog.String("agent", agentName),
iomodo62da94a2025-07-28 19:01:55 +0400638 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400639
640 return nil
641}
642
643// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400644func (m *Manager) GetAgentStatus() map[string]AgentInfo {
645 status := make(map[string]AgentInfo)
646
647 for name, agent := range m.agents {
648 agentStatus := StatusIdle
649 if m.isRunning[name] {
650 if agent.CurrentTask != nil {
651 agentStatus = StatusRunning
652 }
653 } else {
654 agentStatus = StatusStopped
655 }
656
657 status[name] = AgentInfo{
658 Name: agent.Name,
659 Role: agent.Role,
660 Model: agent.Model,
661 Status: agentStatus,
662 CurrentTask: agent.CurrentTask,
663 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400664 }
665 }
iomodo50598c62025-07-27 22:06:32 +0400666
user5a7d60d2025-07-27 21:22:04 +0400667 return status
668}
669
iomodo5c99a442025-07-28 14:23:52 +0400670// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400671func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
672 // Don't generate subtasks for subtasks
673 if task.ParentTaskID != "" {
674 return false
675 }
676
iomodo5c99a442025-07-28 14:23:52 +0400677 // Don't generate if already evaluated
678 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400679 return false
680 }
681
iomodo5c99a442025-07-28 14:23:52 +0400682 // Ask LLM to decide
683 ctx := context.Background()
684 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
685 if err != nil {
iomododea44b02025-07-29 12:55:25 +0400686 m.logger.Warn("Failed to get LLM subtask decision for task",
687 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400688 slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400689 // Fallback to simple heuristics
690 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400691 }
692
iomodo5c99a442025-07-28 14:23:52 +0400693 task.SubtasksEvaluated = true
iomododea44b02025-07-29 12:55:25 +0400694 m.logger.Info("LLM subtask decision for task",
695 slog.String("task_id", task.ID),
696 slog.Bool("needs_subtasks", decision.NeedsSubtasks),
697 slog.Int("complexity_score", decision.ComplexityScore),
iomodo62da94a2025-07-28 19:01:55 +0400698 slog.String("reasoning", decision.Reasoning))
iomodo5c99a442025-07-28 14:23:52 +0400699
700 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400701}
702
703// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
704func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
705 if m.subtaskService == nil {
706 return fmt.Errorf("subtask service not initialized")
707 }
708
709 // Analyze the task for subtasks
710 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
711 if err != nil {
712 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
713 }
714
715 // Generate a PR with the subtask proposals
716 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
717 if err != nil {
718 return fmt.Errorf("failed to generate subtask PR: %w", err)
719 }
720
721 // Update the task with subtask information
722 task.SubtasksPRURL = prURL
723 task.SubtasksGenerated = true
724
iomododea44b02025-07-29 12:55:25 +0400725 m.logger.Info("Generated subtask PR for task",
726 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400727 slog.String("pr_url", prURL))
iomododea44b02025-07-29 12:55:25 +0400728 m.logger.Info("Proposed subtasks and new agents for task",
729 slog.String("task_id", task.ID),
730 slog.Int("subtask_count", len(analysis.Subtasks)),
iomodo62da94a2025-07-28 19:01:55 +0400731 slog.Int("new_agent_count", len(analysis.AgentCreations)))
iomododea44b02025-07-29 12:55:25 +0400732
iomodo5c99a442025-07-28 14:23:52 +0400733 // Log proposed new agents if any
734 if len(analysis.AgentCreations) > 0 {
735 for _, agent := range analysis.AgentCreations {
iomododea44b02025-07-29 12:55:25 +0400736 m.logger.Info("Proposed new agent",
737 slog.String("role", agent.Role),
iomodo62da94a2025-07-28 19:01:55 +0400738 slog.Any("skills", agent.Skills))
iomodo5c99a442025-07-28 14:23:52 +0400739 }
740 }
iomodod9ff8da2025-07-28 11:42:22 +0400741
742 return nil
743}
744
user5a7d60d2025-07-27 21:22:04 +0400745// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400746func (m *Manager) IsAgentRunning(agentName string) bool {
747 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400748}
749
750// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400751func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400752 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400753 for agentName := range m.isRunning {
754 if m.isRunning[agentName] {
755 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400756 }
757 }
758
759 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400760 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400761 if err := agent.Provider.Close(); err != nil {
iomododea44b02025-07-29 12:55:25 +0400762 m.logger.Error("Error closing provider for agent",
763 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400764 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400765 }
766 }
767
768 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400769 if err := m.cloneManager.CleanupAllClones(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400770 m.logger.Error("Error cleaning up agent clones", slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400771 }
772
iomodod9ff8da2025-07-28 11:42:22 +0400773 // Cleanup subtask service
774 if m.subtaskService != nil {
775 if err := m.subtaskService.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400776 m.logger.Error("Error closing subtask service", slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400777 }
778 }
779
user5a7d60d2025-07-27 21:22:04 +0400780 return nil
iomodo50598c62025-07-27 22:06:32 +0400781}