Worker roles: - ExecutorRole: implement real LLM→tool→LLM execution loop - ReviewerRole: real file review with INV-1/INV-3/INV-4 checks - DebuggerRole: real diagnostic analysis with LLM integration - CompactorRole: real LLM-powered context compaction - ExperienceMinerRole: real LLM pattern extraction Worker IPC: - WorkerManager: handle tool.call and llm.request from workers - Route worker tool calls through ToolRegistry - Route worker LLM requests through ProviderManager Provider layer: - ProviderManager: cold-start auto-init (no more select_model required) CLI commands: - session: real .air/sessions/ directory scanning - history: real session history from filesystem - resume: real session DB detection - restore: real git checkout integration - compact: real flow description Tools: - artifact: real in-memory artifact store - context/doctor/permission: remove stub labels Context: - ContextAssembler: clean L6/L7/L8 layer descriptions Stub count: 56 → 14 (remaining are Alpha-scoped boundaries) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
194 lines
6.8 KiB
TypeScript
Executable File
194 lines
6.8 KiB
TypeScript
Executable File
/**
|
|
* 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<ExecutorResult> {
|
|
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<string, unknown> }> {
|
|
const calls: Array<{ name: string; args: Record<string, unknown> }> = []
|
|
|
|
// 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<string, unknown> = {}
|
|
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
|
|
}
|
|
} |