Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,34 @@

All notable changes to the Toolpath workspace are documented here.

## `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/<name>/<version>/` (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
bumped at release.

## `path resume` — one-shot resume into a coding agent — 2026-05-09

`path-cli` 0.9.0. New subcommand `path resume <input>` that fetches a
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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/<name>/<version>/{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/<project-id>/[<sha1(worktree)>]/` 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.
Expand Down
19 changes: 17 additions & 2 deletions RFC.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -276,11 +276,26 @@ 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** 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 <https://toolpath.dev/kinds/>. 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

`meta.actors` maps actor strings to full definitions:
Expand Down Expand Up @@ -565,7 +580,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

Expand Down
21 changes: 18 additions & 3 deletions crates/toolpath-convo/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@
//! 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 = PATH_KIND_AGENT_CODING_SESSION`.

use std::collections::HashMap;

use toolpath::v1::{
ActorDefinition, ArtifactChange, Base, Path, PathIdentity, PathMeta, Step, StepIdentity,
StructuralChange,
ActorDefinition, ArtifactChange, Base, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta,
Step, StepIdentity, StructuralChange,
};

use crate::{ConversationView, Role, ToolCategory, ToolInvocation, Turn};
Expand Down Expand Up @@ -398,6 +399,7 @@ pub fn derive_path(view: &ConversationView, config: &DeriveConfig) -> Path {

let mut meta = PathMeta {
title: Some(title),
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: view.provider_id.clone(),
..Default::default()
};
Expand Down Expand Up @@ -686,6 +688,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_AGENT_CODING_SESSION)
);
// ...and survives a JSON round-trip.
let json = serde_json::to_string(&path).unwrap();
assert!(json.contains(r#""kind":"https://toolpath.dev/kinds/agent-coding-session/v1.0.0""#));
}

#[test]
fn test_single_user_turn() {
let mut turn = base_turn("t1", Role::User);
Expand Down
5 changes: 5 additions & 0 deletions crates/toolpath-pi/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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_AGENT_CODING_SESSION)
);
}

#[test]
Expand Down
6 changes: 6 additions & 0 deletions crates/toolpath/schema/toolpath.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@
"type": "string",
"description": "Human-readable title"
},
"kind": {
"type": "string",
"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",
"description": "Source reference (e.g., PR URL)"
Expand Down
43 changes: 43 additions & 0 deletions crates/toolpath/src/jsonl.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ pub struct PathOpenMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
Expand Down Expand Up @@ -148,6 +150,8 @@ pub struct PathMetaPatch {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down Expand Up @@ -546,6 +554,7 @@ fn resolve_head(explicit: Option<String>, steps: &[Step]) -> Result<String, Json

fn path_meta_is_empty(m: &PathMeta) -> bool {
m.title.is_none()
&& m.kind.is_none()
&& m.source.is_none()
&& m.intent.is_none()
&& m.refs.is_empty()
Expand Down Expand Up @@ -670,12 +679,14 @@ fn step_meta_is_empty(m: &StepMeta) -> bool {
fn path_meta_for_open(m: &PathMeta) -> Option<PathOpenMeta> {
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()
Expand Down Expand Up @@ -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_AGENT_CODING_SESSION.to_string()),
..Default::default()
}),
};
let jsonl = p.to_jsonl_string().unwrap();
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));
}

#[test]
fn path_meta_line_can_set_kind() {
let patch = PathMetaPatch {
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("https://toolpath.dev/kinds/agent-coding-session/v1.0.0"));
}

#[test]
fn change_artifact_roundtrip_preserved() {
// Sanity check that we don't mangle ArtifactChange fields through
Expand Down
5 changes: 3 additions & 2 deletions crates/toolpath/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pub mod v1 {
//! Optional annotations for richer context:
//!
//! - [`StepMeta`], [`PathMeta`], [`GraphMeta`] — metadata containers
//! - [`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
Expand Down Expand Up @@ -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_AGENT_CODING_SESSION, Path, PathIdentity, PathMeta, PathOrRef, PathRef, Ref, Signature,
Step, StepIdentity, StepMeta, StructuralChange, VcsSource,
};
}
25 changes: 25 additions & 0 deletions crates/toolpath/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,12 +140,19 @@ pub struct Base {
pub branch: Option<String>,
}

/// [`PathMeta::kind`] URI for a path derived from an AI coding conversation.
/// Spec at <https://toolpath.dev/kinds/agent-coding-session/v1.0.0>.
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)]
pub struct PathMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub title: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub source: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub intent: Option<String>,
Expand Down Expand Up @@ -810,6 +817,24 @@ mod tests {
assert!(json.contains("issues/1"));
}

#[test]
fn test_path_meta_kind_serde() {
let meta = PathMeta {
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
..Default::default()
};
let json = serde_json::to_string(&meta).unwrap();
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("https://toolpath.dev/kinds/agent-coding-session/v1.0.0"));
}

#[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 {
Expand Down
4 changes: 4 additions & 0 deletions site/eleventy.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions site/kinds/agent-coding-session/index.md
Original file line number Diff line number Diff line change
@@ -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)*
Loading
Loading