---
title: "Trace format · coding.sh docs"
description: "The coding.sh/trace v0 event format every adapter and the OTLP receiver normalize into."
canonical: https://coding.sh/docs/trace-format
human_url: https://coding.sh/docs/trace-format
markdown_url: https://coding.sh/docs/trace-format.md
agents_url: https://coding.sh/agents.md
llms_url: https://coding.sh/llms.txt
updated: 2026-11-11
---

# 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](https://coding.sh/docs/storage#sinks).

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

| 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.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](https://coding.sh/schema.json), its `$id`, and lives in the repository as [`src/schema/trace.schema.json`](https://github.com/jackulau/coding.sh/blob/main/packages/coding.sh/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.

To set coding.sh up for a user, follow the agent workflow at [/agents.md](https://coding.sh/agents.md).
