Docs
Two ways in: the zero-dependency Python / JS SDK for explicit logging, and the tracehouse CLI agentthat records Claude Code & Codex automatically. Sections 11–12 cover A/B experiments and the CLI.
Quickstart — log your agent into a project folder
A project is the folder: pass project="…" and every run of your agent lands in /projects/<name> as its own trace. No setup in the UI — the folder appears on the first run. Want separate folders for prod / evals / experiments? Use different names.
Lazy path — let your coding agent wire it up
Copy this prompt into Claude Code / Codex / Cursor inside your agent's repo, replace <PROJECT_NAME>, and it will instrument every message, thought, tool call and result.
Instrument the agent in this repo with tracehouse so every run is logged
to the tracehouse project "<PROJECT_NAME>" (one trace per run / task).
Setup
- Python: add `tracehouse-sdk` to dependencies, `import tracehouse as cm`.
JS/TS: add `@tracehouse/sdk`, `import { Run } from "@tracehouse/sdk"`.
- Auth comes from the TRACEHOUSE_API_KEY env var (starts with "ba_").
Never hardcode the key. Add it to .env.example.
Where to hook in
- Find the agent loop: the place that calls the LLM, parses tool calls,
executes tools and feeds results back. Wrap ONE full run/task in one trace:
Python: with cm.Run(project="<PROJECT_NAME>", task_name=<short task title>,
model=<model id>) as run:
JS: const run = await Run.start({ project, taskName, model })
… and always call run.finish(...) in finally.
- Concurrent / parallel runs → separate Run objects. Do NOT use the
module-level cm.init/cm.log_* globals.
Log EVERYTHING, in order, as it happens
- the task / each user or system-injected message → run.log_user(text)
- model reasoning, if the API returns it → run.log_thinking(text)
- every assistant text block → run.log_assistant(text)
- every tool call, with the full arguments dict → span = run.log_tool_use(name, args)
- every tool result (stringify; keep it whole) → run.log_tool_result(text, tool=name,
parent_span_id=span.id)
- a tool that raised: log the error text as the tool result with status="error",
then continue the agent's normal error handling.
- sub-agents: give them their own Run in the same project.
- JS names: logUser / logThinking / logAssistant / logToolUse({tool, input}) /
logToolResult({text, tool, parentSpanId}).
Finish
- run.finish(outcome="good" | "bad" | "neutral", metadata={...}) with useful
metadata: steps, stop reason, final answer length, token usage if available.
An unhandled exception must still finish the trace with outcome="bad"
(the Python `with` block does this automatically).
Rules
- Logging must never break the agent: wrap SDK calls so a network error
is logged as a warning and the run continues.
- Don't change agent behaviour, prompts or outputs — only add logging.
- Keep it in one small module (e.g. tracing.py / tracing.ts) + minimal
call sites in the loop.
- When done: run the agent once on a tiny task and confirm the trace shows up
at https://tracehouse.ai/projects/<PROJECT_NAME>.
SDK docs: https://tracehouse.ai/docsBy hand
pip install tracehouse-sdk export TRACEHOUSE_API_KEY="ba_..." # mint one below (section 2)
import tracehouse as cm
# project = the folder. Same name → same folder; nothing to create first.
# One `with` block = one trace (one agent run) inside that folder.
with cm.Run(project="my-agent", task_name=task) as run:
run.log_user(task)
while True:
reply = llm(messages) # your model call, any provider
if reply.thinking:
run.log_thinking(reply.thinking)
if reply.text:
run.log_assistant(reply.text)
if not reply.tool_calls:
break
for call in reply.tool_calls:
span = run.log_tool_use(call.name, call.args)
out = TOOLS[call.name](**call.args)
run.log_tool_result(str(out), tool=call.name, parent_span_id=span.id)
# Leaving the block closes the trace. An exception marks it outcome="bad"
# with the error in metadata — crashed runs still land in the folder.JavaScript / TypeScript
import { Run } from "@tracehouse/sdk";
const run = await Run.start({ project: "my-agent", taskName: task });
try {
await run.logUser(task);
// … your loop: run.logAssistant(text), run.logToolUse({ tool, input }),
// run.logToolResult({ text, tool }) …
await run.finish({ outcome: "good" });
} catch (e) {
await run.finish({ outcome: "bad", metadata: { error: String(e) } });
throw e;
}Then open /projects → your folder. Each trace shows the conversation, tool calls, tokens and cost. Using Claude Code or Codex instead of your own loop? Skip the SDK and install the CLI (section 12) — each session is filed automatically under a project named after the directory you ran it in.
1. Install
Requires Python 3.9+. The PyPI distribution is tracehouse-sdk; the import path stays tracehouse.
pip install tracehouse-sdk
2. Get an API key
Keys are BetterAuth-minted and start with ba_. They work from any machine and don't expire by default. Copy once — the plaintext is never shown again.
Sign in to mint an API key — it's free and the key works from any machine.
Sign in to get a key3. Environment
# Required: export TRACEHOUSE_API_KEY="ba_..." # Optional — override the default https://tracehouse.ai export TRACEHOUSE_API_BASE="https://tracehouse.ai"
4. Chat traces — Run / cm.init / cm.log_*
Module-level wandb-style surface for Claude Code style sessions. Each session_id is resumable: posting again with the same id from the same machine reuses the trace row.
import tracehouse as cm
cm.init(
project="my-bot",
session_id="run-001", # resumable: same id reuses the trace
api_key=..., # or set TRACEHOUSE_API_KEY env
)
cm.log_user("hello")
cm.log_assistant("hi back")
cm.log_tool_use("Read", {"file_path": "x.py"})
cm.log_tool_result("contents of x.py …")
cm.finish(outcome="good", metadata={"model": "claude-sonnet-4-6"})5. Training runs — cm.init_run
Parallel entity to traces. Logs scalar metrics into a fast float-typed column; lists / dicts (gradient norms, lr schedules, histograms) go to JSONB and render as bar charts. Metrics are idempotent on (run_id, key, step) — safe to retry on flaky networks.
import tracehouse as cm
run = cm.init_run(
project="demo",
name="qwen-sft-v1",
config={
"lr": 1e-4,
"batch": 32,
"base_model": "Qwen/Qwen2.5-0.5B",
},
)
for step in range(1000):
run.log({"train/loss": loss, "eval/acc": acc}, step=step)
# Lists / dicts go into a JSON column → rendered as a bar chart.
run.log({"grad/norm_hist": [0.1, 0.2, 0.4, 0.3]}, step=step)
run.link_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft")
run.link_model("your-handle/my-finetune")
run.add_artifact("hparams", data={"warmup_ratio": 0.03, "weight_decay": 0.0})
run.finish(status="finished")Context manager auto-finishes (and marks crashed on exceptions):
# Auto-finish on exit. Exceptions → status="crashed".
with cm.TrainingRun(project="demo", name="exp-42") as run:
for step, batch in enumerate(loader):
loss = train_step(batch)
run.log({"train/loss": loss}, step=step)6. Media — images & videos
Log images and videos to a run with cm.Image / cm.Video — either inside run.log({…}) next to metrics, or via run.log_image / run.log_video. They appear under the run's Media tab, grouped by key. Bytes are sent raw (no base64), capped at 25 MB per item.
import tracehouse as cm
run = cm.init_run(project="demo", name="qwen-sft-v1")
# Log images / videos right next to metrics. cm.Image accepts a file path,
# raw bytes, a PIL image, or a numpy array (Pillow needed only for arrays).
run.log(
{"loss": loss, "samples": cm.Image("out/epoch3.png", caption="epoch 3")},
step=3,
)
# Or log media explicitly:
run.log_image("val/grid", "preview.png", caption="val grid", step=10)
run.log_video("rollout", "clip.mp4", step=10)
# cm.Video takes a file path or raw bytes (mp4 / webm / mov).
# Media shows up under the run's Media tab. Limit: 25 MB per item.7. Push model card to HuggingFace
The server renders a model card from the run's name, config, final metrics, and linked refs — then pushes it as README.md via the HF Hub commit RPC. Needs a write-scope HF token saved on your profile.
# Requires: # 1. run.link_model(...) earlier so we know the repo # 2. A write-scope HF token saved in /profile resp = run.push_model_card(commit_message="Initial card from tracehouse") print(resp["commit_url"]) # https://huggingface.co/.../commit/<sha>
8. Logging
The SDK uses stdlib logging under tracehouse. INFO covers lifecycle events; DEBUG adds every HTTP request and response with byte counts; WARNING fires on API errors and dropped (NaN/Inf) metric points.
import logging
# Library doesn't call basicConfig — set it up once in your application.
logging.basicConfig(level=logging.INFO, format="%(name)s %(message)s")
# Verbose HTTP: every → / ← request, byte counts, dropped points.
logging.getLogger("tracehouse").setLevel(logging.DEBUG)9. Reinforcement learning — runs + rollouts
Log a run's metrics and its per-step rollout conversations together. run.rollout(step=…) opens a chat trace already linked to the run, so each rollout shows up under the run's Rollouts tab (step → trace). It returns a normal Runand inherits the run's auth — an anonymous run produces anonymous rollouts under the same identity, and one claim link covers both.
import tracehouse as cm
run = cm.init_run(project="rl", name="ppo-v1", config={"lr": 1e-5})
for step in range(1000):
# One chat trace per rollout, tied to this run + step.
with run.rollout(step=step) as t:
t.log_user(state)
t.log_assistant(action)
t.log_tool_result(f"reward={reward}")
run.log({"reward": reward, "kl": kl}, step=step) # metrics on the run
run.finish()
# The run page gets a "Rollouts" tab: step 0 → trace, step 1 → trace, …10. Drop-in for wandb
tracehouse ships a wandb-compatible surface under tracehouse.wandb. Swap the import, or override sys.modules["wandb"] to redirect existing import wandb code with no edits. Parity covers init / log / config / summary / finish / Image / Video / Histogram / define_metric.
# Option A — swap the import (new code):
from tracehouse import wandb # or: import tracehouse.wandb as wandb
# Option B — override existing `import wandb` everywhere, zero edits.
# Put this before the first `import wandb` runs:
import sys, tracehouse.wandb
sys.modules["wandb"] = tracehouse.wandb
# Either way, the usual wandb call sites just work:
run = wandb.init(project="demo", name="qwen-sft", config={"lr": 1e-4})
for step in range(1000):
wandb.log({"train/loss": loss}, step=step)
wandb.log({"samples": wandb.Image("out.png")}, step=step)
wandb.config.update({"warmup_ratio": 0.03})
wandb.summary["best_loss"] = best_loss
wandb.finish()11. Experiments & subjects (A/B cohorts)
Tag any run or trace with three cohort dimensions: subject_id (the end user the agent acted for), experiment (an A/B test name) and variant (the arm, e.g. control vs treatment). There's nothing to set up first — the experiment row and its variants materialize automatically on the first tagged run.
Python
import tracehouse as cm
# Tag a run with three cohort dimensions. Nothing to pre-create:
# the experiment + variant materialize on the first tagged run.
run = cm.init_run(
project="agent-evals",
name="run-001",
subject_id="customer-42", # the end user this run acted for
experiment="prompt-rewrite", # the A/B test name
variant="treatment", # this run's arm (vs "control")
config={"model": "claude-sonnet-4-6"},
)
run.log({"reward": reward}, step=0)
run.finish(status="finished")
# Chat traces take the same dimensions, at init or at finish:
cm.init(project="agent-evals", session_id="s-1",
subject_id="customer-42", experiment="prompt-rewrite", variant="control")
cm.finish(outcome="good")JavaScript / TypeScript
import { initRun, init, finish } from "@tracehouse/sdk";
const run = await initRun({
project: "agent-evals",
name: "run-001",
subjectId: "customer-42", // end user this run acted for
experiment: "prompt-rewrite", // A/B test name
variant: "treatment", // this run's arm (vs "control")
config: { model: "claude-sonnet-4-6" },
});
await run.log({ reward }, 0);
await run.finish({ status: "finished" });
// Chat traces accept the same dimensions (camelCase) at init or finish:
await init({ project: "agent-evals", sessionId: "s-1",
subjectId: "customer-42", experiment: "prompt-rewrite", variant: "control" });
await finish({ outcome: "good" });The backend reduces each variant to a cohort aggregate (flag / error / loop rates, cost, latency) and reports control-relative deltas with deterministic significance tests(Wilson intervals, z-test for proportions, Welch's t for means) — no LLM in the loop. The web UI renders this at /experiments; per-user rollups live at /subjects.
# Stats are computed server-side — no LLM, fully deterministic. # GET the comparison for an experiment by name: curl -H "Authorization: Bearer $TRACEHOUSE_API_KEY" \ https://tracehouse.ai/v1/experiments/prompt-rewrite/compare # -> per-variant cohort aggregates (flag/error/loop rates, cost, latency) # + control-relative deltas with Wilson / z / Welch significance. # The UI renders this at /experiments; per-user rollups live at /subjects.
12. CLI agent — record Claude Code & Codex automatically
The tracehouse CLI is a local, zero-config recorder. It installs a PreToolUse hook into ~/.claude/settings.json and runs a small background daemon that tails the JSONL transcripts Claude Code & Codex already write — parsing each prompt, thought, tool call and result into spans and shipping them to the backend as live traces. PII is redacted locally before anything leaves the machine. No SDK calls, no code changes to your agents.
Install
# One line — detects Claude Code & Codex, installs the binary, # patches ~/.claude/settings.json, and starts the background daemon. curl -fsSL https://tracehouse.ai/install.sh | bash # Equivalent manual path: tracehouse login --api-key ba_... # save key + backend URL (once per machine) tracehouse install # add the PreToolUse hook + background service tracehouse agent # start tailing transcripts (`--once` for one pass)
Commands
tracehouse # interactive setup / status TUI (no subcommand) tracehouse login # save API key + backend URL locally (wandb-style) tracehouse install # patch settings.json hook + install the daemon tracehouse uninstall # remove the hook + stop/remove the service tracehouse agent # long-running daemon: watch JSONL roots, ship spans tracehouse agent --once # single sync pass, then exit (good for cron / CI) tracehouse doctor # diagnostics: key valid? machine_id? last sync? tracehouse hook # internal: PreToolUse handler invoked by Claude Code
Run tracehouse with no subcommand for an interactive setup / status TUI, or tracehouse doctor to confirm the key is valid and spans are syncing. Connected machines and their last-sync status show up at /devices.