Files
AirCoding/packages/workers/src/roles/ExecutorRole.ts
AirCoding ea136d600f feat: complete all remaining stubs — V1.0.0 Alpha release-ready
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>
2026-06-05 10:51:25 +08:00

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
}
}