blob: 3cc578e5db92061f78534fd086f1f7768dfe81db [file] [log] [blame]
user5a7d60d2025-07-27 21:22:04 +04001package agent
2
3import (
user5a7d60d2025-07-27 21:22:04 +04004 "fmt"
iomodo62da94a2025-07-28 19:01:55 +04005 "log/slog"
user5a7d60d2025-07-27 21:22:04 +04006 "time"
7
user5a7d60d2025-07-27 21:22:04 +04008 "github.com/iomodo/staff/config"
user5a7d60d2025-07-27 21:22:04 +04009 "github.com/iomodo/staff/llm"
iomododea44b02025-07-29 12:55:25 +040010 "github.com/iomodo/staff/task"
user5a7d60d2025-07-27 21:22:04 +040011 "github.com/iomodo/staff/tm"
12)
13
iomodo50598c62025-07-27 22:06:32 +040014// Manager manages multiple AI agents with Git operations and task processing
15type Manager struct {
iomodoa53240a2025-07-30 17:33:35 +040016 config *config.Config
17 agents map[string]*Agent
18 taskManager tm.TaskManager
19 autoAssigner *task.AutoAssigner
20 isRunning map[string]bool
21 roles []string
22 logger *slog.Logger
user5a7d60d2025-07-27 21:22:04 +040023}
24
iomodo50598c62025-07-27 22:06:32 +040025// NewManager creates a new agent manager
iomodo62da94a2025-07-28 19:01:55 +040026func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
iomododea44b02025-07-29 12:55:25 +040027 autoAssigner := task.NewAutoAssigner(cfg.Agents)
user5a7d60d2025-07-27 21:22:04 +040028
iomodoa53240a2025-07-30 17:33:35 +040029 agentRoles := make([]string, 0, len(cfg.Agents))
30 for _, agentConfig := range cfg.Agents {
31 agentRoles = append(agentRoles, agentConfig.Role)
user5a7d60d2025-07-27 21:22:04 +040032 }
user5a7d60d2025-07-27 21:22:04 +040033
iomodo50598c62025-07-27 22:06:32 +040034 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040035 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040036 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040037 taskManager: taskManager,
38 autoAssigner: autoAssigner,
user5a7d60d2025-07-27 21:22:04 +040039 isRunning: make(map[string]bool),
iomodoa53240a2025-07-30 17:33:35 +040040 roles: agentRoles,
iomodo62da94a2025-07-28 19:01:55 +040041 logger: logger,
user5a7d60d2025-07-27 21:22:04 +040042 }
43
44 // Initialize agents
45 if err := manager.initializeAgents(); err != nil {
46 return nil, fmt.Errorf("failed to initialize agents: %w", err)
47 }
48
49 return manager, nil
50}
51
52// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +040053func (m *Manager) initializeAgents() error {
iomodoa53240a2025-07-30 17:33:35 +040054 llmConfig := llm.Config{
55 Provider: llm.ProviderFake, // Use fake provider for testing
56 APIKey: m.config.OpenAI.APIKey,
57 BaseURL: m.config.OpenAI.BaseURL,
58 Timeout: m.config.OpenAI.Timeout,
59 }
iomodo50598c62025-07-27 22:06:32 +040060 for _, agentConfig := range m.config.Agents {
iomodoa53240a2025-07-30 17:33:35 +040061 agent, err := NewAgent(agentConfig, llmConfig, m.taskManager, m.roles, m.logger)
user5a7d60d2025-07-27 21:22:04 +040062 if err != nil {
63 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
64 }
iomodo50598c62025-07-27 22:06:32 +040065 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +040066 }
67 return nil
68}
69
iomodoa53240a2025-07-30 17:33:35 +040070func (m *Manager) StartAllAgents() {
71 // Start all configured agents with a default loop interval
72 defaultInterval := 1 * time.Second
73
74 for _, a := range m.agents {
75 m.logger.Info("Starting agent",
76 slog.String("name", a.Name),
77 slog.String("role", a.Role),
78 slog.String("model", a.Model))
79 if err := a.Start(defaultInterval); err != nil {
80 m.logger.Error("Failed to start agent",
81 slog.String("agent", a.Name),
82 slog.String("error", err.Error()))
83 continue
84 }
85 m.isRunning[a.Name] = true
iomodod9ff8da2025-07-28 11:42:22 +040086 }
iomodod9ff8da2025-07-28 11:42:22 +040087}
88
iomodo50598c62025-07-27 22:06:32 +040089func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
90 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +040091 if !exists {
92 return fmt.Errorf("agent %s not found", agentName)
93 }
94
iomodo50598c62025-07-27 22:06:32 +040095 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +040096 return fmt.Errorf("agent %s is already running", agentName)
97 }
98
iomodoa53240a2025-07-30 17:33:35 +040099 agent.Start(loopInterval)
iomodo50598c62025-07-27 22:06:32 +0400100 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400101 return nil
102}
103
104// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400105func (m *Manager) StopAgent(agentName string) error {
iomodoa53240a2025-07-30 17:33:35 +0400106 agent, exists := m.agents[agentName]
107 if !exists {
108 return fmt.Errorf("agent %s not found", agentName)
109 }
iomodo50598c62025-07-27 22:06:32 +0400110 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400111 return fmt.Errorf("agent %s is not running", agentName)
112 }
113
iomodoa53240a2025-07-30 17:33:35 +0400114 agent.Stop()
iomodo50598c62025-07-27 22:06:32 +0400115 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400116
iomodo62da94a2025-07-28 19:01:55 +0400117 m.logger.Info("Stopped agent", slog.String("name", agentName))
user5a7d60d2025-07-27 21:22:04 +0400118 return nil
119}
120
user5a7d60d2025-07-27 21:22:04 +0400121// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400122func (m *Manager) AutoAssignTask(taskID string) error {
123 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400124 if err != nil {
125 return fmt.Errorf("failed to get task: %w", err)
126 }
127
iomodo50598c62025-07-27 22:06:32 +0400128 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400129 if err != nil {
130 return fmt.Errorf("failed to auto-assign task: %w", err)
131 }
132
133 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400134 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400135 return fmt.Errorf("failed to update task assignment: %w", err)
136 }
137
iomodo50598c62025-07-27 22:06:32 +0400138 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomododea44b02025-07-29 12:55:25 +0400139 m.logger.Info("Auto-assigned task to agent",
140 slog.String("task_id", taskID),
141 slog.String("agent", agentName),
iomodo62da94a2025-07-28 19:01:55 +0400142 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400143
144 return nil
145}
146
user5a7d60d2025-07-27 21:22:04 +0400147// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400148func (m *Manager) IsAgentRunning(agentName string) bool {
149 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400150}
151
iomodoe5970ba2025-07-31 17:59:52 +0400152// CompleteTaskForAgent finds the agent performing a task and resets its state
153func (m *Manager) CompleteTaskForAgent(taskID string) error {
154 // Get the task to find the assignee
155 task, err := m.taskManager.GetTask(taskID)
156 if err != nil {
157 return fmt.Errorf("failed to get task %s: %w", taskID, err)
158 }
159
160 // Find the agent assigned to this task
161 agent, exists := m.agents[task.Assignee]
162 if !exists {
163 return fmt.Errorf("agent %s not found for task %s", task.Assignee, taskID)
164 }
165
iomodo907b43d2025-07-31 19:43:42 +0400166 // Reset the agent's current task only - keep agent running
iomodoe5970ba2025-07-31 17:59:52 +0400167 agent.CurrentTask = nil
iomodo907b43d2025-07-31 19:43:42 +0400168 // Note: Do NOT set agent.IsRunning = false - agent should continue processing new tasks
iomodoe5970ba2025-07-31 17:59:52 +0400169
170 m.logger.Info("Completed task for agent",
171 slog.String("task_id", taskID),
172 slog.String("agent", agent.Name))
173
174 return nil
175}
176
user5a7d60d2025-07-27 21:22:04 +0400177// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400178func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400179 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400180 for agentName := range m.isRunning {
181 if m.isRunning[agentName] {
182 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400183 }
184 }
185
186 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400187 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400188 if err := agent.Provider.Close(); err != nil {
iomododea44b02025-07-29 12:55:25 +0400189 m.logger.Error("Error closing provider for agent",
190 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400191 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400192 }
193 }
iomodo1c1c60d2025-07-30 17:54:10 +0400194
195 return m.taskManager.Close()
iomodo50598c62025-07-27 22:06:32 +0400196}