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
20 changes: 20 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,26 @@

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`).

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` 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/<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
23 changes: 21 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,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 **`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.

#### Actor Definitions

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

Expand Down
20 changes: 18 additions & 2 deletions crates/toolpath-claude/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use std::collections::HashMap;
use std::path::Path as FsPath;
use std::process::Command;
use toolpath::v1::{
ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
StepIdentity, StructuralChange,
ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::file_write_diff;

Expand Down Expand Up @@ -611,6 +611,7 @@ pub fn derive_path(conversation: &Conversation, config: &DeriveConfig) -> Path {
steps,
meta: Some(PathMeta {
title: Some(format!("Claude session: {}", session_short)),
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("claude-code".to_string()),
actors: if actors.is_empty() {
None
Expand Down Expand Up @@ -768,6 +769,21 @@ mod tests {
assert!(path.steps[1].step.actor.starts_with("agent:"));
}

#[test]
fn test_derive_path_meta_kind_is_convo() {
let convo = make_conversation(vec![make_entry(
"uuid-1111-aaaa",
MessageRole::User,
"Hello",
"2024-01-01T00:00:00Z",
)]);
let path = derive_path(&convo, &DeriveConfig::default());
assert_eq!(
path.meta.as_ref().unwrap().kind.as_deref(),
Some(PATH_KIND_AGENT_CODING_SESSION)
);
}

#[test]
fn test_derive_path_step_parents() {
let entries = vec![
Expand Down
16 changes: 14 additions & 2 deletions crates/toolpath-codex/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ use crate::types::{PatchChange, Session};
use serde_json::{Map, Value, json};
use std::collections::HashMap;
use toolpath::v1::{
ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
StepIdentity, StructuralChange,
ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::{ConversationView, Role, Turn};

Expand Down Expand Up @@ -172,6 +172,7 @@ fn derive_path_from_view(
steps,
meta: Some(PathMeta {
title: Some(format!("Codex session: {}", session_short)),
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("codex".to_string()),
actors: if actors.is_empty() {
None
Expand Down Expand Up @@ -584,6 +585,17 @@ mod tests {
assert!(actors.contains_key("agent:gpt-5.4"));
}

#[test]
fn derive_path_meta_kind_is_convo() {
let (_t, mgr, id) = fixture_session(&minimal_body());
let session = mgr.read_session(&id).unwrap();
let path = derive_path(&session, &DeriveConfig::default());
assert_eq!(
path.meta.as_ref().unwrap().kind.as_deref(),
Some(PATH_KIND_AGENT_CODING_SESSION)
);
}

#[test]
fn derive_path_preserves_conversation_artifact() {
let (_t, mgr, id) = fixture_session(&minimal_body());
Expand Down
22 changes: 19 additions & 3 deletions crates/toolpath-convo/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "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, 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 @@ -243,6 +245,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 @@ -519,6 +522,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":"agent-coding-session""#));
}

#[test]
fn test_single_user_turn() {
let mut turn = base_turn("t1", Role::User);
Expand Down
15 changes: 13 additions & 2 deletions crates/toolpath-gemini/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ use crate::types::{ChatFile, Conversation, GeminiMessage, GeminiRole, ToolCall};
use serde_json::json;
use std::collections::HashMap;
use toolpath::v1::{
ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
StepIdentity, StructuralChange,
ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::ToolCategory;

Expand Down Expand Up @@ -126,6 +126,7 @@ pub fn derive_path(conversation: &Conversation, config: &DeriveConfig) -> Path {
session_short
}
)),
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("gemini-cli".to_string()),
actors: if actors.is_empty() {
None
Expand Down Expand Up @@ -518,6 +519,16 @@ mod tests {
assert_eq!(path.path.head, path.steps.last().unwrap().step.id);
}

#[test]
fn test_derive_path_meta_kind_is_convo() {
let convo = main_only_convo();
let path = derive_path(&convo, &DeriveConfig::default());
assert_eq!(
path.meta.as_ref().unwrap().kind.as_deref(),
Some(PATH_KIND_AGENT_CODING_SESSION)
);
}

#[test]
fn test_derive_path_parents_chain() {
let convo = main_only_convo();
Expand Down
23 changes: 21 additions & 2 deletions crates/toolpath-opencode/src/derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use serde_json::{Map, Value, json};
use std::collections::HashMap;
use std::path::{Path as StdPath, PathBuf};
use toolpath::v1::{
ActorDefinition, ArtifactChange, Base, Identity, Path, PathIdentity, PathMeta, Step,
StepIdentity, StructuralChange,
ActorDefinition, ArtifactChange, Base, Identity, PATH_KIND_AGENT_CODING_SESSION, Path, PathIdentity,
PathMeta, Step, StepIdentity, StructuralChange,
};
use toolpath_convo::{ConversationView, Role, Turn};

Expand Down Expand Up @@ -165,6 +165,7 @@ fn derive_path_from_view(
steps,
meta: Some(PathMeta {
title: Some(format!("opencode session: {}", session.title)),
kind: Some(PATH_KIND_AGENT_CODING_SESSION.to_string()),
source: Some("opencode".to_string()),
actors: if actors.is_empty() {
None
Expand Down Expand Up @@ -691,6 +692,24 @@ mod tests {
assert!(actors.contains_key("agent:claude-sonnet-4-6"));
}

#[test]
fn derive_meta_kind_is_convo() {
let (_t, mgr, resolver) = fixture(BASIC_SQL);
let s = mgr.read_session("ses_abc123").unwrap();
let p = derive_path_with_resolver(
&s,
&DeriveConfig {
no_snapshot_diffs: true,
..Default::default()
},
&resolver,
);
assert_eq!(
p.meta.as_ref().unwrap().kind.as_deref(),
Some(PATH_KIND_AGENT_CODING_SESSION)
);
}

#[test]
fn derive_fallback_file_artifact_from_tool() {
let (_t, mgr, resolver) = fixture(BASIC_SQL);
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
5 changes: 5 additions & 0 deletions crates/toolpath/schema/toolpath.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": ["agent-coding-session"]
},
"source": {
"type": "string",
"description": "Source reference (e.g., PR URL)"
Expand Down
Loading
Loading