Docs Trace format

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

AgentEvent
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

KindPayload
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.start and tool.end pair by session and payload.toolUseId; subagent.start and subagent.end by session and payload.subagentId. llm.call has no pair.
  • Usage. An event's usage holds 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 in payload.reportedUsage, never in usage, 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.start payload { parentSessionId, subagentId }, or through childSessionId on the parent's subagent events.

Sessions

Ingest builds a session row from its events and keeps aggregates on it.

Session
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:

javascript
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.

javascript
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.ts writes 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. payload and map fields such as stats.models stay open.
  • Payloads. The per-kind payload conventions above are in the payload description, not enforced.
  • Tested. Every event the adapters produce from their recorded fixtures, every OTLP fixture, and the sessions built from them validate against it.