/** * Bind step — walks `agent.tools[]` (or every agent's `tools[]` in a * graph) at `tasks.startWithAgent` / `tasks.startWithGraph` time, * extracts any `defineFunctionTool` handler bags, and installs a single * primary `onRequest` handler on the supplied `ToolClient` that * dispatches inbound `tool.request` calls by `name` to the right * handler. * * Idempotency: calling the bind step repeatedly with the same tools is * cheap — the underlying handler-table is a Map keyed by tool name, so * re-binding overrides in-place. The single `onRequest` subscription * is attached lazily the first time any handler is registered and * stays attached for the lifetime of the client; subsequent task * starts only mutate the table. * * Attachment mode — **fallback, not primary**. The router installs via * `ToolClient._attachFallbackHandler(...)`, not the public * `onRequest(...)`. Two reasons: * * 1. **Deterministic-deny preservation.** The dispatcher's * "zero-primary-handlers → emit denied" path keys off * `this.handlers.length === 0`. Installing the router as a * primary handler would defeat that fallback whenever the * consumer hadn't also registered a manual `onRequest` — * a batch with an unregistered tool name would return * `undefined` from our router and then nobody would claim it, * leaving QAR's agent loop stalled until the per-task tool-response * timeout. As a fallback handler, our router runs AFTER the * primary stack but does NOT count toward the * no-primaries check, so unrecognized batches still get the * deterministic deny. * 2. **Consumer-explicit beats SDK-helper.** When a consumer * registers a manual `onRequest` for tool name "X" AND defines * `defineFunctionTool({ name: 'X' })`, the consumer's explicit * handler wins — the primary stack always runs first. * * Dispatch policy: * * - If every call name in the inbound batch matches a registered * function-tool handler, the router claims the batch and returns * the positional result array. Each handler runs in isolation; * per-call exceptions are mapped to per-call * `outcome: 'error'` / `outcome: 'denied'` (the latter when the * handler throws `ToolDeniedError`). * - If ANY call in the batch has a name NOT in the table, the * router returns `undefined`. With no other fallback claiming * AND no primary handler, the dispatcher's deny path fires for * the whole batch. With a primary handler present, the dispatcher * defers (per the documented "consumer will respond later" * semantics — primary handlers already ran first). * * Argument parsing: the router invokes `bag.parameters.safeParse(...)` * on `call.arguments` before invoking the handler. Successful parse * yields the typed output (defaults applied, transforms run); * failures map to a per-call `outcome: 'error'` with the formatted * Zod issue list. This matches the type contract — `handler: (args: * z.infer) => ...` promises the OUTPUT type, not the wire * INPUT type — and gives consumers the DX they expect from a Zod- * derived helper. */ import type { ToolClient } from '../ToolClient.js'; import type { FunctionToolDef } from '../../qar/specs.js'; /** * Per-client registry of `defineFunctionTool` handlers. One instance * per `QodoClient`; shared across every `tasks.startWith*` call so the * same handler stays installed even as the consumer dispatches multiple * tasks. * * Exposed as a class (not a module-level Map) so each client gets an * isolated table — multi-client test harnesses don't see cross-talk. */ export declare class FunctionToolRouter { private readonly bags; private subscription; /** * Resolver invoked when a `requireApproval`-gated tool is about to * dispatch. Owned by the parent `ToolClient` (consumer registers via * `client.tools.setApprovalHandler(...)`); the router holds a * getter so an override registered AFTER `attach()` still applies * to subsequent inbound batches. */ private getApprovalHandler; /** * Predicate read by {@link runOne} AFTER an approval resolves * `approve` and BEFORE the function-tool handler is invoked. When * the wire has died during the approval round-trip (auto-reconnect * exhausted, `failHardAndCleanup` ran, or an explicit * `client.disconnect()` raced the resolver), this returns `false` * and the router synthesizes a per-call * `outcome: 'error'` — the handler does NOT execute, so no * real-world side effect occurs that we wouldn't be able to * acknowledge on the wire. * * Defaulted to a permissive `true` so the router stays * functionally identical before `attach()` (unit tests construct * the router directly + drive `dispatch` without ever calling * `attach`). */ private isConnectionLive; /** * Register every `defineFunctionTool`-produced entry in `tools` with * the router. Hand-authored `FunctionToolDef` literals (no symbol * handler attached) are skipped. Returns the number of newly * registered names — useful for tests + breadcrumb logging. * * If `tools` is empty / `null` / `undefined`, the router is left * alone — no auto-attach, no error. */ register(tools: readonly FunctionToolDef[] | null | undefined): number; /** * Lazy attach. The single fallback handler is registered the first * time the router is asked to attach against a `ToolClient`; later * calls are no-ops as long as the subscription remains live. The * router does NOT auto-detach on `client.disconnect()` — re-binding * after reconnect is the consumer's responsibility (the existing * subscription becomes stale; the dispatcher's own `attachDispatcher` * handles re-attachment). * * Attaches via `_attachFallbackHandler` so the dispatcher's * deterministic-deny path stays intact for unregistered tool names * (see the module header for rationale). */ attach(client: ToolClient): void; /** * Detach the underlying `onRequest` subscription. Used in tests + by * `QodoClient.disconnect()` so a client teardown cleanly drops the * router-installed handler. */ detach(): void; /** * The actual dispatch handler — wired as the body of the single * `onRequest` subscription. * * Returns `undefined` (fall-through) when: * * - The router has no handlers registered (rare — only if `attach` * was called before any `register`). * - Any call in the inbound batch has a name not in the table — * defers to the next handler so mixed surfaces still work. * * Otherwise, invokes every handler in parallel * (`Promise.all([...])`) and returns the positional result array. * Exceptions per-handler are mapped to per-call response shapes. */ private dispatch; /** * Invoke a single handler. Catches throws and maps them to a * per-call `ToolResponseResult`. `ToolDeniedError` becomes `outcome: * 'denied'`; everything else becomes `outcome: 'error'`. Mirrors the * mapping logic in `ToolClient.dispatchInner` so consumers see the * same semantics whether they use `defineFunctionTool` or the manual * `onRequest` surface. */ private runOne; } //# sourceMappingURL=bindFunctionToolDefs.d.ts.map