/** * Cross-extension RPC handlers for the subagents extension. * * Exposes ping, spawn, and stop RPCs over the pi.events event bus, * using per-request scoped reply channels. * * Reply envelope follows pi-mono convention: * success → { success: true, data?: T } * error → { success: false, error: string } */ /** Minimal event bus interface needed by the RPC handlers. */ export interface EventBus { on(event: string, handler: (data: unknown) => void): () => void; emit(event: string, data: unknown): void; } /** RPC reply envelope — matches pi-mono's RpcResponse shape. */ export type RpcReply = | { success: true; data?: T } | { success: false; error: string }; /** RPC protocol version — bumped when the envelope or method contracts change. */ export const PROTOCOL_VERSION = 2; /** Minimal AgentManager interface needed by the spawn/stop RPCs. */ export interface SpawnCapable { spawn(pi: unknown, ctx: unknown, type: string, prompt: string, options: any): string; abort(id: string): boolean; } export interface RpcDeps { events: EventBus; pi: unknown; // passed through to manager.spawn getCtx: () => unknown | undefined; // returns current ExtensionContext manager: SpawnCapable; } export interface RpcHandle { unsubPing: () => void; unsubSpawn: () => void; unsubStop: () => void; } /** * Wire a single RPC handler: listen on `channel`, run `fn(params)`, * emit the reply envelope on `channel:reply:${requestId}`. */ function handleRpc

( events: EventBus, channel: string, fn: (params: P) => unknown | Promise, ): () => void { return events.on(channel, async (raw: unknown) => { const params = raw as P; try { const data = await fn(params); const reply: { success: true; data?: unknown } = { success: true }; if (data !== undefined) reply.data = data; events.emit(`${channel}:reply:${params.requestId}`, reply); } catch (err: any) { events.emit(`${channel}:reply:${params.requestId}`, { success: false, error: err?.message ?? String(err), }); } }); } /** * Register ping, spawn, and stop RPC handlers on the event bus. * Returns unsub functions for cleanup. */ export function registerRpcHandlers(deps: RpcDeps): RpcHandle { const { events, pi, getCtx, manager } = deps; const unsubPing = handleRpc(events, "subagents:rpc:ping", () => { return { version: PROTOCOL_VERSION }; }); const unsubSpawn = handleRpc<{ requestId: string; type: string; prompt: string; options?: any }>( events, "subagents:rpc:spawn", ({ type, prompt, options }) => { const ctx = getCtx(); if (!ctx) throw new Error("No active session"); return { id: manager.spawn(pi, ctx, type, prompt, options ?? {}) }; }, ); const unsubStop = handleRpc<{ requestId: string; agentId: string }>( events, "subagents:rpc:stop", ({ agentId }) => { if (!manager.abort(agentId)) throw new Error("Agent not found"); }, ); return { unsubPing, unsubSpawn, unsubStop }; }