Files
CHORUS/pkg/execution/engine.go
anthonyrawlins df5ec34b4f feat(execution): Add response parser for LLM artifact extraction
Implements regex-based response parser to extract file creation actions
and artifacts from LLM text responses. Agents can now produce actual
work products (files, PRs) instead of just returning instructions.

Changes:
- pkg/ai/response_parser.go: New parser with 4 extraction patterns
  * Markdown code blocks with filename comments
  * Inline backtick filenames followed by "content:" and code blocks
  * File header notation (--- filename: ---)
  * Shell heredoc syntax (cat > file << EOF)

- pkg/execution/engine.go: Skip sandbox when SandboxType empty/none
  * Prevents Docker container errors during testing
  * Preserves artifacts from AI response without sandbox execution

- pkg/ai/{ollama,resetdata}.go: Integrate response parser
  * Both providers now parse LLM output for extractable artifacts
  * Fallback to task_analysis action if no artifacts found

- internal/runtime/agent_support.go: Fix AI provider initialization
  * Set DefaultProvider in RoleModelMapping (prevents "provider not found")

- prompts/defaults.md: Add Rule O for output format guidance
  * Instructs LLMs to format responses for artifact extraction
  * Provides examples and patterns for file creation/modification
  * Explains pipeline: extraction → workspace → tests → PR → review

Test results:
- Before: 0 artifacts, 0 files generated
- After: 2 artifacts extracted successfully from LLM response
- hello.sh (60 bytes) with correct shell script content

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-11 22:08:08 +11:00

872 lines
25 KiB
Go

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
}