package execution import ( "context" "fmt" "log" "strconv" "strings" "time" "chorus/pkg/ai" "chorus/pkg/prompt" ) // TaskExecutionEngine provides AI-powered task execution with isolated sandboxes type TaskExecutionEngine interface { ExecuteTask(ctx context.Context, request *TaskExecutionRequest) (*TaskExecutionResult, error) Initialize(ctx context.Context, config *EngineConfig) error Shutdown() error GetMetrics() *EngineMetrics } // TaskExecutionRequest represents a task to be executed type TaskExecutionRequest struct { ID string `json:"id"` Type string `json:"type"` Description string `json:"description"` Context map[string]interface{} `json:"context,omitempty"` Requirements *TaskRequirements `json:"requirements,omitempty"` Timeout time.Duration `json:"timeout,omitempty"` } // TaskRequirements specifies execution environment needs type TaskRequirements struct { AIModel string `json:"ai_model,omitempty"` SandboxType string `json:"sandbox_type,omitempty"` RequiredTools []string `json:"required_tools,omitempty"` EnvironmentVars map[string]string `json:"environment_vars,omitempty"` ResourceLimits *ResourceLimits `json:"resource_limits,omitempty"` SecurityPolicy *SecurityPolicy `json:"security_policy,omitempty"` } // TaskExecutionResult contains the results of task execution type TaskExecutionResult struct { TaskID string `json:"task_id"` Success bool `json:"success"` Output string `json:"output"` ErrorMessage string `json:"error_message,omitempty"` Artifacts []TaskArtifact `json:"artifacts,omitempty"` Metrics *ExecutionMetrics `json:"metrics"` Metadata map[string]interface{} `json:"metadata,omitempty"` } // TaskArtifact represents a file or data produced during execution type TaskArtifact struct { Name string `json:"name"` Type string `json:"type"` Path string `json:"path,omitempty"` Content []byte `json:"content,omitempty"` Size int64 `json:"size"` CreatedAt time.Time `json:"created_at"` Metadata map[string]string `json:"metadata,omitempty"` } // ExecutionMetrics tracks resource usage and performance type ExecutionMetrics struct { StartTime time.Time `json:"start_time"` EndTime time.Time `json:"end_time"` Duration time.Duration `json:"duration"` AIProviderTime time.Duration `json:"ai_provider_time"` SandboxTime time.Duration `json:"sandbox_time"` ResourceUsage *ResourceUsage `json:"resource_usage,omitempty"` CommandsExecuted int `json:"commands_executed"` FilesGenerated int `json:"files_generated"` } // EngineConfig configures the task execution engine type EngineConfig struct { AIProviderFactory *ai.ProviderFactory `json:"-"` SandboxDefaults *SandboxConfig `json:"sandbox_defaults"` DefaultTimeout time.Duration `json:"default_timeout"` MaxConcurrentTasks int `json:"max_concurrent_tasks"` EnableMetrics bool `json:"enable_metrics"` LogLevel string `json:"log_level"` } // EngineMetrics tracks overall engine performance type EngineMetrics struct { TasksExecuted int64 `json:"tasks_executed"` TasksSuccessful int64 `json:"tasks_successful"` TasksFailed int64 `json:"tasks_failed"` AverageTime time.Duration `json:"average_time"` TotalExecutionTime time.Duration `json:"total_execution_time"` ActiveTasks int `json:"active_tasks"` } // DefaultTaskExecutionEngine implements the TaskExecutionEngine interface type DefaultTaskExecutionEngine struct { config *EngineConfig aiFactory *ai.ProviderFactory metrics *EngineMetrics activeTasks map[string]context.CancelFunc logger *log.Logger } // NewTaskExecutionEngine creates a new task execution engine func NewTaskExecutionEngine() *DefaultTaskExecutionEngine { return &DefaultTaskExecutionEngine{ metrics: &EngineMetrics{}, activeTasks: make(map[string]context.CancelFunc), logger: log.Default(), } } // Initialize configures and prepares the execution engine func (e *DefaultTaskExecutionEngine) Initialize(ctx context.Context, config *EngineConfig) error { if config == nil { return fmt.Errorf("engine config cannot be nil") } if config.AIProviderFactory == nil { return fmt.Errorf("AI provider factory is required") } e.config = config e.aiFactory = config.AIProviderFactory // Set default values if e.config.DefaultTimeout == 0 { e.config.DefaultTimeout = 5 * time.Minute } if e.config.MaxConcurrentTasks == 0 { e.config.MaxConcurrentTasks = 10 } e.logger.Printf("TaskExecutionEngine initialized with %d max concurrent tasks", e.config.MaxConcurrentTasks) return nil } // ExecuteTask executes a task using AI providers and isolated sandboxes func (e *DefaultTaskExecutionEngine) ExecuteTask(ctx context.Context, request *TaskExecutionRequest) (*TaskExecutionResult, error) { if e.config == nil { return nil, fmt.Errorf("engine not initialized") } startTime := time.Now() // Create task context with timeout timeout := request.Timeout if timeout == 0 { timeout = e.config.DefaultTimeout } taskCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() // Track active task e.activeTasks[request.ID] = cancel defer delete(e.activeTasks, request.ID) e.metrics.ActiveTasks++ defer func() { e.metrics.ActiveTasks-- }() result := &TaskExecutionResult{ TaskID: request.ID, Metrics: &ExecutionMetrics{StartTime: startTime}, } // Execute the task err := e.executeTaskInternal(taskCtx, request, result) // Update metrics result.Metrics.EndTime = time.Now() result.Metrics.Duration = result.Metrics.EndTime.Sub(result.Metrics.StartTime) e.metrics.TasksExecuted++ e.metrics.TotalExecutionTime += result.Metrics.Duration if err != nil { result.Success = false result.ErrorMessage = err.Error() e.metrics.TasksFailed++ e.logger.Printf("Task %s failed: %v", request.ID, err) } else { result.Success = true e.metrics.TasksSuccessful++ e.logger.Printf("Task %s completed successfully in %v", request.ID, result.Metrics.Duration) } e.metrics.AverageTime = e.metrics.TotalExecutionTime / time.Duration(e.metrics.TasksExecuted) return result, err } // executeTaskInternal performs the actual task execution func (e *DefaultTaskExecutionEngine) executeTaskInternal(ctx context.Context, request *TaskExecutionRequest, result *TaskExecutionResult) error { if request == nil { return fmt.Errorf("task execution request cannot be nil") } aiStartTime := time.Now() role := e.determineRoleFromTask(request) provider, providerConfig, err := e.aiFactory.GetProviderForRole(role) if err != nil { return fmt.Errorf("failed to get AI provider for role %s: %w", role, err) } roleConfig, _ := e.aiFactory.GetRoleConfig(role) aiRequest := &ai.TaskRequest{ TaskID: request.ID, TaskTitle: extractTaskTitle(request), TaskDescription: request.Description, Context: request.Context, AgentRole: role, AgentID: extractAgentID(request.Context), Repository: extractRepository(request.Context), TaskLabels: extractTaskLabels(request.Context), Priority: extractContextInt(request.Context, "priority"), Complexity: extractContextInt(request.Context, "complexity"), ModelName: providerConfig.DefaultModel, Temperature: providerConfig.Temperature, MaxTokens: providerConfig.MaxTokens, WorkingDirectory: extractWorkingDirectory(request.Context), EnableTools: providerConfig.EnableTools || roleConfig.EnableTools, MCPServers: combineStringSlices(providerConfig.MCPServers, roleConfig.MCPServers), AllowedTools: combineStringSlices(roleConfig.AllowedTools, nil), } if aiRequest.AgentID == "" { aiRequest.AgentID = request.ID } if systemPrompt := e.resolveSystemPrompt(role, roleConfig, request.Context); systemPrompt != "" { aiRequest.SystemPrompt = systemPrompt } aiResponse, err := provider.ExecuteTask(ctx, aiRequest) if err != nil { return fmt.Errorf("AI provider execution failed: %w", err) } result.Metrics.AIProviderTime = time.Since(aiStartTime) commands, artifacts, err := e.parseAIResponse(aiResponse) if err != nil { return fmt.Errorf("failed to parse AI response: %w", err) } // Only execute sandbox if sandbox type is not explicitly disabled (empty string or "none") sandboxType := "" if request.Requirements != nil { sandboxType = request.Requirements.SandboxType } shouldExecuteSandbox := len(commands) > 0 && sandboxType != "" && sandboxType != "none" if shouldExecuteSandbox { sandboxStartTime := time.Now() sandboxResult, err := e.executeSandboxCommands(ctx, request, commands) if err != nil { return fmt.Errorf("sandbox execution failed: %w", err) } result.Metrics.SandboxTime = time.Since(sandboxStartTime) result.Metrics.CommandsExecuted = len(commands) result.Metrics.ResourceUsage = sandboxResult.ResourceUsage artifacts = append(artifacts, sandboxResult.Artifacts...) } result.Output = e.formatOutput(aiResponse, artifacts) result.Artifacts = artifacts result.Metrics.FilesGenerated = len(artifacts) result.Metadata = map[string]interface{}{ "ai_provider": providerConfig.Type, "ai_model": providerConfig.DefaultModel, "role": role, "commands": len(commands), } return nil } // determineRoleFromTask analyzes the task to determine appropriate AI role func (e *DefaultTaskExecutionEngine) determineRoleFromTask(request *TaskExecutionRequest) string { if request == nil { return "developer" } if role := extractRoleFromContext(request.Context); role != "" { return role } typeLower := strings.ToLower(request.Type) descriptionLower := strings.ToLower(request.Description) switch { case strings.Contains(typeLower, "security") || strings.Contains(descriptionLower, "security"): return normalizeRole("security") case strings.Contains(typeLower, "test") || strings.Contains(descriptionLower, "test"): return normalizeRole("tester") case strings.Contains(typeLower, "review") || strings.Contains(descriptionLower, "review"): return normalizeRole("reviewer") case strings.Contains(typeLower, "design") || strings.Contains(typeLower, "architecture") || strings.Contains(descriptionLower, "architecture") || strings.Contains(descriptionLower, "design"): return normalizeRole("architect") case strings.Contains(typeLower, "analysis") || strings.Contains(descriptionLower, "analysis") || strings.Contains(descriptionLower, "analyz"): return normalizeRole("systems analyst") case strings.Contains(typeLower, "doc") || strings.Contains(descriptionLower, "documentation") || strings.Contains(descriptionLower, "docs"): return normalizeRole("technical writer") default: return normalizeRole("developer") } } func (e *DefaultTaskExecutionEngine) resolveSystemPrompt(role string, roleConfig ai.RoleConfig, ctx map[string]interface{}) string { if promptText := extractSystemPromptFromContext(ctx); promptText != "" { return promptText } if strings.TrimSpace(roleConfig.SystemPrompt) != "" { return strings.TrimSpace(roleConfig.SystemPrompt) } if role != "" { if composed, err := prompt.ComposeSystemPrompt(role); err == nil && strings.TrimSpace(composed) != "" { return composed } } if defaultInstr := prompt.GetDefaultInstructions(); strings.TrimSpace(defaultInstr) != "" { return strings.TrimSpace(defaultInstr) } return "" } func extractRoleFromContext(ctx map[string]interface{}) string { if ctx == nil { return "" } if rolesVal, ok := ctx["required_roles"]; ok { if roles := convertToStringSlice(rolesVal); len(roles) > 0 { for _, role := range roles { if normalized := normalizeRole(role); normalized != "" { return normalized } } } } candidates := []string{ extractStringFromContext(ctx, "required_role"), extractStringFromContext(ctx, "role"), extractStringFromContext(ctx, "agent_role"), extractStringFromNestedMap(ctx, "agent_info", "role"), extractStringFromNestedMap(ctx, "task_metadata", "required_role"), extractStringFromNestedMap(ctx, "task_metadata", "role"), extractStringFromNestedMap(ctx, "council", "role"), } for _, candidate := range candidates { if normalized := normalizeRole(candidate); normalized != "" { return normalized } } return "" } func extractSystemPromptFromContext(ctx map[string]interface{}) string { if promptText := extractStringFromContext(ctx, "system_prompt"); promptText != "" { return promptText } if promptText := extractStringFromNestedMap(ctx, "task_metadata", "system_prompt"); promptText != "" { return promptText } if promptText := extractStringFromNestedMap(ctx, "council", "system_prompt"); promptText != "" { return promptText } return "" } func extractTaskTitle(request *TaskExecutionRequest) string { if request == nil { return "" } if title := extractStringFromContext(request.Context, "task_title"); title != "" { return title } if title := extractStringFromNestedMap(request.Context, "task_metadata", "title"); title != "" { return title } if request.Type != "" { return request.Type } return request.ID } func extractRepository(ctx map[string]interface{}) string { if repo := extractStringFromContext(ctx, "repository"); repo != "" { return repo } if repo := extractStringFromNestedMap(ctx, "task_metadata", "repository"); repo != "" { return repo } return "" } func extractAgentID(ctx map[string]interface{}) string { if id := extractStringFromContext(ctx, "agent_id"); id != "" { return id } if id := extractStringFromNestedMap(ctx, "agent_info", "id"); id != "" { return id } return "" } func extractWorkingDirectory(ctx map[string]interface{}) string { if dir := extractStringFromContext(ctx, "working_directory"); dir != "" { return dir } if dir := extractStringFromNestedMap(ctx, "task_metadata", "working_directory"); dir != "" { return dir } return "" } func extractTaskLabels(ctx map[string]interface{}) []string { if ctx == nil { return nil } labels := convertToStringSlice(ctx["labels"]) if meta, ok := ctx["task_metadata"].(map[string]interface{}); ok { labels = append(labels, convertToStringSlice(meta["labels"])...) } return uniqueStrings(labels) } func convertToStringSlice(value interface{}) []string { switch v := value.(type) { case []string: result := make([]string, 0, len(v)) for _, item := range v { item = strings.TrimSpace(item) if item != "" { result = append(result, item) } } return result case []interface{}: result := make([]string, 0, len(v)) for _, item := range v { if str, ok := item.(string); ok { str = strings.TrimSpace(str) if str != "" { result = append(result, str) } } } return result case string: trimmed := strings.TrimSpace(v) if trimmed == "" { return nil } parts := strings.Split(trimmed, ",") if len(parts) == 1 { return []string{trimmed} } result := make([]string, 0, len(parts)) for _, part := range parts { p := strings.TrimSpace(part) if p != "" { result = append(result, p) } } return result default: return nil } } func uniqueStrings(values []string) []string { if len(values) == 0 { return nil } seen := make(map[string]struct{}) result := make([]string, 0, len(values)) for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { continue } if _, exists := seen[trimmed]; exists { continue } seen[trimmed] = struct{}{} result = append(result, trimmed) } if len(result) == 0 { return nil } return result } func extractContextInt(ctx map[string]interface{}, key string) int { if ctx == nil { return 0 } if value, ok := ctx[key]; ok { if intVal, ok := toInt(value); ok { return intVal } } if meta, ok := ctx["task_metadata"].(map[string]interface{}); ok { if value, ok := meta[key]; ok { if intVal, ok := toInt(value); ok { return intVal } } } return 0 } func toInt(value interface{}) (int, bool) { switch v := value.(type) { case int: return v, true case int32: return int(v), true case int64: return int(v), true case float64: return int(v), true case float32: return int(v), true case string: trimmed := strings.TrimSpace(v) if trimmed == "" { return 0, false } parsed, err := strconv.Atoi(trimmed) if err != nil { return 0, false } return parsed, true default: return 0, false } } func extractStringFromContext(ctx map[string]interface{}, key string) string { if ctx == nil { return "" } if value, ok := ctx[key]; ok { switch v := value.(type) { case string: return strings.TrimSpace(v) case fmt.Stringer: return strings.TrimSpace(v.String()) } } return "" } func extractStringFromNestedMap(ctx map[string]interface{}, parentKey, key string) string { if ctx == nil { return "" } nested, ok := ctx[parentKey].(map[string]interface{}) if !ok { return "" } return getStringFromMap(nested, key) } func getStringFromMap(m map[string]interface{}, key string) string { if m == nil { return "" } if value, ok := m[key]; ok { switch v := value.(type) { case string: return strings.TrimSpace(v) case fmt.Stringer: return strings.TrimSpace(v.String()) } } return "" } func combineStringSlices(base []string, extra []string) []string { if len(base) == 0 && len(extra) == 0 { return nil } seen := make(map[string]struct{}) combined := make([]string, 0, len(base)+len(extra)) appendValues := func(values []string) { for _, value := range values { trimmed := strings.TrimSpace(value) if trimmed == "" { continue } if _, exists := seen[trimmed]; exists { continue } seen[trimmed] = struct{}{} combined = append(combined, trimmed) } } appendValues(base) appendValues(extra) if len(combined) == 0 { return nil } return combined } func normalizeRole(role string) string { role = strings.TrimSpace(role) if role == "" { return "" } role = strings.ToLower(role) role = strings.ReplaceAll(role, "_", "-") role = strings.ReplaceAll(role, " ", "-") role = strings.ReplaceAll(role, "--", "-") return role } // parseAIResponse extracts executable commands and artifacts from AI response func (e *DefaultTaskExecutionEngine) parseAIResponse(response *ai.TaskResponse) ([]string, []TaskArtifact, error) { var commands []string var artifacts []TaskArtifact // Parse response content for commands and files // This is a simplified parser - in reality would need more sophisticated parsing if len(response.Actions) > 0 { for _, action := range response.Actions { switch action.Type { case "command", "command_run": // Extract command from content or target if action.Content != "" { commands = append(commands, action.Content) } else if action.Target != "" { commands = append(commands, action.Target) } case "file", "file_create", "file_edit": // Create artifact from file action if action.Target != "" && action.Content != "" { artifact := TaskArtifact{ Name: action.Target, Type: "file", Content: []byte(action.Content), Size: int64(len(action.Content)), CreatedAt: time.Now(), } artifacts = append(artifacts, artifact) } } } } return commands, artifacts, nil } // SandboxExecutionResult contains results from sandbox command execution type SandboxExecutionResult struct { Output string Artifacts []TaskArtifact ResourceUsage *ResourceUsage } // executeSandboxCommands runs commands in an isolated sandbox func (e *DefaultTaskExecutionEngine) executeSandboxCommands(ctx context.Context, request *TaskExecutionRequest, commands []string) (*SandboxExecutionResult, error) { // Create sandbox configuration sandboxConfig := e.createSandboxConfig(request) // Initialize sandbox sandbox := NewDockerSandbox() err := sandbox.Initialize(ctx, sandboxConfig) if err != nil { return nil, fmt.Errorf("failed to initialize sandbox: %w", err) } defer sandbox.Cleanup() var outputs []string var artifacts []TaskArtifact // Execute each command for _, cmdStr := range commands { cmd := &Command{ Executable: "/bin/sh", Args: []string{"-c", cmdStr}, WorkingDir: "/workspace", Timeout: 30 * time.Second, } cmdResult, err := sandbox.ExecuteCommand(ctx, cmd) if err != nil { return nil, fmt.Errorf("command execution failed: %w", err) } outputs = append(outputs, fmt.Sprintf("$ %s\n%s", cmdStr, cmdResult.Stdout)) if cmdResult.ExitCode != 0 { outputs = append(outputs, fmt.Sprintf("Error (exit %d): %s", cmdResult.ExitCode, cmdResult.Stderr)) } } // Get resource usage resourceUsage, _ := sandbox.GetResourceUsage(ctx) // Collect any generated files as artifacts files, err := sandbox.ListFiles(ctx, "/workspace") if err == nil { for _, file := range files { if !file.IsDir && file.Size > 0 { content, err := sandbox.ReadFile(ctx, "/workspace/"+file.Name) if err == nil { artifact := TaskArtifact{ Name: file.Name, Type: "generated_file", Content: content, Size: file.Size, CreatedAt: file.ModTime, } artifacts = append(artifacts, artifact) } } } } return &SandboxExecutionResult{ Output: strings.Join(outputs, "\n"), Artifacts: artifacts, ResourceUsage: resourceUsage, }, nil } // createSandboxConfig creates a sandbox configuration from task requirements func (e *DefaultTaskExecutionEngine) createSandboxConfig(request *TaskExecutionRequest) *SandboxConfig { // Use image selector to choose appropriate development environment imageSelector := NewImageSelector() selectedImage := imageSelector.SelectImageForTask(request) config := &SandboxConfig{ Type: "docker", Image: selectedImage, // Auto-selected based on task language Architecture: "amd64", WorkingDir: "/workspace/data", // Use standardized workspace structure Timeout: 5 * time.Minute, Environment: make(map[string]string), } // Add standardized workspace environment variables config.Environment["WORKSPACE_ROOT"] = "/workspace" config.Environment["WORKSPACE_INPUT"] = "/workspace/input" config.Environment["WORKSPACE_DATA"] = "/workspace/data" config.Environment["WORKSPACE_OUTPUT"] = "/workspace/output" // Apply defaults from engine config if e.config.SandboxDefaults != nil { if e.config.SandboxDefaults.Image != "" { config.Image = e.config.SandboxDefaults.Image } if e.config.SandboxDefaults.Resources.MemoryLimit > 0 { config.Resources = e.config.SandboxDefaults.Resources } if e.config.SandboxDefaults.Security.NoNewPrivileges { config.Security = e.config.SandboxDefaults.Security } } // Apply task-specific requirements if request.Requirements != nil { if request.Requirements.SandboxType != "" { config.Type = request.Requirements.SandboxType } if request.Requirements.EnvironmentVars != nil { for k, v := range request.Requirements.EnvironmentVars { config.Environment[k] = v } } if request.Requirements.ResourceLimits != nil { config.Resources = *request.Requirements.ResourceLimits } if request.Requirements.SecurityPolicy != nil { config.Security = *request.Requirements.SecurityPolicy } } return config } // formatOutput creates a formatted output string from AI response and artifacts func (e *DefaultTaskExecutionEngine) formatOutput(aiResponse *ai.TaskResponse, artifacts []TaskArtifact) string { var output strings.Builder output.WriteString("AI Response:\n") output.WriteString(aiResponse.Response) output.WriteString("\n\n") if len(artifacts) > 0 { output.WriteString("Generated Artifacts:\n") for _, artifact := range artifacts { output.WriteString(fmt.Sprintf("- %s (%s, %d bytes)\n", artifact.Name, artifact.Type, artifact.Size)) } } return output.String() } // GetMetrics returns current engine metrics func (e *DefaultTaskExecutionEngine) GetMetrics() *EngineMetrics { return e.metrics } // Shutdown gracefully shuts down the execution engine func (e *DefaultTaskExecutionEngine) Shutdown() error { e.logger.Printf("Shutting down TaskExecutionEngine...") // Cancel all active tasks for taskID, cancel := range e.activeTasks { e.logger.Printf("Canceling active task: %s", taskID) cancel() } // Wait for tasks to finish (with timeout) shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() for len(e.activeTasks) > 0 { select { case <-shutdownCtx.Done(): e.logger.Printf("Shutdown timeout reached, %d tasks may still be active", len(e.activeTasks)) return nil case <-time.After(100 * time.Millisecond): // Continue waiting } } e.logger.Printf("TaskExecutionEngine shutdown complete") return nil }