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 ¶
- func DoneChunk(stop session.StopReason) port.Chunk
- func PhaseChunk(phase string) port.Chunk
- func ReasoningChunk(text string) port.Chunk
- func ReasoningItemChunk(blob string) port.Chunk
- func ReasoningItemChunkWithID(blob, id string) port.Chunk
- func TextChunk(text string) port.Chunk
- func ToolCallChunk(call session.ToolCall) port.Chunk
- func UsageChunk(u session.Usage) port.Chunk
- type Option
- type Provider
- type Turn
- func ChunksTurn(chunks ...port.Chunk) Turn
- func EmptyTurn() Turn
- func EmptyTurnWithStop(stop session.StopReason) Turn
- func ErrorTurn(err error, chunks ...port.Chunk) Turn
- func ReasoningOnlyTurn(displaySummary, replayBlob string) Turn
- func ReasoningTurn(reasoning, text string) Turn
- func TextTurn(text string) Turn
- func ToolCallTurn(calls ...session.ToolCall) Turn
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 ¶
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 ¶
ReasoningChunk builds a ChunkReasoning (human-readable DISPLAY summary).
func ReasoningItemChunk ¶
ReasoningItemChunk builds a ChunkReasoningItem carrying the opaque REPLAY blob (the analogue of OpenAI's reasoning-item encrypted_content).
func ReasoningItemChunkWithID ¶
ReasoningItemChunkWithID builds a ChunkReasoningItem carrying both the REPLAY blob and the provider's per-item id (stored on Message.ReasoningItemID).
func ToolCallChunk ¶
ToolCallChunk builds a ChunkToolCall.
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 ¶
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 ¶
NewWith constructs a Provider with the given options (e.g. WithCapabilities) and scripted turns.
func (*Provider) Calls ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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 ¶
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.