toolpath-gemini
Derive Toolpath provenance documents from Gemini CLI conversation logs.
When Gemini CLI writes your code, the conversation — the reasoning, the
tool calls, the sub-agent delegations — is the provenance. This crate
reads those conversations directly from ~/.gemini/tmp/... and maps
them to Toolpath documents so every AI-assisted change has a traceable
origin.
Overview
Reads Gemini CLI conversation data from ~/.gemini/tmp/<project>/chats/
and provides:
- Conversation reading: Parse the JSON chat files into typed structures
- Query: Filter and search messages by role, tool use, text content
- Derivation: Map conversations to Toolpath Path documents
- Watching: Monitor chat files for live updates (feature-gated)
Mapping
| Gemini concept | Toolpath concept |
|---|---|
| Session UUID dir | Conversation (main chat + sub-agent chats merged) |
| Project path | path.base.uri as file:///... |
| User message | Step with actor: "human:user" |
| Gemini message | Step with actor: "agent:<model>" |
toolCalls[] with write_file/replace |
change entry keyed by file path |
thoughts[] |
Turn.thinking (joined) |
Sub-agent chat file (kind: "subagent") |
DelegatedWork with populated turns |
Derivation
use ;
let manager = new;
let convo = manager.read_conversation?;
let config = default;
let path = derive_path;
# Ok::
Reading conversations
use GeminiConvo;
let manager = new;
// List projects
let projects = manager.list_projects?;
// List sessions for a project
let sessions = manager.list_conversations?;
// Read a full session (main chat + all sub-agent chats)
let convo = manager.read_conversation?;
// Most recent conversation
let latest = manager.most_recent_conversation?;
// Lightweight metadata, including the first user-prompt text as a
// human-readable title for picker UIs.
for meta in manager.list_conversation_metadata?
# Ok::
Provider-agnostic usage
This crate implements toolpath_convo::ConversationProvider, so
consumers can code against the provider-agnostic types instead of
Gemini-specific structures.
use GeminiConvo;
use ConversationProvider;
let provider = new;
let view = provider.load_conversation?;
for turn in &view.turns
Tool classification
Gemini CLI tool names are mapped to ToolCategory:
| Gemini CLI tool | ToolCategory |
|---|---|
read_file, read_many_files, list_directory, get_internal_docs, read_mcp_resource |
FileRead |
glob, grep_search, search_file_content |
FileSearch |
write_file, replace, edit |
FileWrite |
run_shell_command |
Shell |
web_fetch, google_web_search |
Network |
task, activate_skill |
Delegation |
Unrecognized tools get category: None — consumers still have name
and input.
Sub-agent delegations
Sub-agent invocations are stored as sibling chat files (kind: "subagent")
in the same session UUID directory. When you load a conversation, those
sub-agent chats are folded into DelegatedWork on the parent task tool
invocation with turns populated (unlike toolpath-claude, which leaves
sub-agent turns empty because they live in separate session files).
Environment context
Each turn's EnvironmentSnapshot.working_dir is populated from the chat
file's top-level directories[0].
Token usage
Per-turn TokenUsage includes:
input_tokens←tokens.inputoutput_tokens←tokens.outputcache_read_tokens←tokens.cachedcache_write_tokens→None(Gemini doesn't expose this)
ConversationView.total_usage aggregates across all turns.
Provider-specific metadata
Gemini log entries often carry extra fields (thoughts, tokens.tool,
tokens.total, kind, summary) that don't map to the common schema.
These are forwarded into Turn.extra["gemini"] so trait-only consumers
can access them without importing Gemini-specific types.
Round-trip fidelity
The crate exposes three progressively lossy views of a conversation:
| Layer | Lossless? | Use it when |
|---|---|---|
ChatFile / Conversation (the raw on-disk schema) |
Yes — verified by tests/roundtrip.rs on live fixtures |
You need to re-emit the Gemini JSON byte-equivalent (archival, editing, redaction) |
ConversationView (provider-agnostic projection) |
No — Gemini-specific fields live under Turn.extra["gemini"] |
You want to work across providers with one set of types |
toolpath::v1::Path (provenance digest) |
No — tool results/args are summarized; only file-write bodies are preserved as full diffs | You want a compact Toolpath document for blame, queries, rendering |
For a true round-trip — Gemini → Toolpath → Gemini — stay at the
ChatFile level:
use ;
let raw = read_to_string?;
let chat: ChatFile = from_str?;
// ... inspect or modify chat ...
let back = to_string?; // byte-equivalent to `raw` (modulo key order)
Guarantees baked in:
- Every unknown field — top-level or per-message — rides through via
#[serde(flatten)] extra: HashMap<String, Value>. Future schema additions survive unchanged. GeminiRolepreserves unknown role values ("plan","system", etc.) viaOther(String); known values (user/gemini/info) deserialize into typed variants.ToolCall.result_displayisOption<Value>, so Gemini's structured payloads (dict-with-fileDiff, nested ANSI-styled arrays) round-trip opaquely.- Optional list fields (
directories,thoughts,toolCalls) useOption<Vec<T>>so we distinguish absent from present-but-empty.
Feature flags
| Feature | Default | Description |
|---|---|---|
watcher |
yes | Filesystem watching via notify + tokio |
Part of Toolpath
This crate is part of the Toolpath workspace. See also:
toolpath-- core types and query APItoolpath-convo-- provider-agnostic conversation abstractiontoolpath-claude-- Claude conversation providertoolpath-git-- derive from git historytoolpath-dot-- Graphviz DOT renderingpath-cli-- unified CLI (cargo install path-cli)- RFC -- full format specification