/** * Cleo-owned Pi `StreamFn` — the Gate-13-respecting LLM route (T11761 · S2 · T11898). * * Pi's agent loop performs every LLM call through a single pluggable seam: the * `streamFn` passed as the 6th positional arg to `runAgentLoop`. The DEFAULT * streamFn (`pi-ai`'s `streamSimple`) resolves a provider from `pi-ai`'s own * registry and falls back to reading `process.env[_API_KEY]` when no * explicit key is supplied — a credential-resolution path OUTSIDE Cleo's E9 * chokepoint, and a Gate-13 violation. * * {@link createPiStreamFn} supplies a Cleo-owned replacement that routes the * loop's LLM calls through {@link resolveLLMForSystem} (the E9 resolution * chokepoint) → {@link ModelRunner.build} (the single SSoT transport factory). * Passing this streamFn to `runAgentLoop` means `pi-ai`'s `stream.ts` / * `withEnvApiKey` is NEVER reached for the loop — the registry env-fallback * cannot fire (foundation §5.1, the single most important suppression lever). * * ## Detached-producer teardown (T11898) * * The returned `StreamFn` MUST return its stream synchronously, so the async * producer is detached (`void produce(...)`). To stop that producer outliving * the call that started it, `ctx.signal` (the `wrapPiCall`-managed loop abort * signal) is threaded into the per-call `SendOptions.signal` AND the producer's * consumption loop bails on `signal.aborted` and explicitly returns the * iterator. Teardown therefore holds even against a transport that ignores the * signal option — closing the residual exit-escape window the daemon exit-guard * (`installDaemonExitGuard`) backstops. * * ## Gate-13 cleanliness (verified) * * This file CALLS `resolveLLMForSystem` + `ModelRunner.build` only. It constructs * NO transport (`new *Transport`), NO SDK client (`createAnthropic` / * `new Anthropic`), reads NO `*_API_KEY` env var, and carries NO hardcoded model * literal — all of that stays inside `model-runner.ts` / `transports/**` (the * Gate-13 allowlist). The descriptor's `model` comes from the resolver * (registry / role-config), never a literal here. * * ## Tool wire-through (T1739 — registry consumability) * * The Pi loop carries its tools (the agent registry's `toOpenAITools()` output) on * `Context.tools`. {@link toTransportTools} projects them onto the per-call * `SendOptions.tools`, which `ConcreteSession._buildRequest` forwards onto * `TransportRequest.tools` — so the wire-level transport advertises them to the * model. This closes the streamFn → transport tool link: registry tools given to * the loop are reachable by the model. (Tool-CALL *execution* — binding Pi's * `ExecutionEnv` onto the guarded surface — is a later subtask; only the schema * advertisement flows here.) * * ## Zod ↔ TypeBox quarantine (Gate 10) * * Pi tool schemas are TypeBox, but `pi-ai`'s `validateToolArguments` also accepts * plain JSON-schema tools. Cleo tool definitions are Zod (live in `core/src/llm/`). * {@link zodToolToTransportTool} converts a Zod schema → JSON Schema via Zod v4's * native `z.toJSONSchema` (NO `zod-to-json-schema` dep, NO typebox value-import), * so the cleo↔Pi tool boundary carries ZERO typebox. {@link toTransportTools} * passes the loop's Pi `Tool.parameters` (a TypeBox `TSchema` that is structurally * a JSON Schema object) through verbatim — also WITHOUT a typebox value-import. * typebox remains a transitive dep used only INSIDE `pi-agent-core`; it never * appears in cleo source and never reaches `packages/contracts/`. * * @epic T10403 * @task T11761 * @task T11898 * @task T1739 */ import type { TransportTool } from '@cleocode/contracts/llm/normalized-response.js'; import type { StreamFn } from '@earendil-works/pi-agent-core'; import type { z } from 'zod'; import type { PiAgentRunContext } from './pi-types.js'; /** * A Cleo tool definition in the form the Pi streamFn bridges to Pi's loop. * * Cleo tools carry a Zod parameter schema; {@link createPiStreamFn} converts it * to a JSON-schema {@link TransportTool} so NO typebox value-import is needed at * the boundary (Gate 10). The bridge is intentionally one-directional: cleo → * Pi as JSON-schema; Pi validates internally; cleo re-validates received args * against the Zod schema at the guard boundary (defense in depth, S3+). */ export interface PiZodTool { /** Tool name as Pi/the provider will see it. */ readonly name: string; /** Human-readable description for the model. */ readonly description: string; /** Zod schema for the tool's input parameters. */ readonly parameters: z.ZodType; } /** * Convert a Cleo Zod-schema tool into a JSON-schema {@link TransportTool}. * * Thin wrapper over the SINGLE shared generator * {@link zodSchemaToOpenAITool} (`core/src/tools/schema-gen.ts`) so the Pi bridge * and the agent registry (T1739 · AC3) share ONE conversion doctrine and cannot * drift (DRY). Uses Zod v4's native `z.toJSONSchema` — no `zod-to-json-schema` * dependency and, crucially, NO typebox value-import. The resulting `inputSchema` * is a plain JSON Schema object the transport passes through verbatim. * * @param tool - The Cleo Zod tool. * @returns The transport-shaped JSON-schema tool. */ export declare function zodToolToTransportTool(tool: PiZodTool): TransportTool; /** * Build a Cleo-owned {@link StreamFn} for one Pi agent run. * * The returned function matches Pi's `StreamFn` contract structurally: * `(model, context, options?) => AssistantMessageEventStream`. It MUST NOT throw * — every failure is encoded as a terminal `error` event on the returned stream * (Pi's never-throw streaming contract). * * Flow per call: * 1. `resolveLLMForSystem(ctx.system)` — the E9 resolution chokepoint. * 2. Build a clean {@link ResolvedLLMDescriptor} from the resolved fields. * 3. `ModelRunner.build(descriptor)` — the single SSoT transport factory. * 4. Stream `NormalizedDelta` chunks from the built transport session and * project them onto Pi's `AssistantMessageEvent` protocol, terminating with * a `done` (success) or `error` (failure) event. * * `pi-ai`'s `stream.ts` / `withEnvApiKey` is never reached — the registry * env-fallback cannot fire for the loop. * * @param ctx - The Pi run context (system-of-use label + identity + signal). * @returns A Cleo-owned `StreamFn`. */ export declare function createPiStreamFn(ctx: PiAgentRunContext): StreamFn; //# sourceMappingURL=pi-stream-fn.d.ts.map