Files
AirCoding/packages/workers/src/WorkerRuntime.ts
AirCoding 5e282a39b4 feat(round2+round3): 完整实现 A/B/C/D 主线 + round3-F/H 修复
Round2 主线:
- A: 事件落库地基 (RuntimeApp EventStore 单例 + 14 repo wiring)
- B: 执行体对齐 (read-before-edit, verification-before-completion)
- C: 界面对齐 (@opentui/solid, 删除 runtime 依赖)
- D: 经验闭环 (ExperienceMiner, DebuggerRole, CompactorRole)

Round2 补充修复:
- fail-on-missing 反作弊门禁
- projection-store-apply.test.ts 补写
- 3个空壳测试转行为 (evidence-store, recovery-impl, knowledge-store)
- ask 项目根支持 AIRCODING_PROJECT_ROOT
- Worker 事件契约修复 (task.attempt.started → checkpoint)

Round3-F: cpp 工具切换
- 删除 BuiltInToolRegistrar cpp.* 闭包
- 接入 toolchain-cpp 真实 CppToolRegistrar
- canonical envelope {status/output/metadata}
- ExecutorRole system prompt 对齐新工具名

Round3-H: Doctor 5 类报告
- toolchain (cmake/ninja/cppcheck/clangd/g++)
- display (X11/Wayland + ImageMagick)
- network (internet connectivity)
- provider (api_key/base_url/model/connectivity)

Secret 脱敏:
- 状态交接.md: sk- → \${OPENAI_API_KEY}
- .gitignore: 添加 .air/ .claude/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 16:13:16 +08:00

222 lines
6.0 KiB
TypeScript
Executable File

/**
* WorkerRuntime - In-worker side-effect surface
*
* Implements contracts §10; DD §8.3.
* INV-3: workers reach fs/shell/network/SQLite ONLY through parent-mediated tool IPC.
*
* @module packages/workers/src/WorkerRuntime
*/
export interface WorkerRuntimeConfig {
agent_id: string
session_id: string
}
export interface ToolCallRequest {
call_id: string
name: string
arguments: Record<string, unknown>
}
export interface ToolCallResult {
call_id: string
type: 'text' | 'error' | 'artifact'
content: Record<string, unknown>
}
export interface LLMRequest {
messages: Array<{ role: string; content: unknown }>
model?: string
max_tokens?: number
temperature?: number
tools?: unknown[]
}
export interface LLMResponse {
content: string
usage?: { input_tokens: number; output_tokens: number }
tool_calls?: Array<{ id?: string; name: string; arguments: Record<string, unknown> }>
}
export class WorkerRuntime {
private agent_id: string
private session_id: string
private pending_calls: Map<string, { resolve: (r: ToolCallResult) => void; reject: (e: Error) => void }> = new Map()
private output: (line: string) => void
constructor(config: WorkerRuntimeConfig, output: (line: string) => void) {
this.agent_id = config.agent_id
this.session_id = config.session_id
this.output = output
}
/**
* Call a tool through the parent process via IPC.
* INV-3: This is the ONLY way workers interact with the outside world.
*/
async call_tool(name: string, args: Record<string, unknown>): Promise<ToolCallResult> {
const call_id = crypto.randomUUID()
const promise = new Promise<ToolCallResult>((resolve, reject) => {
this.pending_calls.set(call_id, { resolve, reject })
// Set timeout
setTimeout(() => {
this.pending_calls.delete(call_id)
reject(new Error(`Tool call timeout: ${name}`))
}, 300000) // 5 minutes
})
// Send tool.call via IPC
this.send_message('tool.call', {
call_id,
name,
arguments: args
})
return promise
}
/**
* Call LLM through parent process via IPC.
* INV-3: This is the ONLY way workers access LLM.
*/
async call_llm(request: LLMRequest): Promise<LLMResponse> {
const call_id = crypto.randomUUID()
const promise = new Promise<LLMResponse>((resolve, reject) => {
this.pending_calls.set(call_id, { resolve: resolve as any, reject })
// Set timeout (LLM calls can be long)
setTimeout(() => {
this.pending_calls.delete(call_id)
reject(new Error(`LLM call timeout: ${request.model || 'default'}`))
}, 300000) // 5 minutes
})
// Send llm.request via IPC
this.send_message('llm.request', {
call_id,
messages: request.messages,
model: request.model || 'claude-haiku-4-5-20251001',
max_tokens: request.max_tokens || 4096,
temperature: request.temperature,
tools: request.tools
})
return promise
}
/**
* Emit an event to the parent.
*/
emit(type: string, payload: Record<string, unknown>): void {
this.send_message('event', { event_type: type, ...payload })
}
/**
* Create a checkpoint.
*/
checkpoint(name: string, data?: Record<string, unknown>): void {
this.send_message('worker.checkpoint', { name, data })
}
/**
* Report worker result to parent.
*/
async report_result(result: Record<string, unknown>): Promise<void> {
this.send_message('worker.result', result)
}
/**
* Send heartbeat.
*/
heartbeat(): void {
this.send_message('worker.heartbeat', { timestamp: new Date().toISOString() })
}
/**
* Handle incoming message from parent (tool.result, agent.cancel, agent.ping).
*/
handle_message(type: string, payload: Record<string, unknown>): void {
switch (type) {
case 'tool.result': {
const call_id = payload.call_id as string
const pending = this.pending_calls.get(call_id)
if (pending) {
this.pending_calls.delete(call_id)
pending.resolve(payload as unknown as ToolCallResult)
}
break
}
case 'agent.cancel':
// Cancel all pending calls
for (const [id, pending] of this.pending_calls) {
pending.reject(new Error('Agent cancelled'))
this.pending_calls.delete(id)
}
break
case 'agent.ping':
// Respond to ping
this.send_message('worker.heartbeat', { timestamp: new Date().toISOString() })
break
case 'llm.response': {
const call_id = payload.call_id as string
const pending = this.pending_calls.get(call_id)
if (pending) {
this.pending_calls.delete(call_id)
// Resolve with LLM response
const response: LLMResponse = {
content: (payload.content as string) || '',
usage: payload.usage as LLMResponse['usage'],
tool_calls: payload.tool_calls as LLMResponse['tool_calls']
}
pending.resolve(response as any)
}
break
}
}
}
/**
* Send ready handshake.
*/
send_ready(protocol_version: number, worker_version: string): void {
this.send_message('worker.ready', {
protocol_version,
worker_version,
agent_id: this.agent_id,
session_id: this.session_id
})
}
/**
* Send error.
*/
send_error(message: string): void {
this.send_message('worker.error', { message })
}
// ============================================================================
// Private
// ============================================================================
private send_message(type: string, payload: Record<string, unknown>): void {
const msg = {
id: crypto.randomUUID(),
kind: type, // IpcKind per contracts §10
type,
direction: 'worker_to_parent',
session_id: this.session_id,
agent_id: this.agent_id,
protocol_version: 1,
timestamp: new Date().toISOString(),
payload
}
this.output(JSON.stringify(msg))
}
}