ba919305c2
Features: - Unified Go binary running Task Service, Agent Manager, and Orchestrator - SQLite persistence for boards, columns, and tasks - Docker-based agent execution with ACP message streaming - Augment Agent integration with session resumption support - RESTful API for all operations (port 8080) - In-memory event bus for service communication Architecture: - cmd/kandev: Single entry point for all services - internal/task: Task, board, and column management - internal/agent: Docker container lifecycle and ACP streaming - internal/orchestrator: Agent coordination and monitoring - internal/events: In-memory pub/sub event bus Documentation: - README.md: Project overview and quick start - docs/ARCHITECTURE.md: System architecture (current + planned) - docs/GETTING_STARTED.md: Development setup guide - docs/IMPLEMENTATION_PLAN.md: Original microservices plan - backend/NEXT_STEPS.md: Development roadmap
345 lines
9.3 KiB
Go
345 lines
9.3 KiB
Go
// Package executor manages agent execution for tasks.
|
|
package executor
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/kandev/kandev/internal/common/logger"
|
|
v1 "github.com/kandev/kandev/pkg/api/v1"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// Common errors
|
|
var (
|
|
ErrMaxConcurrentReached = errors.New("maximum concurrent executions reached")
|
|
ErrNoAgentType = errors.New("task has no agent_type configured")
|
|
ErrExecutionNotFound = errors.New("execution not found")
|
|
)
|
|
|
|
// AgentManagerClient is an interface for the Agent Manager service
|
|
// This will be implemented via gRPC or HTTP client
|
|
type AgentManagerClient interface {
|
|
// LaunchAgent starts a new agent container for a task
|
|
LaunchAgent(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error)
|
|
|
|
// StopAgent stops a running agent
|
|
StopAgent(ctx context.Context, agentInstanceID string, force bool) error
|
|
|
|
// GetAgentStatus returns the status of an agent instance
|
|
GetAgentStatus(ctx context.Context, agentInstanceID string) (*v1.AgentInstance, error)
|
|
|
|
// ListAgentTypes returns available agent types
|
|
ListAgentTypes(ctx context.Context) ([]*v1.AgentType, error)
|
|
}
|
|
|
|
// LaunchAgentRequest contains parameters for launching an agent
|
|
type LaunchAgentRequest struct {
|
|
TaskID string
|
|
AgentType string
|
|
RepositoryURL string
|
|
Branch string
|
|
Priority int
|
|
Metadata map[string]interface{}
|
|
}
|
|
|
|
// LaunchAgentResponse contains the result of launching an agent
|
|
type LaunchAgentResponse struct {
|
|
AgentInstanceID string
|
|
ContainerID string
|
|
Status v1.AgentStatus
|
|
}
|
|
|
|
// TaskExecution tracks an active task execution
|
|
type TaskExecution struct {
|
|
TaskID string
|
|
AgentInstanceID string
|
|
AgentType string
|
|
StartedAt time.Time
|
|
Status v1.AgentStatus
|
|
Progress int
|
|
LastUpdate time.Time
|
|
}
|
|
|
|
// Executor manages agent execution for tasks
|
|
type Executor struct {
|
|
agentManager AgentManagerClient
|
|
logger *logger.Logger
|
|
|
|
// Track active executions
|
|
executions map[string]*TaskExecution
|
|
mu sync.RWMutex
|
|
|
|
// Configuration
|
|
maxConcurrent int
|
|
retryLimit int
|
|
retryDelay time.Duration
|
|
}
|
|
|
|
// NewExecutor creates a new executor
|
|
func NewExecutor(agentManager AgentManagerClient, log *logger.Logger, maxConcurrent int) *Executor {
|
|
if maxConcurrent <= 0 {
|
|
maxConcurrent = 5 // default
|
|
}
|
|
return &Executor{
|
|
agentManager: agentManager,
|
|
logger: log.WithFields(zap.String("component", "executor")),
|
|
executions: make(map[string]*TaskExecution),
|
|
maxConcurrent: maxConcurrent,
|
|
retryLimit: 3,
|
|
retryDelay: 5 * time.Second,
|
|
}
|
|
}
|
|
|
|
// Execute starts agent execution for a task
|
|
func (e *Executor) Execute(ctx context.Context, task *v1.Task) (*TaskExecution, error) {
|
|
// Check if max concurrent limit is reached
|
|
if !e.CanExecute() {
|
|
e.logger.Warn("max concurrent executions reached",
|
|
zap.Int("max", e.maxConcurrent),
|
|
zap.Int("current", e.ActiveCount()))
|
|
return nil, ErrMaxConcurrentReached
|
|
}
|
|
|
|
// Verify task has an agent_type configured
|
|
if task.AgentType == nil || *task.AgentType == "" {
|
|
e.logger.Error("task has no agent_type configured", zap.String("task_id", task.ID))
|
|
return nil, ErrNoAgentType
|
|
}
|
|
|
|
// Create a LaunchAgentRequest from the task
|
|
req := &LaunchAgentRequest{
|
|
TaskID: task.ID,
|
|
AgentType: *task.AgentType,
|
|
Priority: task.Priority,
|
|
Metadata: task.Metadata,
|
|
}
|
|
if task.RepositoryURL != nil {
|
|
req.RepositoryURL = *task.RepositoryURL
|
|
}
|
|
if task.Branch != nil {
|
|
req.Branch = *task.Branch
|
|
}
|
|
|
|
e.logger.Info("launching agent for task",
|
|
zap.String("task_id", task.ID),
|
|
zap.String("agent_type", *task.AgentType))
|
|
|
|
// Call the AgentManager to launch the container
|
|
resp, err := e.agentManager.LaunchAgent(ctx, req)
|
|
if err != nil {
|
|
e.logger.Error("failed to launch agent",
|
|
zap.String("task_id", task.ID),
|
|
zap.Error(err))
|
|
return nil, err
|
|
}
|
|
|
|
// Track the execution in the executions map
|
|
now := time.Now()
|
|
execution := &TaskExecution{
|
|
TaskID: task.ID,
|
|
AgentInstanceID: resp.AgentInstanceID,
|
|
AgentType: *task.AgentType,
|
|
StartedAt: now,
|
|
Status: resp.Status,
|
|
Progress: 0,
|
|
LastUpdate: now,
|
|
}
|
|
|
|
e.mu.Lock()
|
|
e.executions[task.ID] = execution
|
|
e.mu.Unlock()
|
|
|
|
e.logger.Info("agent launched successfully",
|
|
zap.String("task_id", task.ID),
|
|
zap.String("agent_instance_id", resp.AgentInstanceID),
|
|
zap.String("container_id", resp.ContainerID))
|
|
|
|
return execution, nil
|
|
}
|
|
|
|
// Stop stops an active execution
|
|
func (e *Executor) Stop(ctx context.Context, taskID string, reason string, force bool) error {
|
|
e.mu.RLock()
|
|
execution, exists := e.executions[taskID]
|
|
e.mu.RUnlock()
|
|
|
|
if !exists {
|
|
return ErrExecutionNotFound
|
|
}
|
|
|
|
e.logger.Info("stopping execution",
|
|
zap.String("task_id", taskID),
|
|
zap.String("agent_instance_id", execution.AgentInstanceID),
|
|
zap.String("reason", reason),
|
|
zap.Bool("force", force))
|
|
|
|
err := e.agentManager.StopAgent(ctx, execution.AgentInstanceID, force)
|
|
if err != nil {
|
|
e.logger.Error("failed to stop agent",
|
|
zap.String("task_id", taskID),
|
|
zap.Error(err))
|
|
return err
|
|
}
|
|
|
|
// Update execution status
|
|
e.mu.Lock()
|
|
if exec, ok := e.executions[taskID]; ok {
|
|
exec.Status = v1.AgentStatusStopped
|
|
exec.LastUpdate = time.Now()
|
|
}
|
|
e.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// GetExecution returns the current execution state for a task
|
|
func (e *Executor) GetExecution(taskID string) (*TaskExecution, bool) {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
execution, exists := e.executions[taskID]
|
|
if !exists {
|
|
return nil, false
|
|
}
|
|
|
|
// Return a copy to avoid data races
|
|
copy := *execution
|
|
return ©, true
|
|
}
|
|
|
|
// ListExecutions returns all active executions
|
|
func (e *Executor) ListExecutions() []*TaskExecution {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
|
|
result := make([]*TaskExecution, 0, len(e.executions))
|
|
for _, exec := range e.executions {
|
|
copy := *exec
|
|
result = append(result, ©)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// ActiveCount returns the number of active executions
|
|
func (e *Executor) ActiveCount() int {
|
|
e.mu.RLock()
|
|
defer e.mu.RUnlock()
|
|
return len(e.executions)
|
|
}
|
|
|
|
// CanExecute returns true if there's capacity for another execution
|
|
func (e *Executor) CanExecute() bool {
|
|
return e.ActiveCount() < e.maxConcurrent
|
|
}
|
|
|
|
// UpdateProgress updates the progress of an execution (called when ACP progress messages are received)
|
|
func (e *Executor) UpdateProgress(taskID string, progress int, status v1.AgentStatus) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if exec, ok := e.executions[taskID]; ok {
|
|
exec.Progress = progress
|
|
exec.Status = status
|
|
exec.LastUpdate = time.Now()
|
|
|
|
e.logger.Debug("updated execution progress",
|
|
zap.String("task_id", taskID),
|
|
zap.Int("progress", progress),
|
|
zap.String("status", string(status)))
|
|
}
|
|
}
|
|
|
|
// MarkCompleted marks an execution as completed
|
|
func (e *Executor) MarkCompleted(taskID string, status v1.AgentStatus) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if exec, ok := e.executions[taskID]; ok {
|
|
exec.Status = status
|
|
exec.Progress = 100
|
|
exec.LastUpdate = time.Now()
|
|
|
|
e.logger.Info("execution completed",
|
|
zap.String("task_id", taskID),
|
|
zap.String("status", string(status)))
|
|
}
|
|
}
|
|
|
|
// RemoveExecution removes a completed/failed execution from tracking
|
|
func (e *Executor) RemoveExecution(taskID string) {
|
|
e.mu.Lock()
|
|
defer e.mu.Unlock()
|
|
|
|
if _, ok := e.executions[taskID]; ok {
|
|
delete(e.executions, taskID)
|
|
e.logger.Debug("removed execution from tracking", zap.String("task_id", taskID))
|
|
}
|
|
}
|
|
|
|
// MockAgentManagerClient is a placeholder implementation
|
|
type MockAgentManagerClient struct {
|
|
logger *logger.Logger
|
|
}
|
|
|
|
// NewMockAgentManagerClient creates a new mock agent manager client
|
|
func NewMockAgentManagerClient(log *logger.Logger) *MockAgentManagerClient {
|
|
return &MockAgentManagerClient{
|
|
logger: log.WithFields(zap.String("component", "mock_agent_manager")),
|
|
}
|
|
}
|
|
|
|
// LaunchAgent mocks launching an agent container
|
|
func (m *MockAgentManagerClient) LaunchAgent(ctx context.Context, req *LaunchAgentRequest) (*LaunchAgentResponse, error) {
|
|
m.logger.Info("mock: launching agent",
|
|
zap.String("task_id", req.TaskID),
|
|
zap.String("agent_type", req.AgentType),
|
|
zap.String("repository_url", req.RepositoryURL),
|
|
zap.String("branch", req.Branch))
|
|
|
|
return &LaunchAgentResponse{
|
|
AgentInstanceID: uuid.New().String(),
|
|
ContainerID: "mock-container-" + uuid.New().String()[:8],
|
|
Status: v1.AgentStatusStarting,
|
|
}, nil
|
|
}
|
|
|
|
// StopAgent mocks stopping an agent
|
|
func (m *MockAgentManagerClient) StopAgent(ctx context.Context, agentInstanceID string, force bool) error {
|
|
m.logger.Info("mock: stopping agent",
|
|
zap.String("agent_instance_id", agentInstanceID),
|
|
zap.Bool("force", force))
|
|
return nil
|
|
}
|
|
|
|
// GetAgentStatus mocks getting agent status
|
|
func (m *MockAgentManagerClient) GetAgentStatus(ctx context.Context, agentInstanceID string) (*v1.AgentInstance, error) {
|
|
m.logger.Info("mock: getting agent status",
|
|
zap.String("agent_instance_id", agentInstanceID))
|
|
|
|
return &v1.AgentInstance{
|
|
ID: agentInstanceID,
|
|
Status: v1.AgentStatusRunning,
|
|
AgentType: "mock-agent",
|
|
}, nil
|
|
}
|
|
|
|
// ListAgentTypes mocks listing available agent types
|
|
func (m *MockAgentManagerClient) ListAgentTypes(ctx context.Context) ([]*v1.AgentType, error) {
|
|
m.logger.Info("mock: listing agent types")
|
|
|
|
return []*v1.AgentType{
|
|
{
|
|
ID: "auggie-cli",
|
|
Name: "Auggie CLI Agent",
|
|
Description: "CLI-based agent for code tasks",
|
|
DockerImage: "auggie-cli",
|
|
DockerTag: "latest",
|
|
Enabled: true,
|
|
},
|
|
}, nil
|
|
}
|