Skip to content
Docs for briefcase-ai v3.3.0see what’s new.

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

  1. Capture@capture wraps classify_ticket and records its inputs, outputs, timing, and type for each call.

  2. Export — the recorded dict is handed to an exporter (console, a .jsonl file, or your own).

  3. Persist — for storage and replay, build a native DecisionSnapshot and save it to a backend.

  4. 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 capture
from 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

ParameterDefaultDescription
decision_typeNoneLabel for the kind of decision recorded
context_versionNoneVersion tag for the surrounding context or prompt
max_input_chars1000Truncate recorded inputs to this length
max_output_chars1000Truncate recorded outputs to this length
exporterNoneExporter that receives each recorded dict
async_captureTrueExport off the calling thread

@capture works with or without arguments:

from briefcase import capture
@capture
def 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 # unchanged

Key classes

  • @capture — decorator that records a dict and exports it
  • DecisionSnapshot — structured record you can persist and replay; exposes fingerprint()
  • Input / Output — typed wrappers; Output.with_confidence(score) attaches a confidence value
  • ModelParameters — model name, provider, and per-call parameters
  • Snapshot — groups multiple decisions; add_decision(decision) appends to it
FieldDescriptionWhy it matters
function_nameThe recorded functionIdentifies which decision this is
inputsTyped inputsThe exact inputs a replay re-runs against
outputsTyped outputsWhat the agent actually decided
tagsArbitrary key/value tagsCarries your own context (e.g. environment, queue)
execution_time_msHow long the call tookAnchors performance over time
fingerprint()Content hashMakes 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 thisWhen you want to…Lifetime
@capture decoratorInstrument a real function (like classify_ticket) with zero boilerplate and stream a lightweight record to an exporterPer call
DecisionSnapshotBuild a structured record by hand — to persist, replay, or fingerprint itIn-memory object
Persisted backend (SqliteBackend)Keep decisions durably so you can query, replay, and audit them weeks laterDurable

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 briefcase
from briefcase import DecisionSnapshot, Input, Output
from 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)

Where this fits