agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
179 lines (178 loc) • 8.05 kB
TypeScript
/**
* MockProvider — deterministic LLMProvider for tests + examples.
*
* Pattern: Adapter (GoF, Design Patterns ch. 4).
* Role: Ports-and-Adapters outer ring (Cockburn, 2005) — implements the
* LLMProvider port without calling out to a real LLM service.
* Emits: N/A (adapters don't emit; recorders observe them).
*
* Two modes, same provider:
* • DEFAULT (no latency options) — instant `complete()`, no streaming.
* Use in unit tests; behavior unchanged from earlier revisions.
* • REALISTIC (`thinkingMs` set) — randomised "thinking" latency before
* the response, plus a `stream()` implementation that emits the
* content word-by-word with `chunkDelayMs` between chunks. Lets
* consumers SEE the run unfold (pauses, streaming tokens, tool
* dispatch) instead of having every step finish in microseconds.
*
* Realistic mode is the right default for the playground / Lens demos:
* a real OpenAI / Anthropic call takes 3–8 s, so the UX you build for
* that timing is what matters. Use `MockProvider.realistic()` for the
* common 3–8 s thinking + 30–80 ms per word streaming preset.
*/
import type { LLMChunk, LLMProvider, LLMRequest, LLMResponse, WireRole } from '../../provider-contract/types.js';
/** Either a fixed value (in ms) or a random `[min, max]` range (inclusive). */
export type LatencyMs = number | readonly [number, number];
/**
* One scripted reply consumed in order from `MockProviderOptions.replies`.
* String → plain text content; Partial<LLMResponse> → can include
* `toolCalls`, `usage`, `stopReason` for tool-using ReAct loops.
*/
export type MockReply = string | Partial<LLMResponse>;
export interface MockProviderOptions {
readonly name?: string;
/** Fixed response content. Overrides `respond` when set. */
readonly reply?: string;
/**
* Scripted replies for multi-turn / tool-using agents. Each entry
* is consumed in order — iteration 1 reads `replies[0]`, iteration
* 2 reads `replies[1]`, and so on. Use Partial<LLMResponse> to
* inject `toolCalls`:
*
* ```ts
* mock({
* replies: [
* { toolCalls: [{ id: '1', name: 'lookup', args: { id: 42 } }] },
* { content: 'Found it: refunds take 3 business days.' },
* ],
* });
* ```
*
* **Exhaustion semantics:** if the agent calls the LLM more times
* than there are replies, `complete()` / `stream()` throw a clear
* error. This makes mock-script bugs loud, not silent. Tune the
* agent's `maxIterations` to bound the call count.
*
* Takes precedence over `reply` and `respond` when set.
*/
readonly replies?: readonly MockReply[];
/**
* Build the response from the request. Returns either a plain
* string (renders as content with no tool calls) or a partial
* `LLMResponse` so consumers can simulate tool calls + multi-turn
* loops without needing a separate `scripted()` helper.
*
* Default: echoes the last message a PERSON wrote (`isSaidByPerson`) —
* never a line this library served in the user role, such as the time
* layer's late line, which is the LAST user-role message of every request
* under `.time()`.
*/
readonly respond?: (req: LLMRequest) => string | Partial<LLMResponse>;
/**
* Simulated wall-clock delay per request (ms).
* Pass a single number for a fixed delay or a `[min, max]` tuple for
* a uniformly random delay (e.g. `[3000, 8000]` for "real LLM"
* thinking time). Default 0 (instant).
*
* Aliased via `delayMs` for backward compatibility.
*/
readonly thinkingMs?: LatencyMs;
/** Alias for `thinkingMs`. Kept for back-compat with prior revisions. */
readonly delayMs?: LatencyMs;
/**
* For `stream()`: delay between successive chunks (ms). Pass a
* single number for a fixed delay or a `[min, max]` tuple for a
* uniformly random delay per chunk (e.g. `[30, 80]` for typing-like
* cadence). Default 30ms.
*
* Has no effect on `complete()`.
*/
readonly chunkDelayMs?: LatencyMs;
/** Fixed stop reason to return. Default 'stop'. */
readonly stopReason?: string;
/** Override usage counts returned. Default: chars/4 heuristic. */
readonly usage?: Readonly<{
input?: number;
output?: number;
cacheRead?: number;
cacheWrite?: number;
}>;
}
export declare class MockProvider implements LLMProvider {
readonly name: string;
readonly carriesInMessages: readonly WireRole[];
/**
* The mock carries a forced tool choice, so `strategy: 'tool-forced'` is
* testable end-to-end with no key and no network: script a reply whose
* `toolCalls` names the synthetic tool and the whole loop — forced request,
* tool-shaped answer, validation, retry — runs deterministically. A mock
* that refused the strategy would make the one path that most needs a
* cheap rehearsal the one path you could only rehearse in production.
*/
readonly carriesForcedToolChoice = true;
/**
* The mock "carries" a structured-output format for the same reason it
* carries a forced choice: the scripted reply IS the constrained answer, so
* `strategy: 'native'` can be rehearsed offline. Its manifest states the
* format whenever the request carried one.
*/
readonly carriesNativeOutputSchema = true;
/** Every mock model is a known synthetic transport, never an opaque endpoint. */
readonly supportsAutoNativeOutputSchema: () => boolean;
/** Normalizes the scripted `rawThinking` a reply carries. */
readonly thinkingHandler: import("../../thinking/types.js").ThinkingHandler;
private readonly reply?;
private readonly replies?;
private repliesCursor;
private readonly respond;
private readonly thinkingMs;
private readonly chunkDelayMs;
private readonly stopReason;
private readonly usageOverride;
constructor(options?: MockProviderOptions);
/**
* Reset the scripted-replies cursor. Useful when reusing one
* `MockProvider` instance across multiple test scenarios — each
* scenario can `provider.resetReplies()` to start from `replies[0]`
* again. No-op when `replies` was not supplied.
*/
resetReplies(): void;
/**
* Convenience factory for the playground / Lens demo defaults: a
* real-feel mock with 3–8 s of "thinking" before the response and
* 30–80 ms per streamed word. Lets users observe pause/resume,
* streaming, and tool dispatch happening live without hitting a
* paid API.
*/
static realistic(options?: MockProviderOptions): MockProvider;
complete(req: LLMRequest): Promise<LLMResponse>;
/**
* Streaming mode — emits the response content word-by-word so
* consumers (Lens commentary, chat UIs) can render tokens as they
* arrive. Tool calls land all at once on the final chunk because
* that is how real providers (OpenAI, Anthropic) deliver them too.
*/
stream(req: LLMRequest): AsyncIterable<LLMChunk>;
private buildResponse;
/**
* Resolve the next reply source for one `complete()` / `stream()` call.
* Priority: `replies` (scripted, throws on exhaustion) → `reply` (single
* fixed string) → `respond` (callback default). Replies are consumed
* in order; the cursor is per-instance, not per-request.
*/
private consumeNextReply;
}
/**
* Lowercase factory for `MockProvider` — matches the v1 `mock()` import
* shape so docs and quick-starts stay copy-pasteable. Equivalent to
* `new MockProvider(options)`.
*
* @example
* import { Agent, defineTool } from 'agentfootprint'
import { mock } from 'agentfootprint/providers';
*
* const agent = Agent.create({ provider: mock({ reply: 'hello' }) })
* .tool(defineTool({ name: 'echo', ... }))
* .build();
*/
export declare function mock(options?: MockProviderOptions): MockProvider;