Decision Recording
Decision recording captures the complete context behind every decision your AI system makes, so it can be replayed, audited, and verified later.
base install For: governance & audit
Why persist a decision
A decision that vanishes the moment it runs can’t be replayed, audited, or proven. Recording it turns a fleeting model call into a durable, verifiable record:
- Replay — re-run the exact inputs later to check whether behavior changed.
- Audit — answer “what did the agent decide, and on what basis?” months later.
- Prove — the
fingerprint()content hash makes tampering detectable.
There are two ways to record. The lightweight @capture decorator records a dict per call and hands it to an exporter. The native DecisionSnapshot builds a structured record you can persist and replay.
How recording flows
-
Capture —
@capturewrapsclassify_ticketand records its inputs, outputs, timing, and type for each call. -
Export — the recorded dict is handed to an exporter (console, a
.jsonlfile, or your own). -
Persist — for storage and replay, build a native
DecisionSnapshotand save it to a backend. -
Replay & verify — later, load the snapshot, re-run it, and compare its
fingerprint().
flowchart LR A["classify_ticket()"] -->|"@capture"| B["recorded dict"] B --> C[Exporter] A -->|"native API"| D[DecisionSnapshot] D --> E[Backend] E --> F["Replay & Verify"]
Record a decision with @capture
The simplest way to record a decision is the @capture decorator. Pass an exporter to send each record somewhere:
from briefcase import capturefrom briefcase.exporters import BaseExporter
class CollectingExporter(BaseExporter): def __init__(self): self.records = []
async def export(self, decision): self.records.append(decision) return True
async def flush(self): ...
async def close(self): ...
exporter = CollectingExporter()
@capture(decision_type="classification", context_version="v1", exporter=exporter, async_capture=False)def classify_ticket(text: str) -> str: # call your model here return "account_access"
classify_ticket("Reset my password")print(exporter.records[0])The decorator wraps the call, records a dict (decision id, inputs, outputs,
timing, decision_type, context_version), and exports it through the
exporter you pass. It does not persist a native DecisionSnapshot on its own;
use the native objects below when you need storage or replay.
@capture parameters
| Parameter | Default | Description |
|---|---|---|
decision_type | None | Label for the kind of decision recorded |
context_version | None | Version tag for the surrounding context or prompt |
max_input_chars | 1000 | Truncate recorded inputs to this length |
max_output_chars | 1000 | Truncate recorded outputs to this length |
exporter | None | Exporter that receives each recorded dict |
async_capture | True | Export off the calling thread |
@capture works with or without arguments:
from briefcase import capture
@capturedef classify(text: str) -> str: # call your model here return "billing"Emit records
@capture records a decision but has nowhere to send it until you configure an
exporter. briefcase.observe() wires one up in a single line and returns it.
import briefcase
mem = briefcase.observe("memory") # or "console", or a "*.jsonl" path
@briefcase.capture(decision_type="classification", async_capture=False)def classify_ticket(text: str) -> str: # call your model here return "account_access"
classify_ticket("Reset my password")print(mem.records[0])The per-call exporter= argument shown above overrides the global one set by
observe(). See Exporters for the stock exporters and
how to write a custom one.
Build a native DecisionSnapshot
When you need storage or replay, build a structured DecisionSnapshot:
from briefcase import DecisionSnapshot, Input, Output, ModelParameters
decision = DecisionSnapshot("classify_ticket")decision.add_input(Input("text", "Reset my password", "string"))
params = ModelParameters("your-model")params.with_provider("your-provider")params.with_parameter("temperature", 0.0)decision.with_model_parameters(params)
output = Output("category", "account_access", "string")output.with_confidence(0.92)decision.add_output(output)
decision.with_execution_time(12.5)decision.with_module("triage_service")decision.add_tag("environment", "production")
print(decision.function_name)print(decision.fingerprint())Fingerprints make a record verifiable
fingerprint() returns a stable hash over inputs, outputs, and model
parameters. Store it alongside the record; recompute it later to detect when the
same decision produces a different result — this is what makes a decision
tamper-evident and replay-checkable.
digest = decision.fingerprint() # stable content hash# later, on a loaded snapshot:assert loaded.fingerprint() == digest # unchangedKey classes
@capture— decorator that records a dict and exports itDecisionSnapshot— structured record you can persist and replay; exposesfingerprint()Input/Output— typed wrappers;Output.with_confidence(score)attaches a confidence valueModelParameters— model name, provider, and per-call parametersSnapshot— groups multiple decisions;add_decision(decision)appends to it
| Field | Description | Why it matters |
|---|---|---|
function_name | The recorded function | Identifies which decision this is |
inputs | Typed inputs | The exact inputs a replay re-runs against |
outputs | Typed outputs | What the agent actually decided |
tags | Arbitrary key/value tags | Carries your own context (e.g. environment, queue) |
execution_time_ms | How long the call took | Anchors performance over time |
fingerprint() | Content hash | Makes the record tamper-evident and verifiable |
@capture vs DecisionSnapshot vs persisted storage
Three layers, each for a different need — pick by what you’re trying to do.
| Use this | When you want to… | Lifetime |
|---|---|---|
@capture decorator | Instrument a real function (like classify_ticket) with zero boilerplate and stream a lightweight record to an exporter | Per call |
DecisionSnapshot | Build a structured record by hand — to persist, replay, or fingerprint it | In-memory object |
Persisted backend (SqliteBackend) | Keep decisions durably so you can query, replay, and audit them weeks later | Durable |
Persist a decision
Save a DecisionSnapshot to a storage backend so it can be queried or replayed
later. This is the bridge from Capture into the Store & Query act.
import briefcasefrom briefcase import DecisionSnapshot, Input, Outputfrom briefcase.storage import SqliteBackend
briefcase.init()
decision = DecisionSnapshot("classify_ticket")decision.add_input(Input("text", "Reset my password", "string"))decision.add_output(Output("category", "account_access", "string"))decision.with_execution_time(12.5)
backend = SqliteBackend.in_memory() # or SqliteBackend("decisions.db")decision_id = backend.save_decision(decision)
restored = backend.load_decision(decision_id)print(restored.function_name)