/** * 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 } export interface ToolCallResult { call_id: string type: 'text' | 'error' | 'artifact' content: Record } 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 }> } export class WorkerRuntime { private agent_id: string private session_id: string private pending_calls: Map 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): Promise { const call_id = crypto.randomUUID() const promise = new Promise((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 { const call_id = crypto.randomUUID() const promise = new Promise((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): void { this.send_message('event', { event_type: type, ...payload }) } /** * Create a checkpoint. */ checkpoint(name: string, data?: Record): void { this.send_message('worker.checkpoint', { name, data }) } /** * Report worker result to parent. */ async report_result(result: Record): Promise { 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): 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): 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)) } }