On this page
The Schema
The normalized event schema and the guarantees behind it.
A normalized session is an ordered list of events. Exactly one payload
field is set per event, matching its kind:
| Kind | Payload | ACP analog |
|---|---|---|
session_meta | Session identity: harness, version, session ID, cwd, subagent markers | none (extension) |
user_message | User-role content, with an origin marker: human or harness | user_message_chunk |
assistant_message | One complete assistant API message | agent_message_chunk |
thinking | Extended-thinking block | agent_thought_chunk |
tool_call | Tool invocation with full input | tool_call |
tool_result | Tool outcome, correlated by tool call ID | tool_call_update |
system | Harness/API activity: injected context (its model-visible text in text), diagnostics, errors | none (extension) |
unknown | Unclassifiable record, preserved verbatim (permissive mode only) | none (extension) |
Points worth knowing
- The event vocabulary is shared. Event kinds and
tool classifications follow the
Agent Client Protocol’s
session-update vocabulary where an analog exists. Token usage fields
follow OTel GenAI semantic conventions. Every field in the schema is
tagged with its provenance (
acp,otel, orext), enforced by a test, so the mapping cannot rot. - Token fields carry provider semantics; totals carry a derived
comparable field. The field names are shared, and the meanings
follow the provider: OpenAI-style usage (codex) reports
input_tokensas the total prompt with the cache fields as subsets of it (the nativecache_write_input_tokensmaps tocache_creation_input_tokens), while Anthropic-style usage (claude-code) reports it as only the uncached remainder alongside disjoint cache read and creation fields. Adapters preserve what the harness recorded, soinput_tokensis never directly comparable across harnesses (and Copilot CLI records no per-message usage at all, so itstotalsare omitted; see Comparing Token Counts). Session totals therefore also carrytotal_prompt_tokens, derived from each harness’s documented convention: that is the number to compare. See the worked comparison for a real case where the naiveinput_tokensreading is wrong by four orders of magnitude, and Comparing Token Counts for the full field guide (caches, reasoning tokens, missing usage, and cost caveats). assistant_messageis the accounting anchor. Harnesses may split one API message across many records with usage written as a growing snapshot. The adapter folds them and takes the final snapshot. Exactly oneassistant_messageis emitted per API message, even when all of its content becamethinkingortool_callevents, so token totals are always derivable. Events from the same API message share amessage_id. When a harness interleaves a subagent’s conversation into the parent transcript (copilot), every event of that conversation carries the subagent’sagent_id; it is empty for the main agent.- One session record covers one transcript; a task can span
several. A harness that delegates to subagents writes each
subagent conversation as its own transcript, and each parses to its
own session with its own
totals. Where the harness records it (claude-code, codex), a subagent’s meta shares the parent’ssession_idand carriessubagent_idwithis_subagent: true, so grouping a task is asession_idmatch. Copilot CLI is the exception: its subagents write into the parent transcript, so one session record already covers the task, andagent_idis what separates the parent’s events from each subagent’s. Task-scope numbers come fromstats --include-subagents(oragentminutes.Task), which gathers a session’s transcripts and reports an aggregate plus aby_agentsplit; a single session’sstatscarriesby_agentitself when its events span more than one agent. See Subagents. tool_callandtool_resultstay separate, in stream order. Ordering is data. Interleaving, parallel tool execution, and retries are visible in the sequence.Session.ToolInteractions()provides the joined view, including unanswered calls and orphaned results.- Results carry what the model saw, plus what the harness knew.
tool_result.contentis the post-pipeline content the model actually received. Harness sidecar data rides along verbatim inenrichment, with retrieval metrics promoted tofetch(URL, raw bytes fetched, status, duration) when present. For summarizing pipelines like Claude Code’s WebFetch, comparingfetch.raw_bytesagainst the content size measures the pipeline’s compression directly. Which field the model saw is a per-tool decision the adapters pin with a test: every long string inenrichmentthat the content does not account for is listed with its reason (the raw file behind a numbered rendering, a UI diff, the call’s own input, output the harness persisted instead of showing). An image the model saw is animagecontent block holding the harness’s own descriptor: the bytes inline for Claude Code, a path beside the transcript for Antigravity, and a reference to a separate asset record for Copilot CLI, so the block is always present and its size is not comparable. is_errormeans the call failed as far as the model could tell, and fetch failures are the exception to compare with care. Every adapter marks a tool that could not run (a denied or absent path, an edit whose target text is missing) and a shell command that exited nonzero as failed, including harnesses that record the nonzero exit as a successful tool run. Fetch failures diverge: Copilot CLI and Antigravity flag a 404, Claude Code does not (the status is infetch.status_codeand the content is prose about the failure), and Codex records nothing that distinguishes a failed fetch from a successful one.stats.tool_errorsinherits these conventions, so a cross-harness error rate needs the fetch calls separated out; see Harness Support.system.textis the record’s model-visible text, when it carries one. Injected context surfaces astext, bare: the system prompt, a CLAUDE.md body, a reminder, a delivered skill body, a listing. The tags of any wrapper the harness delivered it in (Claude Code’s<system-reminder>, Copilot’s<skill-context>) stay indetails, while content the wrapper carries of its own (Copilot’s names the skill’s base directory and lists the files under its directory) is intext;convert --text-form delivered(library:Options.TextForm) puts the wrapped form intextinstead, where the transcript records it.textis also where a diagnostic’s message lands (an abort reason, an API error, a session notice), which the model never saw;subtypetells the two apart. What deliberately stays indetailsonly is enumerated per adapter with a reason (bytes, paths, tool schemas, echoes of text surfaced by another event), and a test holds each adapter to that list over its fixtures and the local corpus.- Every event points back at its source.
provenancecarries the 1-based line range in the native transcript, and optionally the verbatim records (--keep-raw). - The report closes the loop.
reportcounts skipped record types (harness UI bookkeeping with no model-visible content), unknown events, and orphaned tool results. Between events, skips, and errors, every input line is accounted for.
The JSON encoding of Session and Event is the cross-language output
contract; the agentminutes_schema field identifies its revision
(currently 0.2.0).
The Go types in
session
are documentation for it.