blob: f18979411ad8bff8ac2fa1ddd6a95e0d48c47896 [file] [log] [blame]
package agent
import (
"context"
"fmt"
"log/slog"
"os"
"time"
"github.com/iomodo/staff/config"
"github.com/iomodo/staff/llm"
"github.com/iomodo/staff/llm/provider"
"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)
}
prov := provider.CreateProvider(llmConfig)
thinker := NewThinker(prov, agentConfig.Model, systemPrompt, *agentConfig.MaxTokens, *agentConfig.Temperature, agentRoles, logger)
agent := &Agent{
Name: agentConfig.Name,
Role: agentConfig.Role,
Model: agentConfig.Model,
SystemPrompt: systemPrompt,
Provider: prov,
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, a.Name)
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
}