๐ซ BlackBox Recorder
Universal, zero-dependency flight recorder for AI agents.
Records every thought, every tool call, every LLM prompt.
When something goes wrong โ you'll know exactly what happened and why.
Quick Start โข Wiring It In โข Working With Traces โข CLI โข LLM Tracing โข Configuration โข API โข Roadmap
๐คฆโโ๏ธ The Problem
It's 3 AM. Your autonomous AI agent went rogue and offered a user a 99% discount on a Tesla. The pipeline silently swallowed the error like a golden retriever inhaling a dropped meatball.
What happened? Did the LLM hallucinate? Did a tool return garbage? Why?
If you try to add standard tracing (LangSmith, Logfire, Phoenix), you suddenly need a cloud subscription, a PhD in DevOps to deploy PostgreSQL/ClickHouse, or you have to pollute your pristine project with 42 opentelemetry-* dependencies that conflict with everything.
๐ฆธ The Solution
ai-blackbox-recorder is a dashcam for your AI.
Like a flight recorder in an aircraft, it sits quietly in the background, sipping tea, and silently writing every single thought, prompt, and tool call into a local SQLite file. When things go boom, you don't guess โ you just pull the tape.
- Zero external dependencies โ pure Python 3.10+ stdlib (doesn't bloat your
requirements.txt) - Never blocks your agent โ background daemon thread with
queue.Queuehandles the I/O - Survives the crash it is recording โ spans hit disk the moment they start, and a shutdown hook drains the buffer, so a
SIGKILLed or OOM-killed agent still tells you where it died - Silent until used โ importing the library creates no database and starts no thread; the recorder wakes up on your first traced call
- Automatic call hierarchy โ
contextvarsmagically builds the execution tree for you (sync + async) - Configurable retention โ auto-deletes traces older than 7 days, 30 days, etc.
- Disk protection โ hard disk cap at 300 MB, older traces get evicted so your server doesn't crash
- First-class LLM support โ prompts, completions, chain-of-thought, token counts, tool calls
- Incident CLI โ beautiful ASCII tree with everything you need to debug right in the terminal
๐ Quick Start
๐ค Note for AI Agents (Antigravity, Claude, Cursor)
If you are an AI assistant helping a human integrate tracing, you'll love this: It has zero dependencies.
- Install it from a release (see below) โ zero dependencies means no pip resolver conflicts.
- Import it as
ai_blackbox_recorder, add the@tracedecorator to the human's functions.- You can immediately check your own work using
python -m ai_blackbox_recorder listin the terminal!
Installation
Not on PyPI yet. Note that
pip install blackbox-recorderfetches an unrelated project that happens to share that name โ not this library.
# latest release
pip install https://github.com/amyotoff/blackbox-recorder/releases/download/v0.7.0/ai_blackbox_recorder-0.7.0-py3-none-any.whl
# or from source
pip install git+https://github.com/amyotoff/blackbox-recorder@v0.7.0
Python 3.10+, no dependencies. The distribution is ai-blackbox-recorder, the import is ai_blackbox_recorder, and the CLI is ai-blackbox-recorder.
Two Lines to Start Recording
from ai_blackbox_recorder import trace, SpanKind
@trace(kind=SpanKind.TOOL)
def search_web(query: str) -> list:
return google_search(query)
@trace(kind=SpanKind.AGENT)
async def my_agent(user_message: str) -> str:
results = search_web(user_message)
return synthesize(results)
That's it. Every call to search_web and my_agent is now recorded with full inputs, outputs, timing, and automatic parentโchild hierarchy.
Investigate an Incident
# What happened recently?
ai-blackbox-recorder list
# Show the execution tree
ai-blackbox-recorder show <TRACE_ID>
# Full details: prompts, thinking, tokens
ai-blackbox-recorder show <TRACE_ID> -v
๐ Wiring It Into Your Agent
Installing the package records nothing on its own. A useful flight recording comes from where you put the spans and what you bind to them. This is the integration playbook โ follow it once per service and you never think about it again.
1. Bootstrap once, at the process entry point
# main.py โ the first thing your process runs
import os
from ai_blackbox_recorder import BlackBoxConfig, configure
configure(BlackBoxConfig(
db_path=os.getenv("BLACKBOX_DB", "/var/lib/myagent/traces.db"),
retention="30d",
max_db_size_mb=300,
enabled=os.getenv("ENV") != "test",
))
Three rules that cover every deployment:
- Call
configure()before the first traced function runs. Import order doesn't matter โ decorating a function is free and touches nothing. Skipconfigure()entirely and you getblackbox_traces.dbin the current working directory. - Point
db_pathat a writable volume. In Docker, the container's working directory is usually ephemeral: mount a volume, or your black box burns up with the container. - One database per service. Multiple processes of the same service can share one file safely (SQLite WAL), but two unrelated services sharing a file makes every trace list a mess.
2. Decide what becomes a span
The rule of thumb: one span per thing that can independently fail, be slow, or be wrong. Tracing every pure helper drowns the tree in noise and tells you nothing.
| Layer in your agent | SpanKind | What to record | Question it answers at 3 AM |
|---|---|---|---|
| HTTP handler / bot update / queue job | AGENT |
user message, final answer | What did the user actually ask, and what did we answer? |
| Planner / router model call | LLM |
system + prompt, thinking, tool_calls, tokens |
Did the model pick the wrong tool, or hallucinate the args? |
| Every tool, API call, DB write | TOOL |
args, raw result | Did the tool return garbage that the model then trusted? |
| Vector search / RAG fetch | RETRIEVER |
query, doc IDs, scores | Did we answer confidently from the wrong context? |
| Deterministic post-processing | CHAIN |
in / out | Did our own code mangle a good model answer? |
3. Put every model call behind one traced function
This is the highest-value hour you'll spend. Route all LLM traffic through a single helper and prompts, completions, thinking and token counts get recorded everywhere at once โ no per-call-site instrumentation, no forgotten branches.
# llm.py โ the only place in the codebase that talks to the model
from ai_blackbox_recorder import tracer, SpanKind
async def call_llm(prompt: str, *, system: str = "", model: str = "gemini-2.5-flash", tools=None):
with tracer.span(f"llm:{model}", kind=SpanKind.LLM) as span:
response = await client.generate(model=model, system=system, prompt=prompt, tools=tools)
span.set_llm_io(
system_prompt=system,
prompt=prompt,
completion=response.text,
thinking=response.thinking, # if your provider exposes it
tool_calls=response.tool_calls,
model=model,
prompt_tokens=response.usage.input_tokens,
completion_tokens=response.usage.output_tokens,
stop_reason=response.finish_reason,
)
return response
Do the same for tools โ one decorator on the function, and the arguments and return value are captured automatically:
@trace(kind=SpanKind.TOOL)
def charge_customer(order_id: str, amount_cents: int) -> dict:
return billing_api.charge(order_id, amount_cents)
4. Bind every trace to a user and hand back the trace ID
This is what turns "a user is complaining about something that happened yesterday" into "here is the exact recording".
from ai_blackbox_recorder import set_session_id, tracer, SpanKind
@app.post("/chat")
async def chat(req: ChatRequest):
set_session_id(req.user_id) # every child span inherits it
with tracer.span("chat_request", kind=SpanKind.AGENT, inputs={"message": req.text}) as root:
answer = await agent(req.text)
log.info("handled chat", extra={"trace_id": root.trace_id}) # bridge to your normal logs
return {"answer": answer, "trace_id": root.trace_id}
Now ai-blackbox-recorder list --session <user_id> gives you that user's history, and any trace ID appearing in your logs, error tracker or support ticket opens the full recording with show -v.
5. Async, threads and workers โ the one real gotcha
Hierarchy is carried by contextvars, so it behaves differently per concurrency model:
# โ
asyncio โ context propagates into tasks automatically
async def agent(q):
a, b = await asyncio.gather(search(q), classify(q)) # both become children
# โ threads โ a new thread starts with an EMPTY context.
# The child span silently becomes its own orphan root trace.
with ThreadPoolExecutor() as pool:
pool.submit(search, q)
# โ
threads โ carry the context across explicitly
import contextvars
ctx = contextvars.copy_context()
with ThreadPoolExecutor() as pool:
pool.submit(ctx.run, search, q)
For multiprocessing and separate worker processes there is no shared context at all: each process calls configure() itself, and either writes to the same file (WAL handles concurrent writers) or to its own. To stitch a trace across a process or queue boundary, pass the ID and re-bind it on the other side:
job = {"payload": ..., "trace_id": tracer.get_trace_id()} # producer
...
set_trace_id(job["trace_id"]) # consumer, before its first span
6. Deployment checklist
-
db_pathon a mounted volume, not the container's working directory -
retentionandmax_db_size_mbsized to that volume โ the recorder evicts oldest traces rather than filling the disk -
enabled=Falsein unit tests and CI so test runs don't pollute the recording -
*.db,*.db-wal,*.db-shmin.gitignore(already there in this repo) - the CLI is available where the DB lives โ
docker exec -it myagent ai-blackbox-recorder list - nothing else to run: no collector, no sidecar, no port
๐งญ Working With Your Traces
A recording nobody reads is just disk usage. Four workflows where the tape pays for itself.
Designing a feature โ start from what actually happens
Before writing the prompt for a new capability, look at what users are really doing, not at what the spec assumes:
ai-blackbox-recorder export -o corpus.jsonl # everything, one span per line
-- the top requests hitting your agent, straight from SQLite
SELECT json_extract(inputs, '$.user_query') AS request, COUNT(*) AS n
FROM spans WHERE kind = 'AGENT'
GROUP BY request ORDER BY n DESC LIMIT 20;
That distribution tells you which branch is worth building, what the real input lengths are, and which tools already get called for the job. Instrument the current behaviour first, ship the feature second โ otherwise you have no baseline to compare against.
Evolving the project โ measure the change, don't guess
Tag traces with the build or prompt version, then compare like for like:
@trace(name="support_agent", kind=SpanKind.AGENT, metadata={"prompt_version": "v3"})
async def support_agent(query: str) -> str:
...
-- did prompt v3 actually get cheaper and faster, or just feel that way?
SELECT json_extract(metadata, '$.prompt_version') AS version,
COUNT(*) AS runs,
ROUND(AVG(duration_ms)) AS avg_ms,
ROUND(100.0 * SUM(has_error) / COUNT(*), 1) AS error_pct
FROM spans WHERE name = 'support_agent'
GROUP BY version;
The same query shape catches silent regressions after a refactor: latency drifting up, a tool quietly failing more often, token spend doubling because a prompt grew.
Evals โ build the dataset out of production
The best eval set is the traffic you already recorded. Pull real prompts and the completions you shipped, then use them as regression cases:
from ai_blackbox_recorder import BlackBoxConfig, TraceStorage
storage = TraceStorage(BlackBoxConfig(db_path="traces.db"))
cases = []
for t in storage.list_traces(limit=500, has_error=False):
for span in storage.get_trace(t["trace_id"]):
inputs, outputs = span.get("inputs") or {}, span.get("outputs") or {}
if span["kind"] == "LLM" and isinstance(outputs, dict) and inputs.get("prompt"):
cases.append({"prompt": inputs["prompt"], "shipped": outputs.get("completion")})
Run the eval itself under its own session ID so it never mixes with real users:
set_session_id("eval:prompt-v3")
ai-blackbox-recorder list --session eval:prompt-v3 --errors-only # what broke in this run
Because every eval run is itself a set of traces, a failing case opens with show -v and shows the whole chain โ prompt, thinking, tool calls โ instead of a bare pass/fail.
Incident investigation โ the 3 AM runbook
ai-blackbox-recorder errors --limit 20 # 1. what failed recently
ai-blackbox-recorder show <TRACE_ID> -v # 2. the full chain of that failure
ai-blackbox-recorder export --trace <TRACE_ID> -o incident.jsonl # 3. attach to the ticket
Read the tree top-down and the failure usually names itself: an LLM span whose ๐ง Tool Call has wrong arguments is a prompt problem; a TOOL span with a clean call but a junk ๐ค Result is someone else's outage; a clean tool result followed by a wrong final answer is your post-processing.
When the process died instead of erroring. Spans are written to disk the moment they start, so a SIGKILL, an OOM kill or a hard container stop still leaves a record. Unfinished spans are marked rather than hidden:
๐ฆ Trace ID: 2707d6c61ed247788fdb60edb0a6d9d3
โฑ๏ธ Total Spans: 3
โณ Unfinished: 2 (still running, or the process died before they returned)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโ โณ [AGENT] support_agent (unfinished)
โโโ โ
[LLM] classify [gemini-2.5-flash] (0.01ms) ๐ค 12โ3
โโโ โณ [TOOL] charge_api (unfinished)
The deepest โณ span is where the process was when it went down โ here, inside charge_api, with the arguments it was called with. The same marker appears as โณ OPEN in ai-blackbox-recorder list. A span that is still โณ for a long-finished run is a hang, a kill, or a tool that never returns.
SQL cookbook
The database is a plain SQLite file with one spans table โ every question is one query away, no API, no export step.
-- token spend by model
SELECT json_extract(metadata, '$.model') AS model,
COUNT(*) AS calls,
SUM(json_extract(metrics, '$.total_tokens')) AS tokens
FROM spans WHERE kind = 'LLM' GROUP BY model ORDER BY tokens DESC;
-- which tool is the least reliable
SELECT name, COUNT(*) AS runs, SUM(has_error) AS failures,
ROUND(100.0 * SUM(has_error) / COUNT(*), 1) AS failure_pct
FROM spans WHERE kind = 'TOOL' GROUP BY name ORDER BY failure_pct DESC;
-- p95 latency of the entry point
SELECT ROUND(duration_ms, 1) AS p95_ms FROM spans
WHERE name = 'support_agent' AND duration_ms IS NOT NULL
ORDER BY duration_ms
LIMIT 1 OFFSET (SELECT CAST(COUNT(*) * 0.95 AS INT) FROM spans
WHERE name = 'support_agent' AND duration_ms IS NOT NULL);
-- runs that never finished: hangs and kills
SELECT trace_id, name, datetime(created_at, 'unixepoch') AS started
FROM spans WHERE end_time IS NULL ORDER BY created_at DESC;
๐ Incident Investigation (CLI)
The CLI is your primary tool for post-incident analysis. Install the package and it's available globally.
list โ Recent Traces
$ ai-blackbox-recorder list --limit 5
๐ Last 5 Traces:
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Start Time Status Spans Duration Root Operation Trace ID
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
2026-08-18 18:52:08 โ
OK 4 132.7ms support_agent 0f472b36b2a9...
2026-08-18 18:46:17 โ ERR 3 59.6ms billing_agent 61252647020244...
2026-08-18 18:44:02 โณ OPEN 3 0.1ms support_agent 2707d6c61ed247...
โ
OK finished cleanly, โ ERR raised, โณ OPEN never finished โ still running, or the process was killed mid-flight.
show โ Execution Tree
$ ai-blackbox-recorder show 0f472b36b2a941469f7a9ff66b28abd0
๐ฆ Trace ID: 0f472b36b2a941469f7a9ff66b28abd0
โฑ๏ธ Total Spans: 4
๐ค Tokens: 165 in โ 53 out (218 total)
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โโโ โ
[AGENT] support_agent (132.7ms)
โโโ โ
[LLM] plan_step [gemini-2.5-flash] (45.2ms) ๐ค 45โ18
โโโ โ
[TOOL] get_weather (12.1ms)
โโโ โ
[LLM] answer_step [gemini-2.5-flash] (74.3ms) ๐ค 120โ35
show -v โ Full Verbose Output with Prompts and Thinking
$ ai-blackbox-recorder show 0f472b36... -v
โโโ โ
[AGENT] support_agent (132.7ms)
๐ฅ Input: {"user_query": "ะะฐะบะฐั ะฟะพะณะพะดะฐ ะฒ ะะฐัะธะถะต?"}
โโโ โ
[LLM] plan_step [gemini-2.5-flash] (45.2ms) ๐ค 45โ18
โ ๐ System: You are a helpful assistant with access to tools.
โ ๐ฌ Prompt: ะะฐะบะฐั ะฟะพะณะพะดะฐ ะฒ ะะฐัะธะถะต?
โ ๐ง Thinking: ะะพะปัะทะพะฒะฐัะตะปั ัะฟัะฐัะธะฒะฐะตั ะพ ะฟะพะณะพะดะต. ะัะถะฝะพ ะฒัะทะฒะฐัั get_weather...
โ ๐ค Response: ะัะทัะฒะฐั ะธะฝััััะผะตะฝั get_weather ะดะปั ะณะพัะพะดะฐ Paris.
โ ๐ง Tool Call: get_weather({"city": "Paris"})
โ โน๏ธ Stop: tool_use
โโโ โ
[TOOL] get_weather (12.1ms)
โ ๐ฅ Args: {"city": "Paris", "units": "metric"}
โ ๐ค Result: {"temp_c": 24, "condition": "Sunny", "humidity": 45}
โโโ โ
[LLM] answer_step [gemini-2.5-flash] (74.3ms) ๐ค 120โ35
๐ง Thinking: ะะพะปััะธะป ัะตะทัะปััะฐั. ะขะตะผะฟะตัะฐัััะฐ 24ยฐC, ัะพะปะฝะตัะฝะพ...
๐ค Response: ะ ะะฐัะธะถะต ัะตะนัะฐั 24ยฐC, ัะพะปะฝะตัะฝะพ! ๐
โน๏ธ Stop: end_turn
All CLI Commands
| Command | Description |
|---|---|
ai-blackbox-recorder stats |
Database stats: size, span/trace counts, retention policy |
ai-blackbox-recorder list [--limit N] [--session ID] [--errors-only] |
List traces with filtering |
ai-blackbox-recorder errors [--limit N] |
Show only traces with errors |
ai-blackbox-recorder show <TRACE_ID> [-v] [--json] |
Hierarchical tree view; -v for prompts/thinking/tokens |
ai-blackbox-recorder export -o file.jsonl [--trace ID] |
Export to OpenInference-compatible JSONL |
ai-blackbox-recorder cleanup [--retention 7d] |
Manual TTL cleanup and disk reclaim |
Tip: You can also run CLI as a module:
python -m ai_blackbox_recorder stats
๐ง LLM Tracing
BlackBox has first-class support for recording everything that happens inside an LLM call.
Recording Prompts, Thinking, and Completions
from ai_blackbox_recorder import tracer, SpanKind
with tracer.span("reasoning_step", kind=SpanKind.LLM) as span:
# Call your LLM here...
response = call_gemini(prompt, system_prompt)
# Record everything in one call
span.set_llm_io(
system_prompt="You are a helpful assistant.",
prompt="What is the capital of France?",
thinking="User asks about geography. This is a factual question...",
completion="The capital of France is Paris.",
model="gemini-2.5-flash",
temperature=0.1,
prompt_tokens=28,
completion_tokens=9,
stop_reason="end_turn",
)
Recording Tool Calls from LLM Responses
with tracer.span("plan_step", kind=SpanKind.LLM) as span:
span.set_llm_io(
prompt="What's the weather in Berlin?",
completion="I'll check the weather for you.",
model="gpt-4.1",
prompt_tokens=15,
completion_tokens=12,
tool_calls=[
{"name": "get_weather", "args": {"city": "Berlin"}},
],
stop_reason="tool_use",
)
Recording Chat-Style Messages
with tracer.span("chat_turn", kind=SpanKind.LLM) as span:
span.set_llm_io(
messages=[
{"role": "system", "content": "You are a travel advisor."},
{"role": "user", "content": "Suggest a weekend trip."},
{"role": "assistant", "content": "How about Barcelona?"},
{"role": "user", "content": "Too far. Something closer."},
],
completion="How about a day trip to nearby vineyards?",
model="claude-sonnet-5",
prompt_tokens=85,
completion_tokens=14,
)
What Gets Recorded for LLM Spans
| Field | Method | CLI Display |
|---|---|---|
| System prompt | set_llm_io(system_prompt=...) |
๐ System: ... |
| User prompt | set_llm_io(prompt=...) |
๐ฌ Prompt: ... |
| Chat messages | set_llm_io(messages=[...]) |
[user]: ... [assistant]: ... |
| Chain-of-thought | set_llm_io(thinking=...) |
๐ง Thinking: ... |
| Model response | set_llm_io(completion=...) |
๐ค Response: ... |
| Tool calls | set_llm_io(tool_calls=[...]) |
๐ง Tool Call: name(args) |
| Input tokens | set_llm_io(prompt_tokens=N) |
๐ค NโM (inline) |
| Output tokens | set_llm_io(completion_tokens=M) |
Aggregated in trace header |
| Model name | set_llm_io(model=...) |
[model-name] (inline) |
| Stop reason | set_llm_io(stop_reason=...) |
โน๏ธ Stop: ... |
๐ ๏ธ API Reference
Three Ways to Record
1. @trace Decorator โ Automatic Recording
The simplest approach. Works with both sync and async functions. Automatically captures all arguments and return values.
from ai_blackbox_recorder import trace, SpanKind
@trace(kind=SpanKind.TOOL)
def calculate_vat(amount: float, rate: float = 0.20) -> float:
return round(amount * rate, 2)
@trace(name="research_agent", kind=SpanKind.AGENT)
async def run_agent(query: str) -> str:
result = calculate_vat(500.0)
return f"VAT is {result}"
Hierarchy is automatic โ if
run_agentcallscalculate_vat, the VAT span becomes a child of the agent span. No manual wiring needed.
2. with tracer.span() Context Manager โ Manual Control
For fine-grained control over what's recorded, or when you need to set LLM-specific fields.
from ai_blackbox_recorder import tracer, SpanKind
with tracer.span("vector_search", kind=SpanKind.RETRIEVER) as span:
span.set_metadata("index", "knowledge_base_v2")
span.set_metric("top_k", 10)
results = search_vector_db(query, top_k=10)
span.finish(output={"found": len(results), "ids": [r.id for r in results]})
3. Mix Both โ Decorator + Context Manager
@trace(name="support_bot", kind=SpanKind.AGENT)
async def handle_ticket(ticket_id: str):
with tracer.span("classify", kind=SpanKind.LLM) as llm_span:
category = await classify_ticket(ticket_id)
llm_span.set_llm_io(
prompt=f"Classify ticket {ticket_id}",
completion=category,
model="gemini-2.5-flash",
prompt_tokens=30,
completion_tokens=5,
)
with tracer.span("resolve", kind=SpanKind.TOOL) as tool_span:
tool_span.set_tool_call(
tool_name="jira_api",
tool_args={"ticket_id": ticket_id, "action": "resolve"},
tool_result={"status": "resolved"},
)
Span Kinds
Compatible with the OpenInference standard:
| SpanKind | When to Use |
|---|---|
SpanKind.AGENT |
Top-level reasoning loop / orchestrator |
SpanKind.LLM |
Direct LLM API call (prompt โ completion) |
SpanKind.TOOL |
Function call, API request, calculator, code execution |
SpanKind.RETRIEVER |
Vector search, RAG document fetch, knowledge base query |
SpanKind.CHAIN |
Deterministic multi-step pipeline or workflow |
Session Tracking
Bind traces to a user, session, or conversation:
from ai_blackbox_recorder import tracer
# Set once โ all subsequent spans inherit this session ID
tracer.set_session_id("tg_user_12345")
# Later, filter by session in CLI
# ai-blackbox-recorder list --session tg_user_12345
Python Query API
Access traces programmatically without the CLI:
from ai_blackbox_recorder import tracer
# List recent traces
traces = tracer.storage.list_traces(limit=10, has_error=True)
# Get full span tree for a trace
spans = tracer.storage.get_trace("abc123def456")
# Database statistics
stats = tracer.storage.get_stats()
print(f"DB size: {stats['db_size_mb']} MB, Traces: {stats['total_traces']}")
# Export to JSONL
from ai_blackbox_recorder import export_trace_to_jsonl
export_trace_to_jsonl(tracer.storage, "abc123def456", "incident_report.jsonl")
Graceful Shutdown
Buffered spans are flushed automatically when the interpreter exits, so a normal shutdown โ or an unhandled exception โ loses nothing. You only need these when you want to control the timing yourself:
tracer.flush() # Wait for the queue to drain; returns False on timeout
tracer.flush(timeout=2.0) # Bounded wait
tracer.close() # Flush + stop the worker thread (idempotent)
Neither survives SIGKILL or an OOM kill โ nothing running inside the process does. That case is covered by writing spans when they start; see Crash Durability.
โ๏ธ Configuration
Default Configuration
from ai_blackbox_recorder import BlackBoxConfig, Tracer
config = BlackBoxConfig(
db_path="blackbox_traces.db", # SQLite file path
retention="30d", # TTL: "7d", "30d", "60d", or int
max_db_size_mb=300, # Hard disk cap in MB
enabled=True, # Master kill-switch
batch_size=100, # Spans per write batch
flush_interval_seconds=0.5, # Max queue wait time
cleanup_interval_hours=6, # Periodic maintenance interval
capture_inputs=True, # Record function arguments
capture_outputs=True, # Record return values
max_field_chars=100_000, # Truncate oversized payloads
record_open_spans=True, # Write spans on start, so a hard kill still leaves a record
flush_timeout_seconds=5.0, # Max wait for the buffer to drain on close() / exit
)
tracer = Tracer(config=config)
To configure the default tracer โ the one behind the bare @trace decorator and tracer object โ call configure() at your entry point instead of building an instance:
from ai_blackbox_recorder import BlackBoxConfig, configure
configure(BlackBoxConfig(db_path="/var/lib/myagent/traces.db", retention="7d"))
Importing the library creates no file and starts no thread; the default tracer comes up on the first traced call, so configure() just has to run before that.
Crash Durability
record_open_spans=True (the default) writes every span the moment it starts, with end_time still empty; the finished row replaces it by span_id when the function returns. That costs one extra write per span and buys the thing the product is named after: if the process is killed, the recording still shows what was in flight and with which arguments.
On a normal exit โ including an unhandled exception โ a shutdown hook drains the buffer automatically, so spans are not lost between the last call and process teardown. Set record_open_spans=False if you are write-bound and only care about completed spans.
Retention Presets
| Value | Days |
|---|---|
"7d" or "week" |
7 |
"30d" or "month" |
30 |
"60d" or "2months" |
60 |
"90d" or "quarter" |
90 |
14 (any integer) |
14 |
Disk Protection
When the database file exceeds max_db_size_mb, the oldest complete traces are evicted automatically. After eviction, SQLite reclaims disk space via PRAGMA incremental_vacuum.
This runs:
- On tracer startup
- Every
cleanup_interval_hours(default: 6 hours) - Manually via
ai-blackbox-recorder cleanup
Disabling in Tests
config = BlackBoxConfig(enabled=False)
tracer = Tracer(config=config)
# All @trace decorators and context managers become no-ops
Multiple Tracers
You can create isolated tracer instances for different subsystems:
billing_tracer = Tracer(BlackBoxConfig(db_path="billing_traces.db", retention="60d"))
support_tracer = Tracer(BlackBoxConfig(db_path="support_traces.db", retention="7d"))
๐๏ธ Architecture
@trace("agent") with tracer.span("llm_call")
โ โ
โผ โผ
โโโโโโโโโโโโโ contextvars โโโโโโโโโโโโโโโโ
โ Root Span โโโโโโโโโโโโโโโโ Child Span โ
โ trace_id=X โ parent_span โ trace_id=X โ
โ span_id=A โ โ parent_id=A โ
โโโโโโโฌโโโโโโโ โโโโโโโโโฌโโโโโโโโ
โ โ
โผ โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ queue.Queue (thread-safe) โ
โ Non-blocking put() โ never stalls โ
โโโโโโโโโโโโโโโโโโโโฌโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ daemon thread
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ Background Worker Thread โ
โ โข Drains queue in batches (โค100) โ
โ โข INSERT batch into SQLite (WAL mode) โ
โ โข Periodic TTL + size cleanup โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ
โผ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ SQLite Database (WAL mode) โ
โ โ
โ PRAGMA journal_mode = WAL โ
โ PRAGMA synchronous = NORMAL โ
โ PRAGMA auto_vacuum = INCREMENTAL โ
โ โ
โ Indexes: trace_id, session_id, โ
โ created_at, kind, has_error โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Key design decisions:
contextvarsโ Python's built-in mechanism for implicit context propagation across sync and async code. Each decorated function automatically knows its parent span without passing IDs manually.queue.Queue+ daemon thread โ recording never blocks your agent's main loop. Spans are enqueued in nanoseconds and flushed to disk in the background.- Write on start, replace on finish โ a span reaches disk before the work it describes completes, so a process that dies mid-call still leaves the tape at the right frame.
INSERT OR REPLACEonspan_idmeans the finished row overwrites the open one instead of duplicating it. - Lazy default tracer โ importing the library has no side effects: the database file and worker thread are created on the first traced call, which keeps
configure()meaningful and keepsimportcheap in tests. - SQLite WAL mode โ Write-Ahead Logging allows concurrent reads (CLI queries) while the worker thread writes. No locking contention.
- Zero dependencies โ the entire library uses only Python standard library modules:
contextvars,sqlite3,dataclasses,queue,threading,json,uuid,time,inspect,functools,argparse.
๐ Project Structure
ai_blackbox_recorder/
โโโ __init__.py # Public API exports
โโโ __main__.py # python -m ai_blackbox_recorder entry point
โโโ tracer.py # Core: Tracer class, @trace decorator, context manager, worker thread
โโโ span.py # Span dataclass, SpanKind enum, set_llm_io(), set_tool_call()
โโโ storage.py # SQLite WAL engine, TTL cleanup, max-size eviction
โโโ config.py # BlackBoxConfig dataclass with retention parser
โโโ export.py # JSONL export and ASCII tree renderer
โโโ _cli.py # CLI commands: list, show, errors, stats, cleanup, export
๐งช Testing
The project includes a zero-dependency test suite that runs on bare Python without pytest:
# Run all tests (no pip install needed)
python3 run_tests.py
# Or with pytest (if available)
pip install pytest pytest-asyncio
pytest tests/ -v
Tests cover: sync/async tracing, automatic hierarchy, error capture, TTL retention, 300 MB size eviction, CLI commands, JSONL export, and tree rendering.
Durability is tested the only way that proves anything โ by killing a real process: tests/test_crash_recovery.py starts an agent in a subprocess, SIGKILLs it mid-span, and asserts the unfinished span is still on disk with its inputs. It also asserts that importing the package creates no database, and that a process exiting without close() loses nothing.
๐ณ Docker
docker build -t ai-blackbox-recorder .
docker run ai-blackbox-recorder # runs test suite
๐ Comparison with Alternatives
| Feature | ai-blackbox-recorder | LangSmith | Langfuse | Arize Phoenix | ai-trace |
|---|---|---|---|---|---|
| Dependencies | 0 (stdlib) | 50+ | 30+ | 40+ | 0 |
| Requires infra | No | Cloud | PostgreSQL | Docker | No |
| Local-first | โ | โ | โ ๏ธ self-host | โ ๏ธ self-host | โ |
| SQL query traces | โ SQLite | API only | SQL | SQL | โ JSONL |
| Async support | โ | โ | โ | โ | โ |
| Auto hierarchy | โ contextvars | โ | โ | โ OTel | โ |
| LLM prompt/completion | โ set_llm_io | โ | โ | โ | โ ๏ธ manual |
| Chain-of-thought | โ thinking field | โ | โ | โ | โ |
| Token tracking | โ | โ | โ | โ | โ |
| TTL retention | โ built-in | โ | โ | โ | โ |
| Disk size cap | โ 300 MB | N/A cloud | N/A | N/A | โ |
| CLI incident viewer | โ ASCII tree | Web UI | Web UI | Web UI | โ |
| Web UI | ๐ v1.0 | โ | โ | โ | โ |
| PII masking | ๐ v1.0 | โ | โ ๏ธ | โ ๏ธ | โ |
| Cost | Free | Paid | Free tier | Free | Free |
Best for: Solo developers, small teams, edge/local agents, privacy-sensitive environments, and anyone who wants a "plug in 2 lines and forget" flight recorder without infrastructure overhead.
๐ License
MIT โ use it anywhere, modify freely, no strings attached.
๐บ๏ธ v1.0 Roadmap
The following features are planned for the next major release:
PII Masking & Data Redaction
Automatic detection and masking of sensitive data before it's written to the database:
- API keys and tokens (
sk-...,ghp_...,Bearer ...) - Credit card numbers, phone numbers, email addresses
- Custom regex patterns via configuration
- Field-level opt-out (
capture_inputs=Falsealready works per-config, v1.0 adds per-span granularity)
# Planned v1.0 API
config = BlackBoxConfig(
redact_patterns=[
r"sk-[a-zA-Z0-9]{20,}", # OpenAI keys
r"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", # Credit cards
],
redact_fields=["password", "secret", "api_key"],
)
Built-in Web Viewer
A lightweight, single-page HTML dashboard served directly from the CLI โ no Node.js, no npm, no separate process:
ai-blackbox-recorder ui # Open browser at localhost:8080
ai-blackbox-recorder ui --port 9090 # Custom port
Features planned:
- Interactive trace tree with expand/collapse
- Timeline visualization of span durations
- Filter by session, time range, errors
- Search across prompts and completions
- Token usage charts
Standalone HTML Incident Report
Export a single self-contained HTML file with the full trace tree, prompts, thinking, and metadata โ shareable via email or Slack with zero tooling required on the recipient's side:
ai-blackbox-recorder report <TRACE_ID> -o incident_2026-08-18.html
Auto-Instrumentation Hooks
Optional helpers to automatically instrument popular LLM SDKs (Google GenAI, OpenAI, Anthropic) without manual set_llm_io() calls:
# Planned v1.0 API
from ai_blackbox_recorder.integrations import patch_google_genai
patch_google_genai(tracer) # All google.genai calls auto-recorded
Built with โค๏ธ for developers who debug at 3 AM.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file ai_blackbox_recorder-0.7.0.tar.gz.
File metadata
- Download URL: ai_blackbox_recorder-0.7.0.tar.gz
- Upload date:
- Size: 64.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c12ba57ef11a719c8e2b003ab16b553e45f46118c4f4e6aff0d451a6e662dd7e
|
|
| MD5 |
0aab861a557fb9b804e3b564eb71ee24
|
|
| BLAKE2b-256 |
891341c2c5d5e5fcd3f1b3e5e732d1388aa57a2f68b666f398b885e57de79143
|
Provenance
The following attestation bundles were made for ai_blackbox_recorder-0.7.0.tar.gz:
Publisher:
publish.yml on amyotoff/blackbox-recorder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_blackbox_recorder-0.7.0.tar.gz -
Subject digest:
c12ba57ef11a719c8e2b003ab16b553e45f46118c4f4e6aff0d451a6e662dd7e - Sigstore transparency entry: 2511159541
- Sigstore integration time:
-
Permalink:
amyotoff/blackbox-recorder@a45c3c7f7009192312927e9f42631ea3ffb3f48b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/amyotoff
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a45c3c7f7009192312927e9f42631ea3ffb3f48b -
Trigger Event:
workflow_dispatch
-
Statement type:
File details
Details for the file ai_blackbox_recorder-0.7.0-py3-none-any.whl.
File metadata
- Download URL: ai_blackbox_recorder-0.7.0-py3-none-any.whl
- Upload date:
- Size: 34.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8d3bdf6954f51af1f24e5f20fad9f938ab76e6e686f21a871e7be915d18c475d
|
|
| MD5 |
9f212071ac0ab09e44be5cc569ff1f30
|
|
| BLAKE2b-256 |
96c63a32bda69847f8d78e7e9a79363c2910fb1ac17883647651517853b22c98
|
Provenance
The following attestation bundles were made for ai_blackbox_recorder-0.7.0-py3-none-any.whl:
Publisher:
publish.yml on amyotoff/blackbox-recorder
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
ai_blackbox_recorder-0.7.0-py3-none-any.whl -
Subject digest:
8d3bdf6954f51af1f24e5f20fad9f938ab76e6e686f21a871e7be915d18c475d - Sigstore transparency entry: 2511159549
- Sigstore integration time:
-
Permalink:
amyotoff/blackbox-recorder@a45c3c7f7009192312927e9f42631ea3ffb3f48b -
Branch / Tag:
refs/heads/main - Owner: https://github.com/amyotoff
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@a45c3c7f7009192312927e9f42631ea3ffb3f48b -
Trigger Event:
workflow_dispatch
-
Statement type: