blob: a30213369e9691f569d22562d6bf4cdbed0d7c0a [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 {
iomodo50598c62025-07-27 22:06:32 +0400278 if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
279 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
318 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400319 return fmt.Errorf("failed to update task status: %w", err)
320 }
321
iomodo5c99a442025-07-28 14:23:52 +0400322 // Check if this task should generate subtasks (with LLM decision)
iomodod9ff8da2025-07-28 11:42:22 +0400323 if m.shouldGenerateSubtasks(task) {
iomodo62da94a2025-07-28 19:01:55 +0400324 m.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
iomodod9ff8da2025-07-28 11:42:22 +0400325 if err := m.generateSubtasksForTask(ctx, task); err != nil {
iomododea44b02025-07-29 12:55:25 +0400326 m.logger.Warn("Failed to generate subtasks for task",
327 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400328 slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400329 // Continue with normal processing if subtask generation fails
330 } else {
331 // Task has been converted to subtask management, mark as completed
332 task.Status = tm.StatusCompleted
iomodo5c99a442025-07-28 14:23:52 +0400333 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 +0400334 completedAt := time.Now()
335 task.CompletedAt = &completedAt
336 agent.CurrentTask = nil
337
338 if err := m.taskManager.UpdateTask(task); err != nil {
339 return fmt.Errorf("failed to update task with subtasks: %w", err)
340 }
341
iomododea44b02025-07-29 12:55:25 +0400342 m.logger.Info("Task converted to subtasks by agent using LLM analysis",
343 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400344 slog.String("agent", agent.Name))
iomodod9ff8da2025-07-28 11:42:22 +0400345 return nil
346 }
347 }
348
user5a7d60d2025-07-27 21:22:04 +0400349 // Generate solution using LLM
iomodo50598c62025-07-27 22:06:32 +0400350 solution, err := m.generateSolution(ctx, agent, task)
user5a7d60d2025-07-27 21:22:04 +0400351 if err != nil {
352 return fmt.Errorf("failed to generate solution: %w", err)
353 }
354
355 // Create Git branch and commit solution
iomodo50598c62025-07-27 22:06:32 +0400356 branchName := m.generateBranchName(task)
357 if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400358 return fmt.Errorf("failed to commit solution: %w", err)
359 }
360
361 // Create pull request
iomodo50598c62025-07-27 22:06:32 +0400362 prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
user5a7d60d2025-07-27 21:22:04 +0400363 if err != nil {
364 return fmt.Errorf("failed to create pull request: %w", err)
365 }
366
367 // Update task as completed
368 task.Status = tm.StatusCompleted
369 task.Solution = solution
370 task.PullRequestURL = prURL
iomodo50598c62025-07-27 22:06:32 +0400371 completedAt := time.Now()
372 task.CompletedAt = &completedAt
373 agent.CurrentTask = nil
user5a7d60d2025-07-27 21:22:04 +0400374
iomodo50598c62025-07-27 22:06:32 +0400375 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400376 return fmt.Errorf("failed to update completed task: %w", err)
377 }
378
iomodo50598c62025-07-27 22:06:32 +0400379 // Update agent stats
380 duration := time.Since(startTime)
381 if agent.Stats.AvgTime == 0 {
382 agent.Stats.AvgTime = duration.Milliseconds()
383 } else {
384 agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
385 }
386
iomododea44b02025-07-29 12:55:25 +0400387 m.logger.Info("Task completed by agent",
388 slog.String("task_id", task.ID),
389 slog.String("agent", agent.Name),
390 slog.Duration("duration", duration),
iomodo62da94a2025-07-28 19:01:55 +0400391 slog.String("pr_url", prURL))
user5a7d60d2025-07-27 21:22:04 +0400392 return nil
393}
394
395// generateSolution uses the agent's LLM to generate a solution
iomodo50598c62025-07-27 22:06:32 +0400396func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
397 prompt := m.buildTaskPrompt(task)
user5a7d60d2025-07-27 21:22:04 +0400398
399 req := llm.ChatCompletionRequest{
400 Model: agent.Model,
401 Messages: []llm.Message{
402 {
403 Role: llm.RoleSystem,
404 Content: agent.SystemPrompt,
405 },
406 {
407 Role: llm.RoleUser,
408 Content: prompt,
409 },
410 },
411 MaxTokens: agent.MaxTokens,
412 Temperature: agent.Temperature,
413 }
414
415 resp, err := agent.Provider.ChatCompletion(ctx, req)
416 if err != nil {
417 return "", fmt.Errorf("LLM request failed: %w", err)
418 }
419
420 if len(resp.Choices) == 0 {
421 return "", fmt.Errorf("no response from LLM")
422 }
423
424 return resp.Choices[0].Message.Content, nil
425}
426
427// buildTaskPrompt creates a detailed prompt for the LLM
iomodo50598c62025-07-27 22:06:32 +0400428func (m *Manager) buildTaskPrompt(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400429 return fmt.Sprintf(`Task: %s
430
431Priority: %s
432Description: %s
433
434Please provide a complete solution for this task. Include:
4351. Detailed implementation plan
4362. Code changes needed (if applicable)
4373. Files to be created or modified
4384. Testing considerations
4395. Any dependencies or prerequisites
440
441Your response should be comprehensive and actionable.`,
442 task.Title,
443 task.Priority,
444 task.Description)
445}
446
447// generateBranchName creates a Git branch name for the task
iomodo50598c62025-07-27 22:06:32 +0400448func (m *Manager) generateBranchName(task *tm.Task) string {
user5a7d60d2025-07-27 21:22:04 +0400449 // Clean title for use in branch name
450 cleanTitle := strings.ToLower(task.Title)
451 cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
452 cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
453 // Remove special characters
454 var result strings.Builder
455 for _, r := range cleanTitle {
456 if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
457 result.WriteRune(r)
458 }
459 }
460 cleanTitle = result.String()
iomodo50598c62025-07-27 22:06:32 +0400461
user5a7d60d2025-07-27 21:22:04 +0400462 // Limit length
463 if len(cleanTitle) > 40 {
464 cleanTitle = cleanTitle[:40]
465 }
iomodo50598c62025-07-27 22:06:32 +0400466
467 return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
user5a7d60d2025-07-27 21:22:04 +0400468}
469
470// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
iomodo50598c62025-07-27 22:06:32 +0400471func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
user5a7d60d2025-07-27 21:22:04 +0400472 ctx := context.Background()
iomodo50598c62025-07-27 22:06:32 +0400473
user5a7d60d2025-07-27 21:22:04 +0400474 // Get agent's dedicated Git clone
iomodo50598c62025-07-27 22:06:32 +0400475 clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
user5a7d60d2025-07-27 21:22:04 +0400476 if err != nil {
477 return fmt.Errorf("failed to get agent clone: %w", err)
478 }
iomodo50598c62025-07-27 22:06:32 +0400479
iomododea44b02025-07-29 12:55:25 +0400480 m.logger.Info("Agent working in clone",
481 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400482 slog.String("clone_path", clonePath))
user5a7d60d2025-07-27 21:22:04 +0400483
484 // Refresh the clone with latest changes
iomodo50598c62025-07-27 22:06:32 +0400485 if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
iomododea44b02025-07-29 12:55:25 +0400486 m.logger.Warn("Failed to refresh clone for agent",
487 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400488 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400489 }
490
491 // All Git operations use the agent's clone directory
492 gitCmd := func(args ...string) *exec.Cmd {
493 return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
494 }
495
496 // Ensure we're on main branch before creating new branch
497 cmd := gitCmd("checkout", "main")
498 if err := cmd.Run(); err != nil {
499 // Try master branch if main doesn't exist
500 cmd = gitCmd("checkout", "master")
501 if err := cmd.Run(); err != nil {
502 return fmt.Errorf("failed to checkout main/master branch: %w", err)
503 }
504 }
505
506 // Create branch
507 cmd = gitCmd("checkout", "-b", branchName)
508 if err := cmd.Run(); err != nil {
509 return fmt.Errorf("failed to create branch: %w", err)
510 }
511
512 // Create solution file in agent's clone
513 solutionDir := filepath.Join(clonePath, "tasks", "solutions")
514 if err := os.MkdirAll(solutionDir, 0755); err != nil {
515 return fmt.Errorf("failed to create solution directory: %w", err)
516 }
517
518 solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
519 solutionContent := fmt.Sprintf(`# Solution for Task: %s
520
521**Agent:** %s (%s)
522**Model:** %s
523**Completed:** %s
524
525## Task Description
526%s
527
528## Solution
529%s
530
531---
532*Generated by Staff AI Agent System*
533`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
534
535 if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
536 return fmt.Errorf("failed to write solution file: %w", err)
537 }
538
539 // Stage files
540 relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
541 cmd = gitCmd("add", relativeSolutionFile)
542 if err := cmd.Run(); err != nil {
543 return fmt.Errorf("failed to stage files: %w", err)
544 }
545
546 // Commit changes
iomodo50598c62025-07-27 22:06:32 +0400547 commitMsg := m.buildCommitMessage(task, agent)
user5a7d60d2025-07-27 21:22:04 +0400548 cmd = gitCmd("commit", "-m", commitMsg)
549 if err := cmd.Run(); err != nil {
550 return fmt.Errorf("failed to commit: %w", err)
551 }
552
553 // Push branch
554 cmd = gitCmd("push", "-u", "origin", branchName)
555 if err := cmd.Run(); err != nil {
556 return fmt.Errorf("failed to push branch: %w", err)
557 }
558
iomododea44b02025-07-29 12:55:25 +0400559 m.logger.Info("Agent successfully pushed branch",
560 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400561 slog.String("branch", branchName))
user5a7d60d2025-07-27 21:22:04 +0400562 return nil
563}
564
565// buildCommitMessage creates a commit message from template
iomodo50598c62025-07-27 22:06:32 +0400566func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
567 template := m.config.Git.CommitMessageTemplate
568
user5a7d60d2025-07-27 21:22:04 +0400569 replacements := map[string]string{
570 "{task_id}": task.ID,
571 "{task_title}": task.Title,
572 "{agent_name}": agent.Name,
573 "{solution}": "See solution file for details",
574 }
575
576 result := template
577 for placeholder, value := range replacements {
578 result = strings.ReplaceAll(result, placeholder, value)
579 }
580
581 return result
582}
583
584// createPullRequest creates a GitHub pull request
iomodo50598c62025-07-27 22:06:32 +0400585func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
user5a7d60d2025-07-27 21:22:04 +0400586 title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
iomodo50598c62025-07-27 22:06:32 +0400587
user5a7d60d2025-07-27 21:22:04 +0400588 // Build PR description from template
iomodo50598c62025-07-27 22:06:32 +0400589 description := m.buildPRDescription(task, solution, agent)
590
user5a7d60d2025-07-27 21:22:04 +0400591 options := git.PullRequestOptions{
592 Title: title,
593 Description: description,
594 HeadBranch: branchName,
595 BaseBranch: "main",
596 Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
597 Draft: false,
598 }
599
iomodo50598c62025-07-27 22:06:32 +0400600 pr, err := m.prProvider.CreatePullRequest(ctx, options)
user5a7d60d2025-07-27 21:22:04 +0400601 if err != nil {
602 return "", fmt.Errorf("failed to create PR: %w", err)
603 }
604
iomodo578f5042025-07-28 20:46:02 +0400605 // Generate provider-specific PR URL
606 switch m.config.GetPrimaryGitProvider() {
607 case "github":
608 return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
609 case "gerrit":
610 return fmt.Sprintf("%s/c/%s/+/%d", m.config.Gerrit.BaseURL, m.config.Gerrit.Project, pr.Number), nil
611 default:
612 return "", fmt.Errorf("unknown git provider")
613 }
user5a7d60d2025-07-27 21:22:04 +0400614}
615
616// buildPRDescription creates PR description from template
iomodo50598c62025-07-27 22:06:32 +0400617func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
618 template := m.config.Git.PRTemplate
619
user5a7d60d2025-07-27 21:22:04 +0400620 // Truncate solution for PR if too long
621 truncatedSolution := solution
622 if len(solution) > 1000 {
623 truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
624 }
iomodo50598c62025-07-27 22:06:32 +0400625
user5a7d60d2025-07-27 21:22:04 +0400626 replacements := map[string]string{
627 "{task_id}": task.ID,
628 "{task_title}": task.Title,
629 "{task_description}": task.Description,
630 "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
631 "{priority}": string(task.Priority),
632 "{solution}": truncatedSolution,
633 "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
634 }
635
636 result := template
637 for placeholder, value := range replacements {
638 result = strings.ReplaceAll(result, placeholder, value)
639 }
640
641 return result
642}
643
644// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400645func (m *Manager) AutoAssignTask(taskID string) error {
646 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400647 if err != nil {
648 return fmt.Errorf("failed to get task: %w", err)
649 }
650
iomodo50598c62025-07-27 22:06:32 +0400651 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400652 if err != nil {
653 return fmt.Errorf("failed to auto-assign task: %w", err)
654 }
655
656 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400657 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400658 return fmt.Errorf("failed to update task assignment: %w", err)
659 }
660
iomodo50598c62025-07-27 22:06:32 +0400661 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomododea44b02025-07-29 12:55:25 +0400662 m.logger.Info("Auto-assigned task to agent",
663 slog.String("task_id", taskID),
664 slog.String("agent", agentName),
iomodo62da94a2025-07-28 19:01:55 +0400665 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400666
667 return nil
668}
669
670// GetAgentStatus returns the status of all agents
iomodo50598c62025-07-27 22:06:32 +0400671func (m *Manager) GetAgentStatus() map[string]AgentInfo {
672 status := make(map[string]AgentInfo)
673
674 for name, agent := range m.agents {
675 agentStatus := StatusIdle
676 if m.isRunning[name] {
677 if agent.CurrentTask != nil {
678 agentStatus = StatusRunning
679 }
680 } else {
681 agentStatus = StatusStopped
682 }
683
684 status[name] = AgentInfo{
685 Name: agent.Name,
686 Role: agent.Role,
687 Model: agent.Model,
688 Status: agentStatus,
689 CurrentTask: agent.CurrentTask,
690 Stats: agent.Stats,
user5a7d60d2025-07-27 21:22:04 +0400691 }
692 }
iomodo50598c62025-07-27 22:06:32 +0400693
user5a7d60d2025-07-27 21:22:04 +0400694 return status
695}
696
iomodo5c99a442025-07-28 14:23:52 +0400697// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
iomodod9ff8da2025-07-28 11:42:22 +0400698func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
699 // Don't generate subtasks for subtasks
700 if task.ParentTaskID != "" {
701 return false
702 }
703
iomodo5c99a442025-07-28 14:23:52 +0400704 // Don't generate if already evaluated
705 if task.SubtasksEvaluated {
iomodod9ff8da2025-07-28 11:42:22 +0400706 return false
707 }
708
iomodo5c99a442025-07-28 14:23:52 +0400709 // Ask LLM to decide
710 ctx := context.Background()
711 decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
712 if err != nil {
iomododea44b02025-07-29 12:55:25 +0400713 m.logger.Warn("Failed to get LLM subtask decision for task",
714 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400715 slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400716 // Fallback to simple heuristics
717 return task.Priority == tm.PriorityHigh || len(task.Description) > 200
iomodod9ff8da2025-07-28 11:42:22 +0400718 }
719
iomodo5c99a442025-07-28 14:23:52 +0400720 // Update task to mark as evaluated
721 task.SubtasksEvaluated = true
722 if err := m.taskManager.UpdateTask(task); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400723 m.logger.Warn("Failed to update task evaluation status", slog.String("error", err.Error()))
iomodo5c99a442025-07-28 14:23:52 +0400724 }
725
iomododea44b02025-07-29 12:55:25 +0400726 m.logger.Info("LLM subtask decision for task",
727 slog.String("task_id", task.ID),
728 slog.Bool("needs_subtasks", decision.NeedsSubtasks),
729 slog.Int("complexity_score", decision.ComplexityScore),
iomodo62da94a2025-07-28 19:01:55 +0400730 slog.String("reasoning", decision.Reasoning))
iomodo5c99a442025-07-28 14:23:52 +0400731
732 return decision.NeedsSubtasks
iomodod9ff8da2025-07-28 11:42:22 +0400733}
734
735// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
736func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
737 if m.subtaskService == nil {
738 return fmt.Errorf("subtask service not initialized")
739 }
740
741 // Analyze the task for subtasks
742 analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
743 if err != nil {
744 return fmt.Errorf("failed to analyze task for subtasks: %w", err)
745 }
746
747 // Generate a PR with the subtask proposals
748 prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
749 if err != nil {
750 return fmt.Errorf("failed to generate subtask PR: %w", err)
751 }
752
753 // Update the task with subtask information
754 task.SubtasksPRURL = prURL
755 task.SubtasksGenerated = true
756
iomododea44b02025-07-29 12:55:25 +0400757 m.logger.Info("Generated subtask PR for task",
758 slog.String("task_id", task.ID),
iomodo62da94a2025-07-28 19:01:55 +0400759 slog.String("pr_url", prURL))
iomododea44b02025-07-29 12:55:25 +0400760 m.logger.Info("Proposed subtasks and new agents for task",
761 slog.String("task_id", task.ID),
762 slog.Int("subtask_count", len(analysis.Subtasks)),
iomodo62da94a2025-07-28 19:01:55 +0400763 slog.Int("new_agent_count", len(analysis.AgentCreations)))
iomododea44b02025-07-29 12:55:25 +0400764
iomodo5c99a442025-07-28 14:23:52 +0400765 // Log proposed new agents if any
766 if len(analysis.AgentCreations) > 0 {
767 for _, agent := range analysis.AgentCreations {
iomododea44b02025-07-29 12:55:25 +0400768 m.logger.Info("Proposed new agent",
769 slog.String("role", agent.Role),
iomodo62da94a2025-07-28 19:01:55 +0400770 slog.Any("skills", agent.Skills))
iomodo5c99a442025-07-28 14:23:52 +0400771 }
772 }
iomodod9ff8da2025-07-28 11:42:22 +0400773
774 return nil
775}
776
user5a7d60d2025-07-27 21:22:04 +0400777// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400778func (m *Manager) IsAgentRunning(agentName string) bool {
779 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400780}
781
782// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400783func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400784 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400785 for agentName := range m.isRunning {
786 if m.isRunning[agentName] {
787 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400788 }
789 }
790
791 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400792 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400793 if err := agent.Provider.Close(); err != nil {
iomododea44b02025-07-29 12:55:25 +0400794 m.logger.Error("Error closing provider for agent",
795 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400796 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400797 }
798 }
799
800 // Cleanup all agent Git clones
iomodo50598c62025-07-27 22:06:32 +0400801 if err := m.cloneManager.CleanupAllClones(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400802 m.logger.Error("Error cleaning up agent clones", slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400803 }
804
iomodod9ff8da2025-07-28 11:42:22 +0400805 // Cleanup subtask service
806 if m.subtaskService != nil {
807 if err := m.subtaskService.Close(); err != nil {
iomodo62da94a2025-07-28 19:01:55 +0400808 m.logger.Error("Error closing subtask service", slog.String("error", err.Error()))
iomodod9ff8da2025-07-28 11:42:22 +0400809 }
810 }
811
user5a7d60d2025-07-27 21:22:04 +0400812 return nil
iomodo50598c62025-07-27 22:06:32 +0400813}