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>
222 lines
6.0 KiB
TypeScript
Executable File
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))
|
|
}
|
|
}
|