Add Agent manager
Change-Id: Iaa68e9228165bd274f9c5be9d4320ef49a009ca8
diff --git a/server/agent/agent.go b/server/agent/agent.go
deleted file mode 100644
index 1b27831..0000000
--- a/server/agent/agent.go
+++ /dev/null
@@ -1,580 +0,0 @@
-package agent
-
-import (
- "context"
- "fmt"
- "log/slog"
- "os"
- "path/filepath"
- "strings"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- _ "github.com/iomodo/staff/llm/openai" // Import for side effects (registers provider)
- "github.com/iomodo/staff/tm"
-)
-
-// AgentConfig contains configuration for the agent
-type AgentConfig struct {
- Name string
- Role string
- GitUsername string
- GitEmail string
- WorkingDir string
-
- // LLM Configuration
- LLMProvider llm.Provider
- LLMModel string
- LLMConfig llm.Config
-
- // System prompt for the agent
- SystemPrompt string
-
- // Task Manager Configuration
- TaskManager tm.TaskManager
-
- // Git Configuration
- GitRepoPath string
- GitRemote string
- GitBranch string
-
- // Gerrit Configuration
- GerritEnabled bool
- GerritConfig GerritConfig
-}
-
-// GerritConfig holds configuration for Gerrit operations
-type GerritConfig struct {
- Username string
- Password string // Can be HTTP password or API token
- BaseURL string
- Project string
-}
-
-// Agent represents an AI agent that can process tasks
-type Agent struct {
- Config AgentConfig
- llmProvider llm.LLMProvider
- gitInterface git.GitInterface
- ctx context.Context
- cancel context.CancelFunc
- logger *slog.Logger
-}
-
-// NewAgent creates a new agent instance
-func NewAgent(config AgentConfig, logger *slog.Logger) (*Agent, error) {
- // Validate configuration
- if err := validateConfig(config); err != nil {
- return nil, fmt.Errorf("invalid config: %w", err)
- }
-
- // Create LLM provider
- llmProvider, err := llm.CreateProvider(config.LLMConfig)
- if err != nil {
- return nil, fmt.Errorf("failed to create LLM provider: %w", err)
- }
-
- // Create git interface
- var gitInterface git.GitInterface
- if config.GerritEnabled {
- // Create Gerrit pull request provider
- gerritPRProvider := git.NewGerritPullRequestProvider(config.GerritConfig.Project, git.GerritConfig{
- Username: config.GerritConfig.Username,
- Password: config.GerritConfig.Password,
- BaseURL: config.GerritConfig.BaseURL,
- HTTPClient: nil, // Will use default client
- })
-
- // Create git interface with Gerrit pull request provider
- gitConfig := git.GitConfig{
- Timeout: 30 * time.Second,
- PullRequestProvider: gerritPRProvider,
- }
- gitInterface = git.NewGitWithPullRequests(config.GitRepoPath, gitConfig, gerritPRProvider, logger)
- } else {
- // Use default git interface (GitHub)
- gitInterface = git.DefaultGit(config.GitRepoPath)
- }
-
- // Create context with cancellation
- ctx, cancel := context.WithCancel(context.Background())
-
- agent := &Agent{
- Config: config,
- llmProvider: llmProvider,
- gitInterface: gitInterface,
- ctx: ctx,
- cancel: cancel,
- logger: logger,
- }
-
- return agent, nil
-}
-
-// validateConfig validates the agent configuration
-func validateConfig(config AgentConfig) error {
- if config.Name == "" {
- return fmt.Errorf("agent name is required")
- }
- if config.Role == "" {
- return fmt.Errorf("agent role is required")
- }
- if config.WorkingDir == "" {
- return fmt.Errorf("working directory is required")
- }
- if config.SystemPrompt == "" {
- return fmt.Errorf("system prompt is required")
- }
- if config.TaskManager == nil {
- return fmt.Errorf("task manager is required")
- }
- if config.GitRepoPath == "" {
- return fmt.Errorf("git repository path is required")
- }
- return nil
-}
-
-const (
- // Agent polling intervals
- TaskPollingInterval = 60 * time.Second
- ErrorRetryInterval = 30 * time.Second
- DefaultGitTimeout = 30 * time.Second
- DefaultContextTimeout = 5 * time.Minute
- DefaultMaxTaskRetries = 3
- DefaultLLMMaxTokens = 4000
- DefaultLLMTemperature = 0.7
-)
-
-// Run starts the agent's main loop
-func (a *Agent) Run() error {
- a.logger.Info("Starting agent", slog.String("name", a.Config.Name), slog.String("role", a.Config.Role))
- defer a.logger.Info("Agent stopped", slog.String("name", a.Config.Name))
-
- // Initialize git repository if needed
- if err := a.initializeGit(); err != nil {
- return fmt.Errorf("failed to initialize git: %w", err)
- }
-
- // Main agent loop
- for {
- select {
- case <-a.ctx.Done():
- return a.ctx.Err()
- default:
- if err := a.processNextTask(); err != nil {
- a.logger.Error("Error processing task", slog.String("error", err.Error()))
- time.Sleep(ErrorRetryInterval)
- }
- }
- }
-}
-
-// Stop stops the agent
-func (a *Agent) Stop() {
- a.logger.Info("Stopping agent", slog.String("name", a.Config.Name))
- a.cancel()
- if a.llmProvider != nil {
- a.llmProvider.Close()
- }
-}
-
-// initializeGit initializes the git repository
-func (a *Agent) initializeGit() error {
- ctx := context.Background()
-
- if err := a.ensureRepository(ctx); err != nil {
- return err
- }
-
- if err := a.ensureRemoteOrigin(ctx); err != nil {
- return err
- }
-
- if err := a.ensureTargetBranch(ctx); err != nil {
- return err
- }
-
- return nil
-}
-
-// ensureRepository ensures the git repository is initialized
-func (a *Agent) ensureRepository(ctx context.Context) error {
- isRepo, err := a.gitInterface.IsRepository(ctx, a.Config.GitRepoPath)
- if err != nil {
- return fmt.Errorf("failed to check repository: %w", err)
- }
-
- if !isRepo {
- if err := a.gitInterface.Init(ctx, a.Config.GitRepoPath); err != nil {
- return fmt.Errorf("failed to initialize repository: %w", err)
- }
- }
-
- return nil
-}
-
-// ensureRemoteOrigin ensures the remote origin is configured
-func (a *Agent) ensureRemoteOrigin(ctx context.Context) error {
- remotes, err := a.gitInterface.ListRemotes(ctx)
- if err != nil {
- return fmt.Errorf("failed to list remotes: %w", err)
- }
-
- // Check if origin already exists
- for _, remote := range remotes {
- if remote.Name == "origin" {
- return nil
- }
- }
-
- // Add remote origin
- remoteURL := a.buildRemoteURL()
- if err := a.gitInterface.AddRemote(ctx, "origin", remoteURL); err != nil {
- return fmt.Errorf("failed to add remote origin: %w", err)
- }
-
- return nil
-}
-
-// buildRemoteURL builds the appropriate remote URL based on configuration
-func (a *Agent) buildRemoteURL() string {
- if !a.Config.GerritEnabled {
- return a.Config.GitRemote
- }
-
- // Build Gerrit URL
- if strings.HasPrefix(a.Config.GerritConfig.BaseURL, "https://") {
- return fmt.Sprintf("%s/%s.git", a.Config.GerritConfig.BaseURL, a.Config.GerritConfig.Project)
- }
-
- // SSH format
- return fmt.Sprintf("ssh://%s@%s:29418/%s.git",
- a.Config.GerritConfig.Username,
- strings.TrimPrefix(a.Config.GerritConfig.BaseURL, "https://"),
- a.Config.GerritConfig.Project)
-}
-
-// ensureTargetBranch ensures the agent is on the target branch
-func (a *Agent) ensureTargetBranch(ctx context.Context) error {
- if a.Config.GitBranch == "" {
- return nil
- }
-
- currentBranch, err := a.gitInterface.GetCurrentBranch(ctx)
- if err != nil {
- return fmt.Errorf("failed to get current branch: %w", err)
- }
-
- if currentBranch == a.Config.GitBranch {
- a.logger.Info("Already on target branch", slog.String("branch", a.Config.GitBranch))
- return nil
- }
-
- return a.checkoutOrCreateBranch(ctx, a.Config.GitBranch)
-}
-
-// checkoutOrCreateBranch attempts to checkout a branch, creating it if it doesn't exist
-func (a *Agent) checkoutOrCreateBranch(ctx context.Context, branchName string) error {
- if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
- if a.isBranchNotFoundError(err) {
- if createErr := a.gitInterface.CreateBranch(ctx, branchName, ""); createErr != nil {
- return fmt.Errorf("failed to create branch %s: %w", branchName, createErr)
- }
- return nil
- }
- return fmt.Errorf("failed to checkout branch %s: %w", branchName, err)
- }
- return nil
-}
-
-// isBranchNotFoundError checks if the error indicates a branch doesn't exist
-func (a *Agent) isBranchNotFoundError(err error) bool {
- errMsg := err.Error()
- return strings.Contains(errMsg, "did not match any file(s) known to git") ||
- strings.Contains(errMsg, "not found") ||
- strings.Contains(errMsg, "unknown revision") ||
- strings.Contains(errMsg, "reference is not a tree") ||
- strings.Contains(errMsg, "pathspec") ||
- strings.Contains(errMsg, "fatal: invalid reference")
-}
-
-// processNextTask processes the next available task
-func (a *Agent) processNextTask() error {
- ctx := context.Background()
-
- // Get tasks assigned to this agent
- taskList, err := a.Config.TaskManager.GetTasksByOwner(ctx, a.Config.Name, 0, 10)
- a.logger.Info("Total number of Tasks", slog.String("agent", a.Config.Name), slog.Any("tasks", taskList.TotalCount))
- if err != nil {
- return fmt.Errorf("failed to get tasks: %w", err)
- }
-
- // Find a task that's ready to be worked on
- var taskToProcess *tm.Task
- for _, task := range taskList.Tasks {
- if task.Status == tm.StatusToDo {
- taskToProcess = task
- break
- }
- }
-
- a.logger.Info("Task to process", slog.Any("task", taskToProcess))
-
- if taskToProcess == nil {
- // No tasks to process, wait a bit
- time.Sleep(TaskPollingInterval)
- return nil
- }
-
- a.logger.Info("Processing task", slog.String("id", taskToProcess.ID), slog.String("title", taskToProcess.Title))
-
- // Start the task
- startedTask, err := a.Config.TaskManager.StartTask(ctx, taskToProcess.ID)
- if err != nil {
- return fmt.Errorf("failed to start task: %w", err)
- }
-
- // Process the task with LLM
- solution, err := a.processTaskWithLLM(startedTask)
- if err != nil {
- // Mark task as failed or retry
- a.logger.Error("Failed to process task with LLM", slog.String("error", err.Error()))
- return err
- }
-
- // Create PR with the solution
- if err := a.createPullRequest(startedTask, solution); err != nil {
- return fmt.Errorf("failed to create pull request: %w", err)
- }
-
- // Complete the task
- if _, err := a.Config.TaskManager.CompleteTask(ctx, startedTask.ID); err != nil {
- return fmt.Errorf("failed to complete task: %w", err)
- }
-
- a.logger.Info("Successfully completed task", slog.String("id", startedTask.ID))
- return nil
-}
-
-// processTaskWithLLM sends the task to the LLM and gets a solution
-func (a *Agent) processTaskWithLLM(task *tm.Task) (string, error) {
- ctx := context.Background()
-
- // Prepare the prompt
- prompt := a.buildTaskPrompt(task)
-
- // Create chat completion request
- req := llm.ChatCompletionRequest{
- Model: a.Config.LLMModel,
- Messages: []llm.Message{
- {
- Role: llm.RoleSystem,
- Content: a.Config.SystemPrompt,
- },
- {
- Role: llm.RoleUser,
- Content: prompt,
- },
- },
- MaxTokens: intPtr(DefaultLLMMaxTokens),
- Temperature: float64Ptr(DefaultLLMTemperature),
- }
-
- // Get response from LLM
- resp, err := a.llmProvider.ChatCompletion(ctx, req)
- if err != nil {
- return "", fmt.Errorf("LLM chat completion failed: %w", err)
- }
-
- if len(resp.Choices) == 0 {
- return "", fmt.Errorf("no response from LLM")
- }
-
- return resp.Choices[0].Message.Content, nil
-}
-
-// buildTaskPrompt builds the prompt for the LLM based on the task
-func (a *Agent) buildTaskPrompt(task *tm.Task) string {
- var prompt strings.Builder
-
- prompt.WriteString(fmt.Sprintf("Task ID: %s\n", task.ID))
- prompt.WriteString(fmt.Sprintf("Title: %s\n", task.Title))
- prompt.WriteString(fmt.Sprintf("Priority: %s\n", task.Priority))
-
- if task.Description != "" {
- prompt.WriteString(fmt.Sprintf("Description: %s\n", task.Description))
- }
-
- if task.DueDate != nil {
- prompt.WriteString(fmt.Sprintf("Due Date: %s\n", task.DueDate.Format("2006-01-02")))
- }
-
- prompt.WriteString("\nPlease provide a detailed solution for this task. ")
- prompt.WriteString("Include any code, documentation, or other deliverables as needed. ")
- prompt.WriteString("Format your response appropriately for the type of task.")
-
- return prompt.String()
-}
-
-// createPullRequest creates a pull request with the solution
-func (a *Agent) createPullRequest(task *tm.Task, solution string) error {
- ctx := context.Background()
-
- // Generate branch name
- branchName := a.generateBranchName(task)
-
- // Create and checkout to new branch
- if err := a.gitInterface.CreateBranch(ctx, branchName, ""); err != nil {
- return fmt.Errorf("failed to create branch: %w", err)
- }
-
- if err := a.gitInterface.Checkout(ctx, branchName); err != nil {
- return fmt.Errorf("failed to checkout branch: %w", err)
- }
-
- // Create solution file
- solutionPath := filepath.Join(a.Config.WorkingDir, fmt.Sprintf("task-%s-solution.md", task.ID))
- solutionContent := a.formatSolution(task, solution)
-
- if err := os.WriteFile(solutionPath, []byte(solutionContent), 0644); err != nil {
- return fmt.Errorf("failed to write solution file: %w", err)
- }
-
- // Add and commit the solution
- if err := a.gitInterface.Add(ctx, []string{solutionPath}); err != nil {
- return fmt.Errorf("failed to add solution file: %w", err)
- }
-
- commitMessage := fmt.Sprintf("feat: Complete task %s - %s\n\n%s", task.ID, task.Title, a.formatPullRequestDescription(task, solution))
- if err := a.gitInterface.Commit(ctx, commitMessage, git.CommitOptions{
- Author: &git.Author{
- Name: a.Config.GitUsername,
- Email: a.Config.GitEmail,
- Time: time.Now(),
- },
- }); err != nil {
- return fmt.Errorf("failed to commit solution: %w", err)
- }
-
- if a.Config.GerritEnabled {
- // For Gerrit: Push to refs/for/BRANCH to create a change
- gerritRef := fmt.Sprintf("refs/for/%s", a.Config.GitBranch)
- if err := a.gitInterface.Push(ctx, "origin", gerritRef, git.PushOptions{}); err != nil {
- return fmt.Errorf("failed to push to Gerrit: %w", err)
- }
- a.logger.Info("Created Gerrit change for task", slog.String("id", task.ID), slog.String("ref", gerritRef))
- } else {
- // For GitHub: Push branch and create PR
- if err := a.gitInterface.Push(ctx, "origin", branchName, git.PushOptions{SetUpstream: true}); err != nil {
- return fmt.Errorf("failed to push branch: %w", err)
- }
-
- // Create pull request using the git interface
- prOptions := git.PullRequestOptions{
- Title: fmt.Sprintf("Complete task %s: %s", task.ID, task.Title),
- Description: a.formatPullRequestDescription(task, solution),
- BaseBranch: a.Config.GitBranch,
- HeadBranch: branchName,
- BaseRepo: a.Config.GerritConfig.Project,
- HeadRepo: a.Config.GerritConfig.Project,
- }
-
- pr, err := a.gitInterface.CreatePullRequest(ctx, prOptions)
- if err != nil {
- return fmt.Errorf("failed to create pull request: %w", err)
- }
-
- a.logger.Info("Created pull request for task", slog.String("id", task.ID), slog.String("title", pr.Title), slog.String("pr_id", pr.ID))
- }
-
- return nil
-}
-
-// generateBranchName generates a branch name for the task
-func (a *Agent) generateBranchName(task *tm.Task) string {
- // Clean the task title for branch name
- cleanTitle := strings.ReplaceAll(task.Title, " ", "-")
- cleanTitle = strings.ToLower(cleanTitle)
-
- // Remove special characters that are not allowed in git branch names
- // Keep only alphanumeric characters and hyphens
- var result strings.Builder
- for _, char := range cleanTitle {
- if (char >= 'a' && char <= 'z') || (char >= '0' && char <= '9') || char == '-' {
- result.WriteRune(char)
- }
- }
- cleanTitle = result.String()
-
- // Remove consecutive hyphens
- for strings.Contains(cleanTitle, "--") {
- cleanTitle = strings.ReplaceAll(cleanTitle, "--", "-")
- }
-
- // Remove leading and trailing hyphens
- cleanTitle = strings.Trim(cleanTitle, "-")
-
- // Limit length
- if len(cleanTitle) > 50 {
- cleanTitle = cleanTitle[:50]
- // Ensure we don't end with a hyphen after truncation
- cleanTitle = strings.TrimSuffix(cleanTitle, "-")
- }
-
- return fmt.Sprintf("task/%s-%s", task.ID, cleanTitle)
-}
-
-// formatSolution formats the solution for the pull request
-func (a *Agent) formatSolution(task *tm.Task, solution string) string {
- var content strings.Builder
-
- content.WriteString(fmt.Sprintf("# Task Solution: %s\n\n", task.Title))
- content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
- content.WriteString(fmt.Sprintf("**Agent:** %s (%s)\n", a.Config.Name, a.Config.Role))
- content.WriteString(fmt.Sprintf("**Completed:** %s\n\n", time.Now().Format("2006-01-02 15:04:05")))
-
- content.WriteString("## Task Description\n\n")
- content.WriteString(task.Description)
- content.WriteString("\n\n")
-
- content.WriteString("## Solution\n\n")
- content.WriteString(solution)
- content.WriteString("\n\n")
-
- content.WriteString("---\n")
- content.WriteString("*This solution was generated by AI Agent*\n")
-
- return content.String()
-}
-
-// formatPullRequestDescription formats the description for the pull request
-func (a *Agent) formatPullRequestDescription(task *tm.Task, solution string) string {
- var content strings.Builder
-
- content.WriteString(fmt.Sprintf("**Task ID:** %s\n", task.ID))
- content.WriteString(fmt.Sprintf("**Title:** %s\n", task.Title))
- content.WriteString(fmt.Sprintf("**Priority:** %s\n", task.Priority))
-
- if task.Description != "" {
- content.WriteString(fmt.Sprintf("**Description:** %s\n", task.Description))
- }
-
- if task.DueDate != nil {
- content.WriteString(fmt.Sprintf("**Due Date:** %s\n", task.DueDate.Format("2006-01-02")))
- }
-
- content.WriteString("\n**Solution:**\n\n")
- content.WriteString(solution)
-
- return content.String()
-}
-
-// ptr helpers for cleaner code
-func intPtr(i int) *int {
- return &i
-}
-
-func float64Ptr(f float64) *float64 {
- return &f
-}
diff --git a/server/agent/agent_test.go b/server/agent/agent_test.go
deleted file mode 100644
index 6ec2adc..0000000
--- a/server/agent/agent_test.go
+++ /dev/null
@@ -1,421 +0,0 @@
-package agent
-
-import (
- "context"
- "log/slog"
- "os"
- "path/filepath"
- "testing"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- "github.com/iomodo/staff/tm"
- "github.com/iomodo/staff/tm/git_tm"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
-)
-
-// MockLLMProvider implements LLMProvider for testing
-type MockLLMProvider struct{}
-
-func (m *MockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
- return &llm.ChatCompletionResponse{
- ID: "mock-response-id",
- Model: req.Model,
- Choices: []llm.ChatCompletionChoice{
- {
- Index: 0,
- Message: llm.Message{
- Role: llm.RoleAssistant,
- Content: "This is a mock response for testing purposes.",
- },
- FinishReason: "stop",
- },
- },
- Usage: llm.Usage{
- PromptTokens: 10,
- CompletionTokens: 20,
- TotalTokens: 30,
- },
- Provider: llm.ProviderOpenAI,
- }, nil
-}
-
-func (m *MockLLMProvider) CreateEmbeddings(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
- return &llm.EmbeddingResponse{
- Object: "list",
- Data: []llm.Embedding{
- {
- Object: "embedding",
- Embedding: []float64{0.1, 0.2, 0.3},
- Index: 0,
- },
- },
- Usage: llm.Usage{
- PromptTokens: 5,
- TotalTokens: 5,
- },
- Model: req.Model,
- Provider: llm.ProviderOpenAI,
- }, nil
-}
-
-func (m *MockLLMProvider) Close() error {
- return nil
-}
-
-// MockLLMFactory implements ProviderFactory for testing
-type MockLLMFactory struct{}
-
-func (f *MockLLMFactory) CreateProvider(config llm.Config) (llm.LLMProvider, error) {
- return &MockLLMProvider{}, nil
-}
-
-func (f *MockLLMFactory) SupportsProvider(provider llm.Provider) bool {
- return provider == llm.ProviderOpenAI
-}
-
-func setupTestAgent(t *testing.T) (*Agent, func()) {
- // Create temporary directories
- tempDir, err := os.MkdirTemp("", "agent-test")
- require.NoError(t, err)
-
- tasksDir := filepath.Join(tempDir, "tasks")
- workspaceDir := filepath.Join(tempDir, "workspace")
- codeRepoDir := filepath.Join(tempDir, "code-repo")
-
- // Create directories
- require.NoError(t, os.MkdirAll(tasksDir, 0755))
- require.NoError(t, os.MkdirAll(workspaceDir, 0755))
- require.NoError(t, os.MkdirAll(codeRepoDir, 0755))
-
- // Initialize git repositories
- gitInterface := git.DefaultGit(tasksDir)
- ctx := context.Background()
-
- err = gitInterface.Init(ctx, tasksDir)
- require.NoError(t, err)
-
- // Set git user config
- userConfig := git.UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = gitInterface.SetUserConfig(ctx, userConfig)
- require.NoError(t, err)
-
- // Create logger for testing
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, tasksDir, logger)
-
- // Create LLM config (using a mock configuration)
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "test-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent config
- config := AgentConfig{
- Name: "test-agent",
- Role: "Test Engineer",
- GitUsername: "test-agent",
- GitEmail: "test-agent@test.com",
- WorkingDir: workspaceDir,
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-3.5-turbo",
- LLMConfig: llmConfig,
- SystemPrompt: "You are a test agent. Provide simple, clear solutions.",
- TaskManager: taskManager,
- GitRepoPath: codeRepoDir,
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create logger for testing
- logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create agent with mock LLM provider
- agent := &Agent{
- Config: config,
- llmProvider: &MockLLMProvider{},
- gitInterface: git.DefaultGit(codeRepoDir),
- ctx: context.Background(),
- cancel: func() {},
- logger: logger,
- }
-
- cleanup := func() {
- agent.Stop()
- os.RemoveAll(tempDir)
- }
-
- return agent, cleanup
-}
-
-func TestNewAgent(t *testing.T) {
- // Create temporary directories
- tempDir, err := os.MkdirTemp("", "agent-test")
- require.NoError(t, err)
- defer os.RemoveAll(tempDir)
-
- tasksDir := filepath.Join(tempDir, "tasks")
- workspaceDir := filepath.Join(tempDir, "workspace")
- codeRepoDir := filepath.Join(tempDir, "code-repo")
-
- // Create directories
- require.NoError(t, os.MkdirAll(tasksDir, 0755))
- require.NoError(t, os.MkdirAll(workspaceDir, 0755))
- require.NoError(t, os.MkdirAll(codeRepoDir, 0755))
-
- // Initialize git repositories
- gitInterface := git.DefaultGit(tasksDir)
- ctx := context.Background()
-
- err = gitInterface.Init(ctx, tasksDir)
- require.NoError(t, err)
-
- // Set git user config
- userConfig := git.UserConfig{
- Name: "Test User",
- Email: "test@example.com",
- }
- err = gitInterface.SetUserConfig(ctx, userConfig)
- require.NoError(t, err)
-
- // Create logger for testing
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, tasksDir, logger)
-
- // Create LLM config (using a mock configuration)
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "test-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent config
- config := AgentConfig{
- Name: "test-agent",
- Role: "Test Engineer",
- GitUsername: "test-agent",
- GitEmail: "test-agent@test.com",
- WorkingDir: workspaceDir,
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-3.5-turbo",
- LLMConfig: llmConfig,
- SystemPrompt: "You are a test agent. Provide simple, clear solutions.",
- TaskManager: taskManager,
- GitRepoPath: codeRepoDir,
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create logger for testing
- logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create agent using NewAgent function
- agent, err := NewAgent(config, logger)
- require.NoError(t, err)
- defer agent.Stop()
-
- assert.NotNil(t, agent)
- assert.Equal(t, "test-agent", agent.Config.Name)
- assert.Equal(t, "Test Engineer", agent.Config.Role)
-}
-
-func TestValidateConfig(t *testing.T) {
- // Test valid config
- validConfig := AgentConfig{
- Name: "test",
- Role: "test",
- WorkingDir: "/tmp",
- SystemPrompt: "test",
- TaskManager: &git_tm.GitTaskManager{},
- GitRepoPath: "/tmp",
- }
-
- err := validateConfig(validConfig)
- assert.NoError(t, err)
-
- // Test invalid configs
- testCases := []struct {
- name string
- config AgentConfig
- }{
- {"empty name", AgentConfig{Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty role", AgentConfig{Name: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty working dir", AgentConfig{Name: "test", Role: "test", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
- {"empty system prompt", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
- {"nil task manager", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
- {"empty git repo path", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}}},
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- err := validateConfig(tc.config)
- assert.Error(t, err)
- })
- }
-}
-
-func TestGenerateBranchName(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Implement User Authentication",
- }
-
- branchName := agent.generateBranchName(task)
- assert.Contains(t, branchName, "task-123")
- assert.Contains(t, branchName, "implement-user-authentication")
-}
-
-func TestBuildTaskPrompt(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- dueDate := time.Now().AddDate(0, 0, 7)
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task",
- Priority: tm.PriorityHigh,
- DueDate: &dueDate,
- }
-
- prompt := agent.buildTaskPrompt(task)
- assert.Contains(t, prompt, "task-123")
- assert.Contains(t, prompt, "Test Task")
- assert.Contains(t, prompt, "This is a test task")
- assert.Contains(t, prompt, "high")
-}
-
-func TestFormatSolution(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task description",
- Priority: tm.PriorityMedium,
- }
-
- solution := "This is the solution to the task."
- formatted := agent.formatSolution(task, solution)
-
- assert.Contains(t, formatted, "# Task Solution: Test Task")
- assert.Contains(t, formatted, "**Task ID:** task-123")
- assert.Contains(t, formatted, "**Agent:** test-agent (Test Engineer)")
- assert.Contains(t, formatted, "## Task Description")
- assert.Contains(t, formatted, "This is a test task description")
- assert.Contains(t, formatted, "## Solution")
- assert.Contains(t, formatted, "This is the solution to the task.")
- assert.Contains(t, formatted, "*This solution was generated by AI Agent*")
-}
-
-func TestAgentStop(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- // Test that Stop doesn't panic
- assert.NotPanics(t, func() {
- agent.Stop()
- })
-}
-
-func TestGenerateBranchNameWithSpecialCharacters(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- testCases := []struct {
- title string
- expected string
- }{
- {
- title: "Simple Task",
- expected: "task/task-123-simple-task",
- },
- {
- title: "Task with (parentheses) and [brackets]",
- expected: "task/task-123-task-with-parentheses-and-brackets",
- },
- {
- title: "Very Long Task Title That Should Be Truncated Because It Exceeds The Maximum Length Allowed For Branch Names",
- expected: "task/task-123-very-long-task-title-that-should-be-truncated-beca",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.title, func(t *testing.T) {
- task := &tm.Task{
- ID: "task-123",
- Title: tc.title,
- }
-
- branchName := agent.generateBranchName(task)
- assert.Equal(t, tc.expected, branchName)
- })
- }
-}
-
-func TestProcessTaskWithLLM(t *testing.T) {
- agent, cleanup := setupTestAgent(t)
- defer cleanup()
-
- task := &tm.Task{
- ID: "task-123",
- Title: "Test Task",
- Description: "This is a test task",
- Priority: tm.PriorityHigh,
- }
-
- solution, err := agent.processTaskWithLLM(task)
- assert.NoError(t, err)
- assert.Contains(t, solution, "mock response")
-}
-
-func TestMockLLMProvider(t *testing.T) {
- mockProvider := &MockLLMProvider{}
-
- // Test ChatCompletion
- req := llm.ChatCompletionRequest{
- Model: "gpt-3.5-turbo",
- Messages: []llm.Message{
- {Role: llm.RoleUser, Content: "Hello"},
- },
- }
-
- resp, err := mockProvider.ChatCompletion(context.Background(), req)
- assert.NoError(t, err)
- assert.NotNil(t, resp)
- assert.Equal(t, "gpt-3.5-turbo", resp.Model)
- assert.Len(t, resp.Choices, 1)
- assert.Contains(t, resp.Choices[0].Message.Content, "mock response")
-
- // Test CreateEmbeddings
- embedReq := llm.EmbeddingRequest{
- Input: "test",
- Model: "text-embedding-ada-002",
- }
-
- embedResp, err := mockProvider.CreateEmbeddings(context.Background(), embedReq)
- assert.NoError(t, err)
- assert.NotNil(t, embedResp)
- assert.Len(t, embedResp.Data, 1)
- assert.Len(t, embedResp.Data[0].Embedding, 3)
-
- // Test Close
- err = mockProvider.Close()
- assert.NoError(t, err)
-}
diff --git a/server/agent/example.go b/server/agent/example.go
deleted file mode 100644
index a7faea9..0000000
--- a/server/agent/example.go
+++ /dev/null
@@ -1,205 +0,0 @@
-package agent
-
-import (
- "context"
- "log/slog"
- "os"
- "time"
-
- "github.com/iomodo/staff/git"
- "github.com/iomodo/staff/llm"
- "github.com/iomodo/staff/tm"
- "github.com/iomodo/staff/tm/git_tm"
-)
-
-// ExampleAgent demonstrates how to create and run an agent
-func ExampleAgent() {
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create git interface for task management
- gitInterface := git.DefaultGit("./tasks-repo")
-
- // Create task manager
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, "./tasks-repo", logger)
-
- // Create LLM configuration
- llmConfig := llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key-here", // Replace with actual API key
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- }
-
- // Create agent configuration
- config := 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
-
-When responding to tasks, provide:
-- Detailed technical analysis
-- Code examples where appropriate
-- Implementation considerations
-- Testing recommendations
-- Documentation suggestions`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- }
-
- // Create agent
- agent, err := NewAgent(config, logger)
- if err != nil {
- logger.Error("Failed to create agent", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- // Create a sample task
- 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: config.Name,
- Priority: tm.PriorityHigh,
- })
- if err != nil {
- logger.Error("Failed to create task", slog.String("error", err.Error()))
- os.Exit(1)
- }
-
- logger.Info("Created task", slog.String("id", task.ID))
-
- // Run the agent (this will process tasks in an infinite loop)
- go func() {
- if err := agent.Run(); err != nil {
- logger.Error("Agent stopped with error", slog.String("error", err.Error()))
- }
- }()
-
- // Let the agent run for a while
- time.Sleep(5 * time.Minute)
-
- // Stop the agent
- agent.Stop()
-}
-
-// ExampleMultipleAgents demonstrates how to create multiple agents with different roles
-func ExampleMultipleAgents() {
- // Create logger
- logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
-
- // Create shared git interface for task management
- gitInterface := git.DefaultGit("./tasks-repo")
- taskManager := git_tm.NewGitTaskManagerWithLogger(gitInterface, "./tasks-repo", logger)
-
- // Create agents with different roles
- agents := []AgentConfig{
- {
- Name: "backend-engineer-1",
- Role: "Backend Engineer",
- GitUsername: "backend-agent",
- GitEmail: "backend-agent@company.com",
- WorkingDir: "./workspace/backend",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `You are a backend engineer. Focus on:
-- API design and implementation
-- Database design and optimization
-- Security best practices
-- Performance optimization
-- Code quality and testing`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- {
- Name: "frontend-engineer-1",
- Role: "Frontend Engineer",
- GitUsername: "frontend-agent",
- GitEmail: "frontend-agent@company.com",
- WorkingDir: "./workspace/frontend",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `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`,
- TaskManager: taskManager,
- GitRepoPath: "./code-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- {
- Name: "product-manager-1",
- Role: "Product Manager",
- GitUsername: "pm-agent",
- GitEmail: "pm-agent@company.com",
- WorkingDir: "./workspace/product",
- LLMProvider: llm.ProviderOpenAI,
- LLMModel: "gpt-4",
- LLMConfig: llm.Config{
- Provider: llm.ProviderOpenAI,
- APIKey: "your-openai-api-key",
- BaseURL: "https://api.openai.com/v1",
- Timeout: 30 * time.Second,
- },
- SystemPrompt: `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`,
- TaskManager: taskManager,
- GitRepoPath: "./docs-repo",
- GitRemote: "origin",
- GitBranch: "main",
- },
- }
-
- // Create and start all agents
- for _, config := range agents {
- agent, err := NewAgent(config, logger)
- if err != nil {
- logger.Error("Failed to create agent", slog.String("name", config.Name), slog.String("error", err.Error()))
- continue
- }
-
- go func(agent *Agent, name string) {
- logger.Info("Starting agent", slog.String("name", name))
- if err := agent.Run(); err != nil {
- logger.Error("Agent stopped with error", slog.String("name", name), slog.String("error", err.Error()))
- }
- }(agent, config.Name)
- }
-
- // Let agents run for a while
- time.Sleep(10 * time.Minute)
-}
diff --git a/server/agent/simple_manager.go b/server/agent/manager.go
similarity index 67%
rename from server/agent/simple_manager.go
rename to server/agent/manager.go
index 27d522d..2d31b87 100644
--- a/server/agent/simple_manager.go
+++ b/server/agent/manager.go
@@ -14,24 +14,14 @@
"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/tm"
)
-// SimpleAgent represents a simplified AI agent for MVP
-type SimpleAgent struct {
- Name string
- Role string
- Model string
- SystemPrompt string
- Provider llm.LLMProvider
- MaxTokens *int
- Temperature *float64
-}
-
-// SimpleAgentManager manages multiple AI agents with basic Git operations
-type SimpleAgentManager struct {
+// Manager manages multiple AI agents with Git operations and task processing
+type Manager struct {
config *config.Config
- agents map[string]*SimpleAgent
+ agents map[string]*Agent
taskManager tm.TaskManager
autoAssigner *assignment.AutoAssigner
prProvider git.PullRequestProvider
@@ -40,8 +30,8 @@
stopChannels map[string]chan struct{}
}
-// NewSimpleAgentManager creates a simplified agent manager
-func NewSimpleAgentManager(cfg *config.Config, taskManager tm.TaskManager) (*SimpleAgentManager, error) {
+// NewManager creates a new agent manager
+func NewManager(cfg *config.Config, taskManager tm.TaskManager) (*Manager, error) {
// Create auto-assigner
autoAssigner := assignment.NewAutoAssigner(cfg.Agents)
@@ -56,9 +46,9 @@
workspacePath := filepath.Join(".", "workspace")
cloneManager := git.NewCloneManager(repoURL, workspacePath)
- manager := &SimpleAgentManager{
+ manager := &Manager{
config: cfg,
- agents: make(map[string]*SimpleAgent),
+ agents: make(map[string]*Agent),
taskManager: taskManager,
autoAssigner: autoAssigner,
prProvider: prProvider,
@@ -76,21 +66,21 @@
}
// initializeAgents creates agent instances from configuration
-func (am *SimpleAgentManager) initializeAgents() error {
- for _, agentConfig := range am.config.Agents {
- agent, err := am.createAgent(agentConfig)
+func (m *Manager) initializeAgents() error {
+ for _, agentConfig := range m.config.Agents {
+ agent, err := m.createAgent(agentConfig)
if err != nil {
return fmt.Errorf("failed to create agent %s: %w", agentConfig.Name, err)
}
- am.agents[agentConfig.Name] = agent
+ m.agents[agentConfig.Name] = agent
}
return nil
}
// createAgent creates a single agent instance
-func (am *SimpleAgentManager) createAgent(agentConfig config.AgentConfig) (*SimpleAgent, error) {
+func (m *Manager) createAgent(agentConfig config.AgentConfig) (*Agent, error) {
// Load system prompt
- systemPrompt, err := am.loadSystemPrompt(agentConfig.SystemPromptFile)
+ systemPrompt, err := m.loadSystemPrompt(agentConfig.SystemPromptFile)
if err != nil {
return nil, fmt.Errorf("failed to load system prompt: %w", err)
}
@@ -98,17 +88,17 @@
// Create LLM provider
llmConfig := llm.Config{
Provider: llm.ProviderOpenAI,
- APIKey: am.config.OpenAI.APIKey,
- BaseURL: am.config.OpenAI.BaseURL,
- Timeout: am.config.OpenAI.Timeout,
+ 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 := &SimpleAgent{
+ agent := &Agent{
Name: agentConfig.Name,
Role: agentConfig.Role,
Model: agentConfig.Model,
@@ -116,13 +106,14 @@
Provider: provider,
MaxTokens: agentConfig.MaxTokens,
Temperature: agentConfig.Temperature,
+ Stats: AgentStats{},
}
return agent, nil
}
// loadSystemPrompt loads the system prompt from file
-func (am *SimpleAgentManager) loadSystemPrompt(filePath string) (string, error) {
+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)
@@ -131,42 +122,42 @@
}
// StartAgent starts an agent to process tasks in a loop
-func (am *SimpleAgentManager) StartAgent(agentName string, loopInterval time.Duration) error {
- agent, exists := am.agents[agentName]
+func (m *Manager) StartAgent(agentName string, loopInterval time.Duration) error {
+ agent, exists := m.agents[agentName]
if !exists {
return fmt.Errorf("agent %s not found", agentName)
}
- if am.isRunning[agentName] {
+ if m.isRunning[agentName] {
return fmt.Errorf("agent %s is already running", agentName)
}
stopChan := make(chan struct{})
- am.stopChannels[agentName] = stopChan
- am.isRunning[agentName] = true
+ m.stopChannels[agentName] = stopChan
+ m.isRunning[agentName] = true
- go am.runAgentLoop(agent, loopInterval, stopChan)
-
+ go m.runAgentLoop(agent, loopInterval, stopChan)
+
log.Printf("Started agent %s (%s) with %s model", agentName, agent.Role, agent.Model)
return nil
}
// StopAgent stops a running agent
-func (am *SimpleAgentManager) StopAgent(agentName string) error {
- if !am.isRunning[agentName] {
+func (m *Manager) StopAgent(agentName string) error {
+ if !m.isRunning[agentName] {
return fmt.Errorf("agent %s is not running", agentName)
}
- close(am.stopChannels[agentName])
- delete(am.stopChannels, agentName)
- am.isRunning[agentName] = false
+ close(m.stopChannels[agentName])
+ delete(m.stopChannels, agentName)
+ m.isRunning[agentName] = false
log.Printf("Stopped agent %s", agentName)
return nil
}
// runAgentLoop runs the main processing loop for an agent
-func (am *SimpleAgentManager) runAgentLoop(agent *SimpleAgent, interval time.Duration, stopChan <-chan struct{}) {
+func (m *Manager) runAgentLoop(agent *Agent, interval time.Duration, stopChan <-chan struct{}) {
ticker := time.NewTicker(interval)
defer ticker.Stop()
@@ -176,7 +167,7 @@
log.Printf("Agent %s stopping", agent.Name)
return
case <-ticker.C:
- if err := am.processAgentTasks(agent); err != nil {
+ if err := m.processAgentTasks(agent); err != nil {
log.Printf("Error processing tasks for agent %s: %v", agent.Name, err)
}
}
@@ -184,22 +175,36 @@
}
// processAgentTasks processes all assigned tasks for an agent
-func (am *SimpleAgentManager) processAgentTasks(agent *SimpleAgent) error {
+func (m *Manager) processAgentTasks(agent *Agent) error {
+ if agent.CurrentTask != nil {
+ return nil
+ }
+
// Get tasks assigned to this agent
- tasks, err := am.taskManager.GetTasksByAssignee(agent.Name)
+ tasks, err := m.taskManager.GetTasksByAssignee(agent.Name)
if err != nil {
return fmt.Errorf("failed to get tasks for agent %s: %w", agent.Name, err)
}
+ log.Printf("Processing %d tasks for agent %s", len(tasks), agent.Name)
+
for _, task := range tasks {
- if task.Status == tm.StatusPending || task.Status == tm.StatusInProgress {
- if err := am.processTask(agent, task); err != nil {
+ if task.Status == tm.StatusToDo || task.Status == tm.StatusPending {
+ if err := m.processTask(agent, task); err != nil {
log.Printf("Error processing task %s: %v", task.ID, err)
// Mark task as failed
task.Status = tm.StatusFailed
- if err := am.taskManager.UpdateTask(task); err != nil {
+ if err := m.taskManager.UpdateTask(task); err != nil {
log.Printf("Error updating failed task %s: %v", task.ID, err)
}
+ 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
}
}
}
@@ -208,31 +213,33 @@
}
// processTask processes a single task with an agent
-func (am *SimpleAgentManager) processTask(agent *SimpleAgent, task *tm.Task) error {
+func (m *Manager) processTask(agent *Agent, task *tm.Task) error {
ctx := context.Background()
+ startTime := time.Now()
log.Printf("Agent %s processing task %s: %s", agent.Name, task.ID, task.Title)
// Mark task as in progress
task.Status = tm.StatusInProgress
- if err := am.taskManager.UpdateTask(task); err != nil {
+ agent.CurrentTask = &task.ID
+ if err := m.taskManager.UpdateTask(task); err != nil {
return fmt.Errorf("failed to update task status: %w", err)
}
// Generate solution using LLM
- solution, err := am.generateSolution(ctx, agent, task)
+ 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 := am.generateBranchName(task)
- if err := am.createAndCommitSolution(branchName, task, solution, agent); err != nil {
+ 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 := am.createPullRequest(ctx, task, solution, agent, branchName)
+ prURL, err := m.createPullRequest(ctx, task, solution, agent, branchName)
if err != nil {
return fmt.Errorf("failed to create pull request: %w", err)
}
@@ -241,20 +248,29 @@
task.Status = tm.StatusCompleted
task.Solution = solution
task.PullRequestURL = prURL
- task.CompletedAt = &time.Time{}
- *task.CompletedAt = time.Now()
+ completedAt := time.Now()
+ task.CompletedAt = &completedAt
+ agent.CurrentTask = nil
- if err := am.taskManager.UpdateTask(task); err != nil {
+ if err := m.taskManager.UpdateTask(task); err != nil {
return fmt.Errorf("failed to update completed task: %w", err)
}
- log.Printf("Task %s completed by agent %s. PR: %s", task.ID, agent.Name, prURL)
+ // 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
+ }
+
+ log.Printf("Task %s completed by agent %s in %v. PR: %s", task.ID, agent.Name, duration, prURL)
return nil
}
// generateSolution uses the agent's LLM to generate a solution
-func (am *SimpleAgentManager) generateSolution(ctx context.Context, agent *SimpleAgent, task *tm.Task) (string, error) {
- prompt := am.buildTaskPrompt(task)
+func (m *Manager) generateSolution(ctx context.Context, agent *Agent, task *tm.Task) (string, error) {
+ prompt := m.buildTaskPrompt(task)
req := llm.ChatCompletionRequest{
Model: agent.Model,
@@ -285,7 +301,7 @@
}
// buildTaskPrompt creates a detailed prompt for the LLM
-func (am *SimpleAgentManager) buildTaskPrompt(task *tm.Task) string {
+func (m *Manager) buildTaskPrompt(task *tm.Task) string {
return fmt.Sprintf(`Task: %s
Priority: %s
@@ -305,7 +321,7 @@
}
// generateBranchName creates a Git branch name for the task
-func (am *SimpleAgentManager) generateBranchName(task *tm.Task) string {
+func (m *Manager) generateBranchName(task *tm.Task) string {
// Clean title for use in branch name
cleanTitle := strings.ToLower(task.Title)
cleanTitle = strings.ReplaceAll(cleanTitle, " ", "-")
@@ -318,30 +334,29 @@
}
}
cleanTitle = result.String()
-
+
// Limit length
if len(cleanTitle) > 40 {
cleanTitle = cleanTitle[:40]
}
-
- return fmt.Sprintf("%s%s-%s", am.config.Git.BranchPrefix, task.ID, cleanTitle)
+
+ 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
-// Each agent works in its own Git clone, eliminating concurrency issues
-func (am *SimpleAgentManager) createAndCommitSolution(branchName string, task *tm.Task, solution string, agent *SimpleAgent) error {
+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 := am.cloneManager.GetAgentClonePath(agent.Name)
+ clonePath, err := m.cloneManager.GetAgentClonePath(agent.Name)
if err != nil {
return fmt.Errorf("failed to get agent clone: %w", err)
}
-
+
log.Printf("Agent %s working in clone: %s", agent.Name, clonePath)
// Refresh the clone with latest changes
- if err := am.cloneManager.RefreshAgentClone(agent.Name); err != nil {
+ if err := m.cloneManager.RefreshAgentClone(agent.Name); err != nil {
log.Printf("Warning: Failed to refresh clone for agent %s: %v", agent.Name, err)
}
@@ -401,7 +416,7 @@
}
// Commit changes
- commitMsg := am.buildCommitMessage(task, agent)
+ commitMsg := m.buildCommitMessage(task, agent)
cmd = gitCmd("commit", "-m", commitMsg)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to commit: %w", err)
@@ -418,9 +433,9 @@
}
// buildCommitMessage creates a commit message from template
-func (am *SimpleAgentManager) buildCommitMessage(task *tm.Task, agent *SimpleAgent) string {
- template := am.config.Git.CommitMessageTemplate
-
+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,
@@ -437,12 +452,12 @@
}
// createPullRequest creates a GitHub pull request
-func (am *SimpleAgentManager) createPullRequest(ctx context.Context, task *tm.Task, solution string, agent *SimpleAgent, branchName string) (string, error) {
+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 := am.buildPRDescription(task, solution, agent)
-
+ description := m.buildPRDescription(task, solution, agent)
+
options := git.PullRequestOptions{
Title: title,
Description: description,
@@ -452,24 +467,24 @@
Draft: false,
}
- pr, err := am.prProvider.CreatePullRequest(ctx, options)
+ pr, err := m.prProvider.CreatePullRequest(ctx, options)
if err != nil {
return "", fmt.Errorf("failed to create PR: %w", err)
}
- return fmt.Sprintf("https://github.com/%s/%s/pull/%d", am.config.GitHub.Owner, am.config.GitHub.Repo, pr.Number), nil
+ return fmt.Sprintf("https://github.com/%s/%s/pull/%d", m.config.GitHub.Owner, m.config.GitHub.Repo, pr.Number), nil
}
// buildPRDescription creates PR description from template
-func (am *SimpleAgentManager) buildPRDescription(task *tm.Task, solution string, agent *SimpleAgent) string {
- template := am.config.Git.PRTemplate
-
+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,
@@ -489,77 +504,80 @@
}
// AutoAssignTask automatically assigns a task to the best matching agent
-func (am *SimpleAgentManager) AutoAssignTask(taskID string) error {
- task, err := am.taskManager.GetTask(taskID)
+func (m *Manager) AutoAssignTask(taskID string) error {
+ task, err := m.taskManager.GetTask(taskID)
if err != nil {
return fmt.Errorf("failed to get task: %w", err)
}
- agentName, err := am.autoAssigner.AssignTask(task)
+ agentName, err := m.autoAssigner.AssignTask(task)
if err != nil {
return fmt.Errorf("failed to auto-assign task: %w", err)
}
task.Assignee = agentName
- if err := am.taskManager.UpdateTask(task); err != nil {
+ if err := m.taskManager.UpdateTask(task); err != nil {
return fmt.Errorf("failed to update task assignment: %w", err)
}
- explanation := am.autoAssigner.GetRecommendationExplanation(task, agentName)
+ explanation := m.autoAssigner.GetRecommendationExplanation(task, agentName)
log.Printf("Auto-assigned task %s to %s: %s", taskID, agentName, explanation)
return nil
}
// GetAgentStatus returns the status of all agents
-func (am *SimpleAgentManager) GetAgentStatus() map[string]SimpleAgentStatus {
- status := make(map[string]SimpleAgentStatus)
-
- for name, agent := range am.agents {
- status[name] = SimpleAgentStatus{
- Name: agent.Name,
- Role: agent.Role,
- Model: agent.Model,
- IsRunning: am.isRunning[name],
+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
}
-// SimpleAgentStatus represents the status of an agent
-type SimpleAgentStatus struct {
- Name string `json:"name"`
- Role string `json:"role"`
- Model string `json:"model"`
- IsRunning bool `json:"is_running"`
-}
-
// IsAgentRunning checks if an agent is currently running
-func (am *SimpleAgentManager) IsAgentRunning(agentName string) bool {
- return am.isRunning[agentName]
+func (m *Manager) IsAgentRunning(agentName string) bool {
+ return m.isRunning[agentName]
}
// Close shuts down the agent manager
-func (am *SimpleAgentManager) Close() error {
+func (m *Manager) Close() error {
// Stop all running agents
- for agentName := range am.isRunning {
- if am.isRunning[agentName] {
- am.StopAgent(agentName)
+ for agentName := range m.isRunning {
+ if m.isRunning[agentName] {
+ m.StopAgent(agentName)
}
}
// Close all LLM providers
- for _, agent := range am.agents {
+ for _, agent := range m.agents {
if err := agent.Provider.Close(); err != nil {
log.Printf("Error closing provider for agent %s: %v", agent.Name, err)
}
}
// Cleanup all agent Git clones
- if err := am.cloneManager.CleanupAllClones(); err != nil {
+ if err := m.cloneManager.CleanupAllClones(); err != nil {
log.Printf("Error cleaning up agent clones: %v", err)
}
return nil
-}
\ No newline at end of file
+}
diff --git a/server/agent/types.go b/server/agent/types.go
new file mode 100644
index 0000000..a9f021c
--- /dev/null
+++ b/server/agent/types.go
@@ -0,0 +1,53 @@
+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"`
+}