Skip to content

Architecture ​

How Blackbox is put together, for contributors and the curious. The cassette format itself is in trace format.

Two halves ​

Blackbox is split into an execution side, which sits in the path of a running agent, and an analysis side, which only ever reads cassettes.

text
 your agent ──► SDK client ──fetch──► session (record / replay / fork) ──► live API (record, fork --live)
                 wrapped tools ──────►    │
                                          ▼
                                     cassette (JSON, hash-chained)
                                          │
                  replay summary · verify · diff · assert · inspect   (analysis: reads a Trace, nothing else)

The analysis side never imports a model client, a tool, the session, an SDK or a network module. A test walks the import graph of src/replay, src/trace, diffTraces, diffOutcome and assertCassette and fails the build if any of them can reach one.

Execution side ​

ModuleRole
src/session/session.tsThe session behind blackbox(): fetch, tools(), finish(), and the record / replay / fork state machine
src/session/options.tsOptions, and the environment variables the CLI launcher sets
src/integrations/anthropic.ts, openai.tsPure translation between each provider's wire JSON and neutral step payloads, both ways
src/cli/launch.tsRuns record / replay / fork -- <command>: starts the agent with the session switched on and reports the result
src/ingest/*Transcript importers (Claude Code JSONL, chat JSON)
src/agent/*, src/fork/forkRun.tsThe built-in demo agent and its fork, used by demo, check and fork without a command

Record ​

bb.fetch receives each request exactly as the SDK sends it. The session parses the JSON body, normalizes it (normalizeAnthropicRequest / normalizeOpenAIRequest), forwards the request unchanged, and appends a model_input and a model_output step when the response succeeds. Provider tool-call ids are mapped to call-N in the order the model produced them. Wrapped tool invocations are matched to the model's calls by name and arguments, buffered, and written in the model's call order before the next model request.

Replay ​

A cursor walks the recorded steps. Each request is normalized the same way and compared with the recorded model_input (firstDifference). The recorded model_output is turned back into a response in the provider's shape (synthesize*Response) with the neutral ids as tool-call ids, so the agent's next request refers to them and normalizes identically. Wrapped tools look up their recorded steps by call id and return the recorded result.

Fork ​

Fork is replay until the agent invokes the tool whose result is the fork step. At that point the session copies the parent's steps before it verbatim into a new recorder, appends the replacement result with the parent's timestamp, and switches to record behavior with either the live API or scripted replies.

Errors across the SDK boundary ​

A thrown fetch error would be wrapped by the SDK as a retryable connection error, so the session instead answers with a 400 carrying a [blackbox] message (never retried), keeps the typed error, and finish() rethrows it. Error messages are masked for known keys.

Analysis side ​

ModuleRole
src/trace/TraceTypes.ts, hash.ts, TraceRecorder.tsThe trace model, canonical JSON and SHA-256 hashing, append-only recording
src/trace/payloads.tsReaders for the step payload shapes (toolCallsOf)
src/replay/CassetteReplay.tsLoad, save, validate the chain, and summarize a run from a Trace
src/trace/verifyTrace.ts, neutrality.ts, secrets.tsThe ordered verify checks and the credential scan
src/fork/diffTraces.ts, diffOutcome.tsFirst divergence (by hash, or --semantic) and the outcome comparison
src/workflow/assertCassette.tsverify plus exact expectations, for CI

Hashing ​

A step's hash is SHA-256 over the canonical JSON (keys sorted recursively, no whitespace) of index, type, timestamp, payload and prevHash. The step's own id and hash are excluded. Because every hash includes the previous one, changing a step changes every hash after it. diff compares hashes. diff --semantic compares only type and payload, for two independent recordings.

Tests ​

The suite runs offline with no key. Session tests drive the real @anthropic-ai/sdk and openai clients with a scripted fake upstream in place of the network, so a change in either SDK's request or response shape fails a test. Launcher tests run a fixture agent through blackbox record | replay | fork in a child process. CI also packs the package, installs the tarball into an empty project, and uses it from JavaScript and TypeScript.

MIT licensed