Trace format
The coding.sh/trace v0 event format every adapter and the OTLP receiver normalize into.
Every adapter and the OTLP receiver normalize into one format, coding.sh/trace version 0. Its names follow
OpenTelemetry's GenAI conventions where they exist, and it exports to OTLP through the otlp sink.
v0 is a draft. A breaking change will bump the version. Raw source payloads are kept as blobs, so events can always be normalized again from source.
Events
interface AgentEvent {
id: string; // deterministic sha256: re-ingesting the same source is a no-op
v: 0;
sessionId: string;
turnId?: string;
parentId?: string; // parent event (subagent or nested tool)
ts: number; // ms since epoch; for spans, the start time
durationMs?: number; // wall time of the tool call, model call or turn
traceId?: string; // OpenTelemetry ids, when the event came from a span
spanId?: string;
parentSpanId?: string;
subagentId?: string; // the subagent this event belongs to
source?: string; // "hook" | "transcript" | "otel-span" | "otel-log" | "wrapper" | …
agent: string; // "claude-code", "codex", … or any kebab-case id
agentVersion?: string;
model?: string;
kind: EventKind;
cwd?: string;
repo?: { root: string; head: string; snapshot?: string; partial?: boolean; branch?: string };
payload: Record<string, unknown>; // kind-specific, under 4 KB
blobs?: string[]; // sha256 refs: full output, diffs, raw payload ("raw" is blobs[0] when kept)
usage?: { inputTokens?: number; outputTokens?: number; cacheReadTokens?: number; cacheWriteTokens?: number; costUsd?: number };
runId?: string; // set when the event came from an eval run (CODING_SH_RUN_ID)
} Kinds
| Kind | Payload |
|---|---|
session.start | A session began. A child session's payload names its parentSessionId and subagentId. |
session.end | { reason? } |
user.message | { text } |
assistant.message | { text } |
llm.call | One model API call: { provider?, requestModel?, responseModel?, ttftMs?, stopReason?, tokensPerSec?, ok, error? }; durationMs on the event. |
tool.start | { tool, toolUseId, input, server? } (server = MCP server name) |
tool.end | { tool, toolUseId, ok, durationMs?, output? } (output truncated; full output in a blob) |
context.compact | { trigger: auto | manual, tokensBefore? } |
turn.end | The agent finished responding to a prompt. |
subagent.start | { subagentId, type?, description?, prompt? } |
subagent.end | { subagentId, ok?, result?, childSessionId? } |
file.change | { path, op: add | modify | delete, additions?, deletions? } |
test.result | { runner?, passed, failed, skipped? } |
notification | An agent notification. |
span | { name, attributes }: an OpenTelemetry span with no better mapping. |
error | An error, e.g. a span's exception event. |
Rules
- Pairing.
tool.startandtool.endpair by session andpayload.toolUseId;subagent.startandsubagent.endby session andpayload.subagentId.llm.callhas no pair. - Usage. An event's
usageholds the tokens and cost of that event only, so summing a session's events gives its total. A running or final total an agent reports goes inpayload.reportedUsage, never inusage, to avoid double counting. - Size. Payloads stay under 4 KB. Anything larger moves to a blob, referenced from
blobs. - Subagents. A child session links to its parent through its
session.startpayload{ parentSessionId, subagentId }, or throughchildSessionIdon the parent's subagent events.
Sessions
Ingest builds a session row from its events and keeps aggregates on it.
interface Session {
id: string;
agent: string;
model?: string;
cwd?: string;
repo?: RepoState;
title?: string; // first prompt, truncated to 280 characters
startedAt: number;
endedAt?: number;
stats: {
events: number; turns: number; toolCalls: number; toolErrors: number; filesChanged: number;
testsPassed?: number; testsFailed?: number;
usage?: Usage;
latency?: LatencyStats; // wallMs, llmCalls, llmMs, llmP50Ms, llmP95Ms, ttftP50Ms, toolMs, toolP95Ms, subagents
};
flags: string[]; // detected failure patterns
parent?: { sessionId: string; eventId?: string; subagentId?: string; type?: string };
traceIds?: string[];
runId?: string;
} Agent ids
Known ids: claude-code, codex, opencode, cursor, gemini-cli, antigravity, copilot-cli, amp, pi, droid, goose, aider, t3code, wrapper, plus antigravity. A third-party adapter may use any other kebab-case id; OTLP sessions use the kebab-cased service.name.
From code
The CLI package exports the constants under coding.sh/schema:
import { SCHEMA_ID, SCHEMA_VERSION, EVENT_KINDS, KNOWN_AGENTS, isEventKind } from "coding.sh/schema";
TypeScript types ship with it (dist/types/schema/index.d.ts, from packages/coding.sh/src/schema/index.ts)
(built).
JSON Schema
A JSON Schema (draft 2020-12) for AgentEvent and Session ships as coding.sh/schema.json (built). It is served at coding.sh/schema.json, its $id, and lives in the repository as
src/schema/trace.schema.json.
The root accepts either type; validate one with #/$defs/AgentEvent or #/$defs/Session.
import schema from "coding.sh/schema.json" with { type: "json" };
import Ajv2020 from "ajv/dist/2020.js";
const ajv = new Ajv2020();
ajv.addSchema(schema);
const validEvent = ajv.getSchema("https://coding.sh/schema.json#/$defs/AgentEvent");
if (!validEvent(event)) console.error(validEvent.errors); - Generated.
scripts/json-schema.tswrites it from the TypeScript types, with their doc comments as descriptions. A test fails when it is stale. - Closed objects. A field the types don't name fails validation.
payloadand map fields such asstats.modelsstay open. - Payloads. The per-kind payload conventions above are in the
payloaddescription, not enforced. - Tested. Every event the adapters produce from their recorded fixtures, every OTLP fixture, and the sessions built from them validate against it.