snapshot

package
v0.38.2 Latest Latest
Warning

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

Go to latest
Published: Jul 16, 2026 License: MIT Imports: 17 Imported by: 0

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

View Source
const CurrentSchemaVersion = "1.0"

CurrentSchemaVersion is the MAJOR.MINOR schema version of this package's wire format.

1.0 — initial snapshot format (issue #367).

View Source
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`.

View Source
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

type InstructionEntry struct {
	Path   string `json:"path"`
	SHA256 string `json:"sha256,omitempty"`
}

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

func LoadPolicy(path string) (*Policy, error)

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

func (p *Policy) IsSensitiveKey(name string) bool

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) Label

func (p *Policy) Label() string

Label returns the policy label ("default", "custom", "default+custom").

func (*Policy) MatchCount

func (p *Policy) MatchCount() int

MatchCount returns the number of replacements performed across this policy's lifetime. Reset to zero by ResetCounters.

func (*Policy) MatchedRules

func (p *Policy) MatchedRules() []string

MatchedRules returns the sorted set of rule names that matched at least once.

func (*Policy) RedactAny

func (p *Policy) RedactAny(v any) any

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

func (p *Policy) RedactString(s string) string

RedactString applies every rule in order to s and returns the redacted string. Match counters are updated on Policy.

func (*Policy) RedactStringMap

func (p *Policy) RedactStringMap(in map[string]string) map[string]string

RedactStringMap applies redaction to every value in the map. Returns a fresh map; the input is not mutated.

func (*Policy) RedactStringSlice

func (p *Policy) RedactStringSlice(in []string) []string

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

func LoadSnapshotFile(path string) (*Snapshot, error)

LoadSnapshotFile reads and parses a snapshot from disk.

func ParseSnapshot

func ParseSnapshot(data []byte, source string) (*Snapshot, error)

ParseSnapshot decodes a snapshot from bytes and validates its schema version. The source argument is included in error messages for diagnostics.

func (Snapshot) MarshalJSON

func (s Snapshot) MarshalJSON() ([]byte, error)

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

func NewWriter(dir string) *Writer

NewWriter constructs a Writer rooted at dir. Write creates dir on demand, so the caller does not need to pre-create it.

func (*Writer) Root

func (w *Writer) Root() string

Root returns the directory the writer writes to.

func (*Writer) Write

func (w *Writer) Write(snap *Snapshot) (string, error)

Write serializes snap to <root>/<test_id>-run<N>.json and returns the path written. The path is suitable for embedding in a results.json `runs[].snapshot_path` field.

Jump to

Keyboard shortcuts

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