Run folder format¶
This is the contract. A worked example of every file below lives in
examples/telemetry_root/,
regenerated from the real writers by examples/generate.py, so it cannot drift
from the implementation.
Layout¶
.runs/
NAVIGATE.md the map, written on first use
index.jsonl one line per run
latest -> runs/<run_id>
sessions/<session_id> -> ../runs/<run_id>
runs/<YYYYMMDD_HHMMSS_hash>/
outline.md folded map, regenerated at finalize
agent.logs full transcript, append-only across attempts
trace.jsonl one event per line, the source of truth
analytics.json status, timings, per-agent cost, errors
meta.json framework, git sha, argv, attempts
blobs/<sha1>.txt payloads over blob_max_bytes
artifacts/ whatever your app puts there
Run ids sort chronologically. Pointers live outside runs/ so that a
runs/*/... glob cannot count a run twice through a symlink.
agent.logs¶
One line per event, with framed blocks for multi-line payloads.
| Column | |
|---|---|
#0016 |
zero-padded sequence number, the anchor |
14:23:14.000 |
local time with millisecond precision |
+30.0s |
delta from the previous event, not a duration |
tool.error |
event type |
run_python |
event name, - when there is none |
| rest | detail, assembled from the payload |
A payload that is multi-line or long is framed, and the closing line repeats the anchor so a match inside the block can find both edges:
#0004 14:22:35.512 +2.4s llm.response triage stop=end_turn · 386 tok · $0.0005
┌──────────────────────────────────
{"decision": "work"}
└─ #0004 end · 86 B ───────────────
A payload over blob_max_bytes keeps its first lines inline and points at the
blob:
#0007 14:22:35.600 +0.1s llm.prompt planner 4 msgs · 69340 tok · 270.9 KB -> blobs/9fa2c1e4.txt
┌── first 12 lines ────────────────
[system]
You are the planner...
└─ truncated · full text: blobs/9fa2c1e4.txt ─
Attempts are separated by banners, and an attempt that died without run.end
is marked:
---- attempt 1 ended at 14:23:14 without run.end (process exited) ----
==== attempt 2 · started 2026-08-30 14:31:02 · pid 48211 · resume_seq 16 ====
trace.jsonl¶
One JSON object per line.
| Field | |
|---|---|
run_id |
folder name |
seq |
sequence number, matches the #0016 anchor |
attempt |
which attempt emitted it |
ts |
epoch seconds |
iso |
local time with offset |
type |
event type, see below |
name |
agent name, tool name, logger name, or null |
parent_seq |
enclosing event, or null |
status |
ok, error, failed, a log level, or null |
dur_ms |
duration for closing events, run total on run.end |
payload |
type-specific, see below |
Event types¶
| Type | Payload keys |
|---|---|
run.start |
session_id, plus any metadata passed to start() |
run.resume |
attempt, resume_seq, reason |
run.end |
cost_usd, llm_calls, tool_calls, errors |
agent.start |
fields passed to span() |
agent.end |
cost_usd when set, a rollup rather than a new cost |
llm.prompt |
messages, tokens_in, model, text or text_ref + preview, bytes |
llm.response |
stop_reason, tokens_out, cost_usd, text |
tool.call |
args |
tool.result |
text and anything set on the span handle |
tool.error |
error_type, error |
error |
error_type, error, text for a traceback |
log |
level, message, plus your fields |
Types are dotted on purpose: rg 'tool\.' finds all tool activity and
rg '\.error' finds every failure shape.
analytics.json¶
{
"run_id": "20260830_142233_a1b2",
"session_id": "sess_kaggle_eda_7c",
"status": "failed",
"framework": "langgraph",
"model": "claude-sonnet-5",
"started_at": "2026-08-30T14:22:33+05:45",
"ended_at": "2026-08-30T14:31:35.620+05:45",
"wall_sec": 542.62,
"active_sec": 74.62,
"attempts": 2,
"llm_calls": 3,
"tool_calls": 4,
"tool_calls_by_name": {"web_search": 1, "read_file": 1, "run_python": 2},
"errors": [
{"seq": 12, "type": "PermissionError", "name": "read_file", "message": "..."}
],
"cost": {
"input_tokens": 72993,
"output_tokens": 2482,
"cost_usd": 0.023,
"by_agent": {"triage": {"llm_calls": 1, "cost_usd": 0.0005}}
}
}
wall_sec spans the whole session including the gap between attempts.
active_sec sums only the time attempts were running.
meta.json¶
Framework, python, platform, cwd, argv, git (sha, branch,
dirty), tags, and an attempts list recording n, pid, started_at,
ended_at, last_seq and outcome for each attempt. outcome is crashed
for an attempt that never wrote run.end.
index.jsonl¶
One line per run, rewritten in place when a run finalises.
{"run_id": "...", "session_id": "...", "started_at": "...", "status": "failed",
"active_sec": 74.62, "cost_usd": 0.023, "llm_calls": 3, "tool_calls": 4,
"errors": 4, "framework": "langgraph", "model": "claude-sonnet-5",
"last_error": "RuntimeError: executor gave up after 2 attempts"}
Invariants¶
Anything reading these files can rely on the following.
#seqis zero-padded to four digits and identical acrossoutline.md,agent.logsandtrace.jsonl. It never resets, never repeats, and continues across a resume.- Column 3 of
agent.logsis a delta, not a duration. - A framed block repeats its anchor on the closing line.
status: "error"appears onagent.endas well as on real failures, so count failures bypayload.error_type.dur_msonrun.endis the run total, so excluderun.*when ranking steps.agent.endcarries a cost rollup, so sum leaf costs or readanalytics.json, never both.- Nothing is truncated. A payload not inline is in
blobs/, in full.