import type { ClientEvent, ServerEvent } from '../protocol.js'; import { formatToolError } from '../tools.js'; export type ClientToolHandler = (args: Record) => Promise; /** * The subset of `RealtimeClient`'s (or `ResilientRealtimeClient`'s) surface * this needs — accepted structurally so either transport works here without * this module caring which one it's given. */ export interface ToolDispatchClient { send(event: ClientEvent): void; on(handler: (event: ServerEvent) => void): () => void; } /** * Wires a client to a map of client-side tool handlers: listens for * `tool.call`, runs the matching handler, and sends its result back — kept * out of the transport itself, which stays transport-only. Handler * implementations are project-owned (`$lib/client/project/tools`); this is * just the generic dispatch mechanism. * * Returns an unsubscribe function, matching `on()`'s convention. */ export function createToolDispatcher( client: ToolDispatchClient, handlers: Record ): () => void { return client.on((event) => { if (event.type !== 'tool.call') return; void dispatch(client, handlers, event); }); } async function dispatch( client: ToolDispatchClient, handlers: Record, event: { callId: string; name: string; arguments: string } ): Promise { const handler = handlers[event.name]; if (!handler) { client.send({ type: 'tool.result', callId: event.callId, output: formatToolError(`No client handler for tool "${event.name}".`) }); return; } try { const args = JSON.parse(event.arguments); const result = await handler(args); client.send({ type: 'tool.result', callId: event.callId, output: JSON.stringify(result) }); } catch (err) { client.send({ type: 'tool.result', callId: event.callId, output: formatToolError(err instanceof Error ? err.message : 'Tool execution failed.') }); } }