@cad0p/pi-tree-navigator
v0.1.0
Published
agent-callable session tree navigation for pi: anchor named milestones, rewind work into a branch summary, free up context for long autonomous sessions
Downloads
82
Maintainers
Readme
pi-tree-navigator
🌳 Agent-callable session tree navigation for pi.
Lets a pi agent anchor named milestones in its own conversation, then collapse work between them into a model-generated branch_summary to free up context — without tripping Anthropic's tool_use ↔ tool_result validation, and with the freed context immediately available to the next assistant turn (even within the same prompt() call).
Install
pi install git:github.com/cad0p/pi-tree-navigatorStatus: v0.1.0 is not yet on the npm registry (pending OIDC trusted-publisher setup). Install from the git source for now.
Stable (once published to npm):
pi install npm:@cad0p/pi-tree-navigatorPre-release (calver snapshots from
main, published to npm@nexton every push):pi install npm:@cad0p/pi-tree-navigator@next
Requirements
- pi 0.74+ with at least one model provider configured.
- Peer dependencies (the source of truth is
package.jsonpeerDependencies):@earendil-works/pi-coding-agent >=0.74.0@earendil-works/pi-agent-core >=0.74.0typebox ^1.0.0(used to declare the tool's parameter schema; bundled with pi but listed explicitly so a standalone install resolves correctly).
- The reflection bootstrap depends on five plain (not
#-private) internal pi/agent fields:AgentSession.prototype.prompt,agent.state.messages,agent.state.systemPrompt,agent.state.tools, andagent.prepareNextTurn. Verified against pi 0.75.x.
What you get
A single agent-callable tool, navigate_tree, with three actions:
| action | params | effect |
|---|---|---|
| anchor | name | Label the current point in the conversation as a milestone. |
| rewind | labelStart, labelEnd, summaryFocus | Collapse work between labelStart and the current leaf into a branch_summary entry. The summary is itself labeled with labelEnd, so you can chain rewinds. summaryFocus is required (non-trivial focus required; floor enforced at runtime by MIN_SUMMARY_FOCUS_LENGTH). Despite the verb, rewind does not restore prior state — it forks a sibling branch from labelStart and continues forward from a model-generated summary; the original subtree is preserved on disk but no longer on the active path. |
| list | — | Show all anchors on the active branch with cumulative context %. |
name (written by anchor) and labelEnd (written by rewind) both share the reserved anchor: label prefix; labelStart resolves against that same namespace. Every label written by anchor and every labelEnd written by rewind is referenceable by any subsequent rewind's labelStart, and list shows all of them.
How it works
A typical autonomous-loop pattern:
agent: navigate_tree(action="anchor", name="impl-start")
→ [anchor 'impl-start'] set at 1.9% of 1.0M (after: “implement the parser”)
agent: ...does work, runs tools, accumulates context to 30%...
agent: navigate_tree(action="rewind", labelStart="impl-start", labelEnd="impl-end",
summaryFocus="record only the public API of the parser
and the open issue with edge case X")
→ [rewind 'impl-start' → 'impl-end'] · context 30.4% → 4.1% of 1.0M
→ A branch_summary recording the work just collapsed has been appended
to your context. Items under '### Done' are complete. ...
agent: ...continues with the freed context, the next API call is back at ~4%...The freed context is available to the next assistant turn within the same prompt() call, not just on the next user prompt. This is the key feature — autonomous agents don't have to wait for a user round-trip to benefit from a rewind.
Implementation notes
Why this is more involved than just calling pi's branchWithSummary:
Anthropic's tool_use ↔ tool_result pairing. When a tool call rewinds the session tree, the tool's own
tool_uselives in the assistant message that issued it — whichbranchWithSummaryputs on the abandoned branch. Pi unconditionally writes the tool'stool_resultto the new branch, leaving the result orphaned. Anthropic 400s the next API call withImproperly formed request. The fix is to inject a synthetic assistant message whose singletool_callhas the same id as the in-flight call, afterbranchWithSummarybut before the tool returns. Pi then writes the realtool_resultas a child of that synthetic assistant — and the chain stays structurally valid.In-loop context refresh. Pi's
Agentclass snapshotsstate.messagesonce at the start ofprompt()and pushes new messages onto its own array. A rewind issued mid-loop wouldn't reduce the next API call's size until the user sent a fresh prompt. We wireagent.prepareNextTurnfrom a prototype patch onAgentSession.prototype.prompt, returning a fresh context built fromsessionManager.buildSessionContext()between every turn boundary. After a rewind, the very next assistant turn within the sameprompt()sees the rewound chain.Reflection bootstrap. Pi's slash-command
navigateTreehas access tocommandCtx.navigateTree, which mutatesagent.state.messages. Tool executes don't get that ctx, so we capture everyAgentSessioninstance via the prompt patch and replicate the mutation manually. Without it, the on-disk leaf moves butagent.state.messagesstays stale.summaryFocusis mandatory. The summary is the only thing the agent will see of the collapsed work. The first time the agent usesrewind, blanket prompts produce vague summaries; subsequent rewinds are weaker. Forcing the agent to articulatesummaryFocus(passed to pi'sgenerateBranchSummaryascustomInstructions) measurably improves what survives.
Synthetic assistant token bias
The synthetic assistant we inject after each rewind carries the post-rewind chain estimate in usage.totalTokens (so estimateContextTokens reads a sensible baseline immediately after the move). The synthetic itself adds a ~50-token toolCall block re-emitted on every subsequent turn until the next rewind — that overhead is not reflected in any usage.* field, so future estimateContextTokens calls understate the chain by ~50 tokens until the next assistant turn writes a fresh usage block. Negligible at typical anchor cadence; mention if you're benchmarking exact token deltas, ignore otherwise.
Limitations
Brittle to pi version bumps. The fix uses five independent reflection points on internals that aren't part of pi's public API:
AgentSession.prototype.prompt,agent.state.messages,agent.state.systemPrompt,agent.state.tools, andagent.prepareNextTurn. If a future pi release renames any of these, switches them to private (#) fields, or restructures the class hierarchy, this breaks. The extension fails loudly:anchorstill works,rewindreports⚠ reflection bootstrap missing — the rewind landed on disk but the next assistant turn may still see the pre-rewind context. Run \/reload` (or restart pi) to recover.`, and you'd see context corruption return on the next prompt.Anchor early in the turn. Whatever's in
agent.state.messagesbefore theanchortool call stays in the kept chain. Everything after gets summarized. Anchor at the start of a stage for maximum context savings.Abandoned branches grow the JSONL forever. Each rewind preserves the abandoned subtree on disk. Session files get bigger over time even as live context shrinks. For very long autonomous runs (days), session files can hit hundreds of MB.
Tested against Anthropic and Kiro providers. The synthetic-tool_use trick is specifically for Anthropic's strict tool_use/tool_result pairing; the synthetic's
stopReason: "toolUse"survives Kiro'snormalizeMessagesfilter. Other providers may have different validation rules — untested.Loading the extension monkey-patches
AgentSession.prototype.promptglobally. Every session in the host pi process picks up the patch on import, including sessions that never callnavigate_tree. The patch is install-on-import and not reversible within a running pi process; restart pi to fully unload it.anchor:is a reserved label prefix. Any label written via pi's/labelcommand or by another extension that begins withanchor:will be picked up bylistand addressable byrewind'slabelStart/labelEnd. Avoid the prefix in manually-set labels.Disk-fault during
rewind(rare). Pi'sbranchWithSummaryadvances the in-memory leaf before persisting the new entry to disk. If pi's session-write fails mid-call (full disk, FS error on a persisted session), the in-memory leaf has already moved past the original assistant turn but the synthetic-assistant injection in this extension never runs — pi's tool-result then lands without a matching tool_use, surfacing as the samecontext_length_exceeded400 the synthetic exists to prevent. Production risk: low (in-memory tests don't reach this case; pi's session-write is robust on POSIX disk). Tracked for an additional salvage layer wrappingbranchWithSummaryitself in v0.2.0.
Development
bun install
bun test # helpers + dispatch / reflection bootstrap / salvage path
bunx biome check extensions/
bunx tsc --noEmitTests cover extensions/navigate-tree/helpers.ts (pure helpers in helpers.test.ts) and extensions/navigate-tree/index.ts (action dispatch, schema shape, synthetic-assistant injection, reflection bootstrap, salvage path — in index.test.ts). The summarize factory option injects a stub for generateBranchSummary so no real LLM call fires during rewind tests. Additional manual e2e validation against pi 0.75.x is recommended for any pi version bump (the reflection bootstrap depends on internal field shapes).
License
MIT — see LICENSE.
