Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1560a76
docs(formats): document harness compaction + capture real compaction …
benbaarber Jun 10, 2026
88fc9b2
feat(convo): enforce unique step ids in derive_path (keep-first)
benbaarber Jun 10, 2026
bf35537
refactor(convo): unify turns/events into ConversationView.items
benbaarber Jun 10, 2026
95d5c17
feat(convo): emit conversation.compact steps for compaction boundaries
benbaarber Jun 10, 2026
fc140c1
feat(providers): populate Item::Compaction from each harness marker
benbaarber Jun 10, 2026
6f7de58
feat(kind): add agent-coding-session v1.1.0 with conversation.compact
benbaarber Jun 10, 2026
38e4f0d
chore: version bumps for conversation-items + compaction feature
benbaarber Jun 10, 2026
bc3e57e
feat(providers): project Item::Compaction back to harness markers
benbaarber Jun 10, 2026
f8e2315
fix(convo): error on duplicate step ids instead of silently dropping …
benbaarber Jun 11, 2026
7556419
docs(changelog): correct uniqueness behavior to error, not keep-first
benbaarber Jun 11, 2026
50bfd0e
feat(compaction): model kept as surviving turn-ids for cross-harness …
benbaarber Jun 11, 2026
794d166
fix(derive): map Claude's <synthetic> model sentinel to tool:<provider>
benbaarber Jun 11, 2026
92181b8
test(matrix): check compaction survival as a cross-harness invariant
benbaarber Jun 11, 2026
b2769a0
refactor(claude): stop re-emitting kept turns on projection
benbaarber Jun 12, 2026
eef5ccb
fix(cursor): reconcile with the items/compaction API after rebase
benbaarber Jun 12, 2026
9340246
docs(cursor): document /summarize compaction; treat it like gemini
benbaarber Jun 22, 2026
359fa3d
fix: reconcile compaction items IR with main's token-usage accounting
benbaarber Jun 22, 2026
c4b07f1
fix(opencode): pair each compaction with its own summary, not a sessi…
benbaarber Jun 24, 2026
2676af7
docs(formats): note compaction projection coercions for pi and opencode
benbaarber Jun 24, 2026
ba45ab8
docs(test): correct stale compaction_roundtrip header
benbaarber Jun 24, 2026
7cb45a7
feat(convo): resolve duplicate step ids in derive_path instead of err…
benbaarber Jun 25, 2026
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
55 changes: 55 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,61 @@

All notable changes to the Toolpath workspace are documented here.

## Conversation items IR + compaction provenance — 2026-06-22

`ConversationView` now exposes a single ordered `items` stream (the new
`Item` enum) in place of the separate `turns`/`events` lists, giving every
provider one timeline to populate and every consumer one timeline to walk.
On top of that, compaction is now first-class provenance: when an agent
summarizes and drops earlier context, we record what was kept and why
instead of silently losing the boundary.

- **`toolpath-convo`** (BREAKING): `ConversationView.turns`/`events` are
unified into `ConversationView.items: Vec<Item>`, with `turns()`,
`events()`, and `compactions()` accessors for walking the stream. New
public types `Compaction` and `CompactionTrigger` model a
context-compaction boundary — `kept` is the set of prior turn ids that
survived, and `trigger` records what caused it. `derive_path` resolves
step-id collisions as it emits steps — a byte-identical re-emission is
dropped, a same-id-but-different step is re-IDed to `<id>#<n>` — so it
is infallible (returns `Path`, not `Result`) and the result is always
collision-free. The per-provider `derive::derive_path` / `derive_project`
(and pi's `derive_graph`) wrappers are likewise infallible now; only the
disk-reading entry points (e.g. pi's `derive_project`) still return
`Result`.
- **New step type**: `conversation.compact` records a compaction event as
a Step in the derived `Path`.
- **Per-provider compaction population**: `toolpath-claude`,
`toolpath-codex`, `toolpath-opencode`, and `toolpath-pi` emit
`Compaction` items from their on-disk compaction signals.
`toolpath-gemini` and `toolpath-cursor` participate in the items IR but
persist no compaction: Gemini records none, and Cursor's `/summarize`
writes only a boundary marker (the summary and kept set live server-side
and are unrecoverable from local data), so the marker is skipped on read.
- **`agent-coding-session` kind → v1.2.0**: the kind constant is now
`https://toolpath.net/kinds/agent-coding-session/v1.2.0`, extending
v1.1.0 (token usage) with the `conversation.compact` step type. The
v1.2.0 schema is bundled in both `toolpath` and `path-cli` alongside the
retained v1.0.0/v1.1.0 schemas; producers (the shared
`toolpath_convo::derive_path` and every provider built on it) emit the
new URI.
- **Token-usage idempotency fixes** (surfaced by the cross-harness
round-trip matrix once compaction fixtures were added): message totals
are canonicalized per `group_id` across the whole turn sequence rather
than per consecutive run, so a group interrupted by an intervening turn
no longer double-counts on re-read (`toolpath-claude`); consecutive
same-id Gemini lines (one split message) now share a `group_id` so their
repeated `tokens` snapshot is counted once (`toolpath-gemini`); and
Codex's cumulative `token_count` advances by a group's total once, on the
group's last turn (`toolpath-codex`).

Crates bumped: `toolpath` 0.8.0, `toolpath-convo` 0.12.0,
`toolpath-claude` 0.13.0, `toolpath-codex` 0.7.0, `toolpath-gemini`
0.7.0, `toolpath-opencode` 0.6.0, `toolpath-pi` 0.7.0,
`toolpath-cursor` 0.3.0, `path-cli` 0.15.0, `toolpath-cli` 0.15.0.
Unchanged: `toolpath-git`, `toolpath-github`, `toolpath-dot`,
`toolpath-md`, `pathbase-client`.

## Token usage: once per message, with per-step attribution + kind v1.1.0 — 2026-06-17

Fixes token over-counting in derived documents (~3× output-token
Expand Down
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,10 @@ Build the site after changes: `cd site && pnpm run build` (should produce 11 pag
- `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.net/kinds/agent-coding-session/v1.1.0` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`; `…_V1_0_0` names the superseded URI); 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}` (schema.json is a symlink into `crates/path-cli/kinds/`, which `path p validate` bundles — both versions) and publish under `https://toolpath.net/kinds/`; the registry index is `site/kinds/index.md`. RFC: "Document Kind". JSON Schema: `$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 kind defined so far is `agent-coding-session`, currently at `https://toolpath.net/kinds/agent-coding-session/v1.2.0` (constant `toolpath::v1::PATH_KIND_AGENT_CODING_SESSION`; `…_V1_0_0` names the superseded v1.0.0 URI); v1.2.0 adds the `conversation.compact` step type for context-compaction boundaries on top of v1.1.0's message-level token accounting, and the earlier v1.1.0 and v1.0.0 URIs stay registered (their schemas kept in `KIND_SCHEMAS`) and documented for backward compatibility. 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}` (schema.json is a symlink into `crates/path-cli/kinds/`, which `path p validate` bundles — all versions) and publish under `https://toolpath.net/kinds/`; the registry index is `site/kinds/index.md`. RFC: "Document Kind". JSON Schema: `$defs/pathMeta`.
- Token accounting (kind v1.1.0): two keys on `conversation.append`/`Turn`, both optional. `token_usage` = "the total for a message" (on the group's final step; `Σ` over a path = session total). `attributed_token_usage` = "this step's own attributed spend", populated only where the source genuinely reports per-step spend (its own key, so the sum is unaffected; remainder = group total − Σ attributed, computed not stored). One provider message can span several steps (Claude writes one JSONL line per content block); `Turn.group_id` groups them. `toolpath-claude` fills `group_id` from `message.id` and takes the **field-wise-max** group total (line order not trusted). Claude's per-line `usage` is a cumulative *streaming snapshot* (Anthropic streaming API: `message_start` seeds output near 0, `message_delta` is cumulative), NOT a per-block cost — so Claude emits no `attributed_token_usage`; the projector re-expands the total onto every line. `toolpath-codex` differences the cumulative `total_token_usage` (dedup-safe: never sum `last_token_usage` — Codex re-emits it stale; openai/codex #14489), attributes each per-call delta to the step it follows, and derives the round total from those attributions. pi/opencode decode all-zero wire counters as `None`. Never stamp a cumulative counter, a repeated message total, or zero-filled placeholders onto a step; never derive attribution from Claude's streaming snapshots.
- Token usage `breakdowns` (kind v1.1.0, additive): an optional third key on `TokenUsage` — a decomposition of a top-level class into named sub-classes, keyed by class (e.g. `"output"`), inner map sub-class → tokens (e.g. `breakdowns["output"]["reasoning"] = 243`). INFORMATIONAL ONLY: **never summed into any total** (the parent class already counts those tokens, so the session-total guarantee is untouched); invariant `Σ(inner) ≤ parent`; omitted when empty; rides both `token_usage` and `attributed_token_usage`. Per-provider reality: **Gemini** reports `thoughts` (reasoning) as an additive sibling that the derivation used to **drop** (under-counting output) — it's now folded into `output_tokens` *and* recorded as `breakdowns["output"]["reasoning"]`, with the projector un-folding it on the reverse path for a lossless round-trip (`Some(0)` preserved as a real Gemini-3 zero-reasoning signal). **OpenCode** folds `reasoning` into output and records the same breakdown. **Codex** differences `reasoning_output_tokens` (⊆ output, cumulative) into `breakdowns["output"]["reasoning"]` on both per-step `attributed_token_usage` and per-round `token_usage`. **Claude** records no breakdown (its JSONL `usage` doesn't itemize thinking tokens).
- Compaction boundary (kind v1.2.0): the `conversation.compact` step type — a context-compaction boundary recorded as its own step between the turns it separates (turns after the boundary parent on it, so the `head`-ancestry walk crosses it in order). `structural` fields are all optional but `type`: `trigger` (`auto`|`manual`), `summary`, `pre_tokens`, and `kept` (ids of prior turns surviving verbatim into the post-compaction window; non-contiguous allowed; empty = wholesale). It is not a turn (no `text`/`role`/`tool_uses`); `step.actor` is `tool:<provider>`. Populated by each provider's `Item::Compaction` derivation and projected back to harness markers.
- 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
18 changes: 9 additions & 9 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 9 additions & 9 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ edition = "2024"
license = "Apache-2.0"

[workspace.dependencies]
toolpath = { version = "0.7.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.11.0", path = "crates/toolpath-convo" }
toolpath = { version = "0.8.0", path = "crates/toolpath" }
toolpath-convo = { version = "0.12.0", path = "crates/toolpath-convo" }
toolpath-git = { version = "0.6.0", path = "crates/toolpath-git" }
toolpath-claude = { version = "0.12.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.6.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.6.0", path = "crates/toolpath-codex" }
toolpath-opencode = { version = "0.5.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.2.0", path = "crates/toolpath-cursor" }
toolpath-claude = { version = "0.13.0", path = "crates/toolpath-claude", default-features = false }
toolpath-gemini = { version = "0.7.0", path = "crates/toolpath-gemini", default-features = false }
toolpath-codex = { version = "0.7.0", path = "crates/toolpath-codex" }
toolpath-opencode = { version = "0.6.0", path = "crates/toolpath-opencode" }
toolpath-cursor = { version = "0.3.0", path = "crates/toolpath-cursor" }
toolpath-github = { version = "0.6.0", path = "crates/toolpath-github" }
toolpath-dot = { version = "0.5.0", path = "crates/toolpath-dot" }
toolpath-md = { version = "0.7.0", path = "crates/toolpath-md" }
toolpath-pi = { version = "0.6.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.14.0", path = "crates/path-cli" }
toolpath-pi = { version = "0.7.0", path = "crates/toolpath-pi" }
path-cli = { version = "0.15.0", path = "crates/path-cli" }
pathbase-client = { version = "0.2.0", path = "crates/pathbase-client" }

reqwest = { version = "0.13", default-features = false, features = ["blocking", "json", "rustls"] }
Expand Down
12 changes: 8 additions & 4 deletions RFC.md
Original file line number Diff line number Diff line change
Expand Up @@ -296,10 +296,14 @@ 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.net/kinds/>. The only one defined
so far is `https://toolpath.net/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.
so far is `agent-coding-session`, currently at
`https://toolpath.net/kinds/agent-coding-session/v1.2.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, and context-compaction boundaries carry a `"conversation.compact"` step.
The earlier `v1.1.0` URI (message-level token accounting, without
`conversation.compact`) and `v1.0.0` URI both remain valid and documented. See
the linked spec for the full contract.

#### Actor Definitions

Expand Down
2 changes: 1 addition & 1 deletion crates/path-cli/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "path-cli"
version = "0.14.0"
version = "0.15.0"
edition.workspace = true
license.workspace = true
repository = "https://github.com/empathic/toolpath"
Expand Down
Loading
Loading