llmrec

package
v0.12.6 Latest Latest
Warning

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

Go to latest
Published: Oct 19, 2026 License: AGPL-3.0 Imports: 12 Imported by: 0

Documentation

Overview

Package llmrec implements a recording decorator for llm.Provider. It intercepts every Stream call, captures the full request and accumulated response, and persists them to PostgreSQL for later inspection.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func TaskIDFrom

func TaskIDFrom(ctx context.Context) string

TaskIDFrom returns the explicit task registry id attached by the task runtime.

func WithTaskID

func WithTaskID(ctx context.Context, taskID string) context.Context

WithTaskID attaches the owning task registry id to an LLM call. Session ids are based on exploration ids, which are not interchangeable with task ids.

func WithWorker

func WithWorker(ctx context.Context, worker string) context.Context

WithWorker overrides the agent-lane ("worker") label for an LLM call. The lane is normally parsed from the transcript session id (exp<N>-<role>); calls made outside the engine's worker/planner sessions — e.g. the intercept fallback judge — carry no such session, so they attach their lane explicitly here. This lets the usage ledger single out that spend (worker='judge') for the config page.

Types

type Attempt

type Attempt struct {
	Status int
	Body   string
}

Attempt is one HTTP round trip's status code and response bytes.

type Capture

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

Capture collects the untouched wire bodies of one logical LLM call. The Recorder creates it and puts it on the context; the HTTP transport that norma dials through (agent.quotaAwareTransport) finds it there and fills it in.

This exists because everything the Recorder itself sees is already normalized: llm.CompletionRequest is re-serialized rather than the body buildBody() sent, and the response arrives as decoded StreamEvents, not the SSE frames. For debugging a live provider, the bytes on the wire are the only ground truth.

norma's doStream retries the request-establishment phase, so one Stream can issue several HTTP attempts. Each attempt is kept: the discarded ones (see norma/llm/retry.go, which closes non-final bodies unread) are exactly what makes rate-limit and gateway failures diagnosable.

The transport writes from norma's stream-reading goroutine while the Recorder snapshots at stream end, so all state is mutex-guarded.

func CaptureFrom

func CaptureFrom(ctx context.Context) *Capture

CaptureFrom returns the Capture attached to ctx, or nil when raw capture is off. Callers must tolerate nil — recording is a toggle, and non-recorded providers dial through the same transport.

func NewCapture

func NewCapture(ctx context.Context) (context.Context, *Capture)

NewCapture returns a context carrying a fresh Capture, plus the Capture itself.

func (*Capture) Attempts

func (c *Capture) Attempts() []Attempt

Attempts returns every round trip in order. Unlike RawResponse — which drops the header line for a lone attempt so the bytes stay replayable — this always carries the status code, for callers that must report "HTTP 401 + body".

func (*Capture) RawRequest

func (c *Capture) RawRequest() string

RawRequest returns the request body as sent, or "" if nothing was captured.

func (*Capture) RawResponse

func (c *Capture) RawResponse() string

RawResponse returns the response bytes as received. A single attempt yields the untouched original (copy-pasteable straight into a replay); multiple attempts are concatenated behind per-attempt header lines so a retry sequence stays readable.

func (*Capture) SetRequest

func (c *Capture) SetRequest(body string)

SetRequest stores the outgoing request body. Retries re-send identical bytes, so only the first attempt's body is kept.

func (*Capture) TeeResponse

func (c *Capture) TeeResponse(status int, rc io.ReadCloser) io.ReadCloser

TeeResponse opens a new attempt and wraps rc so everything read from it is mirrored into that attempt. It tees rather than reads because a successful response is an SSE stream that must keep streaming to the caller.

type Recorder

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

Recorder wraps an llm.Provider and records every completion call.

func Wrap

func Wrap(inner llm.Provider, pg *db.DB, model, profName, thinkingType, reasoningEffort string, enabled func() bool) *Recorder

Wrap returns a Provider that records calls to pg when enabled() reports true. profName is the LLM profile name (e.g. "default"); may be empty. thinkingType / reasoningEffort are the config-level thinking params actually sent to the API (empty = not sent). A nil enabled predicate records unconditionally.

func (*Recorder) Complete

Complete implements the atomic completion path while preserving the same usage metering, normalized body recording and raw wire capture as Stream.

func (*Recorder) Stream

Stream implements llm.Provider. It delegates to the inner provider, accumulates the streamed events to reconstruct the response, and records the full exchange.

The session identifier (e.g. "exp1-worker-i3") is carried on ctx by the norma harness via transcript.WithSessionID; reading it per-call is race-free even when planner and multiple workers share one Recorder instance.

Jump to

Keyboard shortcuts

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