A session map is a redacted, chronological replay of one coding-agent session. It records every tool call, file touch, MCP call, skill, subagent and model request, in order, with repo-relative paths. It does not contain the content of any of them.
One map is one JSON object. Its machine-readable definition is schema/agentplay-v1.schema.json (JSON Schema 2020-12). This page explains the meaning of each part.
- Media type:
application/json. It is commonly stored gzipped as*.json.gz. - Schema id:
"schema": "agentplay/v1". Readers must also accept"entelligence.session-map/v1", the format's earlier name. The shape is identical. - Evolution: fields are only ever added inside v1, never removed or repurposed. Readers must ignore fields they do not know. A breaking change would be
agentplay/v2.
- Redaction contract
- Top level
sessiontotalsrepos,filesagentstasksprsevents- Outcomes
- Content hash
- Writing a producer
These rules are hard rules. A producer that breaks one of them is not producing a valid session map.
- No text content. A map never carries prompt text, assistant or reasoning text, tool inputs, tool outputs, file contents, or command arguments.
- Shell commands contribute a verb only. The verb is the program plus a subcommand for multi-verb tools such as
git log,npm run,pytestorgh pr view. Never flags, paths or values. - Subagents contribute only
subagentTypeandmodel. Never their description or prompt. - User turns contribute only shape:
chars(the length), plus counts ofimagesandurls. Theimagescount is the only trace of an image; producers must never embed image data. - Model responses contribute only metadata: token counts, latency, stop reason, whether there was thinking, the number of tool calls requested, and the length of text blocks (
textChars). - Paths. Inside a git repository, a path is repo-relative. Outside any repository, it collapses to a bucket label:
/tmp (scratch),~/Documents,~/Downloads,~/Desktop,~/.claude/<area>,~ (home)oroutside-repo. - Path modes (
session.pathMode):relative(default): repo-relative paths as above.dirs_only: each file name becomes•<sha256(path)[:6]>(the repo-relative path); directories are kept. The hash is short and unkeyed, so a common name in a known directory (README.md,package.json) can be guessed by hashing candidates; usehashedwith a key for stronger protection.hashed: every path segment becomesHMAC-SHA256(key, segment)[:10]. Without a key, this mode falls back todirs_only.
Identifiers that may appear are repository names and remotes, MCP server and tool names, skill names, subagent types, model names, web host names, hook names, and PR or ticket numbers and URLs. These are needed to make a map useful. Producers should treat them as metadata the user can see and remove.
The reference producer (python/src/agentplay/builder.py) is tested against this contract. tests/test_builder.py::test_builder_never_leaks_free_text builds a map from a transcript seeded with secret prompt text, secret arguments and secret subagent descriptions, and asserts that none of it appears in the output.
| field | type | meaning |
|---|---|---|
id |
string | Stable session id, conventionally local:<agent>:<native id> |
agentTool |
string | claude-code, codex, cursor, copilot, copilot-vscode, opencode, kiro, … |
cliVersion |
string | Version of the producer. Excluded from the content hash |
startedAt |
ISO-8601 UTC | Time of the first event |
durationMs |
number | Time from the first event to the last |
models |
string[] | Every model that served a request |
pathMode |
relative | dirs_only | hashed |
See the redaction contract |
truncated |
bool | true when the producer stopped at its event cap (20,000 in the reference producer). events then holds the earliest events, while calls, requests, errors, tokens and costUsd in totals still count the whole session |
outcome |
string | See Outcomes |
role |
root | subagent | helper | unknown | null |
subagent and helper sessions are usually hidden from lists; their work already appears inside their parent |
parentSessionId |
string | null | Set when role is subagent |
These are session-wide counters, so a list or dashboard never has to scan events.
| field | meaning |
|---|---|
calls |
tool calls. Excludes llm, user, compaction, error and delay events |
requests |
model requests |
userTurns |
user messages |
subagents |
subagent lanes |
errors |
events with err: true |
tokens |
all input, cache-write, cache-read and output tokens |
costUsd |
estimated cost at list prices. Advisory only |
filesTouched, filesCreated, filesDeleted |
distinct files |
errorsBySource |
{tool, mcp, llm, hook, orchestration: n} |
errorsByClass |
{exit_code, timeout, rate_limit, …: n} |
retries |
events with retryOf |
delays, delayMs |
delay events and their total duration |
maxContext |
largest main-agent context (in + cw + cr) seen |
compactions |
compaction events |
llmLatencyP50, llmLatencyP95 |
model latency percentiles, in ms |
outputTokens |
output tokens only |
tasks |
number of task segments |
prsCreated, prsMerged, prsClosed, prComments, commits, ticketsCreated, ticketsUpdated, artifacts, skillsCreated, skillsUpdated, modelSwitches |
outputs the session produced (confirmed only) |
promptChars, promptImages, promptUrls |
the shape of what the user sent |
"repos": [{ "id": "r0", "name": "api", "remote": "acme/api" }],
"files": [{ "id": 0, "repo": "r0", "path": "src/orders.ts", "reads": 2, "edits": 2, "hits": 1, "first": 17, "op": "modify" }]files[].firstis theseqof the event that first touched the file.files[].opis the last file operation seen for it:create,modify,delete,rename-fromorrename-to.- Touch levels (used by
targets[].touch):hitmeans the file was named or listed, for example matched by a search.readmeans its contents were read.editmeans it was changed.
"agents": [
{ "id": "main", "parent": null, "kind": "main", "start": 0, "end": 250000,
"calls": 20, "requests": 16, "tokens": 900000, "cost": 1.61, "outcome": "pr_created" },
{ "id": "sa-ae824d54211f", "parent": "main", "kind": "subagent", "subagentType": "Explore",
"model": "claude-haiku-4-5", "launchSeq": 14, "start": 31000, "end": 70000, "…": "same counters" }
]Every event carries agent, which is main or one of these ids. launchSeq points at the subagent event that started the lane. A subagent has no further nesting.
This field is optional. It records how the session divides into units of work:
"tasks": [{ "n": 1, "id": "t1", "name": "Explore the request pipeline", "source": "segments",
"startSeq": 0, "endSeq": 12, "startT": 0, "endT": 70000, "firstTurn": 0, "lastTurn": 1 }]sourceis eithersegmentsorturns.segmentsmeans the tasks came from a task segmenter, which may use a model and produce names.turnsmeans one task per user turn.startSeqandendSeqare inclusive.events[].taskholds the task numbern.- Fallback: a malformed
tasksblock must never fail a reader. Drop it and fall back to one task per user turn.
"prs": [{ "provider": "github", "repo": "acme/api", "number": 142, "url": "https://github.com/acme/api/pull/142", "seq": 51, "state": "open" }]A PR or MR is listed only once its URL is confirmed in a tool result. seq points at the event that created it.
Every event has these fields:
| field | meaning |
|---|---|
seq |
index into events |
t |
ms since the first event |
kind |
see the table below |
agent |
main or a subagent id |
err |
the event failed |
task |
task number (when tasks is present) |
model |
the model driving the agent at this point |
cat |
tool category: file, shell, vcs, test, cloud, http, fs, package, build, mcp, subagent, skill, web or tool |
| kind | extra fields |
|---|---|
read, search, edit, exec, verify |
tool, verb (shell only), targets[], outBytes, dur, vcs? |
mcp |
server, tool, outBytes, dur |
skill |
skill |
subagent |
subagentType, model?, child (agent id) |
web |
host (a hostname, or search) |
tool |
tool: any other tool (ToolSearch, AskUserQuestion, TodoWrite, …) |
llm |
model, in (fresh input), cw (cache write), cr (cache read), out, cost?, latency, stop, thinking, toolCalls, textChars |
user |
chars, images, urls |
error |
source (the builder writes llm, hook or orchestration; the schema also accepts tool, mcp and null, which a builder could use for a standalone error event, but a failed tool or MCP call is instead flagged on its own tool/mcp event and counted under that source in totals.errorsBySource), errClass, hook?, hookEvent?, srcUuid?, ref?, retry? |
delay |
cause (tool, subagent, llm or stall), ms, ref? (the causing event) |
compaction |
trigger (auto or manual), preTokens, postTokens, ms |
commit |
repo, sha, files, ins, del |
pr_merge, pr_close, pr_comment |
repo, number, url? |
ticket |
tracker, action (create or update), id |
artifact |
url, title?, version? |
skill_write |
name, action (create or update) |
model_switch |
from, to |
verify is a shell command recognised as a test, build, lint or type check (for example pytest, pnpm test, tsc, cargo clippy). Output kinds such as commit and pr_merge are emitted only when confirmed in a tool result.
"targets": [{ "file": 0, "touch": "edit", "op": "modify" }, { "scope": "outside", "label": "/tmp (scratch)", "touch": "read" }]- Failures. A failed tool, MCP or subagent call can carry the following:
errClass: one ofexit_code,timeout,not_found,validation,needs_approval,permission_denied,blocked,browser,patch_failed,stale_file,model_unavailable,rate_limit,api_error,interrupted,no_result,subagent_failed,subagent_killedorother;exitCode;retryOf: theseqof the failed attempt it repeats (same agent, tool, verb or server, and first target, within the last 8 calls);attempt: 2 or higher.
errorevents record failures that are not tied to one tool call. TheirerrClassis one ofrate_limit,usage_limit,spend_limit,overloaded,server_error,auth,bad_request,api_error,refusal_fallback,hook_blocking,hook_errororinterrupted.- For a hook failure,
hookis the hook's name (such asPostToolUse:Bash) andhookEventthe event it ran on (such asPostToolUse).refis theseqof the tool call the hook ran after, when there is one.srcUuidis the id of the transcript record, an opaque random value that lets a viewer on the machine that recorded the session look the hook's message and command up; neither is ever stored.
- For a hook failure,
delayevents are produced at these thresholds in the reference producer: a tool call of 30 s or more, model latency of 60 s or more, or a stall where the main agent was idle for 120 s or more while not waiting on the user.vcsis present on any VCS operation:{provider: git|github|gitlab|bitbucket, action, repo?, number?}.- For git,
actionis the subcommand. For gh or glab it is<noun>_<verb>. pr_createis set only once the PR URL is confirmed.pr_create_attemptandpr_create_failedcover the rest.
- For git,
session.outcome and agents[].outcome take one of the values below. The first rule that matches wins.
pr_created: a confirmed PR or MR creation in the lane.interrupted: aninterruptederror in the last 12 events.blocked: ablockedorpermission_deniederror in the last 12 events.verified: the lastverifyevent succeeded, with no edit after it.unresolved_errors: 2 or more errors in the last 12 events.incomplete: the lane ends on an edit or exec with no assistant text after it.completed: none of the above.
contentHash is the SHA-256 of the canonical JSON of the map, with session.cliVersion removed. Canonical JSON means sorted keys, compact separators and UTF-8. Two producers that agree on a map produce the same hash, so independent builders can be checked against each other.
To make your agent's sessions work with the viewer:
- Emit one map per session following this page. Start with the required fields:
schema,session.id,session.startedAt,totalsandevents[].tandkind. Everything else is additive. - Validate your maps against the JSON Schema.
- Keep to the redaction contract. If your agent's own logs contain text, drop it at the producer. Readers assume maps are safe to show.
- Open a map by dragging the file onto the viewer, or add it to your local store with
agentplay import map.json.gzso it shows in your session list. For a new agent, the better path is a parser in this repo; see CONTRIBUTING.md.
{ "schema": "agentplay/v1", "session": { … }, "totals": { … }, "repos": [ … ], "files": [ … ], "agents": [ … ], "tasks": [ … ], // optional "prs": [ … ], "events": [ … ] // sorted by t; events[i].seq === i }