/** * An `LlmProvider` seam over the LLM call so the agentic loop can be unit-tested * deterministically, without a live model or network. * * The agent already takes an injected OpenAI-compatible chat client * ({@link ChatClientLike}). This module *formalizes* that as the provider seam — * `LlmProvider` is an alias of `ChatClientLike`, so the existing `SmoothAgent` * constructor is unchanged and backward compatible (the real `openai` SDK still * satisfies it). * * It also ships a reusable, exported {@link MockLlmProvider} that replaces the * ad-hoc fake clients the tests rolled by hand. The mock: * * - is constructed with a script of responses — plain text, tool-call responses, * and errors; * - returns them in FIFO order across calls; * - records each request (the messages + tool specs it was given) so a test can * assert on what the agent sent. * * This mirrors the BEHAVIOR of the Rust reference's `MockLlmClient` * (`rust/smooth-operator-core/src/llm_provider.rs`). The mock implements both the * non-streaming `create` seam (used by {@link SmoothAgent.run}) and the streaming * `createStream` seam (used by {@link SmoothAgent.runStream}): it replays the SAME * FIFO script as chunked deltas — text split into a few pieces, tool-call * `arguments` split across two chunks to exercise the accumulator, and a final * chunk carrying usage. Structured-output lands when that feature lands here. */ import type { ChatChunk, ChatClientLike } from './agent.js'; /** The LLM call surface the agent loop depends on. Identical to {@link ChatClientLike}. */ export type LlmProvider = ChatClientLike; /** An OpenAI-shaped assistant message — the `choices[0].message` the agent reads. */ export interface ScriptedMessage { content: string | null; tool_calls?: Array<{ id: string; function: { name: string; arguments: string; }; }> | null; } /** Build a plain-text scripted response (no tool calls). */ export declare function textResponse(content: string): ScriptedMessage; /** Build a scripted response that requests a single tool call. */ export declare function toolCallResponse(id: string, name: string, args: string): ScriptedMessage; /** One request the mock received, captured for assertions. */ export interface RecordedCall { /** The full request body passed to `chat.completions.create`. */ body: Record; /** The messages passed on this call. */ messages: Array>; /** The tool specs offered to the model, if any. */ tools?: Array>; } /** Optional token usage to attach to a scripted response (the model/gateway reports it). */ export interface ScriptedUsage { prompt_tokens?: number; completion_tokens?: number; } /** * The token usage a **scripted** mock response reports. Fixed, and identical in * all five engines' mocks (Rust · Go · Python · TypeScript · C#), so the shared * server scenario corpus can assert `eventual_response.usage` as a real * cross-language invariant instead of documenting five different answers * (pearl th-4f1263). * * Only the FIFO scripting helpers ({@link MockLlmProvider.pushText} / * {@link MockLlmProvider.pushToolCall}) attach it. An *unscripted* response — the * benign empty reply a drained script falls back to — still reports nothing, so * "the script ran out" stays distinguishable from "the model answered". Pass an * explicit `usage` to override. */ export declare const SCRIPTED_USAGE: Readonly>; /** * A deterministic {@link LlmProvider} for tests. Script the responses it should * return (FIFO), drive your code, then assert on {@link MockLlmProvider.calls}. * * Construct empty and build up fluently (`pushText` / `pushToolCall` / `pushError`), * or pass an initial script of {@link ScriptedMessage}s. * * @example * const mock = new MockLlmProvider(); * mock.pushText('hello there'); * const agent = new SmoothAgent(mock, {}); * const result = await agent.run('hi'); * expect(result.text).toBe('hello there'); * expect(mock.callCount).toBe(1); */ export declare class MockLlmProvider implements ChatClientLike { private readonly script; private readonly recorded; constructor(script?: ScriptedMessage[]); /** Queue a raw OpenAI-shaped assistant message (with optional usage) for the next call. */ pushResponse(message: ScriptedMessage, usage?: ScriptedUsage): this; /** Queue a plain-text response for the next call, reporting {@link SCRIPTED_USAGE} unless `usage` overrides it. */ pushText(content: string, usage?: ScriptedUsage): this; /** Queue a single-tool-call response for the next call, reporting {@link SCRIPTED_USAGE} unless `usage` overrides it. */ pushToolCall(id: string, name: string, args: string, usage?: ScriptedUsage): this; /** Queue an error to be thrown on the next call. */ pushError(message: string): this; /** Every request the mock has received so far, in order. */ get calls(): readonly RecordedCall[]; /** Number of requests received. */ get callCount(): number; /** The most recent request, if any. */ get lastCall(): RecordedCall | undefined; private record; readonly chat: { completions: { create: (body: Record) => Promise<{ choices: { message: ScriptedMessage; }[]; usage: ScriptedUsage | null; }>; createStream: (body: Record) => AsyncIterable; }; }; } //# sourceMappingURL=llmProvider.d.ts.map