Event model
Every adapter turns its source’s on-disk format into the same normalized events. The types live in the core package, @ayagmar/agent-sessions (packages/core/src/domain/usage-event.ts and session-event.ts); the CLI and library users see the same shapes.
Four kinds
Section titled “Four kinds”SessionEvent is a union on kind (EVENT_KINDS, in this order):
usage: one usage record as the source logged it. An event withsingleRequest: trueis one model request; other records can sum several requests (see per-request usage). RequiresinputTokens,outputTokens,reasoningTokens,cacheReadTokens,cacheWriteTokens,cacheWrite1hTokens, andtotalTokens.message: a user, assistant, or system turn. Requiresrole(user,assistant, orsystem) andtextLength.tool_call: one tool call, yielded when its result is seen. Requiresnameandstatus(ok,error, orunknown).session_meta: facts about the session. Every field is optional:cwd,title,gitBranch,agentVersion,parentSessionId,model,rawProvider.
Every kind also carries source, sessionId, and timestamp (ISO 8601, UTC). raw holds the source record only when the read asks for it.
Message text is optional: text is set only when the read asks for it, and is kept exactly as logged. textLength is always set. A tool call with no result by the end of the file is still yielded, with status: 'unknown'.
Two ways to read
Section titled “Two ways to read”adapter.read(filePath)yields plainUsageEventobjects: the usage fields withoutkind. This is what the CLI uses for every report.readEvents(adapter, filePath, options)yieldsSessionEventobjects. The claude, codex, and pi adapters implement it and can yield all four kinds; for any other adapter it wrapsread()and yieldskind: 'usage'records.capabilities.kindslists what an adapter can yield; unset means usage only.
A usage record from readEvents can also carry messageId, requestId, and rawProvider (the provider as logged, unset when the adapter inferred it). The library guide shows both reads.
Token semantics
Section titled “Token semantics”The usage fields follow one convention across all sources:
inputTokensis fresh input. It excludes cache reads and cache writes.outputTokensincludes reasoning.reasoningTokensis a breakdown of it, not an extra bucket.cacheWrite1hTokensis the part ofcacheWriteTokenscached for one hour; it is capped atcacheWriteTokens.totalTokensis the total the source logged. When a source logs none,createUsageEventsums input, output, reasoning, cache reads, and cache writes, so an adapter whose output includes reasoning passestotalTokensitself (claude does).costUsdis a cost the source logged, in USD. It is unset when the source logged none; pricing fills in estimates later.
Normalization at the boundary
Section titled “Normalization at the boundary”The constructors (createUsageEvent, createUsageRecord, createMessageEvent, createToolCallEvent, createSessionMetaEvent) are the only way adapters build events, so normalization happens once:
- An empty
sourceorsessionId, or a timestamp that does not parse, throws. An ISO date and time with no offset (2026-01-02T03:04:05) reads as UTC, so the events do not depend on the machine’s time zone. - Token counts become non-negative integers; a missing or non-finite count becomes 0. A usage count above 10¹² throws, and the adapter skips the row and counts it among the skipped rows.
modelis trimmed, lowercased, and capped at 256 characters. Text fields lose control characters.provideris normalized to the billing entity:openai-codexand anyopenai-oropenai/prefix becomeopenai,github-copilotbecomesgithub. When a source logs no provider, adapters infer one from the model name (gpt-,o1/o3/o4→openai,claude→anthropic,gemini→google).createUsageEventalways setsrepoRoot,provider,model, andcostUsd, toundefinedwhen unknown, soObject.hasOwn(event, 'costUsd')can be true with no cost. The other constructors leave undefined keys out, as doescreateUsageRecordfor themessageId,requestId,rawProvider, andrawit adds.JSON.stringifydrops undefined values, so serialized events of every kind hold only the fields that are set.
USAGE_EVENT_NORMALIZATION_VERSION (currently 3) is part of every parser version. Changing normalization changes it, which re-reads every stored file; see Dedup and cursors.
Projects
Section titled “Projects”No event has a project field. Usage events carry repoRoot, the working directory the source logged (for Gemini, looked up in its project map), never resolved to a .git root; session metadata carries cwd. Reports group by project on repoRoot without trailing separators; usage with no repoRoot is (unattributed).
What the CLI adds
Section titled “What the CLI adds”The CLI’s own UsageEvent (src/domain/usage-event.ts) extends the core type with two fields:
costMode:explicitwhen the event has a loggedcostUsd, elseestimated. Pricing reads it to decide which events to estimate.machine: the configured machine an event came from; unset for this machine. See Machine merge.
Related pages
Section titled “Related pages”- Dedup and cursors: the content hash and the stages that remove duplicate events.
- Parse pipeline: how files become events.
- Library: reading events from your own code.