From c09918075b4e31daa5e0a18b0e58aa85a19a285d Mon Sep 17 00:00:00 2001 From: Ben Barber Date: Tue, 12 May 2026 16:41:49 -0400 Subject: [PATCH 1/3] feat(toolpath): add meta.kind path-kind field; tag conversation paths "convo" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New optional `meta.kind` on `Path` (`PathMeta::kind` + the `PATH_KIND_CONVERSATION` constant), plumbed through the JSONL form. Every conversation -> Path derivation now sets `meta.kind = "convo"` — the shared `toolpath_convo::derive_path` and each conversation provider crate's own. Documented in the RFC ("Document Kind") and the JSON Schema. Additive; no version bumps yet. --- CHANGELOG.md | 20 ++++++++++ CLAUDE.md | 1 + RFC.md | 23 ++++++++++- crates/toolpath-convo/src/derive.rs | 22 +++++++++-- crates/toolpath-pi/src/derive.rs | 5 +++ crates/toolpath/schema/toolpath.schema.json | 5 +++ crates/toolpath/src/jsonl.rs | 43 +++++++++++++++++++++ crates/toolpath/src/lib.rs | 5 ++- crates/toolpath/src/types.rs | 24 ++++++++++++ 9 files changed, 141 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14d5c40..25baea8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,26 @@ All notable changes to the Toolpath workspace are documented here. +## `meta.kind` — new path-kind field; conversation paths tagged `convo` — unreleased + +New optional `meta.kind` field on `Path` (`toolpath::v1::PathMeta::kind`, plus +the `toolpath::v1::PATH_KIND_CONVERSATION` constant) — a hint to renderers and +generic parsers that a path follows a recognizable shape. The only defined +value is `"convo"`: each step is a `conversation.append` change carrying that +turn's `role`, `text`, and so on, and `meta.source` names the producing +harness. Absent means generic — existing documents parse and validate +unchanged, and `kind` is omitted when unset. + +Every conversation → `Path` derivation now sets `meta.kind = "convo"` (the +shared `toolpath_convo::derive_path` and each conversation provider crate's +own). The JSONL form carries `kind` through `PathOpen.meta` and `PathMeta` +patch lines. Documented in the RFC ("Document Kind") and the JSON Schema +(`$defs/pathMeta`). + +Touches `toolpath`, `toolpath-convo`, `toolpath-claude`, `toolpath-gemini`, +`toolpath-codex`, `toolpath-opencode`, and `toolpath-pi`; versions to be +bumped at release. + ## `path resume` — one-shot resume into a coding agent — 2026-05-09 `path-cli` 0.9.0. New subcommand `path resume ` that fetches a diff --git a/CLAUDE.md b/CLAUDE.md index 1556973..e556de4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,6 +221,7 @@ Build the site after changes: `cd site && pnpm run build` (should produce 7 page - `toolpath-gemini` treats main file + sibling sub-agent UUID dir as one conversation. Sub-agent files are folded into `DelegatedWork` with populated `turns` (unlike `toolpath-claude`, whose sub-agent turns live in separate session files and stay empty). See `docs/agents/formats/gemini.md` for the full format reference. - Provider-specific extras convention: `Turn.extra` and `WatcherEvent::Progress.data` use provider-namespaced keys (e.g. `extra["claude"]`, `extra["gemini"]`). `toolpath-claude` populates `Turn.extra["claude"]` from `ConversationEntry.extra`; `toolpath-gemini` populates `Turn.extra["gemini"]` with the full `tokens` struct, per-thought metadata, and tool-call status. This lets trait-only consumers access provider metadata without importing provider types. - Shared derivation: `toolpath-convo` provides a provider-agnostic `ConversationView → Path` mapping via `toolpath_convo::derive_path`. New conversation providers should build on it rather than re-implementing the mapping. +- Path kinds: `toolpath::v1::PathMeta` has an optional `kind` field — a hint to renderers/parsers about the path's shape. Currently the only value is `"convo"` (constant `toolpath::v1::PATH_KIND_CONVERSATION`): a path where each step is a `conversation.append` change carrying that turn's content. Every conversation→`Path` derivation sets it — the shared `derive_path` and each provider crate's own (`toolpath-claude`/`-gemini`/`-codex`/`-opencode`; `-pi` inherits it via the shared mapping). It rides through the JSONL form too (`PathOpen.meta` / `PathMeta` patch lines). Documented in `RFC.md` ("Document Kind") and the JSON Schema's `$defs/pathMeta`. - Pi provider: `toolpath-pi` reads Pi session JSONL from `~/.pi/agent/sessions/`. Sessions use a tree (id/parentId) in a single file, and may link to a parent file via `parentSession` in the header. The tree is preserved as a DAG in the derived `Path`. - Codex provider: `toolpath-codex` reads Codex CLI rollout files from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. Sessions are date-bucketed (not project-keyed). File-change fidelity is excellent — Codex's `patch_apply_end` events carry either the unified diff (for updates) or the full file content (for adds), so the derived `Path` gets a real `raw` perspective on every file artifact. See `docs/agents/formats/codex.md` for the full format reference. - opencode provider: `toolpath-opencode` reads a SQLite database at `~/.local/share/opencode/opencode.db` (opened read-only). Each session's messages and 12 typed part variants (text, reasoning, tool, step-start/-finish, snapshot, patch, file, agent, subtask, retry, compaction) land as one step per message with tool invocations attached. File diffs come from a sibling bare git repo at `snapshot//[]/` via `git2` tree↔tree diffs — opencode respects the user's `.gitignore`, so changes under gitignored paths fall back to tool-input-derived structural changes with no `raw` perspective. Project id is the SHA of the repo's first root commit. See `docs/agents/formats/opencode.md` for the full format reference. diff --git a/RFC.md b/RFC.md index a3d062c..5113f85 100644 --- a/RFC.md +++ b/RFC.md @@ -169,7 +169,7 @@ A **path** collects steps and provides root context: | -------- | --------------------------------------------------- | | `path` | Identity, base context, and head reference | | `steps` | Array of step objects | -| `meta` | Path-level metadata (title, actors, signatures) | +| `meta` | Path-level metadata (title, kind, actors, signatures) | The `path.base` anchors the entire tree to a specific state (repo + ref + commit). Steps within inherit this context. @@ -276,11 +276,30 @@ paths. | Field | Description | | ------------ | -------------------------------------------------- | +| `kind` | Path kind — see [Document Kind](#document-kind) (paths only) | | `intent` | Human-readable description of purpose | | `refs` | Links to issues, docs, reasoning | | `actors` | Actor definitions with identities and keys | | `signatures` | Cryptographic signatures for verification | +#### Document Kind + +`meta.kind` on a **path** classifies it — a hint that the path follows a +recognizable shape worth special-casing. It is always optional; an absent or +unrecognized `kind` should be treated as a generic path. The only value defined +so far is **`convo`** (future revisions may register more). + +A **`convo`** path is an AI coding conversation. Each conversational-turn step +carries one [`ArtifactChange`](#change-perspectives) whose `structural.type` is +`"conversation.append"` — find it by that `type`, not by artifact key. That +change's `structural` object always has `role` (`"user"` / `"assistant"` / +`"system"` / producer-specific) and, when the turn has prose, `text`; it may +also carry `thinking`, `tool_uses`, token counts, `stop_reason`, environment +fields, and a producer-namespaced bag of anything else. `meta.source` names the +producing harness (`claude-code`, `gemini-cli`, `codex`, `opencode`, `pi`); +structure beyond the `conversation.append` change — synthetic steps, file-write +artifacts, tool records — is producer-specific. + #### Actor Definitions `meta.actors` maps actor strings to full definitions: @@ -565,7 +584,7 @@ The path provides: - **base**: Where this tree branches from (repo + ref + commit) - **head**: Current tip of the active path - **steps**: All steps including dead ends (step-001a has no descendants) -- **meta**: Path-level metadata including actors and signatures +- **meta**: Path-level metadata including `kind` (see [Document Kind](#document-kind)), actors, and signatures ### Base Context diff --git a/crates/toolpath-convo/src/derive.rs b/crates/toolpath-convo/src/derive.rs index faabfb5..06b0f3d 100644 --- a/crates/toolpath-convo/src/derive.rs +++ b/crates/toolpath-convo/src/derive.rs @@ -3,13 +3,15 @@ //! Provider-agnostic mapping used by the Pi, Claude, and future conversation //! providers. Takes a [`ConversationView`] and emits a [`Path`] document with //! one step per turn and a `conversation.append` structural change carrying -//! the turn's text, thinking, tool uses, and token usage. +//! the turn's text, thinking, tool uses, and token usage. The emitted path is +//! tagged with `meta.kind = "convo"` (`toolpath::v1::PATH_KIND_CONVERSATION`) +//! so renderers and parsers know it follows the conversation shape. use std::collections::HashMap; use toolpath::v1::{ - ActorDefinition, ArtifactChange, Base, Path, PathIdentity, PathMeta, Step, StepIdentity, - StructuralChange, + ActorDefinition, ArtifactChange, Base, PATH_KIND_CONVERSATION, Path, PathIdentity, PathMeta, + Step, StepIdentity, StructuralChange, }; use crate::{ConversationView, Role, ToolCategory, ToolInvocation, Turn}; @@ -398,6 +400,7 @@ pub fn derive_path(view: &ConversationView, config: &DeriveConfig) -> Path { let mut meta = PathMeta { title: Some(title), + kind: Some(PATH_KIND_CONVERSATION.to_string()), source: view.provider_id.clone(), ..Default::default() }; @@ -686,6 +689,19 @@ mod tests { assert_eq!(path.path.head, ""); } + #[test] + fn test_meta_kind_is_convo() { + let view = view_with(vec![base_turn("t1", Role::User)]); + let path = derive_path(&view, &DeriveConfig::default()); + assert_eq!( + path.meta.as_ref().unwrap().kind.as_deref(), + Some(PATH_KIND_CONVERSATION) + ); + // ...and survives a JSON round-trip. + let json = serde_json::to_string(&path).unwrap(); + assert!(json.contains(r#""kind":"convo""#)); + } + #[test] fn test_single_user_turn() { let mut turn = base_turn("t1", Role::User); diff --git a/crates/toolpath-pi/src/derive.rs b/crates/toolpath-pi/src/derive.rs index d60a8be..db6214c 100644 --- a/crates/toolpath-pi/src/derive.rs +++ b/crates/toolpath-pi/src/derive.rs @@ -106,6 +106,11 @@ mod tests { "got: {}", path.path.id ); + // The shared derivation tags conversation paths with `meta.kind`. + assert_eq!( + path.meta.as_ref().unwrap().kind.as_deref(), + Some(toolpath::v1::PATH_KIND_CONVERSATION) + ); } #[test] diff --git a/crates/toolpath/schema/toolpath.schema.json b/crates/toolpath/schema/toolpath.schema.json index aab7962..791f94a 100644 --- a/crates/toolpath/schema/toolpath.schema.json +++ b/crates/toolpath/schema/toolpath.schema.json @@ -336,6 +336,11 @@ "type": "string", "description": "Human-readable title" }, + "kind": { + "type": "string", + "description": "The kind of this path — a hint to renderers and parsers that it follows a particular shape and may carry kind-specific conventions. Currently the only defined value is \"convo\": a path derived from an agent conversation, where each step is a `conversation.append` change carrying that turn's content. Absent means generic — no kind-specific structure is implied.", + "examples": ["convo"] + }, "source": { "type": "string", "description": "Source reference (e.g., PR URL)" diff --git a/crates/toolpath/src/jsonl.rs b/crates/toolpath/src/jsonl.rs index 69f5898..266d032 100644 --- a/crates/toolpath/src/jsonl.rs +++ b/crates/toolpath/src/jsonl.rs @@ -101,6 +101,8 @@ pub struct PathOpenMeta { #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub intent: Option, @@ -148,6 +150,8 @@ pub struct PathMetaPatch { #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub intent: Option, @@ -347,6 +351,7 @@ impl Path { let mut meta = PathMeta::default(); if let Some(m) = po.meta { meta.title = m.title; + meta.kind = m.kind; meta.source = m.source; meta.intent = m.intent; meta.refs = m.refs; @@ -504,6 +509,9 @@ fn apply_meta_patch(path_meta: &mut PathMeta, patch: PathMetaPatch) { if let Some(v) = patch.title { path_meta.title = Some(v); } + if let Some(v) = patch.kind { + path_meta.kind = Some(v); + } if let Some(v) = patch.source { path_meta.source = Some(v); } @@ -546,6 +554,7 @@ fn resolve_head(explicit: Option, steps: &[Step]) -> Result bool { m.title.is_none() + && m.kind.is_none() && m.source.is_none() && m.intent.is_none() && m.refs.is_empty() @@ -670,12 +679,14 @@ fn step_meta_is_empty(m: &StepMeta) -> bool { fn path_meta_for_open(m: &PathMeta) -> Option { let open = PathOpenMeta { title: m.title.clone(), + kind: m.kind.clone(), source: m.source.clone(), intent: m.intent.clone(), refs: m.refs.clone(), extra: m.extra.clone(), }; if open.title.is_none() + && open.kind.is_none() && open.source.is_none() && open.intent.is_none() && open.refs.is_empty() @@ -1222,6 +1233,38 @@ mod tests { assert_eq!(canonical_json(&p), canonical_json(&back)); } + #[test] + fn roundtrip_kind_in_path_meta() { + let p = Path { + path: PathIdentity { + id: "p".into(), + base: None, + head: "s1".into(), + graph_ref: None, + }, + steps: vec![make_step("s1", None)], + meta: Some(PathMeta { + kind: Some(crate::v1::PATH_KIND_CONVERSATION.to_string()), + ..Default::default() + }), + }; + let jsonl = p.to_jsonl_string().unwrap(); + assert!(jsonl.contains(r#""kind":"convo""#)); + let back = Path::from_jsonl_str(&jsonl).unwrap(); + assert_eq!(canonical_json(&p), canonical_json(&back)); + } + + #[test] + fn path_meta_line_can_set_kind() { + let patch = PathMetaPatch { + kind: Some("convo".into()), + ..Default::default() + }; + let mut meta = PathMeta::default(); + apply_meta_patch(&mut meta, patch); + assert_eq!(meta.kind.as_deref(), Some("convo")); + } + #[test] fn change_artifact_roundtrip_preserved() { // Sanity check that we don't mangle ArtifactChange fields through diff --git a/crates/toolpath/src/lib.rs b/crates/toolpath/src/lib.rs index 19fe93b..2afb7f5 100644 --- a/crates/toolpath/src/lib.rs +++ b/crates/toolpath/src/lib.rs @@ -50,6 +50,7 @@ pub mod v1 { //! Optional annotations for richer context: //! //! - [`StepMeta`], [`PathMeta`], [`GraphMeta`] — metadata containers + //! - [`PATH_KIND_CONVERSATION`] — value for [`PathMeta::kind`] on conversation-derived paths //! - [`ActorDefinition`] — full actor details (name, provider, keys) //! - [`Identity`] — external identity reference //! - [`Key`] — cryptographic key reference @@ -146,7 +147,7 @@ pub mod v1 { pub use crate::types::{ ActorDefinition, ArtifactChange, Base, Graph, GraphIdentity, GraphMeta, Identity, Key, - Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature, Step, StepIdentity, - StepMeta, StructuralChange, VcsSource, + PATH_KIND_CONVERSATION, Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature, + Step, StepIdentity, StepMeta, StructuralChange, VcsSource, }; } diff --git a/crates/toolpath/src/types.rs b/crates/toolpath/src/types.rs index a6551bd..9d514a6 100644 --- a/crates/toolpath/src/types.rs +++ b/crates/toolpath/src/types.rs @@ -140,12 +140,18 @@ pub struct Base { pub branch: Option, } +/// [`PathMeta::kind`] value for a path derived from an AI coding conversation. +/// See the Toolpath RFC's "Document Kind" section. +pub const PATH_KIND_CONVERSATION: &str = "convo"; + /// Path metadata #[derive(Debug, Clone, Default, Serialize, Deserialize)] pub struct PathMeta { #[serde(default, skip_serializing_if = "Option::is_none")] pub title: Option, #[serde(default, skip_serializing_if = "Option::is_none")] + pub kind: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub intent: Option, @@ -810,6 +816,24 @@ mod tests { assert!(json.contains("issues/1")); } + #[test] + fn test_path_meta_kind_serde() { + let meta = PathMeta { + kind: Some(PATH_KIND_CONVERSATION.to_string()), + ..Default::default() + }; + let json = serde_json::to_string(&meta).unwrap(); + assert!(json.contains(r#""kind":"convo""#)); + let parsed: PathMeta = serde_json::from_str(&json).unwrap(); + assert_eq!(parsed.kind.as_deref(), Some("convo")); + } + + #[test] + fn test_path_meta_kind_omitted_when_none() { + let json = serde_json::to_string(&PathMeta::default()).unwrap(); + assert!(!json.contains("kind")); + } + #[test] fn test_identity_serialization() { let id = super::Identity { From 4b488b5a32a87ab273c4ee47a9818167b819377f Mon Sep 17 00:00:00 2001 From: Ben Barber Date: Wed, 13 May 2026 13:31:58 -0400 Subject: [PATCH 2/3] rename `convo` kind value to `agent-coding-session` Renames the kind value `"convo"` to `"agent-coding-session"` and the constant `PATH_KIND_CONVERSATION` to `PATH_KIND_AGENT_CODING_SESSION`. Updates schema, RFC, CHANGELOG, CLAUDE.md, and all derive crates + tests. --- CHANGELOG.md | 8 ++++---- CLAUDE.md | 2 +- RFC.md | 4 ++-- crates/toolpath-convo/src/derive.rs | 10 +++++----- crates/toolpath-pi/src/derive.rs | 2 +- crates/toolpath/schema/toolpath.schema.json | 2 +- crates/toolpath/src/jsonl.rs | 8 ++++---- crates/toolpath/src/lib.rs | 4 ++-- crates/toolpath/src/types.rs | 8 ++++---- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25baea8..deb634a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,17 +2,17 @@ All notable changes to the Toolpath workspace are documented here. -## `meta.kind` — new path-kind field; conversation paths tagged `convo` — unreleased +## `meta.kind` — new path-kind field; conversation paths tagged `agent-coding-session` — unreleased New optional `meta.kind` field on `Path` (`toolpath::v1::PathMeta::kind`, plus -the `toolpath::v1::PATH_KIND_CONVERSATION` constant) — a hint to renderers and +the `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION` constant) — a hint to renderers and generic parsers that a path follows a recognizable shape. The only defined -value is `"convo"`: each step is a `conversation.append` change carrying that +value is `"agent-coding-session"`: each step is a `conversation.append` change carrying that turn's `role`, `text`, and so on, and `meta.source` names the producing harness. Absent means generic — existing documents parse and validate unchanged, and `kind` is omitted when unset. -Every conversation → `Path` derivation now sets `meta.kind = "convo"` (the +Every conversation → `Path` derivation now sets `meta.kind = "agent-coding-session"` (the shared `toolpath_convo::derive_path` and each conversation provider crate's own). The JSONL form carries `kind` through `PathOpen.meta` and `PathMeta` patch lines. Documented in the RFC ("Document Kind") and the JSON Schema diff --git a/CLAUDE.md b/CLAUDE.md index e556de4..34686ab 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,7 +221,7 @@ Build the site after changes: `cd site && pnpm run build` (should produce 7 page - `toolpath-gemini` treats main file + sibling sub-agent UUID dir as one conversation. Sub-agent files are folded into `DelegatedWork` with populated `turns` (unlike `toolpath-claude`, whose sub-agent turns live in separate session files and stay empty). See `docs/agents/formats/gemini.md` for the full format reference. - Provider-specific extras convention: `Turn.extra` and `WatcherEvent::Progress.data` use provider-namespaced keys (e.g. `extra["claude"]`, `extra["gemini"]`). `toolpath-claude` populates `Turn.extra["claude"]` from `ConversationEntry.extra`; `toolpath-gemini` populates `Turn.extra["gemini"]` with the full `tokens` struct, per-thought metadata, and tool-call status. This lets trait-only consumers access provider metadata without importing provider types. - Shared derivation: `toolpath-convo` provides a provider-agnostic `ConversationView → Path` mapping via `toolpath_convo::derive_path`. New conversation providers should build on it rather than re-implementing the mapping. -- Path kinds: `toolpath::v1::PathMeta` has an optional `kind` field — a hint to renderers/parsers about the path's shape. Currently the only value is `"convo"` (constant `toolpath::v1::PATH_KIND_CONVERSATION`): a path where each step is a `conversation.append` change carrying that turn's content. Every conversation→`Path` derivation sets it — the shared `derive_path` and each provider crate's own (`toolpath-claude`/`-gemini`/`-codex`/`-opencode`; `-pi` inherits it via the shared mapping). It rides through the JSONL form too (`PathOpen.meta` / `PathMeta` patch lines). Documented in `RFC.md` ("Document Kind") and the JSON Schema's `$defs/pathMeta`. +- Path kinds: `toolpath::v1::PathMeta` has an optional `kind` field — a hint to renderers/parsers about the path's shape. Currently the only value is `"agent-coding-session"` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`): a path where each step is a `conversation.append` change carrying that turn's content. Every conversation→`Path` derivation sets it — the shared `derive_path` and each provider crate's own (`toolpath-claude`/`-gemini`/`-codex`/`-opencode`; `-pi` inherits it via the shared mapping). It rides through the JSONL form too (`PathOpen.meta` / `PathMeta` patch lines). Documented in `RFC.md` ("Document Kind") and the JSON Schema's `$defs/pathMeta`. - Pi provider: `toolpath-pi` reads Pi session JSONL from `~/.pi/agent/sessions/`. Sessions use a tree (id/parentId) in a single file, and may link to a parent file via `parentSession` in the header. The tree is preserved as a DAG in the derived `Path`. - Codex provider: `toolpath-codex` reads Codex CLI rollout files from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. Sessions are date-bucketed (not project-keyed). File-change fidelity is excellent — Codex's `patch_apply_end` events carry either the unified diff (for updates) or the full file content (for adds), so the derived `Path` gets a real `raw` perspective on every file artifact. See `docs/agents/formats/codex.md` for the full format reference. - opencode provider: `toolpath-opencode` reads a SQLite database at `~/.local/share/opencode/opencode.db` (opened read-only). Each session's messages and 12 typed part variants (text, reasoning, tool, step-start/-finish, snapshot, patch, file, agent, subtask, retry, compaction) land as one step per message with tool invocations attached. File diffs come from a sibling bare git repo at `snapshot//[]/` via `git2` tree↔tree diffs — opencode respects the user's `.gitignore`, so changes under gitignored paths fall back to tool-input-derived structural changes with no `raw` perspective. Project id is the SHA of the repo's first root commit. See `docs/agents/formats/opencode.md` for the full format reference. diff --git a/RFC.md b/RFC.md index 5113f85..90c3dde 100644 --- a/RFC.md +++ b/RFC.md @@ -287,9 +287,9 @@ paths. `meta.kind` on a **path** classifies it — a hint that the path follows a recognizable shape worth special-casing. It is always optional; an absent or unrecognized `kind` should be treated as a generic path. The only value defined -so far is **`convo`** (future revisions may register more). +so far is **`agent-coding-session`** (future revisions may register more). -A **`convo`** path is an AI coding conversation. Each conversational-turn step +A **`agent-coding-session`** path is an AI coding conversation. Each conversational-turn step carries one [`ArtifactChange`](#change-perspectives) whose `structural.type` is `"conversation.append"` — find it by that `type`, not by artifact key. That change's `structural` object always has `role` (`"user"` / `"assistant"` / diff --git a/crates/toolpath-convo/src/derive.rs b/crates/toolpath-convo/src/derive.rs index 06b0f3d..3805538 100644 --- a/crates/toolpath-convo/src/derive.rs +++ b/crates/toolpath-convo/src/derive.rs @@ -4,13 +4,13 @@ //! providers. Takes a [`ConversationView`] and emits a [`Path`] document with //! one step per turn and a `conversation.append` structural change carrying //! the turn's text, thinking, tool uses, and token usage. The emitted path is -//! tagged with `meta.kind = "convo"` (`toolpath::v1::PATH_KIND_CONVERSATION`) +//! tagged with `meta.kind = "agent-coding-session"` (`toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`) //! so renderers and parsers know it follows the conversation shape. use std::collections::HashMap; use toolpath::v1::{ - ActorDefinition, ArtifactChange, Base, PATH_KIND_CONVERSATION, Path, PathIdentity, PathMeta, + ActorDefinition, ArtifactChange, Base, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta, Step, StepIdentity, StructuralChange, }; @@ -400,7 +400,7 @@ pub fn derive_path(view: &ConversationView, config: &DeriveConfig) -> Path { let mut meta = PathMeta { title: Some(title), - kind: Some(PATH_KIND_CONVERSATION.to_string()), + kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()), source: view.provider_id.clone(), ..Default::default() }; @@ -695,11 +695,11 @@ mod tests { let path = derive_path(&view, &DeriveConfig::default()); assert_eq!( path.meta.as_ref().unwrap().kind.as_deref(), - Some(PATH_KIND_CONVERSATION) + Some(PATH_KIND_AGENT_CODING_SESSION) ); // ...and survives a JSON round-trip. let json = serde_json::to_string(&path).unwrap(); - assert!(json.contains(r#""kind":"convo""#)); + assert!(json.contains(r#""kind":"agent-coding-session""#)); } #[test] diff --git a/crates/toolpath-pi/src/derive.rs b/crates/toolpath-pi/src/derive.rs index db6214c..7b04deb 100644 --- a/crates/toolpath-pi/src/derive.rs +++ b/crates/toolpath-pi/src/derive.rs @@ -109,7 +109,7 @@ mod tests { // The shared derivation tags conversation paths with `meta.kind`. assert_eq!( path.meta.as_ref().unwrap().kind.as_deref(), - Some(toolpath::v1::PATH_KIND_CONVERSATION) + Some(toolpath::v1::PATH_KIND_AGENT_CODING_SESSION) ); } diff --git a/crates/toolpath/schema/toolpath.schema.json b/crates/toolpath/schema/toolpath.schema.json index 791f94a..22db4f2 100644 --- a/crates/toolpath/schema/toolpath.schema.json +++ b/crates/toolpath/schema/toolpath.schema.json @@ -339,7 +339,7 @@ "kind": { "type": "string", "description": "The kind of this path — a hint to renderers and parsers that it follows a particular shape and may carry kind-specific conventions. Currently the only defined value is \"convo\": a path derived from an agent conversation, where each step is a `conversation.append` change carrying that turn's content. Absent means generic — no kind-specific structure is implied.", - "examples": ["convo"] + "examples": ["agent-coding-session"] }, "source": { "type": "string", diff --git a/crates/toolpath/src/jsonl.rs b/crates/toolpath/src/jsonl.rs index 266d032..d9da2e0 100644 --- a/crates/toolpath/src/jsonl.rs +++ b/crates/toolpath/src/jsonl.rs @@ -1244,12 +1244,12 @@ mod tests { }, steps: vec![make_step("s1", None)], meta: Some(PathMeta { - kind: Some(crate::v1::PATH_KIND_CONVERSATION.to_string()), + kind: Some(crate::v1::PATH_KIND_AGENT_CODING_SESSION.to_string()), ..Default::default() }), }; let jsonl = p.to_jsonl_string().unwrap(); - assert!(jsonl.contains(r#""kind":"convo""#)); + assert!(jsonl.contains(r#""kind":"agent-coding-session""#)); let back = Path::from_jsonl_str(&jsonl).unwrap(); assert_eq!(canonical_json(&p), canonical_json(&back)); } @@ -1257,12 +1257,12 @@ mod tests { #[test] fn path_meta_line_can_set_kind() { let patch = PathMetaPatch { - kind: Some("convo".into()), + kind: Some("agent-coding-session".into()), ..Default::default() }; let mut meta = PathMeta::default(); apply_meta_patch(&mut meta, patch); - assert_eq!(meta.kind.as_deref(), Some("convo")); + assert_eq!(meta.kind.as_deref(), Some("agent-coding-session")); } #[test] diff --git a/crates/toolpath/src/lib.rs b/crates/toolpath/src/lib.rs index 2afb7f5..8974fa3 100644 --- a/crates/toolpath/src/lib.rs +++ b/crates/toolpath/src/lib.rs @@ -50,7 +50,7 @@ pub mod v1 { //! Optional annotations for richer context: //! //! - [`StepMeta`], [`PathMeta`], [`GraphMeta`] — metadata containers - //! - [`PATH_KIND_CONVERSATION`] — value for [`PathMeta::kind`] on conversation-derived paths + //! - [`PATH_KIND_AGENT_CODING_SESSION`] — value for [`PathMeta::kind`] on conversation-derived paths //! - [`ActorDefinition`] — full actor details (name, provider, keys) //! - [`Identity`] — external identity reference //! - [`Key`] — cryptographic key reference @@ -147,7 +147,7 @@ pub mod v1 { pub use crate::types::{ ActorDefinition, ArtifactChange, Base, Graph, GraphIdentity, GraphMeta, Identity, Key, - PATH_KIND_CONVERSATION, Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature, + PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature, Step, StepIdentity, StepMeta, StructuralChange, VcsSource, }; } diff --git a/crates/toolpath/src/types.rs b/crates/toolpath/src/types.rs index 9d514a6..a95b7c2 100644 --- a/crates/toolpath/src/types.rs +++ b/crates/toolpath/src/types.rs @@ -142,7 +142,7 @@ pub struct Base { /// [`PathMeta::kind`] value for a path derived from an AI coding conversation. /// See the Toolpath RFC's "Document Kind" section. -pub const PATH_KIND_CONVERSATION: &str = "convo"; +pub const PATH_KIND_AGENT_CODING_SESSION: &str = "agent-coding-session"; /// Path metadata #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -819,13 +819,13 @@ mod tests { #[test] fn test_path_meta_kind_serde() { let meta = PathMeta { - kind: Some(PATH_KIND_CONVERSATION.to_string()), + kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()), ..Default::default() }; let json = serde_json::to_string(&meta).unwrap(); - assert!(json.contains(r#""kind":"convo""#)); + assert!(json.contains(r#""kind":"agent-coding-session""#)); let parsed: PathMeta = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.kind.as_deref(), Some("convo")); + assert_eq!(parsed.kind.as_deref(), Some("agent-coding-session")); } #[test] From 94f7387a5ea04fb22c02487390a2b7cc1b8d6a27 Mon Sep 17 00:00:00 2001 From: Ben Barber Date: Thu, 14 May 2026 10:27:28 -0400 Subject: [PATCH 3/3] host kind specs at /kinds/; make `meta.kind` a versioned URI The `kind` value flips from the short name `"agent-coding-session"` to the full hosted-spec URI `"https://toolpath.dev/kinds/agent-coding-session/v1.0.0"` (semver-versioned, immutable per URI). The constant `PATH_KIND_AGENT_CODING_SESSION` carries the URI. Adds the `site/kinds/` hierarchy: a registry index at `/kinds/`, a per-kind landing page at `/kinds/agent-coding-session/`, and the versioned spec at `/kinds/agent-coding-session/v1.0.0/` with a sibling `schema.json` (additive JSON Schema fragment, served via 11ty passthrough). The kind contract moves out of RFC.md into the spec page; the RFC now just points at the registry. Schema description on `pathMeta.kind` updated to declare URI form and link to the registry. --- CHANGELOG.md | 38 +++++++---- CLAUDE.md | 2 +- RFC.md | 26 +++---- crates/toolpath-convo/src/derive.rs | 5 +- crates/toolpath/schema/toolpath.schema.json | 5 +- crates/toolpath/src/jsonl.rs | 6 +- crates/toolpath/src/types.rs | 11 +-- site/eleventy.config.js | 4 ++ site/kinds/agent-coding-session/index.md | 15 ++++ .../agent-coding-session/v1.0.0/index.md | 68 +++++++++++++++++++ .../agent-coding-session/v1.0.0/schema.json | 19 ++++++ site/kinds/index.md | 17 +++++ 12 files changed, 172 insertions(+), 44 deletions(-) create mode 100644 site/kinds/agent-coding-session/index.md create mode 100644 site/kinds/agent-coding-session/v1.0.0/index.md create mode 100644 site/kinds/agent-coding-session/v1.0.0/schema.json create mode 100644 site/kinds/index.md diff --git a/CHANGELOG.md b/CHANGELOG.md index deb634a..8cca66c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,21 +2,29 @@ All notable changes to the Toolpath workspace are documented here. -## `meta.kind` — new path-kind field; conversation paths tagged `agent-coding-session` — unreleased - -New optional `meta.kind` field on `Path` (`toolpath::v1::PathMeta::kind`, plus -the `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION` constant) — a hint to renderers and -generic parsers that a path follows a recognizable shape. The only defined -value is `"agent-coding-session"`: each step is a `conversation.append` change carrying that -turn's `role`, `text`, and so on, and `meta.source` names the producing -harness. Absent means generic — existing documents parse and validate -unchanged, and `kind` is omitted when unset. - -Every conversation → `Path` derivation now sets `meta.kind = "agent-coding-session"` (the -shared `toolpath_convo::derive_path` and each conversation provider crate's -own). The JSONL form carries `kind` through `PathOpen.meta` and `PathMeta` -patch lines. Documented in the RFC ("Document Kind") and the JSON Schema -(`$defs/pathMeta`). +## `meta.kind` — new path-kind field; hosted kind spec registry — unreleased + +New optional `meta.kind` field on `Path` (`toolpath::v1::PathMeta::kind`, +plus the `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION` constant). `kind` is a +URI naming a *kind specification* — a hosted, immutable, semver-versioned +contract describing the additional shape a path follows on top of the base +format. Absent or unrecognized `kind` ⇒ generic path; existing documents +parse and validate unchanged. + +The first defined kind is `https://toolpath.dev/kinds/agent-coding-session/v1.0.0`, +which marks a path as an AI coding conversation (each step is a +`conversation.append` change carrying that turn's `role`, `text`, and so +on; `meta.source` names the producing harness). Every conversation → `Path` +derivation now sets it — the shared `toolpath_convo::derive_path` and each +conversation provider crate's own. The JSONL form carries `kind` through +`PathOpen.meta` and `PathMeta` patch lines. + +Kind specs are sourced under `site/kinds///` (Markdown spec +plus an additive JSON Schema fragment) and published under +`https://toolpath.dev/kinds/`. A registry index lives at +`https://toolpath.dev/kinds/`. The Toolpath RFC ("Document Kind") and the +JSON Schema (`$defs/pathMeta`) reference the registry rather than carrying +kind-specific contracts inline. Touches `toolpath`, `toolpath-convo`, `toolpath-claude`, `toolpath-gemini`, `toolpath-codex`, `toolpath-opencode`, and `toolpath-pi`; versions to be diff --git a/CLAUDE.md b/CLAUDE.md index 34686ab..6102dd7 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -221,7 +221,7 @@ Build the site after changes: `cd site && pnpm run build` (should produce 7 page - `toolpath-gemini` treats main file + sibling sub-agent UUID dir as one conversation. Sub-agent files are folded into `DelegatedWork` with populated `turns` (unlike `toolpath-claude`, whose sub-agent turns live in separate session files and stay empty). See `docs/agents/formats/gemini.md` for the full format reference. - Provider-specific extras convention: `Turn.extra` and `WatcherEvent::Progress.data` use provider-namespaced keys (e.g. `extra["claude"]`, `extra["gemini"]`). `toolpath-claude` populates `Turn.extra["claude"]` from `ConversationEntry.extra`; `toolpath-gemini` populates `Turn.extra["gemini"]` with the full `tokens` struct, per-thought metadata, and tool-call status. This lets trait-only consumers access provider metadata without importing provider types. - Shared derivation: `toolpath-convo` provides a provider-agnostic `ConversationView → Path` mapping via `toolpath_convo::derive_path`. New conversation providers should build on it rather than re-implementing the mapping. -- Path kinds: `toolpath::v1::PathMeta` has an optional `kind` field — a hint to renderers/parsers about the path's shape. Currently the only value is `"agent-coding-session"` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`): a path where each step is a `conversation.append` change carrying that turn's content. Every conversation→`Path` derivation sets it — the shared `derive_path` and each provider crate's own (`toolpath-claude`/`-gemini`/`-codex`/`-opencode`; `-pi` inherits it via the shared mapping). It rides through the JSONL form too (`PathOpen.meta` / `PathMeta` patch lines). Documented in `RFC.md` ("Document Kind") and the JSON Schema's `$defs/pathMeta`. +- Path kinds: `toolpath::v1::PathMeta.kind` is an optional URI naming a hosted kind spec; URIs are immutable and semver-versioned. The only one defined so far is `https://toolpath.dev/kinds/agent-coding-session/v1.0.0` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`); every conversation → `Path` derivation sets it via the shared `toolpath_convo::derive_path` or each provider crate's own. Carried through the JSONL form via `PathOpen.meta` and `PathMeta` patch lines. Spec sources live in `site/kinds///{index.md,schema.json}` and publish under `https://toolpath.dev/kinds/`; the registry index is `site/kinds/index.md`. RFC: "Document Kind". JSON Schema: `$defs/pathMeta`. - Pi provider: `toolpath-pi` reads Pi session JSONL from `~/.pi/agent/sessions/`. Sessions use a tree (id/parentId) in a single file, and may link to a parent file via `parentSession` in the header. The tree is preserved as a DAG in the derived `Path`. - Codex provider: `toolpath-codex` reads Codex CLI rollout files from `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl`. Sessions are date-bucketed (not project-keyed). File-change fidelity is excellent — Codex's `patch_apply_end` events carry either the unified diff (for updates) or the full file content (for adds), so the derived `Path` gets a real `raw` perspective on every file artifact. See `docs/agents/formats/codex.md` for the full format reference. - opencode provider: `toolpath-opencode` reads a SQLite database at `~/.local/share/opencode/opencode.db` (opened read-only). Each session's messages and 12 typed part variants (text, reasoning, tool, step-start/-finish, snapshot, patch, file, agent, subtask, retry, compaction) land as one step per message with tool invocations attached. File diffs come from a sibling bare git repo at `snapshot//[]/` via `git2` tree↔tree diffs — opencode respects the user's `.gitignore`, so changes under gitignored paths fall back to tool-input-derived structural changes with no `raw` perspective. Project id is the SHA of the repo's first root commit. See `docs/agents/formats/opencode.md` for the full format reference. diff --git a/RFC.md b/RFC.md index 90c3dde..db8ab04 100644 --- a/RFC.md +++ b/RFC.md @@ -284,21 +284,17 @@ paths. #### Document Kind -`meta.kind` on a **path** classifies it — a hint that the path follows a -recognizable shape worth special-casing. It is always optional; an absent or -unrecognized `kind` should be treated as a generic path. The only value defined -so far is **`agent-coding-session`** (future revisions may register more). - -A **`agent-coding-session`** path is an AI coding conversation. Each conversational-turn step -carries one [`ArtifactChange`](#change-perspectives) whose `structural.type` is -`"conversation.append"` — find it by that `type`, not by artifact key. That -change's `structural` object always has `role` (`"user"` / `"assistant"` / -`"system"` / producer-specific) and, when the turn has prose, `text`; it may -also carry `thinking`, `tool_uses`, token counts, `stop_reason`, environment -fields, and a producer-namespaced bag of anything else. `meta.source` names the -producing harness (`claude-code`, `gemini-cli`, `codex`, `opencode`, `pi`); -structure beyond the `conversation.append` change — synthetic steps, file-write -artifacts, tool records — is producer-specific. +`meta.kind` on a **path** is a URI naming a *kind specification* — a contract +describing the additional shape the path follows on top of the base format. +Consumers that recognize the URI may rely on the structure that spec describes; +unrecognized URIs should be treated as a generic path. Kind URIs are +immutable, semver-versioned, and revisions ship at a new version URI. + +Defined kinds are listed at . The only one defined +so far is `https://toolpath.dev/kinds/agent-coding-session/v1.0.0` — a path +recording an AI coding conversation, where each conversational-turn step +carries a `"conversation.append"` structural change with the turn's role, +text, and so on. See the linked spec for the full contract. #### Actor Definitions diff --git a/crates/toolpath-convo/src/derive.rs b/crates/toolpath-convo/src/derive.rs index 3805538..6377670 100644 --- a/crates/toolpath-convo/src/derive.rs +++ b/crates/toolpath-convo/src/derive.rs @@ -4,8 +4,7 @@ //! providers. Takes a [`ConversationView`] and emits a [`Path`] document with //! one step per turn and a `conversation.append` structural change carrying //! the turn's text, thinking, tool uses, and token usage. The emitted path is -//! tagged with `meta.kind = "agent-coding-session"` (`toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`) -//! so renderers and parsers know it follows the conversation shape. +//! tagged with `meta.kind = PATH_KIND_AGENT_CODING_SESSION`. use std::collections::HashMap; @@ -699,7 +698,7 @@ mod tests { ); // ...and survives a JSON round-trip. let json = serde_json::to_string(&path).unwrap(); - assert!(json.contains(r#""kind":"agent-coding-session""#)); + assert!(json.contains(r#""kind":"https://toolpath.dev/kinds/agent-coding-session/v1.0.0""#)); } #[test] diff --git a/crates/toolpath/schema/toolpath.schema.json b/crates/toolpath/schema/toolpath.schema.json index 22db4f2..822c888 100644 --- a/crates/toolpath/schema/toolpath.schema.json +++ b/crates/toolpath/schema/toolpath.schema.json @@ -338,8 +338,9 @@ }, "kind": { "type": "string", - "description": "The kind of this path — a hint to renderers and parsers that it follows a particular shape and may carry kind-specific conventions. Currently the only defined value is \"convo\": a path derived from an agent conversation, where each step is a `conversation.append` change carrying that turn's content. Absent means generic — no kind-specific structure is implied.", - "examples": ["agent-coding-session"] + "format": "uri", + "description": "URI naming a kind specification this path conforms to. Defined kinds are listed at https://toolpath.dev/kinds/. Kind URIs are immutable; revisions ship at a new version URI. Consumers should treat an absent or unrecognized URI as a generic path.", + "examples": ["https://toolpath.dev/kinds/agent-coding-session/v1.0.0"] }, "source": { "type": "string", diff --git a/crates/toolpath/src/jsonl.rs b/crates/toolpath/src/jsonl.rs index d9da2e0..bab1d44 100644 --- a/crates/toolpath/src/jsonl.rs +++ b/crates/toolpath/src/jsonl.rs @@ -1249,7 +1249,7 @@ mod tests { }), }; let jsonl = p.to_jsonl_string().unwrap(); - assert!(jsonl.contains(r#""kind":"agent-coding-session""#)); + assert!(jsonl.contains(r#""kind":"https://toolpath.dev/kinds/agent-coding-session/v1.0.0""#)); let back = Path::from_jsonl_str(&jsonl).unwrap(); assert_eq!(canonical_json(&p), canonical_json(&back)); } @@ -1257,12 +1257,12 @@ mod tests { #[test] fn path_meta_line_can_set_kind() { let patch = PathMetaPatch { - kind: Some("agent-coding-session".into()), + kind: Some("https://toolpath.dev/kinds/agent-coding-session/v1.0.0".into()), ..Default::default() }; let mut meta = PathMeta::default(); apply_meta_patch(&mut meta, patch); - assert_eq!(meta.kind.as_deref(), Some("agent-coding-session")); + assert_eq!(meta.kind.as_deref(), Some("https://toolpath.dev/kinds/agent-coding-session/v1.0.0")); } #[test] diff --git a/crates/toolpath/src/types.rs b/crates/toolpath/src/types.rs index a95b7c2..80b21ab 100644 --- a/crates/toolpath/src/types.rs +++ b/crates/toolpath/src/types.rs @@ -140,9 +140,10 @@ pub struct Base { pub branch: Option, } -/// [`PathMeta::kind`] value for a path derived from an AI coding conversation. -/// See the Toolpath RFC's "Document Kind" section. -pub const PATH_KIND_AGENT_CODING_SESSION: &str = "agent-coding-session"; +/// [`PathMeta::kind`] URI for a path derived from an AI coding conversation. +/// Spec at . +pub const PATH_KIND_AGENT_CODING_SESSION: &str = + "https://toolpath.dev/kinds/agent-coding-session/v1.0.0"; /// Path metadata #[derive(Debug, Clone, Default, Serialize, Deserialize)] @@ -823,9 +824,9 @@ mod tests { ..Default::default() }; let json = serde_json::to_string(&meta).unwrap(); - assert!(json.contains(r#""kind":"agent-coding-session""#)); + assert!(json.contains(r#""kind":"https://toolpath.dev/kinds/agent-coding-session/v1.0.0""#)); let parsed: PathMeta = serde_json::from_str(&json).unwrap(); - assert_eq!(parsed.kind.as_deref(), Some("agent-coding-session")); + assert_eq!(parsed.kind.as_deref(), Some("https://toolpath.dev/kinds/agent-coding-session/v1.0.0")); } #[test] diff --git a/site/eleventy.config.js b/site/eleventy.config.js index a13877e..ccf6f92 100644 --- a/site/eleventy.config.js +++ b/site/eleventy.config.js @@ -34,6 +34,10 @@ export default function (eleventyConfig) { eleventyConfig.addPassthroughCopy("css"); eleventyConfig.addPassthroughCopy("js"); eleventyConfig.addPassthroughCopy("wasm"); + // Kind schema fragments — JSON files under site/kinds/ are served verbatim + // alongside their HTML spec pages so a versioned kind URI can resolve to + // either form. + eleventyConfig.addPassthroughCopy("kinds/**/*.json"); // Self-hosted fonts (latin subset only) — pulled from @fontsource packages // at install time, copied to /fonts/ at build time. Filenames are stable diff --git a/site/kinds/agent-coding-session/index.md b/site/kinds/agent-coding-session/index.md new file mode 100644 index 0000000..a3df8a4 --- /dev/null +++ b/site/kinds/agent-coding-session/index.md @@ -0,0 +1,15 @@ +--- +layout: base.njk +title: "Kind: agent-coding-session" +permalink: /kinds/agent-coding-session/ +--- + +# Kind: `agent-coding-session` + +A Toolpath path that records an AI coding conversation. Each conversational-turn step carries a `"conversation.append"` structural change with the turn's role, text, and so on. + +Documents reference a specific version URI — they do not depend on this landing page. + +## Versions + +- [**v1.0.0**](/kinds/agent-coding-session/v1.0.0/) — `https://toolpath.dev/kinds/agent-coding-session/v1.0.0` *(current)* diff --git a/site/kinds/agent-coding-session/v1.0.0/index.md b/site/kinds/agent-coding-session/v1.0.0/index.md new file mode 100644 index 0000000..1362521 --- /dev/null +++ b/site/kinds/agent-coding-session/v1.0.0/index.md @@ -0,0 +1,68 @@ +--- +layout: base.njk +title: "Kind: agent-coding-session v1.0.0" +permalink: /kinds/agent-coding-session/v1.0.0/ +--- + +# Kind: `agent-coding-session` v1.0.0 + +
+
URI
+
https://toolpath.dev/kinds/agent-coding-session/v1.0.0
+
Schema
+
schema.json
+
Status
+
Stable. This URI is immutable; subsequent revisions ship at a new version URI.
+
+ +A Toolpath path whose `meta.kind` is this URI records an AI coding conversation. It is an ordinary Toolpath path — `head`-ancestry, dead ends, signatures, and `base` all work as in the [base format](/format/) — with the additional structure below. + +## The turn payload + +A step that represents a conversational turn has one entry in its `change` map that is an `ArtifactChange` whose `structural.type` is `"conversation.append"`. **Locate it by that `type`, not by artifact key** — the key is producer-specific (`agent://claude/`, `gemini://`, `codex://`, `opencode://`, or `://`). + +That change's `structural` object always carries: + +| Field | Type | Meaning | +| ------ | ------ | ------------------------------------------------------------------ | +| `type` | string | the literal `"conversation.append"` | +| `role` | string | `"user"`, `"assistant"`, `"system"`, or a producer-specific string | + +and, when the turn has prose, also carries `text` (string; treat a missing `text` as empty). It may additionally carry any of the following — all optional and producer-dependent, so check before using each: + +| Field | Meaning | +| ----- | ------- | +| `thinking` | the model's reasoning text for this turn | +| `tool_uses` | the tools the agent invoked this turn; element shape is producer-specific — a bare tool-name string, or an object like `{ "id", "name", "input", "category", "result"? }` | +| `token_usage`, or `input_tokens` / `output_tokens` / `cache_read_tokens` / `cache_write_tokens` | token accounting for the turn | +| `stop_reason` | why the model stopped (`end_turn`, `tool_use`, …) | +| `environment`, or `cwd` / `git_branch` / `version` / `user_type` | the session environment at this turn | +| `delegations` | sub-agent work spawned from this turn | +| `model` | the model that produced an assistant turn | +| `turn_extra` / `entry_extra` / `` | a producer-namespaced bag of everything else, keyed by the producer's short name (`"claude"`, `"gemini"`, …) | + +## Actors + +`step.actor` follows the usual `type:name` convention: + +| Actor pattern | Turn | +| ------------- | ---- | +| `human:user` | a user message | +| `agent:` | a model reply, named by model when the source records one (e.g. `agent:gpt-5.4`) | +| `agent:` | a model reply when the model is not recorded (e.g. `agent:claude-code`) | +| `system:` or `tool:` | a synthetic entry — session init, system prompt, environment note | +| `agent:/tool:` | a tool execution that the producer broke out as its own step (e.g. `agent:claude-code/tool:Write`) | + +Walk steps in `head`-ancestry order for the linear transcript. + +## Path metadata + +`meta.source` names the producing harness: `claude-code`, `gemini-cli`, `codex`, `opencode`, or `pi`. `meta.actors` defines the actors the steps reference. `meta.extra` may carry a producer-namespaced aggregate (e.g. `meta.extra.codex.files_changed`). + +## What's producer-specific + +Anything not described above. Some producers add synthetic steps (a session-init step, system-message steps) with their own `structural.type` rather than `conversation.append`; some attach tool calls as additional `change` entries on the turn's step (keyed by file path, with a producer-specific `structural.type` such as `file.write`, `codex.update`, or `gemini.write_file`, and often a unified diff in `raw`), while others record a tool execution as a separate child step. Treat anything that is not a `conversation.append` change as an ordinary Toolpath change — `meta.source` tells you whose conventions to expect. + +## Producers + +`agent-coding-session` paths are produced by every conversation provider crate (`toolpath-claude`, `toolpath-gemini`, `toolpath-codex`, `toolpath-opencode`) and by the shared `ConversationView → Path` derivation in `toolpath-convo` (which `toolpath-pi` uses). diff --git a/site/kinds/agent-coding-session/v1.0.0/schema.json b/site/kinds/agent-coding-session/v1.0.0/schema.json new file mode 100644 index 0000000..1e73582 --- /dev/null +++ b/site/kinds/agent-coding-session/v1.0.0/schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://toolpath.dev/kinds/agent-coding-session/v1.0.0/schema.json", + "title": "Toolpath kind: agent-coding-session v1.0.0", + "description": "Additive constraints on a Toolpath `Path` whose `meta.kind` is the agent-coding-session v1.0.0 URI. Apply alongside the base Toolpath schema; the path is valid when both pass. The full contract — required vs. optional fields on `conversation.append` changes, actor patterns, what's producer-specific — is at https://toolpath.dev/kinds/agent-coding-session/v1.0.0/.", + "type": "object", + "properties": { + "meta": { + "type": "object", + "properties": { + "kind": { + "const": "https://toolpath.dev/kinds/agent-coding-session/v1.0.0" + } + }, + "required": ["kind"] + } + }, + "required": ["meta"] +} diff --git a/site/kinds/index.md b/site/kinds/index.md new file mode 100644 index 0000000..a29db04 --- /dev/null +++ b/site/kinds/index.md @@ -0,0 +1,17 @@ +--- +layout: base.njk +title: "Path Kinds" +permalink: /kinds/ +--- + +# Path Kinds + +A Toolpath path's optional `meta.kind` is a URI naming a *kind specification* — a contract describing the additional shape the path follows on top of the base format. Consumers that recognize the URI may rely on the structure that spec describes; unrecognized URIs should be treated as a generic path. + +Kind URIs are immutable: revisions ship at a new version URI, and old URIs keep meaning what they always meant. Versioning follows [semver](https://semver.org/). + +## Defined kinds + +| Kind | Current URI | Spec | +| ---- | ----------- | ---- | +| [`agent-coding-session`](/kinds/agent-coding-session/) | `https://toolpath.dev/kinds/agent-coding-session/v1.0.0` | [v1.0.0](/kinds/agent-coding-session/v1.0.0/) |