mockllm

package
v0.14.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 4, 2026 License: Apache-2.0 Imports: 5 Imported by: 0

Documentation

Overview

Package mockllm provides a deterministic, scripted implementation of port.LLMProvider for testing the agent loop without any network access.

A Provider is constructed from an ordered list of "turns"; each turn is a canned sequence of port.Chunk values. Every call to Stream emits the next turn's chunks and advances an internal cursor, so successive model calls in a loop replay successive scripted turns. Turn-builder helpers (TextTurn, ToolCallTurn, ...) make scripts terse:

p := mockllm.New(
	mockllm.TextTurn("hello"),
	mockllm.ToolCallTurn(call),
)

Stream honours context cancellation: if ctx is done it stops yielding.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func DoneChunk

func DoneChunk(stop session.StopReason) port.Chunk

DoneChunk builds a ChunkDone carrying the given stop reason.

func PhaseChunk

func PhaseChunk(phase string) port.Chunk

PhaseChunk builds a ChunkPhase carrying the opaque phase marker (the analogue of OpenAI's assistant-message phase, "commentary"/"final_answer"). The value is passed through verbatim — the harness never interprets it — so a test can script any opaque string to prove the engine threads it without branching.

func ReasoningChunk

func ReasoningChunk(text string) port.Chunk

ReasoningChunk builds a ChunkReasoning (human-readable DISPLAY summary).

func ReasoningItemChunk

func ReasoningItemChunk(blob string) port.Chunk

ReasoningItemChunk builds a ChunkReasoningItem carrying the opaque REPLAY blob (the analogue of OpenAI's reasoning-item encrypted_content).

func ReasoningItemChunkWithID

func ReasoningItemChunkWithID(blob, id string) port.Chunk

ReasoningItemChunkWithID builds a ChunkReasoningItem carrying both the REPLAY blob and the provider's per-item id (stored on Message.ReasoningItemID).

func TextChunk

func TextChunk(text string) port.Chunk

TextChunk builds a ChunkText.

func ToolCallChunk

func ToolCallChunk(call session.ToolCall) port.Chunk

ToolCallChunk builds a ChunkToolCall.

func UsageChunk

func UsageChunk(u session.Usage) port.Chunk

UsageChunk builds a ChunkUsage.

Types

type Option

type Option func(*Provider)

Option configures a Provider.

func WithCapabilities

func WithCapabilities(caps port.ProviderCapabilities) Option

WithCapabilities sets the capabilities the mock advertises. The default is text-only (the zero ProviderCapabilities). Tests use it to flip the mock to image- or audio-capable without reaching for the OpenAI adapter.

func WithRequestObserver

func WithRequestObserver(fn func(port.LLMRequest)) Option

WithRequestObserver registers an optional observer invoked with each port.LLMRequest the mock receives, BEFORE the scripted turn is yielded. It lets a test assert what actually reached the provider — e.g. that a multimodal prompt carried its media Parts across the wire→domain→engine→provider path. It is purely additive: a Provider built without it behaves exactly as before (the default observer is nil and never called), so existing mockllm users are unaffected. The observer runs on the calling goroutine, under no lock; keep it cheap and side-effect-light (a test capture typically copies the field it needs).

type Provider

type Provider struct {
	// contains filtered or unexported fields
}

Provider is a deterministic, scripted port.LLMProvider. Each Stream call replays the next programmed Turn. It is safe for concurrent use; the cursor is guarded by a mutex.

func New

func New(turns ...Turn) *Provider

New constructs a Provider that replays the given turns in order, one per Stream call. It advertises text-only capabilities unless NewWith is used.

func NewWith

func NewWith(opts []Option, turns ...Turn) *Provider

NewWith constructs a Provider with the given options (e.g. WithCapabilities) and scripted turns.

func (*Provider) Calls

func (p *Provider) Calls() int

Calls reports how many times Stream has been invoked (i.e. the current cursor position). It is useful for assertions in tests.

func (*Provider) Capabilities

func (p *Provider) Capabilities() port.ProviderCapabilities

Capabilities reports the configured capabilities (text-only by default).

func (*Provider) Reset

func (p *Provider) Reset()

Reset rewinds the cursor to the first turn, so the same Provider can be replayed again.

func (*Provider) Stream

func (p *Provider) Stream(ctx context.Context, req port.LLMRequest) (iter.Seq2[port.Chunk, error], error)

Stream returns an iterator over the next scripted turn's chunks, advancing the internal cursor. If the script is exhausted it returns an empty iterator (no chunks, no error). The returned iterator stops early if ctx is cancelled. The outer error is always nil; the mock never fails to start a stream — a scripted Turn.Err is yielded IN-stream after the turn's chunks (a mid-stream failure), matching how the real adapters surface a broken stream.

type Turn

type Turn struct {
	// Chunks is the ordered sequence emitted for this turn.
	Chunks []port.Chunk
	// Err, when non-nil, is yielded as a GENUINE in-stream error (the iterator's
	// error value) AFTER Chunks — the shape of a transient provider failure
	// mid-stream (an upstream 5xx that exhausted the resilience layer's
	// retries). The agent loop maps it to a StopError terminal that Fail()s the
	// session, distinct from a provider-REPORTED terminal carried on the
	// ChunkDone stop (see EmptyTurnWithStop). Build one with ErrorTurn.
	Err error
}

Turn is one scripted model response: the ordered chunks Stream emits for a single call. Build one with the Turn* helpers or assemble Chunks by hand.

func ChunksTurn

func ChunksTurn(chunks ...port.Chunk) Turn

ChunksTurn wraps an explicit chunk sequence as a Turn for full control over the script (e.g. custom usage, a specific StopReason, or interleaved kinds).

func EmptyTurn

func EmptyTurn() Turn

EmptyTurn builds an UNCOOPERATIVE turn that emits NO text and NO tool call — only a zero usage chunk and a ChunkDone(StopEndTurn). It scripts the "completed turn with no progress" shape a reasoning model can produce (it "finished" without a deliverable), which the loop's no-progress handler must catch and nudge rather than terminate silently. A cooperative happy-path mock (TextTurn/ToolCallTurn) can never produce this shape — that is exactly the gap this helper closes.

func EmptyTurnWithStop

func EmptyTurnWithStop(stop session.StopReason) Turn

EmptyTurnWithStop builds an UNCOOPERATIVE turn that emits NO text and NO tool call, then a zero usage chunk and a ChunkDone carrying the GIVEN stop reason. It scripts the "empty turn caused by a real terminal condition" shape: both adapters' mapStop relay max_tokens / refusal / incomplete / failed as session.StopError (and cancelled as StopCancelled) on the ChunkDone stop, NOT as a Go error — and such a truncated/refused response can come back with no text. The loop must SURFACE that real stop reason, NOT nudge "please continue" or relabel it StopNoProgress. This is the regression guard for the streamStop-masking bug. EmptyTurn() is this with a benign StopEndTurn (the genuine no-progress shape that DOES get nudged).

func ErrorTurn

func ErrorTurn(err error, chunks ...port.Chunk) Turn

ErrorTurn builds a turn that yields the given chunks (commonly none) and then a GENUINE in-stream error — the mid-stream failure shape of a transient provider outage (an upstream 5xx that exhausted the resilience layer's retries). The agent loop maps a stream error to a StopError terminal that calls session.Fail(), landing the session in StateFailed. It is DISTINCT from EmptyTurnWithStop(session.StopError), which scripts a provider-REPORTED terminal condition relayed on the ChunkDone stop with NO Go error. Use this to exercise the failed-session recovery seam (Session.Recover, issue #51).

func ReasoningOnlyTurn

func ReasoningOnlyTurn(displaySummary, replayBlob string) Turn

ReasoningOnlyTurn builds an UNCOOPERATIVE turn that emits a reasoning DISPLAY delta (displaySummary, human-readable, display-only) and a reasoning REPLAY-item blob (replayBlob, the opaque encrypted_content analogue) but NO visible text and NO tool call, then a zero usage chunk and a ChunkDone(StopEndTurn). It scripts a reasoning-model turn that "thought" but produced no deliverable — the exact no-progress trigger — with the replay blob present so a test can also assert the blob is preserved on the recorded empty assistant message and replayed across the nudge. Either argument may be empty to omit that chunk.

func ReasoningTurn

func ReasoningTurn(reasoning, text string) Turn

ReasoningTurn builds a turn that emits a reasoning delta and then text, followed by a zero usage chunk and a ChunkDone carrying StopEndTurn.

func TextTurn

func TextTurn(text string) Turn

TextTurn builds a turn that streams text as a single text delta, then a usage chunk (zero usage) and a ChunkDone carrying StopEndTurn. It is the common "model answered with prose and stopped" case.

func ToolCallTurn

func ToolCallTurn(calls ...session.ToolCall) Turn

ToolCallTurn builds a turn that emits one or more fully-assembled tool calls, then a usage chunk (zero usage) and a ChunkDone carrying StopEndTurn (the loop continues because tool calls were produced). At least one call is expected; calling it with none yields a turn that just stops.

The call NAME is NOT validated against any catalog: a test can script a wrong/unknown tool name (e.g. session.NewToolCall("c1", "Nonexistent", nil)) to exercise the loop's unknown-tool path (which must open a visible card before the error result). That is the adversarial shape this helper supports unchanged.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL