Documentation
¶
Overview ¶
Package snapshot implements waza's per-task snapshot/replay artifact.
A snapshot is a self-contained, versioned record of one task execution: prompt sequence, every tool call (name/args/result/timing), engine/model config, fixture file hashes, and an env-var allow-list capture. Snapshots are written under `--output-dir` when `waza run --snapshot` is passed and referenced from `results.json` (additive in outcome schema 1.2).
`waza replay <snapshot.json>` consumes snapshots to deterministically re-run a task without burning LLM calls (model-replay mode), to detect drift against the real engine (live mode), or to bisect divergence between two snapshots.
The snapshot wire format is its own MAJOR.MINOR schema independent of the results.json schema. Additions are MINOR bumps; renames/removals are MAJOR. Readers MUST refuse to load a snapshot whose MAJOR does not match CurrentSchemaVersion (issue #368 policy).
Index ¶
- Constants
- func SummariseDivergences(r CompareResult) string
- type CaptureInput
- type CompareResult
- type Divergence
- type DivergenceKind
- type FixtureDigest
- type InstructionEntry
- type Mode
- type Policy
- func (p *Policy) IsSensitiveKey(name string) bool
- func (p *Policy) Label() string
- func (p *Policy) MatchCount() int
- func (p *Policy) MatchedRules() []string
- func (p *Policy) RedactAny(v any) any
- func (p *Policy) RedactString(s string) string
- func (p *Policy) RedactStringMap(in map[string]string) map[string]string
- func (p *Policy) RedactStringSlice(in []string) []string
- func (p *Policy) ResetCounters()
- type RedactionRule
- type Snapshot
- type SnapshotEngine
- type SnapshotEnv
- type SnapshotPrompt
- type SnapshotRedaction
- type SnapshotResult
- type SnapshotTask
- type Writer
Constants ¶
const CurrentSchemaVersion = "1.0"
CurrentSchemaVersion is the MAJOR.MINOR schema version of this package's wire format.
1.0 — initial snapshot format (issue #367).
const Kind = "task-snapshot"
Kind identifies this artifact in tooling that may also handle results.json or eval specs. Always "task-snapshot" for snapshots produced by `waza run`.
const RedactionPlaceholder = "[REDACTED]"
RedactionPlaceholder is what we write in place of a redacted value. Stable, easy to grep for, distinguishable from any plausible credential.
Variables ¶
This section is empty.
Functions ¶
func SummariseDivergences ¶
func SummariseDivergences(r CompareResult) string
SummariseDivergences returns a short, human-friendly summary of a CompareResult. The summary is intended for CLI output, not analytics.
Types ¶
type CaptureInput ¶
type CaptureInput struct {
WazaVersion string
EvalID string
EvalName string
Skill string
Task *models.TestCase
Request *execution.ExecutionRequest
Run *models.RunResult
Engine SnapshotEngine
FixturesRoot string
// SkipDirs is an optional list of absolute directories under
// FixturesRoot whose contents will be omitted from the captured
// fixture digests. The runner uses this to exclude the configured
// snapshot output directory, which prevents previously-emitted
// snapshots from perturbing the fixture hash on re-runs.
SkipDirs []string
EnvAllowList []string
Policy *Policy
}
CaptureInput is everything Capture needs to build a Snapshot. It is assembled by the runner inside executeRun once the RunResult is finalized.
type CompareResult ¶
type CompareResult struct {
Match bool `json:"match"`
Divergences []Divergence `json:"divergences"`
BaselinePath string `json:"baseline,omitempty"`
ReplayedPath string `json:"replayed,omitempty"`
}
CompareResult is the full report from Compare.
func Bisect ¶
func Bisect(baseline, failing *Snapshot) (turn int, result CompareResult)
Bisect identifies the first divergent turn between baseline and failing. Returns the 1-based turn number of the first event that diverges, or 0 when the snapshots match. The same Divergence slice that Compare emits is returned so callers can render details.
Bisect prefers tool_events.Turn when populated (engines that track turn boundaries); when Turn is 0 it falls back to the event sequence index.
Bisect runs Compare in non-strict mode: it ignores tool result payloads and compares only the ordered (name, args) sequence so the reported "first divergent turn" reflects where the agent took a different action, not where downstream side effects differed.
func Compare ¶
func Compare(baseline, replayed *Snapshot, strictResult bool) CompareResult
Compare returns the divergence report between baseline and replay. When strictResult is true, baseline.Result.Status and validations are also compared; live mode generally wants false because the new run produces its own grader results that should not be expected to match byte-for-byte.
The comparison walks tool events by index (0..min(len(a),len(b))). Args and Result are compared via deep-equal of the canonical JSON form so `map[string]any{"a":1}` matches `map[string]any{"a":1.0}` after a JSON round-trip (no false positives on numeric kinding).
type Divergence ¶
type Divergence struct {
Kind DivergenceKind `json:"kind"`
FirstDivergentIndex int `json:"first_divergent_index"`
BaselineValue string `json:"baseline_value,omitempty"`
ReplayedValue string `json:"replayed_value,omitempty"`
Description string `json:"description"`
}
Divergence is a single discrepancy between two snapshots (or between a snapshot and a freshly-captured run in live mode). FirstDivergentIndex is the 0-based position in the tool_events slice; for non-tool-event divergences it is -1.
type DivergenceKind ¶
type DivergenceKind string
DivergenceKind classifies a single discrepancy reported by Compare.
const ( DivLengthMismatch DivergenceKind = "length_mismatch" DivToolName DivergenceKind = "tool_name" DivToolArgs DivergenceKind = "tool_args" DivToolSuccess DivergenceKind = "tool_success" DivToolResult DivergenceKind = "tool_result" DivFinalStatus DivergenceKind = "final_status" DivValidation DivergenceKind = "validation" )
type FixtureDigest ¶
type FixtureDigest struct {
Path string `json:"path"`
Size int64 `json:"size,omitempty"`
SHA256 string `json:"sha256"`
}
FixtureDigest is the sha256 of a single fixture file at snapshot time.
func HashFixtures ¶
func HashFixtures(root string) ([]FixtureDigest, error)
HashFixtures walks root and returns one FixtureDigest per regular file found, with paths recorded relative to root. Symlinks are followed only when they resolve to a regular file. Dot-prefixed directories are skipped (matches the runner's loadInstructions behavior for `.git`, `.vscode`, etc.).
The returned slice is sorted by Path for deterministic snapshots.
func HashFixturesExcluding ¶
func HashFixturesExcluding(root string, skipDirs []string) ([]FixtureDigest, error)
HashFixturesExcluding is HashFixtures but also skips any directories (and their contents) whose absolute path matches an entry in skipDirs. This is used by the orchestrator to exclude the configured snapshot output directory so freshly-written snapshots do not perturb the fixture hash on subsequent runs.
type InstructionEntry ¶
InstructionEntry records an instruction file applied to the agent. The body itself is NOT captured (instructions can be large and they are already hashed via Fixtures when they live in the workspace); the path and digest are sufficient for replay drift detection.
type Mode ¶
type Mode string
Mode controls how `waza replay` re-runs a snapshot.
const ( // ModeModelReplay is the default mode: replay the captured tool_events // without any LLM/engine call. Graders that consume tool_events // (tool_calls, tool_constraint, etc.) verify byte-for-byte against the // snapshot, and the snapshot's grader results are re-checked against // the saved status. No tokens are spent. Diverges only if the snapshot // is internally inconsistent. ModeModelReplay Mode = "model-replay" // ModeLive re-runs the task against the real engine using the same // prompt + fixtures, then compares the resulting tool_events to the // snapshot's. Live mode is intentionally fuzzy: non-deterministic // fields (durations, raw model output text) are ignored; only the // ordered tool name + args fingerprint is compared. ModeLive Mode = "live" )
type Policy ¶
type Policy struct {
// Rules is the ordered list of redaction rules applied to every captured
// string. Order matters: later rules see strings that earlier rules
// have already partially redacted.
Rules []RedactionRule `yaml:"rules" json:"rules"`
// EnvKeyDenyList is a list of env-var name *substrings* (case-insensitive)
// that mark an env var as sensitive even if its value does not match any
// regex. Shipped defaults cover SECRET/TOKEN/PASSWORD/KEY/CREDENTIAL.
EnvKeyDenyList []string `yaml:"envKeyDenyList" json:"envKeyDenyList"`
// contains filtered or unexported fields
}
Policy is the redaction configuration used when capturing a snapshot. A nil *Policy means redaction is disabled — capture verbatim. This is useful for tests but the CLI never produces a nil policy in production.
func DefaultPolicy ¶
func DefaultPolicy() *Policy
DefaultPolicy returns the shipped redaction policy. The rules target the most common secret formats so a snapshot is safe to share by default: GitHub PATs, OpenAI/Azure keys, generic 32+ hex/base64 tokens, JWT-shaped strings, AWS access keys, basic-auth headers, and email addresses.
func LoadPolicy ¶
LoadPolicy reads a YAML file that may either replace or extend the shipped defaults. The on-disk format:
# extend: when true, defaults are prepended; otherwise the file's rules
# replace the defaults entirely.
extend: true
rules:
- name: internal_token
pattern: "INT-[A-Z0-9]{20}"
envKeyDenyList:
- CUSTOM_SECRET
func (*Policy) IsSensitiveKey ¶
IsSensitiveKey reports whether the env-var name should be treated as secret-bearing regardless of value. Allow-listed keys that match the deny list are captured with their value redacted, NOT entirely dropped, so the snapshot still records that the variable was present.
func (*Policy) MatchCount ¶
MatchCount returns the number of replacements performed across this policy's lifetime. Reset to zero by ResetCounters.
func (*Policy) MatchedRules ¶
MatchedRules returns the sorted set of rule names that matched at least once.
func (*Policy) RedactAny ¶
RedactAny walks a JSON-like value (string / []any / map[string]any / map[string]string / and pointers thereof) and applies redaction to every string it finds. Other types are returned as-is.
func (*Policy) RedactString ¶
RedactString applies every rule in order to s and returns the redacted string. Match counters are updated on Policy.
func (*Policy) RedactStringMap ¶
RedactStringMap applies redaction to every value in the map. Returns a fresh map; the input is not mutated.
func (*Policy) RedactStringSlice ¶
RedactStringSlice applies redaction to every element. Returns a fresh slice; the input is not mutated.
func (*Policy) ResetCounters ¶
func (p *Policy) ResetCounters()
ResetCounters clears the per-capture statistics.
type RedactionRule ¶
type RedactionRule struct {
Name string `yaml:"name" json:"name"`
Pattern string `yaml:"pattern" json:"pattern"`
// contains filtered or unexported fields
}
RedactionRule is a single named regex that matches a secret-like string. Rule names appear in Snapshot.Redaction.AppliedRules so users can audit which rule fired without seeing the secret.
type Snapshot ¶
type Snapshot struct {
// SchemaVersion is the MAJOR.MINOR version of the snapshot wire format.
SchemaVersion string `json:"schemaVersion"`
// Kind is always "task-snapshot" so a single tool inspecting JSON files
// can route by kind.
Kind string `json:"kind"`
// WazaVersion is the version of waza that produced this snapshot.
// Captured for diagnostics; not used for compatibility decisions
// (SchemaVersion is the source of truth).
WazaVersion string `json:"wazaVersion,omitempty"`
// CreatedAt is the UTC timestamp at which this snapshot was written.
CreatedAt time.Time `json:"createdAt"`
// EvalID, EvalName, and Skill mirror the outcome fields so a standalone
// snapshot can be attributed to its parent eval without re-reading
// results.json.
EvalID string `json:"evalId,omitempty"`
EvalName string `json:"evalName,omitempty"`
Skill string `json:"skill,omitempty"`
// Task describes the task that was executed.
Task SnapshotTask `json:"task"`
// Engine records the engine/model configuration so a live replay can
// reproduce the same request shape.
Engine SnapshotEngine `json:"engine"`
// Prompt holds the inputs the engine was given. FollowUps is the static
// follow-up list when configured; responder-driven multi-turn is
// captured via ToolEvents (which include turn boundaries).
Prompt SnapshotPrompt `json:"prompt"`
// ToolEvents is the canonical replay tape: the ordered, normalised
// tool-call record exactly as it appears in
// EvaluationOutcome.RunResult.ToolEvents (schema 1.1+). It is the
// deterministic input that replay model-replay mode feeds back into
// graders that consume tool events.
ToolEvents []models.ToolEvent `json:"toolEvents,omitempty"`
// Fixtures records the sha256 digest of every fixture/resource file the
// task started with. Replay live mode uses this to detect drift in the
// fixtures directory.
Fixtures []FixtureDigest `json:"fixtures,omitempty"`
// Env captures the env-var allow-list and (for auditing) the names of
// vars present in os.Environ() that were denied capture.
Env SnapshotEnv `json:"env"`
// Redaction documents what was scrubbed before serialization.
Redaction SnapshotRedaction `json:"redaction"`
// Result holds the outcome of the captured run (status / final output /
// grader results) so model-replay can verify graders deterministically.
Result SnapshotResult `json:"result"`
}
Snapshot is the on-disk record of a single task run that `waza replay` can consume to either deterministically re-run graders (model-replay) or to compare against a fresh live execution (live mode / bisect).
Snapshot is intentionally self-contained: all fields needed to reproduce a run are captured in the JSON document. Consumers should treat unknown fields as a forward-compat signal (same MAJOR, future MINOR) and log a warning rather than fail.
func Capture ¶
func Capture(in CaptureInput) (*Snapshot, error)
Capture builds a Snapshot from input. The returned snapshot's redaction counters reflect everything scrubbed during this call; the caller may inspect input.Policy.MatchCount() / MatchedRules() after.
Capture does NOT write to disk; see Writer.Write for that.
func LoadSnapshotFile ¶
LoadSnapshotFile reads and parses a snapshot from disk.
func ParseSnapshot ¶
ParseSnapshot decodes a snapshot from bytes and validates its schema version. The source argument is included in error messages for diagnostics.
func (Snapshot) MarshalJSON ¶
MarshalJSON ensures SchemaVersion and Kind are always populated.
type SnapshotEngine ¶
type SnapshotEngine struct {
Type string `json:"type"`
ModelID string `json:"modelId"`
JudgeModel string `json:"judgeModel,omitempty"`
TimeoutSec int `json:"timeoutSec,omitempty"`
Temperature *float64 `json:"temperature,omitempty"`
TopP *float64 `json:"topP,omitempty"`
Seed *int64 `json:"seed,omitempty"`
}
SnapshotEngine records engine/model configuration. Fields that the engine does not surface (e.g., seed for the Copilot SDK) are simply left empty — the issue calls them out as best-effort metadata.
type SnapshotEnv ¶
type SnapshotEnv struct {
// AllowList is the configured allow-list applied at capture time.
// Empty means default-deny captured nothing.
AllowList []string `json:"allowList,omitempty"`
// Captured holds the actually-captured KEY=VALUE pairs after the
// allow-list and redaction were applied. Values may be redacted
// placeholders if a Redaction rule matched.
Captured map[string]string `json:"captured,omitempty"`
// DeniedKeys lists the names (NOT values) of env vars present in the
// process environment that were not in AllowList. Useful for auditing
// what would have been redacted under a stricter policy. Empty when
// the process environment is small (<= 0 entries) or the writer chose
// to omit the audit list.
DeniedKeys []string `json:"deniedKeys,omitempty"`
}
SnapshotEnv documents env-var capture under the default-deny / allow-list policy described in the issue.
func CaptureEnv ¶
func CaptureEnv(allowList []string, policy *Policy) SnapshotEnv
CaptureEnv returns the captured-env section of a snapshot under the default-deny / allow-list policy described in the issue.
Rules:
- When allowList is empty, no env values are captured and DeniedKeys is left empty so the snapshot does not leak the full set of environment variable names from the host (which can be large, unstable across machines, and occasionally sensitive on its own).
- When allowList is non-empty, variables that do not match are added to DeniedKeys for auditing. This makes the allow-list explicit without enumerating every unrelated key from the host.
- When allowList contains a name, that variable's value is captured. If the policy marks the key as sensitive (IsSensitiveKey), the value is replaced with RedactionPlaceholder; otherwise the value is run through the redaction policy's regex rules.
- allowList entries may end with `*` to match prefixes (e.g. `WAZA_*`).
The returned SnapshotEnv is always non-nil; absent fields are nil/empty.
type SnapshotPrompt ¶
type SnapshotPrompt struct {
Message string `json:"message"`
FollowUps []string `json:"followUps,omitempty"`
Context map[string]any `json:"context,omitempty"`
Instructions []InstructionEntry `json:"instructions,omitempty"`
}
SnapshotPrompt is the input side of the run.
type SnapshotRedaction ¶
type SnapshotRedaction struct {
// Policy is "default", "default+custom", or "custom" depending on
// whether the shipped defaults were applied, augmented, or replaced.
Policy string `json:"policy"`
// AppliedRules lists the rule names that matched at least once during
// this capture, sorted for stable diffing.
AppliedRules []string `json:"appliedRules,omitempty"`
// RedactionCount is the total number of replacements made across the
// captured payload.
RedactionCount int `json:"redactionCount,omitempty"`
}
SnapshotRedaction documents what was scrubbed before serialization.
type SnapshotResult ¶
type SnapshotResult struct {
Status models.Status `json:"status"`
FinalOutput string `json:"finalOutput,omitempty"`
ErrorMsg string `json:"errorMsg,omitempty"`
DurationMs int64 `json:"durationMs,omitempty"`
Validations map[string]models.GraderResults `json:"validations,omitempty"`
}
SnapshotResult mirrors the relevant subset of models.RunResult so a snapshot can be replayed without also loading results.json.
type SnapshotTask ¶
type SnapshotTask struct {
TestID string `json:"testId"`
DisplayName string `json:"displayName,omitempty"`
Group string `json:"group,omitempty"`
Golden bool `json:"golden,omitempty"`
Tags []string `json:"tags,omitempty"`
RunNumber int `json:"runNumber"`
}
SnapshotTask carries identifying task metadata.
type Writer ¶
type Writer struct {
// contains filtered or unexported fields
}
Writer writes captured snapshots to <root>/<test_id>-run<N>.json. Writer is concurrency-safe: callers may invoke Write from multiple goroutines.
func NewWriter ¶
NewWriter constructs a Writer rooted at dir. Write creates dir on demand, so the caller does not need to pre-create it.