Skip to main content
A trace records what a run did: one span per Predict call, carrying the rendered prompt, the parsed output, and a request fingerprint. Replay serves a later run back from that recording instead of a live provider, and exports project a finished trace onto external observability and training conventions. The dsrs CLI that checks, formats, and serves .dsrs artifacts has its own page: CLI.

Recording and replaying a run

Wrap any call in capture to get the run back as a value, then persist it as JSONL:
ReplayMode::Strict proves the pipeline still behaves exactly as recorded: every call is served from the log, with zero provider calls.
ReplayMode::UntilDivergence replays the unchanged prefix free and goes live only from the first call your change actually touches:
Each span’s fingerprint covers the full rendered prompt and the model settings; divergence is detected at the first call whose fingerprint differs from its recording, and only calls from there run live.

Capture

capture records every Predict call on the current task into a Trace while the scope is active.
  • capture(f) runs the closure and returns its result plus the recorded Trace.
  • capture_with_meta(meta, f) is the same with caller-provided rollout metadata (TraceMeta: rollout input, candidate hash, free-form tags). A missing trace_id or start time is minted at scope start.
Scoping is task-local: spawned subtasks do not inherit the scope, and nested scopes are exclusive (the innermost records). With no scope active the cost is one task-local probe per call. Traces serialize to JSONL via Trace::to_jsonl and Trace::from_jsonl.

What a span records

One span is one Predict invocation. At a high level it records:
  • Who ran: the component name (the same name the params system uses, so spans join back to tunable slots) plus a per-component sequence number; (component, seq) is unique per trace and is what replay keys on.
  • What went in: the rendered prompt (an interned system-and-demos prefix plus the live suffix), the typed input fields as JSON, and the redacted model config.
  • What happened inside: ordered events, one Exchange per provider round-trip and one ToolRun per tool execution.
  • What came out: the raw assistant text, the parsed output fields, aggregated token usage, and any error (kinds: lm, parse, tool, cancelled).
  • A request fingerprint: request_hash, a stable hash over the redacted model config plus the full rendered prompt. This is the replay key and the determinism check.
  • Timing and completeness: start time, duration, and a complete flag (false when the span was truncated or redacted; replay refuses incomplete spans).

Replay

replay serves Predict calls from a recorded trace instead of a live provider: each call’s request_hash is compared against the next recorded span for its component, and on a match the recorded output is served with zero API calls and no tool execution.

Modes

ReplayReport fields

Replay error kinds

Replay scoping mirrors capture: task-local, not inherited by spawned subtasks, innermost scope wins. Compose replay outside with capture inside to record a counterfactual rollout while serving its unchanged prefix from the base trace.

Exports

Exports are pure serialization-side projections of a finished Trace: no new capture machinery, no external dependencies. Both live under dspy_rs::trace.
Exports serialize traces after a run finishes. For live, human-readable console output while a program runs, call init_tracing (documented on Utils): it installs a process-global pretty tracing subscriber (respecting RUST_LOG, defaulting to dspy_rs=debug) and is independent of trace capture.

OpenTelemetry

Trace::to_otel_spans(include_content: bool) maps a trace onto OpenTelemetry GenAI semantic conventions as plain serializable structs in the OTLP/JSON wire shape (proto3 JSON mapping: camelCase keys, 64-bit integers as decimal strings, ids as lowercase hex), with no OpenTelemetry dependency. Trace::to_otlp_json(service_name, include_content) wraps those spans in a complete resourceSpans envelope that any OTLP/HTTP collector (Jaeger, Tempo, otel-collector) accepts at POST /v1/traces as-is. Prompt, completion, and tool payloads are opt-in via include_content, mirroring OTel’s GenAI content-capture switch: with include_content: false the spans carry identity, usage, and timing attributes only, so they stay exportable to shared collectors without leaking prompt text. The emitted types, all Serialize structs: The OTLP enum constants are exported as plain integers: SPAN_KIND_INTERNAL = 1, SPAN_KIND_CLIENT = 3, STATUS_CODE_ERROR = 2.
ToolRun events record only their duration, so tool child spans start at their parent span’s start time: durations are exact, offsets within the parent are not.

RL dataset

Trace::to_rl_rollout() projects the trace onto the Agent Lightning / verifiers rollout convention: one rollout as message lists plus a reward plus per-subcall transitions. It returns None when no eval was recorded (trace.outcome.eval unset); a rollout without a reward is not trainable. Because spans keep full Message structure (tool-call blocks, reasoning blocks), the projection needs no lossy text munging.
RlRollout serializes to a single JSON object; RlRollout::to_json_line() produces the one-line JSONL record RL trainers consume. Spans whose policy emitted nothing (provider failures, cancelled spans: no Exchange event) are omitted; they contribute no completion to train on. Parse-failure spans keep their transition: the emitted text exists even though it did not parse.

See also

  • CLI: dsrs serve returns the capture-scope trace artifact from POST /run?trace=1
  • Utils: init_tracing for live pretty console output
  • Optimizers: traces as the evidence base for reflective optimization
  • Evaluation: the evaluation loop that hands each rollout’s trace to your metric
  • Example: 24-frontdesk-replay.rs, capture, strict replay, and until-divergence replay end to end
  • Example: 12-tracing.rs, scoped trace capture for a composed module
  • Example: 17-pretty-tracing.rs, init_tracing output against an offline LM