Add inital implementation of an agent
Change-Id: Ib60c33e8c1a44bc9341cac5c1f1fdc518fb5ed1e
diff --git a/server/agent/README.md b/server/agent/README.md
new file mode 100644
index 0000000..0015d90
--- /dev/null
+++ b/server/agent/README.md
@@ -0,0 +1,298 @@
+# 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
index e901cf6..e3aa766 100644
--- a/server/agent/agent.go
+++ b/server/agent/agent.go
@@ -1,23 +1,399 @@
package agent
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/iomodo/staff/git"
+ "github.com/iomodo/staff/llm"
+ "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
}
+// Agent represents an AI agent that can process tasks
type Agent struct {
- Config AgentConfig
+ Config AgentConfig
+ llmProvider llm.LLMProvider
+ gitInterface git.GitInterface
+ ctx context.Context
+ cancel context.CancelFunc
}
-func NewAgent(config AgentConfig) *Agent {
- return &Agent{
- Config: config,
+// NewAgent creates a new agent instance
+func NewAgent(config AgentConfig) (*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
+ 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,
+ }
+
+ 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
+}
+
+// Run starts the agent's main loop
+func (a *Agent) Run() error {
+ log.Printf("Starting agent %s (%s)", a.Config.Name, a.Config.Role)
+ defer log.Printf("Agent %s stopped", 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 {
+ log.Printf("Error processing task: %v", err)
+ // Continue running even if there's an error
+ time.Sleep(30 * time.Second)
+ }
+ }
}
}
-func (a *Agent) Run() {
+// Stop stops the agent
+func (a *Agent) Stop() {
+ log.Printf("Stopping agent %s", 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()
+
+ // Check if repository exists
+ isRepo, err := a.gitInterface.IsRepository(ctx, a.Config.GitRepoPath)
+ if err != nil {
+ return fmt.Errorf("failed to check repository: %w", err)
+ }
+
+ if !isRepo {
+ // Initialize new repository
+ if err := a.gitInterface.Init(ctx, a.Config.GitRepoPath); err != nil {
+ return fmt.Errorf("failed to initialize repository: %w", err)
+ }
+ }
+
+ // Set git user configuration
+ userConfig := git.UserConfig{
+ Name: a.Config.GitUsername,
+ Email: a.Config.GitEmail,
+ }
+ if err := a.gitInterface.SetUserConfig(ctx, userConfig); err != nil {
+ return fmt.Errorf("failed to set git user config: %w", err)
+ }
+
+ // Checkout to the specified branch
+ if a.Config.GitBranch != "" {
+ if err := a.gitInterface.Checkout(ctx, a.Config.GitBranch); err != nil {
+ // Try to create the branch if it doesn't exist
+ if err := a.gitInterface.CreateBranch(ctx, a.Config.GitBranch, ""); err != nil {
+ return fmt.Errorf("failed to create branch %s: %w", a.Config.GitBranch, err)
+ }
+ }
+ }
+
+ return nil
+}
+
+// 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)
+ 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
+ }
+ }
+
+ if taskToProcess == nil {
+ // No tasks to process, wait a bit
+ time.Sleep(60 * time.Second)
+ return nil
+ }
+
+ log.Printf("Processing task: %s - %s", taskToProcess.ID, 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
+ log.Printf("Failed to process task with LLM: %v", err)
+ 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)
+ }
+
+ log.Printf("Successfully completed task: %s", 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(4000),
+ Temperature: float64Ptr(0.7),
+ }
+
+ // 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", task.ID, task.Title)
+ if err := a.gitInterface.Commit(ctx, commitMessage, git.CommitOptions{}); err != nil {
+ return fmt.Errorf("failed to commit solution: %w", err)
+ }
+
+ // Push the branch
+ if err := a.gitInterface.Push(ctx, "origin", branchName, git.PushOptions{SetUpstream: true}); err != nil {
+ return fmt.Errorf("failed to push branch: %w", err)
+ }
+
+ log.Printf("Created pull request for task %s on branch %s", task.ID, branchName)
+ 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()
+}
+
+// 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
new file mode 100644
index 0000000..405e5b7
--- /dev/null
+++ b/server/agent/agent_test.go
@@ -0,0 +1,348 @@
+package agent
+
+import (
+ "context"
+ "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 task manager
+ taskManager := git_tm.NewGitTaskManager(gitInterface, tasksDir)
+
+ // 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 agent with mock LLM provider
+ agent := &Agent{
+ Config: config,
+ llmProvider: &MockLLMProvider{},
+ gitInterface: git.DefaultGit(codeRepoDir),
+ ctx: context.Background(),
+ cancel: func() {},
+ }
+
+ cleanup := func() {
+ agent.Stop()
+ os.RemoveAll(tempDir)
+ }
+
+ return agent, cleanup
+}
+
+func TestNewAgent(t *testing.T) {
+ agent, cleanup := setupTestAgent(t)
+ defer cleanup()
+
+ 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
new file mode 100644
index 0000000..1233bdc
--- /dev/null
+++ b/server/agent/example.go
@@ -0,0 +1,196 @@
+package agent
+
+import (
+ "context"
+ "log"
+ "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 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", // 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)
+ if err != nil {
+ log.Fatalf("Failed to create agent: %v", err)
+ }
+
+ // 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 {
+ log.Fatalf("Failed to create task: %v", err)
+ }
+
+ log.Printf("Created task: %s", task.ID)
+
+ // Run the agent (this will process tasks in an infinite loop)
+ go func() {
+ if err := agent.Run(); err != nil {
+ log.Printf("Agent stopped with error: %v", err)
+ }
+ }()
+
+ // 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 shared git interface for task management
+ gitInterface := git.DefaultGit("./tasks-repo")
+ taskManager := git_tm.NewGitTaskManager(gitInterface, "./tasks-repo")
+
+ // 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)
+ if err != nil {
+ log.Printf("Failed to create agent %s: %v", config.Name, err)
+ continue
+ }
+
+ go func(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)
+ }
+
+ // Let agents run for a while
+ time.Sleep(10 * time.Minute)
+}