- ProviderManager: align API with contracts ProviderAdapter - WorkerProtocol: add llm.request/llm.response message types - WorkerRuntime: add call_llm() for worker→parent→LLM flow - WorkerManager: support tool_registry and provider_manager injection P1-1 complete, P1-2 protocol layer complete. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
222 lines
5.9 KiB
TypeScript
Executable File
222 lines
5.9 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<{ 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', { 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))
|
|
}
|
|
}
|