{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://toolpath.net/kinds/agent-coding-session/v1.1.0/schema.json",
"title": "Toolpath kind: agent-coding-session v1.1.0",
"description": "Additive constraints on a Toolpath `Path` whose `meta.kind` is the agent-coding-session v1.1.0 URI. Apply alongside the base Toolpath schema; the path is valid when both pass. These constraints are by structural `type`, not by artifact key — a `change` entry is checked only when its `structural.type` matches one this kind defines (`conversation.append`, `file.write`, `conversation.event`). Everything is additive: unmentioned properties are allowed, so producer-specific extras never invalidate a path. New in v1.1.0: `group_id` on the turn payload, and the group-level accounting rule — within a `group_id` group, the last step (document order) carries the group's total `token_usage` verbatim from the source and the other steps carry none; values are per-group amounts (never cumulative session counters); summing `token_usage` over a path's steps therefore yields the session totals. The once-per-group rule is normative prose (JSON Schema cannot express it); producers must enforce it. The human-readable contract is at https://toolpath.net/kinds/agent-coding-session/v1.1.0/.",
"type": "object",
"$defs": {
"tokenUsage": {
"type": "object",
"description": "Token accounting for one source group (a message for Claude, a round for Codex). `token_usage` always means the total for a group, verbatim from the source — never a cumulative session counter, never a per-step share. Within a `group_id` group the total sits on the group's last step (document order) and the other steps carry none; a step without a `group_id` is a group of one, so its value is that group's total. Summing over a path's steps therefore yields the session totals. `input_tokens`/`output_tokens` are always emitted (possibly null); cache counters appear only when the source records them.",
"properties": {
"input_tokens": { "type": ["integer", "null"] },
"output_tokens": { "type": ["integer", "null"] },
"cache_read_tokens": { "type": "integer" },
"cache_write_tokens": { "type": "integer" },
"breakdowns": {
"type": "object",
"description": "Optional decomposition of a top-level class into named sub-classes, keyed by the class being broken down (e.g. \"output\"); each value maps sub-class → tokens. INFORMATIONAL: never summed into the total — the parent class already counts these. Invariant: Σ(inner) ≤ the parent class's value.",
"additionalProperties": { "type": "object", "additionalProperties": { "type": "integer" } }
}
},
"required": ["input_tokens", "output_tokens"]
},
"attributedTokenUsage": {
"type": "object",
"description": "This step's own attributed spend, when the source provides step-aligned data — distinct from `token_usage` (the group total). Optional and orthogonal: it rides its own key so summing `token_usage` over steps is unaffected. Within a `group_id` group, the sum of `attributed_token_usage` over its steps is the group's attributed spend; the unattributed remainder (`group token_usage − Σ attributed`) is computed by consumers, never recorded. Same field shape as `tokenUsage`. A producer populates it only when the source genuinely reports per-step spend — among current producers, Codex does (each step is a separate API call with a reported per-call delta); Claude does not (its per-block usage is a cumulative streaming snapshot, not a per-block cost), so Claude-derived steps carry the group total only.",
"properties": {
"input_tokens": { "type": ["integer", "null"] },
"output_tokens": { "type": ["integer", "null"] },
"cache_read_tokens": { "type": "integer" },
"cache_write_tokens": { "type": "integer" }
}
},
"toolResult": {
"type": "object",
"properties": {
"content": { "type": "string" },
"is_error": { "type": "boolean" }
},
"required": ["content", "is_error"]
},
"toolUse": {
"type": "object",
"description": "One tool invocation. `input` is producer-specific JSON (left unconstrained). `category` is Toolpath's classification, or null when the tool is unrecognized. `result` is present only when the result was available in the same turn.",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"input": true,
"category": {
"type": ["string", "null"],
"enum": [
"file_read",
"file_write",
"file_search",
"shell",
"network",
"delegation",
null
]
},
"result": { "$ref": "#/$defs/toolResult" }
},
"required": ["id", "name", "input", "category"]
},
"environment": {
"type": "object",
"description": "Working environment captured at the turn. All fields optional.",
"properties": {
"working_dir": { "type": "string" },
"vcs_branch": { "type": "string" },
"vcs_revision": { "type": "string" }
}
},
"delegation": {
"type": "object",
"description": "Sub-agent work spawned from a turn. `turns` carries the sub-agent's own turns when the producer inlines them.",
"properties": {
"agent_id": { "type": "string" },
"prompt": { "type": "string" },
"turns": { "type": "array" },
"result": { "type": "string" }
},
"required": ["agent_id", "prompt"]
},
"conversationAppend": {
"type": "object",
"description": "The turn payload: the `structural` object of the one `change` entry whose `type` is `conversation.append`. `role` and `text` are always present (text may be empty); everything else appears only when the turn carries it. `group_id` is the provider's identifier for the source accounting unit this turn was derived from — a message for Claude (`message.id`), a round for Codex (`turn_id`). A grouping key, not a step identifier: steps sharing a `group_id` came from one accounting unit (Claude Code writes one JSONL line per content block; a Codex round emits several turns).",
"properties": {
"type": { "const": "conversation.append" },
"role": { "type": "string" },
"text": { "type": "string" },
"thinking": { "type": "string" },
"group_id": { "type": "string" },
"tool_uses": {
"type": "array",
"items": { "$ref": "#/$defs/toolUse" }
},
"token_usage": { "$ref": "#/$defs/tokenUsage" },
"attributed_token_usage": { "$ref": "#/$defs/attributedTokenUsage" },
"stop_reason": { "type": "string" },
"delegations": {
"type": "array",
"items": { "$ref": "#/$defs/delegation" }
},
"environment": { "$ref": "#/$defs/environment" }
},
"required": ["type", "role", "text"]
},
"fileWrite": {
"type": "object",
"description": "The `structural` object of a sibling `file.write` change keyed by file path. The unified diff (when present) lives on the artifact change's `raw`, not here. `tool_id`/`tool` link the mutation to the `ToolInvocation` that caused it when attributable.",
"properties": {
"type": { "const": "file.write" },
"tool_id": { "type": "string" },
"tool": { "type": "string" },
"operation": { "type": "string" },
"before": { "type": "string" },
"after": { "type": "string" },
"rename_to": { "type": "string" }
},
"required": ["type"]
},
"conversationEvent": {
"type": "object",
"description": "The `structural` object of a `conversation.event` change — a non-turn entry (attachment, preamble line, snapshot, …) preserved for round-trip fidelity. `entry_type` names the source entry kind; the producer's flattened event data rides alongside.",
"properties": {
"type": { "const": "conversation.event" },
"entry_type": { "type": "string" },
"event_source_id": { "type": "string" }
},
"required": ["type", "entry_type"]
},
"artifactChange": {
"type": "object",
"description": "An artifact change, constrained only when its `structural.type` is one this kind defines.",
"allOf": [
{
"if": {
"type": "object",
"properties": {
"structural": {
"type": "object",
"properties": { "type": { "const": "conversation.append" } },
"required": ["type"]
}
},
"required": ["structural"]
},
"then": {
"properties": {
"structural": { "$ref": "#/$defs/conversationAppend" }
}
}
},
{
"if": {
"type": "object",
"properties": {
"structural": {
"type": "object",
"properties": { "type": { "const": "file.write" } },
"required": ["type"]
}
},
"required": ["structural"]
},
"then": {
"properties": {
"structural": { "$ref": "#/$defs/fileWrite" }
}
}
},
{
"if": {
"type": "object",
"properties": {
"structural": {
"type": "object",
"properties": { "type": { "const": "conversation.event" } },
"required": ["type"]
}
},
"required": ["structural"]
},
"then": {
"properties": {
"structural": { "$ref": "#/$defs/conversationEvent" }
}
}
}
]
}
},
"properties": {
"meta": {
"type": "object",
"description": "Path metadata. `kind` pins this spec; `source` names the producing harness; `producer`/`files_changed`/`vcs_remote` are flattened session-level fields (PathMeta carries `extra` via serde flatten, so they sit directly under `meta`, not under `meta.extra`).",
"properties": {
"kind": {
"const": "https://toolpath.net/kinds/agent-coding-session/v1.1.0"
},
"source": { "type": "string" },
"files_changed": {
"type": "array",
"items": { "type": "string" }
},
"vcs_remote": { "type": "string" },
"producer": {
"type": "object",
"properties": {
"name": { "type": "string" },
"version": { "type": "string" }
},
"required": ["name"]
}
},
"required": ["kind"]
},
"steps": {
"type": "array",
"items": {
"type": "object",
"properties": {
"change": {
"type": "object",
"additionalProperties": { "$ref": "#/$defs/artifactChange" }
}
}
}
}
},
"required": ["meta"]
}