Refactor everything
Change-Id: Ic3a37c38cfecba943c91f6ae545ce1c5b551c0d5
diff --git a/server/agent/README.md b/server/agent/README.md
deleted file mode 100644
index 0015d90..0000000
--- a/server/agent/README.md
+++ /dev/null
@@ -1,298 +0,0 @@
-# Agent Package
-
-The `agent` package provides an AI agent system that can autonomously process tasks using LLM services, manage tasks through a task management system, and create pull requests with solutions.
-
-## Overview
-
-The agent system consists of:
-
-- **AI Agent**: Processes tasks using LLM services
-- **Task Manager**: Manages task lifecycle and assignment
-- **Git Integration**: Creates pull requests with solutions
-- **Infinite Loop**: Continuously processes assigned tasks
-
-## Features
-
-- **Autonomous Task Processing**: Agents automatically pick up and process assigned tasks
-- **LLM Integration**: Uses configurable LLM providers (OpenAI, Claude, etc.)
-- **Task Management**: Integrates with task management systems
-- **Git Operations**: Creates branches and pull requests for solutions
-- **Configurable Roles**: Different agents can have different roles and system prompts
-- **Error Handling**: Robust error handling with graceful recovery
-
-## Quick Start
-
-### 1. Basic Setup
-
-```go
-package main
-
-import (
- "log"
- "time"
-
- "github.com/iomodo/staff/agent"
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- "github.com/iomodo/staff/tm"
- "github.com/iomodo/staff/tm/git_tm"
-)
-
-func main() {
- // Create git interface for task management
- gitInterface := git.DefaultGit("./tasks-repo")
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManager(gitInterface, "./tasks-repo")
-
- // Create LLM configuration
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key-here",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent configuration
- config := agent.AgentConfig{
- Name: "backend-engineer-1",
- Role: "Backend Engineer",
- GitUsername: "backend-agent",
- GitEmail: "backend-agent@company.com",
- WorkingDir: "./workspace",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llmConfig,
- SystemPrompt: `You are a skilled backend engineer. Your role is to:
-1. Analyze tasks and provide technical solutions
-2. Write clean, maintainable code
-3. Consider performance, security, and scalability
-4. Provide clear documentation for your solutions
-5. Follow best practices and coding standards`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create agent
- agent, err := agent.NewAgent(config)
- if err != nil {
- log.Fatalf("Failed to create agent: %v", err)
- }
-
- // Run the agent
- if err := agent.Run(); err != nil {
- log.Fatalf("Agent failed: %v", err)
- }
-}
-```
-
-### 2. Create a Task
-
-```go
-// Create a task for the agent to process
-ctx := context.Background()
-task, err := taskManager.CreateTask(ctx, &tm.TaskCreateRequest{
- Title: "Implement user authentication API",
- Description: "Create a REST API endpoint for user authentication with JWT tokens. Include login, logout, and token refresh functionality.",
- OwnerID: "backend-engineer-1", // Must match agent name
- Priority: tm.PriorityHigh,
-})
-if err != nil {
- log.Fatalf("Failed to create task: %v", err)
-}
-```
-
-## Configuration
-
-### AgentConfig
-
-The `AgentConfig` struct contains all configuration for an agent:
-
-```go
-type AgentConfig struct {
- Name string // Agent identifier
- Role string // Agent role (e.g., "Backend Engineer")
- GitUsername string // Git username for commits
- GitEmail string // Git email for commits
- WorkingDir string // Working directory for files
-
- // LLM Configuration
- LLMProvider llm.Provider // LLM provider type
- LLMModel string // Model name (e.g., "gpt-4")
- LLMConfig llm.Config // LLM provider configuration
-
- // System prompt for the agent
- SystemPrompt string // Instructions for the LLM
-
- // Task Manager Configuration
- TaskManager tm.TaskManager // Task management interface
-
- // Git Configuration
- GitRepoPath string // Path to git repository
- GitRemote string // Remote name (usually "origin")
- GitBranch string // Default branch name
-}
-```
-
-### System Prompts
-
-System prompts define the agent's behavior and expertise. Here are some examples:
-
-#### Backend Engineer
-```
-You are a skilled backend engineer. Your role is to:
-1. Analyze tasks and provide technical solutions
-2. Write clean, maintainable code
-3. Consider performance, security, and scalability
-4. Provide clear documentation for your solutions
-5. Follow best practices and coding standards
-
-When responding to tasks, provide:
-- Detailed technical analysis
-- Code examples where appropriate
-- Implementation considerations
-- Testing recommendations
-- Documentation suggestions
-```
-
-#### Frontend Engineer
-```
-You are a frontend engineer. Focus on:
-- User interface design and implementation
-- React/Vue/Angular development
-- Responsive design and accessibility
-- Performance optimization
-- User experience best practices
-```
-
-#### Product Manager
-```
-You are a product manager. Focus on:
-- Product strategy and roadmap
-- User research and requirements gathering
-- Feature prioritization and planning
-- Stakeholder communication
-- Product documentation and specifications
-```
-
-## How It Works
-
-### 1. Task Processing Loop
-
-The agent runs in an infinite loop that:
-
-1. **Fetches Tasks**: Gets tasks assigned to the agent from the task manager
-2. **Filters Tasks**: Looks for tasks with "todo" status
-3. **Starts Task**: Marks the task as "in progress"
-4. **Processes with LLM**: Sends task description to LLM for solution
-5. **Creates PR**: Creates a git branch and pull request with the solution
-6. **Completes Task**: Marks the task as completed
-
-### 2. Git Operations
-
-For each task, the agent:
-
-1. Creates a new branch: `task/{task-id}-{clean-title}`
-2. Writes solution to a markdown file
-3. Commits the solution
-4. Pushes the branch to create a pull request
-
-### 3. Solution Format
-
-Solutions are formatted as markdown files containing:
-
-- Task metadata (ID, title, agent info)
-- Original task description
-- LLM-generated solution
-- Timestamp and attribution
-
-## Multiple Agents
-
-You can run multiple agents with different roles:
-
-```go
-// Create agents with different roles
-agents := []agent.AgentConfig{
- {
- Name: "backend-engineer-1",
- Role: "Backend Engineer",
- // ... backend configuration
- },
- {
- Name: "frontend-engineer-1",
- Role: "Frontend Engineer",
- // ... frontend configuration
- },
- {
- Name: "product-manager-1",
- Role: "Product Manager",
- // ... product manager configuration
- },
-}
-
-// Start all agents
-for _, config := range agents {
- agent, err := agent.NewAgent(config)
- if err != nil {
- log.Printf("Failed to create agent %s: %v", config.Name, err)
- continue
- }
-
- go func(agent *agent.Agent, name string) {
- log.Printf("Starting agent: %s", name)
- if err := agent.Run(); err != nil {
- log.Printf("Agent %s stopped with error: %v", name, err)
- }
- }(agent, config.Name)
-}
-```
-
-## Error Handling
-
-The agent includes robust error handling:
-
-- **Configuration Validation**: Validates all required fields
-- **Graceful Recovery**: Continues running even if individual tasks fail
-- **Logging**: Comprehensive logging of all operations
-- **Resource Cleanup**: Proper cleanup of LLM connections
-
-## Testing
-
-Run the tests with:
-
-```bash
-go test ./server/agent/...
-```
-
-The test suite includes:
-
-- Configuration validation
-- Branch name generation
-- Task prompt building
-- Solution formatting
-- Error handling
-
-## Dependencies
-
-The agent package depends on:
-
-- `github.com/iomodo/staff/llm` - LLM service interface
-- `github.com/iomodo/staff/tm` - Task management interface
-- `github.com/iomodo/staff/git` - Git operations interface
-
-## Examples
-
-See `example.go` for complete working examples:
-
-- `ExampleAgent()` - Single agent setup
-- `ExampleMultipleAgents()` - Multiple agents with different roles
-
-## Best Practices
-
-1. **Unique Agent Names**: Ensure each agent has a unique name
-2. **Role-Specific Prompts**: Tailor system prompts to the agent's role
-3. **Task Assignment**: Assign tasks to agents by setting the `OwnerID` to the agent's name
-4. **Monitoring**: Monitor agent logs for errors and performance
-5. **Resource Management**: Ensure proper cleanup when stopping agents
\ No newline at end of file
diff --git a/server/agent/agent.go b/server/agent/agent.go
new file mode 100644
index 0000000..fba80b7
--- /dev/null
+++ b/server/agent/agent.go
@@ -0,0 +1,239 @@
+package agent
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "os"
+ "time"
+
+ "github.com/iomodo/staff/config"
+ "github.com/iomodo/staff/llm"
+ "github.com/iomodo/staff/tm"
+)
+
+type Agent struct {
+ // Identity
+ Name string
+ Role string
+
+ // LLM Configuration
+ Model string
+ SystemPrompt string
+ MaxTokens *int
+ Temperature *float64
+
+ // Runtime
+ Provider llm.LLMProvider
+ CurrentTask *string // Task ID currently being processed
+
+ IsRunning bool
+ StopChan chan struct{}
+
+ logger *slog.Logger
+
+ taskManager tm.TaskManager
+ thinker *Thinker
+}
+
+func NewAgent(agentConfig config.AgentConfig, llmConfig llm.Config, taskManager tm.TaskManager, agentRoles []string, logger *slog.Logger) (*Agent, error) {
+ // Load system prompt
+ systemPrompt, err := loadSystemPrompt(agentConfig.SystemPromptFile)
+ if err != nil {
+ return nil, fmt.Errorf("failed to load system prompt: %w", err)
+ }
+
+ provider, err := llm.CreateProvider(llmConfig)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create LLM provider: %w", err)
+ }
+
+ thinker := NewThinker(provider, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger)
+
+ agent := &Agent{
+ Name: agentConfig.Name,
+ Role: agentConfig.Role,
+ Model: agentConfig.Model,
+ SystemPrompt: systemPrompt,
+ Provider: provider,
+ MaxTokens: agentConfig.MaxTokens,
+ Temperature: agentConfig.Temperature,
+ taskManager: taskManager,
+ logger: logger,
+ thinker: thinker,
+ }
+
+ return agent, nil
+}
+
+// Start starts an agent to process tasks in a loop
+func (a *Agent) Start(loopInterval time.Duration) error {
+ if a.IsRunning {
+ return fmt.Errorf("agent %s is already running", a.Name)
+ }
+
+ a.IsRunning = true
+ a.StopChan = make(chan struct{})
+
+ go a.runLoop(loopInterval)
+
+ a.logger.Info("Started agent",
+ slog.String("name", a.Name),
+ slog.String("role", a.Role),
+ slog.String("model", a.Model))
+ return nil
+}
+
+func (a *Agent) Stop() {
+ close(a.StopChan)
+ a.IsRunning = false
+}
+
+func (a *Agent) runLoop(interval time.Duration) {
+ ticker := time.NewTicker(interval)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-a.StopChan:
+ a.logger.Info("Agent stopping", slog.String("name", a.Name))
+ return
+ case <-ticker.C:
+ if err := a.processTasks(); err != nil {
+ a.logger.Error("Error processing tasks for agent",
+ slog.String("agent", a.Name),
+ slog.String("error", err.Error()))
+ }
+ }
+ }
+}
+
+// processAgentTasks processes all assigned tasks for an agent
+func (a *Agent) processTasks() error {
+ if a.CurrentTask != nil {
+ return nil
+ }
+
+ // Get tasks assigned to this agent
+ tasks, err := a.taskManager.GetTasksByAssignee(a.Name)
+ if err != nil {
+ return fmt.Errorf("failed to get tasks for agent %s: %w", a.Name, err)
+ }
+
+ a.logger.Info("Processing tasks for agent",
+ slog.Int("task_count", len(tasks)),
+ slog.String("agent", a.Name))
+
+ for _, task := range tasks {
+ if task.Status == tm.StatusToDo {
+ if err := a.processTask(task); err != nil {
+ a.logger.Error("Error processing task",
+ slog.String("task_id", task.ID),
+ slog.String("error", err.Error()))
+ }
+ }
+ }
+
+ return nil
+}
+
+// processTask processes a single task with an agent
+func (a *Agent) processTask(task *tm.Task) error {
+ ctx := context.Background()
+ startTime := time.Now()
+
+ a.logger.Info("Agent processing task",
+ slog.String("agent", a.Name),
+ slog.String("task_id", task.ID),
+ slog.String("title", task.Title))
+
+ // Mark task as in progress
+ task.Status = tm.StatusInProgress
+ a.CurrentTask = &task.ID
+
+ // Check if this task should generate subtasks (with LLM decision)
+ if a.thinker.ShouldGenerateSubtasks(task) {
+ err := a.processSubtask(ctx, task)
+ if err == nil {
+ a.logger.Info("Task converted to subtasks by agent using LLM analysis",
+ slog.String("task_id", task.ID),
+ slog.String("agent", a.Name))
+ return nil
+ }
+ a.logger.Error("Error processing subtask",
+ slog.String("task_id", task.ID),
+ slog.String("error", err.Error()))
+ }
+
+ err := a.processSolution(ctx, task)
+ if err != nil {
+ return fmt.Errorf("failed to process solution for task: %w", err)
+ }
+ duration := time.Since(startTime)
+ a.logger.Info("Task completed by agent",
+ slog.String("task_id", task.ID),
+ slog.String("agent", a.Name),
+ slog.Duration("duration", duration))
+ return nil
+}
+
+func (a *Agent) processSubtask(ctx context.Context, task *tm.Task) error {
+ a.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
+ analysis, err := a.thinker.GenerateSubtasksForTask(ctx, task)
+ if err != nil {
+ return fmt.Errorf("failed to generate subtasks for task: %w", err)
+ }
+
+ solutionURL, err2 := a.taskManager.ProposeSubTasks(ctx, task, analysis)
+ if err2 != nil {
+ return fmt.Errorf("failed to propose subtasks for task: %w", err2)
+ }
+ task.SolutionURL = solutionURL
+
+ a.logger.Info("Generated subtask Solution for task",
+ slog.String("task_id", task.ID),
+ slog.String("solution_url", solutionURL))
+ a.logger.Info("Proposed subtasks and new agents for task",
+ slog.String("task_id", task.ID),
+ slog.Int("subtask_count", len(analysis.Subtasks)),
+ slog.Int("new_agent_count", len(analysis.AgentCreations)))
+
+ // Log proposed new agents if any
+ if len(analysis.AgentCreations) > 0 {
+ for _, agent := range analysis.AgentCreations {
+ a.logger.Info("Proposed new agent",
+ slog.String("role", agent.Role),
+ slog.Any("skills", agent.Skills))
+ }
+ }
+
+ return nil
+}
+
+func (a *Agent) processSolution(ctx context.Context, task *tm.Task) error {
+ solution, err := a.thinker.GenerateSolution(ctx, task)
+ if err != nil {
+ return fmt.Errorf("failed to generate solution: %w", err)
+ }
+
+ solutionURL, err := a.taskManager.ProposeSolution(ctx, task, solution, a.Name)
+ if err != nil {
+ return fmt.Errorf("failed to propose solution: %w", err)
+ }
+ task.SolutionURL = solutionURL
+
+ a.logger.Info("Generated Solution for task",
+ slog.String("task_id", task.ID),
+ slog.String("agent", a.Name),
+ slog.String("solution_url", solutionURL))
+ return nil
+}
+
+// loadSystemPrompt loads the system prompt from file
+func loadSystemPrompt(filePath string) (string, error) {
+ content, err := os.ReadFile(filePath)
+ if err != nil {
+ return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
+ }
+ return string(content), nil
+}
diff --git a/server/agent/manager.go b/server/agent/manager.go
index 191d42e..ea7474d 100644
--- a/server/agent/manager.go
+++ b/server/agent/manager.go
@@ -1,17 +1,11 @@
package agent
import (
- "context"
"fmt"
"log/slog"
- "os"
- "os/exec"
- "path/filepath"
- "strings"
"time"
"github.com/iomodo/staff/config"
- "github.com/iomodo/staff/git"
"github.com/iomodo/staff/llm"
_ "github.com/iomodo/staff/llm/providers" // Auto-register all providers
"github.com/iomodo/staff/task"
@@ -20,70 +14,31 @@
// Manager manages multiple AI agents with Git operations and task processing
type Manager struct {
- config *config.Config
- agents map[string]*Agent
- taskManager tm.TaskManager
- autoAssigner *task.AutoAssigner
- prProvider git.PullRequestProvider
- cloneManager *git.CloneManager
- subtaskService *task.SubtaskService
- isRunning map[string]bool
- stopChannels map[string]chan struct{}
- logger *slog.Logger
+ config *config.Config
+ agents map[string]*Agent
+ taskManager tm.TaskManager
+ autoAssigner *task.AutoAssigner
+ isRunning map[string]bool
+ roles []string
+ logger *slog.Logger
}
// NewManager creates a new agent manager
func NewManager(cfg *config.Config, taskManager tm.TaskManager, logger *slog.Logger) (*Manager, error) {
- if logger == nil {
- logger = slog.Default()
- }
- // Create auto-assigner
autoAssigner := task.NewAutoAssigner(cfg.Agents)
- // Create PR provider based on configuration
- var prProvider git.PullRequestProvider
- var repoURL string
-
- switch cfg.GetPrimaryGitProvider() {
- case "github":
- githubConfig := git.GitHubConfig{
- Token: cfg.GitHub.Token,
- Logger: logger,
- }
- prProvider = git.NewGitHubPullRequestProvider(cfg.GitHub.Owner, cfg.GitHub.Repo, githubConfig)
- repoURL = fmt.Sprintf("https://github.com/%s/%s.git", cfg.GitHub.Owner, cfg.GitHub.Repo)
- logger.Info("Using GitHub as pull request provider",
- slog.String("owner", cfg.GitHub.Owner),
- slog.String("repo", cfg.GitHub.Repo))
- case "gerrit":
- gerritConfig := git.GerritConfig{
- Username: cfg.Gerrit.Username,
- Password: cfg.Gerrit.Password,
- BaseURL: cfg.Gerrit.BaseURL,
- Logger: logger,
- }
- prProvider = git.NewGerritPullRequestProvider(cfg.Gerrit.Project, gerritConfig)
- repoURL = fmt.Sprintf("%s/%s", cfg.Gerrit.BaseURL, cfg.Gerrit.Project)
- logger.Info("Using Gerrit as pull request provider",
- slog.String("base_url", cfg.Gerrit.BaseURL),
- slog.String("project", cfg.Gerrit.Project))
- default:
- return nil, fmt.Errorf("no valid Git provider configured")
+ agentRoles := make([]string, 0, len(cfg.Agents))
+ for _, agentConfig := range cfg.Agents {
+ agentRoles = append(agentRoles, agentConfig.Role)
}
- // Create clone manager for per-agent Git repositories
- workspacePath := filepath.Join(".", "workspace")
- cloneManager := git.NewCloneManager(repoURL, workspacePath)
-
manager := &Manager{
config: cfg,
agents: make(map[string]*Agent),
taskManager: taskManager,
autoAssigner: autoAssigner,
- prProvider: prProvider,
- cloneManager: cloneManager,
isRunning: make(map[string]bool),
- stopChannels: make(map[string]chan struct{}),
+ roles: agentRoles,
logger: logger,
}
@@ -92,18 +47,19 @@
return nil, fmt.Errorf("failed to initialize agents: %w", err)
}
- // Initialize subtask service after agents are created
- if err := manager.initializeSubtaskService(); err != nil {
- return nil, fmt.Errorf("failed to initialize subtask service: %w", err)
- }
-
return manager, nil
}
// initializeAgents creates agent instances from configuration
func (m *Manager) initializeAgents() error {
+ llmConfig := llm.Config{
+ Provider: llm.ProviderFake, // Use fake provider for testing
+ APIKey: m.config.OpenAI.APIKey,
+ BaseURL: m.config.OpenAI.BaseURL,
+ Timeout: m.config.OpenAI.Timeout,
+ }
for _, agentConfig := range m.config.Agents {
- agent, err := m.createAgent(agentConfig)
+ agent, err := NewAgent(agentConfig, llmConfig, m.taskManager, m.roles, m.logger)
if err != nil {
return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
}
@@ -112,95 +68,25 @@
return nil
}
-// initializeSubtaskService creates the subtask service with available agent roles
-func (m *Manager) initializeSubtaskService() error {
- // Get agent roles from configuration
- agentRoles := make([]string, 0, len(m.config.Agents))
- for _, agentConfig := range m.config.Agents {
- agentRoles = append(agentRoles, agentConfig.Name)
+func (m *Manager) StartAllAgents() {
+ // Start all configured agents with a default loop interval
+ defaultInterval := 1 * time.Second
+
+ for _, a := range m.agents {
+ m.logger.Info("Starting agent",
+ slog.String("name", a.Name),
+ slog.String("role", a.Role),
+ slog.String("model", a.Model))
+ if err := a.Start(defaultInterval); err != nil {
+ m.logger.Error("Failed to start agent",
+ slog.String("agent", a.Name),
+ slog.String("error", err.Error()))
+ continue
+ }
+ m.isRunning[a.Name] = true
}
-
- // Use the first agent's LLM provider for subtask analysis
- if len(m.agents) == 0 {
- return fmt.Errorf("no agents available for subtask service")
- }
-
- var firstAgent *Agent
- for _, agent := range m.agents {
- firstAgent = agent
- break
- }
-
- // Get owner and repo for subtask service based on provider
- var owner, repo string
- switch m.config.GetPrimaryGitProvider() {
- case "github":
- owner = m.config.GitHub.Owner
- repo = m.config.GitHub.Repo
- case "gerrit":
- owner = m.config.Gerrit.Project
- repo = m.config.Gerrit.Project
- }
-
- m.subtaskService = task.NewSubtaskService(
- firstAgent.Provider,
- m.taskManager,
- agentRoles,
- m.prProvider,
- owner,
- repo,
- m.cloneManager,
- m.logger,
- )
-
- return nil
}
-// createAgent creates a single agent instance
-func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
- // Load system prompt
- systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
- if err != nil {
- return nil, fmt.Errorf("failed to load system prompt: %w", err)
- }
-
- // Create LLM provider
- llmConfig := llm.Config{
- Provider: llm.ProviderFake, // Use fake provider for testing
- APIKey: m.config.OpenAI.APIKey,
- BaseURL: m.config.OpenAI.BaseURL,
- Timeout: m.config.OpenAI.Timeout,
- }
-
- provider, err := llm.CreateProvider(llmConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to create LLM provider: %w", err)
- }
-
- agent := &Agent{
- Name: agentConfig.Name,
- Role: agentConfig.Role,
- Model: agentConfig.Model,
- SystemPrompt: systemPrompt,
- Provider: provider,
- MaxTokens: agentConfig.MaxTokens,
- Temperature: agentConfig.Temperature,
- Stats: AgentStats{},
- }
-
- return agent, nil
-}
-
-// loadSystemPrompt loads the system prompt from file
-func (m *Manager) loadSystemPrompt(filePath string) (string, error) {
- content, err := os.ReadFile(filePath)
- if err != nil {
- return "", fmt.Errorf("failed to read system prompt file %s: %w", filePath, err)
- }
- return string(content), nil
-}
-
-// StartAgent starts an agent to process tasks in a loop
func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
agent, exists := m.agents[agentName]
if !exists {
@@ -211,409 +97,28 @@
return fmt.Errorf("agent %s is already running", agentName)
}
- stopChan := make(chan struct{})
- m.stopChannels[agentName] = stopChan
+ agent.Start(loopInterval)
m.isRunning[agentName] = true
-
- go m.runAgentLoop(agent, loopInterval, stopChan)
-
- m.logger.Info("Started agent",
- slog.String("name", agentName),
- slog.String("role", agent.Role),
- slog.String("model", agent.Model))
return nil
}
// StopAgent stops a running agent
func (m *Manager) StopAgent(agentName string) error {
+ agent, exists := m.agents[agentName]
+ if !exists {
+ return fmt.Errorf("agent %s not found", agentName)
+ }
if !m.isRunning[agentName] {
return fmt.Errorf("agent %s is not running", agentName)
}
- close(m.stopChannels[agentName])
- delete(m.stopChannels, agentName)
+ agent.Stop()
m.isRunning[agentName] = false
m.logger.Info("Stopped agent", slog.String("name", agentName))
return nil
}
-// runAgentLoop runs the main processing loop for an agent
-func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
- ticker := time.NewTicker(interval)
- defer ticker.Stop()
-
- for {
- select {
- case <-stopChan:
- m.logger.Info("Agent stopping", slog.String("name", agent.Name))
- return
- case <-ticker.C:
- if err := m.processAgentTasks(agent); err != nil {
- m.logger.Error("Error processing tasks for agent",
- slog.String("agent", agent.Name),
- slog.String("error", err.Error()))
- }
- }
- }
-}
-
-// processAgentTasks processes all assigned tasks for an agent
-func (m *Manager) processAgentTasks(agent *Agent) error {
- if agent.CurrentTask != nil {
- return nil
- }
-
- // Get tasks assigned to this agent
- tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
- if err != nil {
- return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
- }
-
- m.logger.Info("Processing tasks for agent",
- slog.Int("task_count", len(tasks)),
- slog.String("agent", agent.Name))
-
- for _, task := range tasks {
- if task.Status == tm.StatusToDo {
- if err := m.processTask(agent, task); err != nil {
- m.logger.Error("Error processing task",
- slog.String("task_id", task.ID),
- slog.String("error", err.Error()))
- // Mark task as failed
- task.Status = tm.StatusFailed
- if err := m.taskManager.UpdateTask(task); err != nil {
- m.logger.Error("Error updating failed task",
- slog.String("task_id", task.ID),
- slog.String("error", err.Error()))
- }
- agent.Stats.TasksFailed++
- } else {
- agent.Stats.TasksCompleted++
- }
- // Update success rate
- total := agent.Stats.TasksCompleted + agent.Stats.TasksFailed
- if total > 0 {
- agent.Stats.SuccessRate = float64(agent.Stats.TasksCompleted) / float64(total) * 100
- }
- }
- }
-
- return nil
-}
-
-// processTask processes a single task with an agent
-func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
- ctx := context.Background()
- startTime := time.Now()
-
- m.logger.Info("Agent processing task",
- slog.String("agent", agent.Name),
- slog.String("task_id", task.ID),
- slog.String("title", task.Title))
-
- // Mark task as in progress
- task.Status = tm.StatusInProgress
- agent.CurrentTask = &task.ID
-
- // Check if this task should generate subtasks (with LLM decision)
- if m.shouldGenerateSubtasks(task) {
- m.logger.Info("LLM determined task should generate subtasks", slog.String("task_id", task.ID))
- if err := m.generateSubtasksForTask(ctx, task); err != nil {
- m.logger.Warn("Failed to generate subtasks for task",
- slog.String("task_id", task.ID),
- slog.String("error", err.Error()))
- } else {
- m.logger.Info("Task converted to subtasks by agent using LLM analysis",
- slog.String("task_id", task.ID),
- slog.String("agent", agent.Name))
- return nil
- }
- }
-
- // Generate solution using LLM
- solution, err := m.generateSolution(ctx, agent, task)
- if err != nil {
- return fmt.Errorf("failed to generate solution: %w", err)
- }
-
- // Create Git branch and commit solution
- branchName := m.generateBranchName(task)
- if err := m.createAndCommitSolution(branchName, task, solution, agent); err != nil {
- return fmt.Errorf("failed to commit solution: %w", err)
- }
-
- // Create pull request
- prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
- if err != nil {
- return fmt.Errorf("failed to create pull request: %w", err)
- }
-
- // Update agent stats
- duration := time.Since(startTime)
- if agent.Stats.AvgTime == 0 {
- agent.Stats.AvgTime = duration.Milliseconds()
- } else {
- agent.Stats.AvgTime = (agent.Stats.AvgTime + duration.Milliseconds()) / 2
- }
-
- m.logger.Info("Task completed by agent",
- slog.String("task_id", task.ID),
- slog.String("agent", agent.Name),
- slog.Duration("duration", duration),
- slog.String("pr_url", prURL))
- return nil
-}
-
-// generateSolution uses the agent's LLM to generate a solution
-func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
- prompt := m.buildTaskPrompt(task)
-
- req := llm.ChatCompletionRequest{
- Model: agent.Model,
- Messages: []llm.Message{
- {
- Role: llm.RoleSystem,
- Content: agent.SystemPrompt,
- },
- {
- Role: llm.RoleUser,
- Content: prompt,
- },
- },
- MaxTokens: agent.MaxTokens,
- Temperature: agent.Temperature,
- }
-
- resp, err := agent.Provider.ChatCompletion(ctx, req)
- if err != nil {
- return "", fmt.Errorf("LLM request failed: %w", err)
- }
-
- if len(resp.Choices) == 0 {
- return "", fmt.Errorf("no response from LLM")
- }
-
- return resp.Choices[0].Message.Content, nil
-}
-
-// buildTaskPrompt creates a detailed prompt for the LLM
-func (m *Manager) buildTaskPrompt(task *tm.Task) string {
- return fmt.Sprintf(`Task: %s
-
-Priority: %s
-Description: %s
-
-Please provide a complete solution for this task. Include:
-1. Detailed implementation plan
-2. Code changes needed (if applicable)
-3. Files to be created or modified
-4. Testing considerations
-5. Any dependencies or prerequisites
-
-Your response should be comprehensive and actionable.`,
- task.Title,
- task.Priority,
- task.Description)
-}
-
-// generateBranchName creates a Git branch name for the task
-func (m *Manager) generateBranchName(task *tm.Task) string {
- // Clean title for use in branch name
- cleanTitle := strings.ToLower(task.Title)
- cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
- cleanTitle = strings.ReplaceAll(cleanTitle, "/", "-")
- // Remove special characters
- var result strings.Builder
- for _, r := range cleanTitle {
- if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') || r == '-' {
- result.WriteRune(r)
- }
- }
- cleanTitle = result.String()
-
- // Limit length
- if len(cleanTitle) > 40 {
- cleanTitle = cleanTitle[:40]
- }
-
- return fmt.Sprintf("%s%s-%s", m.config.Git.BranchPrefix, task.ID, cleanTitle)
-}
-
-// createAndCommitSolution creates a Git branch and commits the solution using per-agent clones
-func (m *Manager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *Agent) error {
- ctx := context.Background()
-
- // Get agent's dedicated Git clone
- clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
- if err != nil {
- return fmt.Errorf("failed to get agent clone: %w", err)
- }
-
- m.logger.Info("Agent working in clone",
- slog.String("agent", agent.Name),
- slog.String("clone_path", clonePath))
-
- // Refresh the clone with latest changes
- if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
- m.logger.Warn("Failed to refresh clone for agent",
- slog.String("agent", agent.Name),
- slog.String("error", err.Error()))
- }
-
- // All Git operations use the agent's clone directory
- gitCmd := func(args ...string) *exec.Cmd {
- return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
- }
-
- // Ensure we're on main branch before creating new branch
- cmd := gitCmd("checkout", "main")
- if err := cmd.Run(); err != nil {
- // Try master branch if main doesn't exist
- cmd = gitCmd("checkout", "master")
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to checkout main/master branch: %w", err)
- }
- }
-
- // Create branch
- cmd = gitCmd("checkout", "-b", branchName)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to create branch: %w", err)
- }
-
- // Create solution file in agent's clone
- solutionDir := filepath.Join(clonePath, "tasks", "solutions")
- if err := os.MkdirAll(solutionDir, 0755); err != nil {
- return fmt.Errorf("failed to create solution directory: %w", err)
- }
-
- solutionFile := filepath.Join(solutionDir, fmt.Sprintf("%s-solution.md", task.ID))
- solutionContent := fmt.Sprintf(`# Solution for Task: %s
-
-**Agent:** %s (%s)
-**Model:** %s
-**Completed:** %s
-
-## Task Description
-%s
-
-## Solution
-%s
-
----
-*Generated by Staff AI Agent System*
-`, task.Title, agent.Name, agent.Role, agent.Model, time.Now().Format(time.RFC3339), task.Description, solution)
-
- if err := os.WriteFile(solutionFile, []byte(solutionContent), 0644); err != nil {
- return fmt.Errorf("failed to write solution file: %w", err)
- }
-
- // Stage files
- relativeSolutionFile := filepath.Join("tasks", "solutions", fmt.Sprintf("%s-solution.md", task.ID))
- cmd = gitCmd("add", relativeSolutionFile)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to stage files: %w", err)
- }
-
- // Commit changes
- commitMsg := m.buildCommitMessage(task, agent)
- cmd = gitCmd("commit", "-m", commitMsg)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to commit: %w", err)
- }
-
- // Push branch
- cmd = gitCmd("push", "-u", "origin", branchName)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to push branch: %w", err)
- }
-
- m.logger.Info("Agent successfully pushed branch",
- slog.String("agent", agent.Name),
- slog.String("branch", branchName))
- return nil
-}
-
-// buildCommitMessage creates a commit message from template
-func (m *Manager) buildCommitMessage(task *tm.Task, agent *Agent) string {
- template := m.config.Git.CommitMessageTemplate
-
- replacements := map[string]string{
- "{task_id}": task.ID,
- "{task_title}": task.Title,
- "{agent_name}": agent.Name,
- "{solution}": "See solution file for details",
- }
-
- result := template
- for placeholder, value := range replacements {
- result = strings.ReplaceAll(result, placeholder, value)
- }
-
- return result
-}
-
-// createPullRequest creates a GitHub pull request
-func (m *Manager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *Agent, branchName string) (string, error) {
- title := fmt.Sprintf("Task %s: %s", task.ID, task.Title)
-
- // Build PR description from template
- description := m.buildPRDescription(task, solution, agent)
-
- options := git.PullRequestOptions{
- Title: title,
- Description: description,
- HeadBranch: branchName,
- BaseBranch: "main",
- Labels: []string{"ai-generated", "staff-agent", strings.ToLower(agent.Role)},
- Draft: false,
- }
-
- pr, err := m.prProvider.CreatePullRequest(ctx, options)
- if err != nil {
- return "", fmt.Errorf("failed to create PR: %w", err)
- }
-
- // Generate provider-specific PR URL
- switch m.config.GetPrimaryGitProvider() {
- case "github":
- return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
- case "gerrit":
- return fmt.Sprintf("%s/c/%s/+/%d", m.config.Gerrit.BaseURL, m.config.Gerrit.Project, pr.Number), nil
- default:
- return "", fmt.Errorf("unknown git provider")
- }
-}
-
-// buildPRDescription creates PR description from template
-func (m *Manager) buildPRDescription(task *tm.Task, solution string, agent *Agent) string {
- template := m.config.Git.PRTemplate
-
- // Truncate solution for PR if too long
- truncatedSolution := solution
- if len(solution) > 1000 {
- truncatedSolution = solution[:1000] + "...\n\n*See solution file for complete details*"
- }
-
- replacements := map[string]string{
- "{task_id}": task.ID,
- "{task_title}": task.Title,
- "{task_description}": task.Description,
- "{agent_name}": fmt.Sprintf("%s (%s)", agent.Name, agent.Role),
- "{priority}": string(task.Priority),
- "{solution}": truncatedSolution,
- "{files_changed}": fmt.Sprintf("- `tasks/solutions/%s-solution.md`", task.ID),
- }
-
- result := template
- for placeholder, value := range replacements {
- result = strings.ReplaceAll(result, placeholder, value)
- }
-
- return result
-}
-
// AutoAssignTask automatically assigns a task to the best matching agent
func (m *Manager) AutoAssignTask(taskID string) error {
task, err := m.taskManager.GetTask(taskID)
@@ -640,108 +145,6 @@
return nil
}
-// GetAgentStatus returns the status of all agents
-func (m *Manager) GetAgentStatus() map[string]AgentInfo {
- status := make(map[string]AgentInfo)
-
- for name, agent := range m.agents {
- agentStatus := StatusIdle
- if m.isRunning[name] {
- if agent.CurrentTask != nil {
- agentStatus = StatusRunning
- }
- } else {
- agentStatus = StatusStopped
- }
-
- status[name] = AgentInfo{
- Name: agent.Name,
- Role: agent.Role,
- Model: agent.Model,
- Status: agentStatus,
- CurrentTask: agent.CurrentTask,
- Stats: agent.Stats,
- }
- }
-
- return status
-}
-
-// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
-func (m *Manager) shouldGenerateSubtasks(task *tm.Task) bool {
- // Don't generate subtasks for subtasks
- if task.ParentTaskID != "" {
- return false
- }
-
- // Don't generate if already evaluated
- if task.SubtasksEvaluated {
- return false
- }
-
- // Ask LLM to decide
- ctx := context.Background()
- decision, err := m.subtaskService.ShouldGenerateSubtasks(ctx, task)
- if err != nil {
- m.logger.Warn("Failed to get LLM subtask decision for task",
- slog.String("task_id", task.ID),
- slog.String("error", err.Error()))
- // Fallback to simple heuristics
- return task.Priority == tm.PriorityHigh || len(task.Description) > 200
- }
-
- task.SubtasksEvaluated = true
- m.logger.Info("LLM subtask decision for task",
- slog.String("task_id", task.ID),
- slog.Bool("needs_subtasks", decision.NeedsSubtasks),
- slog.Int("complexity_score", decision.ComplexityScore),
- slog.String("reasoning", decision.Reasoning))
-
- return decision.NeedsSubtasks
-}
-
-// generateSubtasksForTask analyzes a task and creates a PR with proposed subtasks
-func (m *Manager) generateSubtasksForTask(ctx context.Context, task *tm.Task) error {
- if m.subtaskService == nil {
- return fmt.Errorf("subtask service not initialized")
- }
-
- // Analyze the task for subtasks
- analysis, err := m.subtaskService.AnalyzeTaskForSubtasks(ctx, task)
- if err != nil {
- return fmt.Errorf("failed to analyze task for subtasks: %w", err)
- }
-
- // Generate a PR with the subtask proposals
- prURL, err := m.subtaskService.GenerateSubtaskPR(ctx, analysis)
- if err != nil {
- return fmt.Errorf("failed to generate subtask PR: %w", err)
- }
-
- // Update the task with subtask information
- task.SubtasksPRURL = prURL
- task.SubtasksGenerated = true
-
- m.logger.Info("Generated subtask PR for task",
- slog.String("task_id", task.ID),
- slog.String("pr_url", prURL))
- m.logger.Info("Proposed subtasks and new agents for task",
- slog.String("task_id", task.ID),
- slog.Int("subtask_count", len(analysis.Subtasks)),
- slog.Int("new_agent_count", len(analysis.AgentCreations)))
-
- // Log proposed new agents if any
- if len(analysis.AgentCreations) > 0 {
- for _, agent := range analysis.AgentCreations {
- m.logger.Info("Proposed new agent",
- slog.String("role", agent.Role),
- slog.Any("skills", agent.Skills))
- }
- }
-
- return nil
-}
-
// IsAgentRunning checks if an agent is currently running
func (m *Manager) IsAgentRunning(agentName string) bool {
return m.isRunning[agentName]
@@ -764,18 +167,5 @@
slog.String("error", err.Error()))
}
}
-
- // Cleanup all agent Git clones
- if err := m.cloneManager.CleanupAllClones(); err != nil {
- m.logger.Error("Error cleaning up agent clones", slog.String("error", err.Error()))
- }
-
- // Cleanup subtask service
- if m.subtaskService != nil {
- if err := m.subtaskService.Close(); err != nil {
- m.logger.Error("Error closing subtask service", slog.String("error", err.Error()))
- }
- }
-
return nil
}
diff --git a/server/agent/thinker.go b/server/agent/thinker.go
new file mode 100644
index 0000000..8e70230
--- /dev/null
+++ b/server/agent/thinker.go
@@ -0,0 +1,522 @@
+package agent
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+ "strings"
+
+ "github.com/iomodo/staff/llm"
+ "github.com/iomodo/staff/tm"
+ "golang.org/x/text/cases"
+ "golang.org/x/text/language"
+)
+
+type Thinker struct {
+ roles []string
+ llmProvider llm.LLMProvider
+ model string // TODO: abstract away in llmProvider
+ systemPrompt string // TODO abstract away in llmProvider
+ maxTokens int
+ temperature float64
+ logger *slog.Logger
+}
+
+func NewThinker(llmProvider llm.LLMProvider, model string, systemPrompt string, maxTokens int, temperature float64, roles []string, logger *slog.Logger) *Thinker {
+ return &Thinker{llmProvider: llmProvider, model: model, maxTokens: maxTokens, temperature: temperature, roles: roles, logger: logger}
+}
+
+// shouldGenerateSubtasks determines if a task should be broken down into subtasks using LLM
+func (t *Thinker) ShouldGenerateSubtasks(task *tm.Task) bool {
+ // Don't generate subtasks for subtasks
+ if task.ParentTaskID != "" {
+ return false
+ }
+
+ // Don't generate if already evaluated
+ if task.SubtasksEvaluated {
+ return false
+ }
+
+ // Ask LLM to decide
+ ctx := context.Background()
+ decision, err := t.shouldGenerateSubtasks(ctx, task)
+ if err != nil {
+ t.logger.Warn("Failed to get LLM subtask decision for task",
+ slog.String("task_id", task.ID),
+ slog.String("error", err.Error()))
+ // Fallback to simple heuristics
+ return task.Priority == tm.PriorityHigh || len(task.Description) > 200
+ }
+
+ task.SubtasksEvaluated = true
+ t.logger.Info("LLM subtask decision for task",
+ slog.String("task_id", task.ID),
+ slog.Bool("needs_subtasks", decision.NeedsSubtasks),
+ slog.Int("complexity_score", decision.ComplexityScore),
+ slog.String("reasoning", decision.Reasoning))
+
+ return decision.NeedsSubtasks
+}
+
+// AnalyzeTaskForSubtasks uses LLM to analyze a task and propose subtasks
+func (t *Thinker) GenerateSubtasksForTask(ctx context.Context, task *tm.Task) (*tm.SubtaskAnalysis, error) {
+ prompt := buildSubtaskAnalysisPrompt(task)
+
+ req := llm.ChatCompletionRequest{
+ Model: t.model,
+ Messages: []llm.Message{
+ {
+ Role: llm.RoleSystem,
+ Content: getSubtaskAnalysisSystemPrompt(t.roles),
+ },
+ {
+ Role: llm.RoleUser,
+ Content: prompt,
+ },
+ },
+ MaxTokens: &[]int{4000}[0],
+ Temperature: &[]float64{0.3}[0],
+ }
+
+ resp, err := t.llmProvider.ChatCompletion(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("LLM analysis failed: %w", err)
+ }
+
+ if len(resp.Choices) == 0 {
+ return nil, fmt.Errorf("no response from LLM")
+ }
+
+ // Parse the LLM response
+ analysis, err := parseSubtaskAnalysis(resp.Choices[0].Message.Content, task.ID, t.roles, t.logger)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse LLM response: %w", err)
+ }
+
+ return analysis, nil
+}
+
+// generateSolution uses the agent's LLM to generate a solution
+func (t *Thinker) GenerateSolution(ctx context.Context, task *tm.Task) (string, error) {
+ prompt := buildTaskPrompt(task)
+
+ req := llm.ChatCompletionRequest{
+ Model: t.model,
+ Messages: []llm.Message{
+ {
+ Role: llm.RoleSystem,
+ Content: t.systemPrompt,
+ },
+ {
+ Role: llm.RoleUser,
+ Content: prompt,
+ },
+ },
+ MaxTokens: &t.maxTokens,
+ Temperature: &t.temperature,
+ }
+
+ resp, err := t.llmProvider.ChatCompletion(ctx, req)
+ if err != nil {
+ return "", fmt.Errorf("LLM request failed: %w", err)
+ }
+
+ if len(resp.Choices) == 0 {
+ return "", fmt.Errorf("no response from LLM")
+ }
+
+ return resp.Choices[0].Message.Content, nil
+}
+
+// ShouldGenerateSubtasks asks LLM whether a task needs subtasks based on existing agents
+func (t *Thinker) shouldGenerateSubtasks(ctx context.Context, task *tm.Task) (*tm.SubtaskDecision, error) {
+ prompt := buildSubtaskDecisionPrompt(task)
+
+ req := llm.ChatCompletionRequest{
+ Model: t.model,
+ Messages: []llm.Message{
+ {
+ Role: llm.RoleSystem,
+ Content: getSubtaskDecisionSystemPrompt(t.roles),
+ },
+ {
+ Role: llm.RoleUser,
+ Content: prompt,
+ },
+ },
+ MaxTokens: &[]int{1000}[0],
+ Temperature: &[]float64{0.3}[0],
+ }
+
+ resp, err := t.llmProvider.ChatCompletion(ctx, req)
+ if err != nil {
+ return nil, fmt.Errorf("LLM decision failed: %w", err)
+ }
+
+ if len(resp.Choices) == 0 {
+ return nil, fmt.Errorf("no response from LLM")
+ }
+
+ // Parse the LLM response
+ decision, err := parseSubtaskDecision(resp.Choices[0].Message.Content)
+ if err != nil {
+ return nil, fmt.Errorf("failed to parse LLM decision: %w", err)
+ }
+
+ return decision, nil
+}
+
+func buildSubtaskDecisionPrompt(task *tm.Task) string {
+ return fmt.Sprintf(`Please evaluate whether the following task needs to be broken down into subtasks:
+
+**Task Title:** %s
+
+**Description:** %s
+
+**Priority:** %s
+
+**Current Status:** %s
+
+Consider:
+- Can this be completed by a single agent with existing capabilities?
+- Does it require multiple specialized skills?
+- Is the scope too large for one person?
+- Are there logical components that could be parallelized?
+
+Provide your decision in the JSON format specified in the system prompt.`,
+ task.Title,
+ task.Description,
+ task.Priority,
+ task.Status)
+}
+
+func getSubtaskDecisionSystemPrompt(roles []string) string {
+ availableRoles := strings.Join(roles, ", ")
+
+ return fmt.Sprintf(`You are an expert project manager and task analyst. Your job is to determine whether a task needs to be broken down into subtasks.
+
+Currently available team roles and their capabilities: %s
+
+When evaluating a task, consider:
+1. Task complexity and scope
+2. Whether multiple specialized skills are needed
+3. If the task can be completed by a single agent with current capabilities
+4. Whether new agent roles might be needed for specialized skills
+
+Respond with a JSON object in this exact format:
+{
+ "needs_subtasks": true/false,
+ "reasoning": "Clear explanation of why subtasks are or aren't needed",
+ "complexity_score": 5,
+ "required_skills": ["skill1", "skill2", "skill3"]
+}
+
+Complexity score should be 1-10 where:
+- 1-3: Simple tasks that can be handled by one agent
+- 4-6: Moderate complexity, might benefit from subtasks
+- 7-10: Complex tasks that definitely need breaking down
+
+Required skills should list all technical/domain skills needed to complete the task.`, availableRoles)
+}
+
+func parseSubtaskDecision(response string) (*tm.SubtaskDecision, error) {
+ // Try to extract JSON from the response
+ jsonStart := strings.Index(response, "{")
+ jsonEnd := strings.LastIndex(response, "}")
+
+ if jsonStart == -1 || jsonEnd == -1 {
+ return nil, fmt.Errorf("no JSON found in LLM response")
+ }
+
+ jsonStr := response[jsonStart : jsonEnd+1]
+
+ var decision tm.SubtaskDecision
+ if err := json.Unmarshal([]byte(jsonStr), &decision); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
+ }
+
+ return &decision, nil
+}
+
+func buildSubtaskAnalysisPrompt(task *tm.Task) string {
+ return fmt.Sprintf(`Please analyze the following task and break it down into subtasks:
+
+**Task Title:** %s
+
+**Description:** %s
+
+**Priority:** %s
+
+**Current Status:** %s
+
+Please analyze this task and provide a detailed breakdown into subtasks. Consider:
+- Technical complexity and requirements
+- Logical task dependencies
+- Appropriate skill sets needed for each subtask
+- Risk factors and potential blockers
+- Estimated effort for each component
+
+Provide the analysis in the JSON format specified in the system prompt.`,
+ task.Title,
+ task.Description,
+ task.Priority,
+ task.Status)
+}
+
+func getSubtaskAnalysisSystemPrompt(roles []string) string {
+ availableRoles := strings.Join(roles, ", ")
+
+ return fmt.Sprintf(`You are an expert project manager and technical architect. Your job is to analyze complex tasks and break them down into well-defined subtasks that can be assigned to specialized team members.
+
+Currently available team roles: %s
+
+When analyzing a task, you should:
+1. Understand the task requirements and scope
+2. Break it down into logical, manageable subtasks
+3. Assign each subtask to the most appropriate team role OR propose creating new agents
+4. Estimate effort and identify dependencies
+5. Provide a clear execution strategy
+
+If you need specialized skills not covered by existing roles, propose new agent creation.
+
+Respond with a JSON object in this exact format:
+{
+ "analysis_summary": "Brief analysis of the task and approach",
+ "subtasks": [
+ {
+ "title": "Subtask title",
+ "description": "Detailed description of what needs to be done",
+ "priority": "high|medium|low",
+ "assigned_to": "role_name",
+ "estimated_hours": 8,
+ "dependencies": ["subtask_index_1", "subtask_index_2"],
+ "required_skills": ["skill1", "skill2"]
+ }
+ ],
+ "agent_creations": [
+ {
+ "role": "new_role_name",
+ "skills": ["specialized_skill1", "specialized_skill2"],
+ "description": "Description of what this agent does",
+ "justification": "Why this new agent is needed"
+ }
+ ],
+ "recommended_approach": "High-level strategy for executing these subtasks",
+ "estimated_total_hours": 40,
+ "risk_assessment": "Potential risks and mitigation strategies"
+}
+
+For existing roles, use: %s
+For new agents, propose appropriate role names and skill sets.
+Dependencies should reference subtask indices (e.g., ["0", "1"] means depends on first and second subtasks).`, availableRoles, availableRoles)
+}
+
+func parseSubtaskAnalysis(response string, parentTaskID string, agentRoles []string, logger *slog.Logger) (*tm.SubtaskAnalysis, error) {
+ // Try to extract JSON from the response (LLM might wrap it in markdown)
+ jsonStart := strings.Index(response, "{")
+ jsonEnd := strings.LastIndex(response, "}")
+
+ if jsonStart == -1 || jsonEnd == -1 {
+ return nil, fmt.Errorf("no JSON found in LLM response")
+ }
+
+ jsonStr := response[jsonStart : jsonEnd+1]
+
+ var rawAnalysis struct {
+ AnalysisSummary string `json:"analysis_summary"`
+ Subtasks []struct {
+ Title string `json:"title"`
+ Description string `json:"description"`
+ Priority string `json:"priority"`
+ AssignedTo string `json:"assigned_to"`
+ EstimatedHours int `json:"estimated_hours"`
+ Dependencies []string `json:"dependencies"`
+ RequiredSkills []string `json:"required_skills"`
+ } `json:"subtasks"`
+ AgentCreations []tm.AgentCreationProposal `json:"agent_creations"`
+ RecommendedApproach string `json:"recommended_approach"`
+ EstimatedTotalHours int `json:"estimated_total_hours"`
+ RiskAssessment string `json:"risk_assessment"`
+ }
+
+ if err := json.Unmarshal([]byte(jsonStr), &rawAnalysis); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal JSON: %w", err)
+ }
+
+ // Convert to our types
+ analysis := &tm.SubtaskAnalysis{
+ ParentTaskID: parentTaskID,
+ AnalysisSummary: rawAnalysis.AnalysisSummary,
+ AgentCreations: rawAnalysis.AgentCreations,
+ RecommendedApproach: rawAnalysis.RecommendedApproach,
+ EstimatedTotalHours: rawAnalysis.EstimatedTotalHours,
+ RiskAssessment: rawAnalysis.RiskAssessment,
+ }
+
+ // Convert subtasks
+ for _, st := range rawAnalysis.Subtasks {
+ priority := tm.PriorityMedium // default
+ switch strings.ToLower(st.Priority) {
+ case "high":
+ priority = tm.PriorityHigh
+ case "low":
+ priority = tm.PriorityLow
+ }
+
+ subtask := tm.SubtaskProposal{
+ Title: st.Title,
+ Description: st.Description,
+ Priority: priority,
+ AssignedTo: st.AssignedTo,
+ EstimatedHours: st.EstimatedHours,
+ Dependencies: st.Dependencies,
+ RequiredSkills: st.RequiredSkills,
+ }
+
+ analysis.Subtasks = append(analysis.Subtasks, subtask)
+ }
+
+ // Validate agent assignments and handle new agent creation
+ if err := validateAndHandleAgentAssignments(analysis, agentRoles, logger); err != nil {
+ logger.Warn("Warning during agent assignment handling", slog.String("error", err.Error()))
+ }
+
+ return analysis, nil
+}
+
+func validateAndHandleAgentAssignments(analysis *tm.SubtaskAnalysis, agentRoles []string, logger *slog.Logger) error {
+ // Collect all agent roles that will be available (existing + proposed new ones)
+ availableRoles := make(map[string]bool)
+ for _, role := range agentRoles {
+ availableRoles[role] = true
+ }
+
+ // Add proposed new agent roles
+ for _, agentCreation := range analysis.AgentCreations {
+ availableRoles[agentCreation.Role] = true
+
+ // Create a subtask for agent creation
+ agentCreationSubtask := tm.SubtaskProposal{
+ Title: fmt.Sprintf("Create %s Agent", cases.Title(language.English).String(agentCreation.Role)),
+ Description: fmt.Sprintf("Create and configure a new %s agent with skills: %s. %s", agentCreation.Role, strings.Join(agentCreation.Skills, ", "), agentCreation.Justification),
+ Priority: tm.PriorityHigh, // Agent creation is high priority
+ AssignedTo: "ceo", // CEO creates new agents
+ EstimatedHours: 4, // Estimated time to set up new agent
+ Dependencies: []string{}, // No dependencies for agent creation
+ RequiredSkills: []string{"agent_configuration", "system_design"},
+ }
+
+ // Insert at the beginning so agent creation happens first
+ analysis.Subtasks = append([]tm.SubtaskProposal{agentCreationSubtask}, analysis.Subtasks...)
+
+ // Update dependencies to account for the new subtask at index 0
+ for i := 1; i < len(analysis.Subtasks); i++ {
+ for j, dep := range analysis.Subtasks[i].Dependencies {
+ // Convert dependency index and increment by 1
+ if depIndex := parseDependencyIndex(dep); depIndex >= 0 {
+ analysis.Subtasks[i].Dependencies[j] = fmt.Sprintf("%d", depIndex+1)
+ }
+ }
+ }
+ }
+
+ // Now validate all assignments against available roles
+ defaultRole := "ceo" // fallback role
+ if len(agentRoles) > 0 {
+ defaultRole = agentRoles[0]
+ }
+
+ for i := range analysis.Subtasks {
+ if !availableRoles[analysis.Subtasks[i].AssignedTo] {
+ logger.Warn("Unknown agent role for subtask, using default",
+ slog.String("unknown_role", analysis.Subtasks[i].AssignedTo),
+ slog.String("subtask_title", analysis.Subtasks[i].Title),
+ slog.String("assigned_role", defaultRole))
+ analysis.Subtasks[i].AssignedTo = defaultRole
+ }
+ }
+
+ return nil
+}
+
+func parseDependencyIndex(dep string) int {
+ var idx int
+ if _, err := fmt.Sscanf(dep, "%d", &idx); err == nil {
+ return idx
+ }
+ return -1 // Invalid dependency format
+}
+
+func generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
+ var content strings.Builder
+
+ content.WriteString(fmt.Sprintf("# Subtasks Created for Task %s\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("This PR creates **%d individual task files** in `/operations/tasks/` ready for agent assignment.\n\n", len(analysis.Subtasks)))
+ content.WriteString(fmt.Sprintf("✅ **Parent task `%s` has been marked as completed** - the complex task has been successfully broken down into actionable subtasks.\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
+ content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
+ content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
+
+ // List the created task files
+ content.WriteString("## Created Task Files\n\n")
+ for i, subtask := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ content.WriteString(fmt.Sprintf("### %d. `%s.md`\n", i+1, taskID))
+ content.WriteString(fmt.Sprintf("- **Title:** %s\n", subtask.Title))
+ content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+ content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+ content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+ }
+
+ if analysis.RiskAssessment != "" {
+ content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
+ }
+
+ content.WriteString("## Proposed Subtasks\n\n")
+
+ for i, subtask := range analysis.Subtasks {
+ content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
+ content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+ content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+
+ if len(subtask.Dependencies) > 0 {
+ deps := strings.Join(subtask.Dependencies, ", ")
+ content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", deps))
+ }
+
+ content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+ }
+
+ content.WriteString("---\n")
+ content.WriteString("*Generated by Staff AI Agent System*\n\n")
+ content.WriteString("**Instructions:**\n")
+ content.WriteString("- Review the proposed subtasks\n")
+ content.WriteString("- Approve or request changes\n")
+ content.WriteString("- When merged, the subtasks will be automatically created and assigned\n")
+
+ return content.String()
+}
+
+// buildTaskPrompt creates a detailed prompt for the LLM
+func buildTaskPrompt(task *tm.Task) string {
+ return fmt.Sprintf(`Task: %s
+
+Priority: %s
+Description: %s
+
+Please provide a complete solution for this task. Include:
+1. Detailed implementation plan
+2. Code changes needed (if applicable)
+3. Files to be created or modified
+4. Testing considerations
+5. Any dependencies or prerequisites
+
+Your response should be comprehensive and actionable.`,
+ task.Title,
+ task.Priority,
+ task.Description)
+}
diff --git a/server/agent/types.go b/server/agent/types.go
deleted file mode 100644
index a9f021c..0000000
--- a/server/agent/types.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package agent
-
-import (
- "github.com/iomodo/staff/llm"
-)
-
-// Agent represents an AI agent that can process tasks autonomously
-type Agent struct {
- // Identity
- Name string
- Role string
-
- // LLM Configuration
- Model string
- SystemPrompt string
- MaxTokens *int
- Temperature *float64
-
- // Runtime
- Provider llm.LLMProvider
- CurrentTask *string // Task ID currently being processed
-
- // Statistics
- Stats AgentStats
-}
-
-// AgentStats tracks agent performance metrics
-type AgentStats struct {
- TasksCompleted int `json:"tasks_completed"`
- TasksFailed int `json:"tasks_failed"`
- SuccessRate float64 `json:"success_rate"`
- AvgTime int64 `json:"avg_completion_time_seconds"`
-}
-
-// AgentStatus represents the current state of an agent
-type AgentStatus string
-
-const (
- StatusIdle AgentStatus = "idle"
- StatusRunning AgentStatus = "running"
- StatusError AgentStatus = "error"
- StatusStopped AgentStatus = "stopped"
-)
-
-// AgentInfo provides status information about an agent
-type AgentInfo struct {
- Name string `json:"name"`
- Role string `json:"role"`
- Model string `json:"model"`
- Status AgentStatus `json:"status"`
- CurrentTask *string `json:"current_task,omitempty"`
- Stats AgentStats `json:"stats"`
-}