import { spawn, type ChildProcessByStdio } from "node:child_process"; import { createInterface } from "node:readline"; import type { Readable } from "node:stream"; import type { DelegateEvent, DelegateProcess, DelegateProcessAdapter, DelegateRequest, } from "./delegate-runner.js"; function assistantText(message: any): string { if (!Array.isArray(message?.content)) return ""; return message.content .filter((block: any) => block?.type === "text") .map((block: any) => String(block.text ?? "")) .join("\n"); } function toEvent(message: any): DelegateEvent | undefined { if (message?.role !== "assistant" || !message.usage) return undefined; return { type: "assistant", text: assistantText(message), usage: { input: message.usage.input ?? 0, output: message.usage.output ?? 0, cacheRead: message.usage.cacheRead ?? 0, cacheWrite: message.usage.cacheWrite ?? 0, cost: message.usage.cost?.total ?? 0, }, }; } class PiDelegateProcess implements DelegateProcess { constructor( private readonly child: ChildProcessByStdio, private readonly request: DelegateRequest, ) {} async *events(): AsyncIterable { const lines = createInterface({ input: this.child.stdout }); let stderr = ""; this.child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); const abort = () => this.terminate(); this.request.signal?.addEventListener("abort", abort, { once: true }); try { for await (const line of lines) { let event: any; try { event = JSON.parse(line); } catch { continue; } if (event.type !== "message_end") continue; const delegateEvent = toEvent(event.message); if (delegateEvent) yield delegateEvent; } const exitCode = await new Promise((resolve) => { if (this.child.exitCode !== null) resolve(this.child.exitCode); else this.child.once("close", (code) => resolve(code ?? 1)); }); if (exitCode !== 0 && !this.request.signal?.aborted) { throw new Error(stderr.trim() || `Delegate process exited with ${exitCode}`); } } finally { this.request.signal?.removeEventListener("abort", abort); lines.close(); } } terminate(): void { if (this.child.exitCode === null) this.child.kill("SIGTERM"); } } export function createPiDelegateProcessAdapter(): DelegateProcessAdapter { return { async start(request) { const args = [ "--mode", "json", "--print", "--no-session", "--model", request.model, "--thinking", "low", "--tools", request.tools.join(","), `Task: ${request.task}\nReturn concise findings only.`, ]; const child = spawn("pi", args, { cwd: request.cwd, shell: false, stdio: ["ignore", "pipe", "pipe"], }); return new PiDelegateProcess(child, request); }, }; }