/** * ExecutorRole - Implementation worker * Implements DD §8.4. Executes tasks using LLM→tool→LLM loop. * * @module packages/workers/src/roles/ExecutorRole */ import { WorkerRuntime } from '../WorkerRuntime.js' export interface ExecutorResult { status: 'completed' | 'failed' | 'blocked' changes?: Array<{ file: string; type: 'create' | 'edit' | 'delete' }> verification?: { passed: boolean; output: string } error?: string evidence_refs?: string[] } export class ExecutorRole { private runtime: WorkerRuntime private max_turns: number = 10 constructor(runtime: WorkerRuntime) { this.runtime = runtime } async run(task_spec: { id: string; title: string; description: string; acceptance_criteria: string[] }): Promise { this.runtime.emit('task.attempt.started', { task_id: task_spec.id }) try { const messages: Array<{ role: string; content: unknown }> = [ { role: 'system', content: `You are an AI coding executor. Complete the task by reading files, writing code, and running verification. When you must read or write a file, output a JSON tool_call block. When you are done, output "TASK_COMPLETE" followed by a summary. Available tools: fs.read(path), fs.write(path, content), fs.edit(path, old_str, new_str), fs.list(dir), git.status(), shell.run(command)` }, { role: 'user', content: `Task: ${task_spec.title}\n\nDescription: ${task_spec.description}\n\nAcceptance criteria:\n${task_spec.acceptance_criteria.map((c, i) => `${i + 1}. ${c}`).join('\n')}` } ] let turn = 0 const changes: Array<{ file: string; type: 'create' | 'edit' | 'delete' }> = [] let verification: { passed: boolean; output: string } | undefined while (turn < this.max_turns) { turn++ this.runtime.heartbeat() // Call LLM const llm_response = await this.runtime.call_llm({ messages, max_tokens: 4096, temperature: 0.3 }) const response_text = llm_response.content || '' // Check for completion signal if (response_text.includes('TASK_COMPLETE')) { const summary = response_text.split('TASK_COMPLETE')[1]?.trim() || 'Task completed' await this.runtime.checkpoint('task_completed', { task_id: task_spec.id, summary }) return { status: 'completed', changes, verification, evidence_refs: [] } } // Parse tool calls from LLM response const tool_calls = this.parse_tool_calls(response_text) if (tool_calls.length === 0) { // No tool calls - LLM is just talking, add to messages and continue messages.push({ role: 'assistant', content: response_text }) messages.push({ role: 'user', content: 'Continue. What actions will you take? Use tool calls (JSON format) to read/write files.' }) continue } // Execute each tool call for (const tc of tool_calls) { try { const result = await this.runtime.call_tool(tc.name, tc.args) const tool_output = result.type === 'error' ? `Error: ${JSON.stringify(result.content)}` : JSON.stringify(result.content) // Track file changes if (tc.name === 'fs.write' && tc.args.path) { changes.push({ file: tc.args.path as string, type: 'create' }) } else if (tc.name === 'fs.edit' && tc.args.path) { changes.push({ file: tc.args.path as string, type: 'edit' }) } // Add assistant tool call + tool result to messages messages.push({ role: 'assistant', content: `Tool call: ${tc.name}(${JSON.stringify(tc.args)})` }) messages.push({ role: 'user', content: `Tool result: ${tool_output}` }) } catch (e: any) { messages.push({ role: 'user', content: `Tool error: ${e.message}` }) } } // After tool execution, ask LLM to verify and continue messages.push({ role: 'user', content: 'Tools executed. Review the results. If the task is complete, respond with TASK_COMPLETE. Otherwise, continue with more tool calls.' }) } // Max turns reached return { status: 'blocked', error: `Task exceeded ${this.max_turns} turns without completion`, changes, evidence_refs: [] } } catch (error) { this.runtime.emit('task.blocked', { task_id: task_spec.id, error: error instanceof Error ? error.message : String(error) }) return { status: 'blocked', error: error instanceof Error ? error.message : String(error) } } } /** * Parse tool calls from LLM response text. * Supports JSON tool_call format and function-call markdown blocks. */ private parse_tool_calls(text: string): Array<{ name: string; args: Record }> { const calls: Array<{ name: string; args: Record }> = [] // Pattern 1: JSON tool_call blocks const json_pattern = /\{[\s\n]*"tool_call"[\s\n]*:[\s\n]*\{[^}]+\}[\s\n]*\}/g for (const match of text.match(json_pattern) || []) { try { const parsed = JSON.parse(match) if (parsed.tool_call) { calls.push({ name: parsed.tool_call.name, args: parsed.tool_call.args || {} }) } } catch { /* skip invalid JSON */ } } // Pattern 2: function(name, args) format const func_pattern = /(\w+)\.(\w+)\(([^)]*)\)/g for (const match of text.matchAll(func_pattern)) { const [_, namespace, func, args_str] = match const name = `${namespace}.${func}` const args: Record = {} if (args_str) { // Simple key:value parsing const pairs = args_str.match(/(\w+)\s*:\s*("[^"]*"|'[^']*'|[^,]+)/g) || [] for (const pair of pairs) { const [key, ...value_parts] = pair.split(':') const value = value_parts.join(':').trim().replace(/^["']|["']$/g, '') args[key.trim()] = value } } calls.push({ name, args }) } // Pattern 3: ```tool_call JSON blocks const block_pattern = /```(?:json)?\s*\n?\{[\s\n]*"tool"[\s\n]*:[\s\n]*"[^"]+"[\s\n]*,[\s\n]*"args"[\s\n]*:[\s\n]*\{[^}]*\}[\s\n]*\}[\s\n]*```/g for (const match of text.match(block_pattern) || []) { try { const json_str = match.replace(/```(?:json)?\s*\n?/g, '').replace(/```/g, '').trim() const parsed = JSON.parse(json_str) if (parsed.tool) { calls.push({ name: parsed.tool, args: parsed.args || {} }) } } catch { /* skip */ } } return calls } }