blob: db8f27cb77b7817982f4da54bd1f96be775aedd0 [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"
iomodo50598c62025-07-27 22:06:32 +040010 _ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
iomododea44b02025-07-29 12:55:25 +040011 "github.com/iomodo/staff/task"
user5a7d60d2025-07-27 21:22:04 +040012 "github.com/iomodo/staff/tm"
13)
14
iomodo50598c62025-07-27 22:06:32 +040015// Manager manages multiple AI agents with Git operations and task processing
16type Manager struct {
iomodoa53240a2025-07-30 17:33:35 +040017 config *config.Config
18 agents map[string]*Agent
19 taskManager tm.TaskManager
20 autoAssigner *task.AutoAssigner
21 isRunning map[string]bool
22 roles []string
23 logger *slog.Logger
user5a7d60d2025-07-27 21:22:04 +040024}
25
iomodo50598c62025-07-27 22:06:32 +040026// NewManager creates a new agent manager
iomodo62da94a2025-07-28 19:01:55 +040027func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
iomododea44b02025-07-29 12:55:25 +040028 autoAssigner := task.NewAutoAssigner(cfg.Agents)
user5a7d60d2025-07-27 21:22:04 +040029
iomodoa53240a2025-07-30 17:33:35 +040030 agentRoles := make([]string, 0, len(cfg.Agents))
31 for _, agentConfig := range cfg.Agents {
32 agentRoles = append(agentRoles, agentConfig.Role)
user5a7d60d2025-07-27 21:22:04 +040033 }
user5a7d60d2025-07-27 21:22:04 +040034
iomodo50598c62025-07-27 22:06:32 +040035 manager := &Manager{
user5a7d60d2025-07-27 21:22:04 +040036 config: cfg,
iomodo50598c62025-07-27 22:06:32 +040037 agents: make(map[string]*Agent),
user5a7d60d2025-07-27 21:22:04 +040038 taskManager: taskManager,
39 autoAssigner: autoAssigner,
user5a7d60d2025-07-27 21:22:04 +040040 isRunning: make(map[string]bool),
iomodoa53240a2025-07-30 17:33:35 +040041 roles: agentRoles,
iomodo62da94a2025-07-28 19:01:55 +040042 logger: logger,
user5a7d60d2025-07-27 21:22:04 +040043 }
44
45 // Initialize agents
46 if err := manager.initializeAgents(); err != nil {
47 return nil, fmt.Errorf("failed to initialize agents: %w", err)
48 }
49
50 return manager, nil
51}
52
53// initializeAgents creates agent instances from configuration
iomodo50598c62025-07-27 22:06:32 +040054func (m *Manager) initializeAgents() error {
iomodoa53240a2025-07-30 17:33:35 +040055 llmConfig := llm.Config{
56 Provider: llm.ProviderFake, // Use fake provider for testing
57 APIKey: m.config.OpenAI.APIKey,
58 BaseURL: m.config.OpenAI.BaseURL,
59 Timeout: m.config.OpenAI.Timeout,
60 }
iomodo50598c62025-07-27 22:06:32 +040061 for _, agentConfig := range m.config.Agents {
iomodoa53240a2025-07-30 17:33:35 +040062 agent, err := NewAgent(agentConfig, llmConfig, m.taskManager, m.roles, m.logger)
user5a7d60d2025-07-27 21:22:04 +040063 if err != nil {
64 return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
65 }
iomodo50598c62025-07-27 22:06:32 +040066 m.agents[agentConfig.Name] = agent
user5a7d60d2025-07-27 21:22:04 +040067 }
68 return nil
69}
70
iomodoa53240a2025-07-30 17:33:35 +040071func (m *Manager) StartAllAgents() {
72 // Start all configured agents with a default loop interval
73 defaultInterval := 1 * time.Second
74
75 for _, a := range m.agents {
76 m.logger.Info("Starting agent",
77 slog.String("name", a.Name),
78 slog.String("role", a.Role),
79 slog.String("model", a.Model))
80 if err := a.Start(defaultInterval); err != nil {
81 m.logger.Error("Failed to start agent",
82 slog.String("agent", a.Name),
83 slog.String("error", err.Error()))
84 continue
85 }
86 m.isRunning[a.Name] = true
iomodod9ff8da2025-07-28 11:42:22 +040087 }
iomodod9ff8da2025-07-28 11:42:22 +040088}
89
iomodo50598c62025-07-27 22:06:32 +040090func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
91 agent, exists := m.agents[agentName]
user5a7d60d2025-07-27 21:22:04 +040092 if !exists {
93 return fmt.Errorf("agent %s not found", agentName)
94 }
95
iomodo50598c62025-07-27 22:06:32 +040096 if m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +040097 return fmt.Errorf("agent %s is already running", agentName)
98 }
99
iomodoa53240a2025-07-30 17:33:35 +0400100 agent.Start(loopInterval)
iomodo50598c62025-07-27 22:06:32 +0400101 m.isRunning[agentName] = true
user5a7d60d2025-07-27 21:22:04 +0400102 return nil
103}
104
105// StopAgent stops a running agent
iomodo50598c62025-07-27 22:06:32 +0400106func (m *Manager) StopAgent(agentName string) error {
iomodoa53240a2025-07-30 17:33:35 +0400107 agent, exists := m.agents[agentName]
108 if !exists {
109 return fmt.Errorf("agent %s not found", agentName)
110 }
iomodo50598c62025-07-27 22:06:32 +0400111 if !m.isRunning[agentName] {
user5a7d60d2025-07-27 21:22:04 +0400112 return fmt.Errorf("agent %s is not running", agentName)
113 }
114
iomodoa53240a2025-07-30 17:33:35 +0400115 agent.Stop()
iomodo50598c62025-07-27 22:06:32 +0400116 m.isRunning[agentName] = false
user5a7d60d2025-07-27 21:22:04 +0400117
iomodo62da94a2025-07-28 19:01:55 +0400118 m.logger.Info("Stopped agent", slog.String("name", agentName))
user5a7d60d2025-07-27 21:22:04 +0400119 return nil
120}
121
user5a7d60d2025-07-27 21:22:04 +0400122// AutoAssignTask automatically assigns a task to the best matching agent
iomodo50598c62025-07-27 22:06:32 +0400123func (m *Manager) AutoAssignTask(taskID string) error {
124 task, err := m.taskManager.GetTask(taskID)
user5a7d60d2025-07-27 21:22:04 +0400125 if err != nil {
126 return fmt.Errorf("failed to get task: %w", err)
127 }
128
iomodo50598c62025-07-27 22:06:32 +0400129 agentName, err := m.autoAssigner.AssignTask(task)
user5a7d60d2025-07-27 21:22:04 +0400130 if err != nil {
131 return fmt.Errorf("failed to auto-assign task: %w", err)
132 }
133
134 task.Assignee = agentName
iomodo50598c62025-07-27 22:06:32 +0400135 if err := m.taskManager.UpdateTask(task); err != nil {
user5a7d60d2025-07-27 21:22:04 +0400136 return fmt.Errorf("failed to update task assignment: %w", err)
137 }
138
iomodo50598c62025-07-27 22:06:32 +0400139 explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
iomododea44b02025-07-29 12:55:25 +0400140 m.logger.Info("Auto-assigned task to agent",
141 slog.String("task_id", taskID),
142 slog.String("agent", agentName),
iomodo62da94a2025-07-28 19:01:55 +0400143 slog.String("explanation", explanation))
user5a7d60d2025-07-27 21:22:04 +0400144
145 return nil
146}
147
user5a7d60d2025-07-27 21:22:04 +0400148// IsAgentRunning checks if an agent is currently running
iomodo50598c62025-07-27 22:06:32 +0400149func (m *Manager) IsAgentRunning(agentName string) bool {
150 return m.isRunning[agentName]
user5a7d60d2025-07-27 21:22:04 +0400151}
152
153// Close shuts down the agent manager
iomodo50598c62025-07-27 22:06:32 +0400154func (m *Manager) Close() error {
user5a7d60d2025-07-27 21:22:04 +0400155 // Stop all running agents
iomodo50598c62025-07-27 22:06:32 +0400156 for agentName := range m.isRunning {
157 if m.isRunning[agentName] {
158 m.StopAgent(agentName)
user5a7d60d2025-07-27 21:22:04 +0400159 }
160 }
161
162 // Close all LLM providers
iomodo50598c62025-07-27 22:06:32 +0400163 for _, agent := range m.agents {
user5a7d60d2025-07-27 21:22:04 +0400164 if err := agent.Provider.Close(); err != nil {
iomododea44b02025-07-29 12:55:25 +0400165 m.logger.Error("Error closing provider for agent",
166 slog.String("agent", agent.Name),
iomodo62da94a2025-07-28 19:01:55 +0400167 slog.String("error", err.Error()))
user5a7d60d2025-07-27 21:22:04 +0400168 }
169 }
iomodo1c1c60d2025-07-30 17:54:10 +0400170
171 return m.taskManager.Close()
iomodo50598c62025-07-27 22:06:32 +0400172}