@cad0p/pi-tree-navigator 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/README.md +13 -11
- package/extensions/navigate-tree/index.ts +114 -168
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.2] - 2026-08-20
|
|
6
|
+
|
|
7
|
+
<!-- USER-EDITABLE SECTION START -->
|
|
8
|
+
Patch release: fixes rewind summarization for custom-api providers and eliminates 3 of the 5 reflection points via public APIs.
|
|
9
|
+
|
|
10
|
+
**Rewind summarization for custom providers (#13):** `rewind` failed with `No API provider registered for api: <custom-id>` for any provider registered via `pi.registerProvider(name, { api: <custom-id>, streamSimple })` (e.g. pi-commandcode-provider 0.5.x with `api: "commandcode-custom"`). The summarizer now routes through the composed provider's `streamSimple`, obtained via the public `ctx.modelRegistry.getProvider(providerId)` API (pi ≥0.81.0) — the same routing pi's own `branchWithSummary` uses. Peer floor bumped to `>=0.81.0`; `null` header-deletion markers stripped (pi 0.84+ `ProviderHeaders`).
|
|
11
|
+
|
|
12
|
+
**Reflection surface five → two (#14):** the per-turn in-loop context refresh no longer reads `agent.state.systemPrompt` / `agent.state.tools` or wraps `agent.prepareNextTurnWithContext` / `agent.prepareNextTurn`. Instead a `pi.on("context")` handler (the public `context` extension event, fired via `Agent.transformContext` before every LLM call) returns the session-tree projection `sessionManager.buildContextEntries().flatMap(sessionEntryToContextMessages)`. Pi's own `_installAgentNextTurnRefresh` keeps systemPrompt/tools/model/thinkingLevel fresh per turn. Remaining reflection points: `AgentSession.prototype.prompt` (session capture) and `agent.state.messages` (post-rewind refresh so the next prompt snapshots the rewound chain) — neither is eliminable via public APIs.
|
|
13
|
+
|
|
14
|
+
Verified live on pi 0.84.2 (tmux TUI, commandcode provider): a 580KB file read (22.6% of 400k) rewound mid-loop to 1.0% of 400k with the very next LLM call at the rewound cache prefix (cacheRead 90,368 → 2,944). 125 tests pass; CI green.
|
|
15
|
+
<!-- USER-EDITABLE SECTION END -->
|
|
16
|
+
|
|
17
|
+
### 🐛 Bug Fixes
|
|
18
|
+
|
|
19
|
+
- *(rewind)* Route summarization through provider streamSimple (public API) (closes #13)
|
|
20
|
+
|
|
21
|
+
### 🚜 Refactor
|
|
22
|
+
|
|
23
|
+
- Eliminate 3 of 5 reflection points via public context event (closes #14)
|
|
24
|
+
|
|
25
|
+
### 📚 Documentation
|
|
26
|
+
|
|
27
|
+
- Update reflection field count/versions and dev commands for v0.1.1 reality ([#9](https://github.com/cad0p/pi-tree-navigator/pull/9))
|
|
28
|
+
- Port AGENTS.md bootstrap instructions from pi-napkin ([#11](https://github.com/cad0p/pi-tree-navigator/pull/11))
|
|
29
|
+
|
|
30
|
+
|
|
5
31
|
## [0.1.1] - 2026-07-31
|
|
6
32
|
|
|
7
33
|
<!-- USER-EDITABLE SECTION START -->
|
package/README.md
CHANGED
|
@@ -34,12 +34,12 @@ This repo uses [`cad0p/semver-calver-release`](https://github.com/cad0p/semver-c
|
|
|
34
34
|
|
|
35
35
|
### Requirements
|
|
36
36
|
|
|
37
|
-
- **pi 0.
|
|
37
|
+
- **pi 0.81+** (node ≥22.19, which pi 0.81+ itself requires) with at least one model provider configured.
|
|
38
38
|
- Peer dependencies (the source of truth is `package.json` `peerDependencies`):
|
|
39
|
-
- `@earendil-works/pi-coding-agent >=0.
|
|
40
|
-
- `@earendil-works/pi-agent-core >=0.
|
|
39
|
+
- `@earendil-works/pi-coding-agent >=0.81.0`
|
|
40
|
+
- `@earendil-works/pi-agent-core >=0.81.0`
|
|
41
41
|
- `typebox ^1.0.0` (used to declare the tool's parameter schema; bundled with pi but listed explicitly so a standalone install resolves correctly).
|
|
42
|
-
- The reflection bootstrap depends on
|
|
42
|
+
- The reflection bootstrap depends on two plain (not `#`-private) internal pi/agent fields: `AgentSession.prototype.prompt` (patched for session capture) and `agent.state.messages` (refreshed after a rewind so the next prompt snapshots the rewound chain). Per-turn in-loop context refresh runs through the **public** `context` extension event (see “In-loop context refresh” below) — no `agent.prepareNextTurn*` reflection. Verified against pi 0.81.0 / 0.83.0 / 0.84.2.
|
|
43
43
|
|
|
44
44
|
## What you get
|
|
45
45
|
|
|
@@ -81,7 +81,7 @@ Why this is more involved than just calling pi's `branchWithSummary`:
|
|
|
81
81
|
|
|
82
82
|
1. **Anthropic's tool_use ↔ tool_result pairing.** When a tool call rewinds the session tree, the tool's own `tool_use` lives in the assistant message that issued it — which `branchWithSummary` puts on the abandoned branch. Pi unconditionally writes the tool's `tool_result` to the new branch, leaving the result orphaned. Anthropic 400s the next API call with `Improperly formed request`. The fix is to inject a synthetic assistant message whose single `tool_call` has the same id as the in-flight call, *after* `branchWithSummary` but *before* the tool returns. Pi then writes the real `tool_result` as a child of that synthetic assistant — and the chain stays structurally valid.
|
|
83
83
|
|
|
84
|
-
2. **In-loop context refresh.** Pi's `Agent` class snapshots `state.messages` once at the start of `prompt()` 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
|
|
84
|
+
2. **In-loop context refresh.** Pi's `Agent` class snapshots `state.messages` once at the start of `prompt()` 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 register a handler on pi's **public `context` extension event** — fired via `Agent.transformContext` → `runner.emitContext` before *every* LLM call, including the turn right after a mid-loop rewind — that replaces the wire messages with the session-tree projection (`sessionManager.buildContextEntries()` → `sessionEntryToContextMessages`). After a rewind, the very next assistant turn within the same `prompt()` sees the rewound chain. (This replaced the earlier `agent.prepareNextTurnWithContext` reflection wrapper.)
|
|
85
85
|
|
|
86
86
|
3. **Reflection bootstrap.** Pi's slash-command `navigateTree` has access to `commandCtx.navigateTree`, which mutates `agent.state.messages`. Tool executes don't get that ctx, so we capture every `AgentSession` instance via the prompt patch and replicate the mutation manually. Without it, the on-disk leaf moves but `agent.state.messages` stays stale.
|
|
87
87
|
|
|
@@ -93,7 +93,9 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
|
|
|
93
93
|
|
|
94
94
|
## Limitations
|
|
95
95
|
|
|
96
|
-
- **Brittle to pi version bumps.** The fix uses
|
|
96
|
+
- **Brittle to pi version bumps.** The fix uses two independent reflection points on internals that aren't part of pi's public API: `AgentSession.prototype.prompt` (session capture) and `agent.state.messages` (refreshed after a rewind). A third reflection point (`agent.prepareNextTurn*`) was eliminated in v0.2.0 via the public `context` extension event; the per-turn systemPrompt/tools/model/thinkingLevel refreshes come from pi's own `_installAgentNextTurnRefresh` (construction-installed since 0.80.3). If a future pi release renames the two remaining fields, switches them to private (`#`) fields, or restructures the class hierarchy, this breaks. The extension fails loudly: `anchor` still works, `rewind` reports `⚠ 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.
|
|
97
|
+
|
|
98
|
+
**Audited against pi 0.81.0 / 0.83.0 / 0.84.2 (2026-08):** three of the five reflection points have been eliminated — `agent.state.systemPrompt` and `agent.state.tools` reads (deleted with the `prepareNextTurn` double-wrap; no longer needed since pi's own per-turn wrapper and the public `ctx.getSystemPrompt()` / `pi.getAllTools()` cover them) and `agent.prepareNextTurnWithContext` (replaced by the public `context` extension event, which fires via `transformContext` before **every** LLM call). The remaining two are NOT eliminable for the tool-based design: `AgentSession.prototype.prompt` (no public per-prompt hook for tool executes — the `context` event fires too late to install hooks before `createLoopConfig`) and `agent.state.messages` (pi's own `navigateTree` refresh is only reachable via `ctx.navigateTree()`, which exists solely on `ExtensionCommandContext`, not the `ExtensionContext` a `tool.execute` receives).
|
|
97
99
|
|
|
98
100
|
- **Anchor early in the turn.** Whatever's in `agent.state.messages` *before* the `anchor` tool call stays in the kept chain. Everything after gets summarized. Anchor at the *start* of a stage for maximum context savings.
|
|
99
101
|
|
|
@@ -110,13 +112,13 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
|
|
|
110
112
|
## Development
|
|
111
113
|
|
|
112
114
|
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
pnpm install
|
|
116
|
+
pnpm test # helpers + dispatch / reflection bootstrap / salvage path
|
|
117
|
+
pnpm run lint # biome check extensions/
|
|
118
|
+
pnpm run typecheck # tsc --noEmit
|
|
117
119
|
```
|
|
118
120
|
|
|
119
|
-
Tests 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.
|
|
121
|
+
Tests 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, context-event projection, 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 the current pi release (0.84.x at time of writing) is recommended for any pi version bump (the reflection bootstrap depends on `AgentSession.prototype.prompt` / `agent.state.messages` field shapes).
|
|
120
122
|
|
|
121
123
|
## License
|
|
122
124
|
|
|
@@ -22,10 +22,8 @@
|
|
|
22
22
|
* this.agent.state.messages = this.sessionManager.buildSessionContext().messages;
|
|
23
23
|
*
|
|
24
24
|
* Risks of the reflection approach:
|
|
25
|
-
* • If pi switches
|
|
26
|
-
* `AgentSession.prototype.prompt
|
|
27
|
-
* `agent.state.systemPrompt`, `agent.state.tools`,
|
|
28
|
-
* `agent.prepareNextTurn`, or `agent.prepareNextTurnWithContext` —
|
|
25
|
+
* • If pi switches either of the two fields this extension reads —
|
|
26
|
+
* `AgentSession.prototype.prompt` or `agent.state.messages` —
|
|
29
27
|
* to ES `#` private fields, this breaks fundamentally.
|
|
30
28
|
* • If pi renames or restructures any of these fields, this breaks.
|
|
31
29
|
* • Patches `AgentSession.prototype.prompt` globally on import; not
|
|
@@ -33,20 +31,25 @@
|
|
|
33
31
|
* the pi process, including sessions that never call
|
|
34
32
|
* `navigate_tree`.
|
|
35
33
|
*
|
|
36
|
-
* Verified against pi 0.
|
|
37
|
-
*
|
|
38
|
-
* `
|
|
34
|
+
* Verified against pi 0.81.0 / 0.83.0 / 0.84.2. In-loop context
|
|
35
|
+
* refresh runs through the public `context` extension event (see
|
|
36
|
+
* `buildContextMessages`), not `agent.prepareNextTurn*` reflection.
|
|
39
37
|
*/
|
|
40
38
|
|
|
41
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
estimateContextTokens,
|
|
41
|
+
type StreamFn,
|
|
42
|
+
} from "@earendil-works/pi-agent-core";
|
|
42
43
|
import {
|
|
43
44
|
AgentSession,
|
|
44
45
|
buildSessionContext,
|
|
45
46
|
collectEntriesForBranchSummary,
|
|
46
47
|
type ExtensionAPI,
|
|
47
48
|
generateBranchSummary,
|
|
49
|
+
type ModelRegistry,
|
|
48
50
|
type SessionEntry,
|
|
49
51
|
type SessionManager,
|
|
52
|
+
sessionEntryToContextMessages,
|
|
50
53
|
} from "@earendil-works/pi-coding-agent";
|
|
51
54
|
import { Type } from "typebox";
|
|
52
55
|
import {
|
|
@@ -117,8 +120,6 @@ const LIST_PCT_COL_WIDTH = 5;
|
|
|
117
120
|
// is 40, but the typical kebab-case name is 8–20 chars; 28 keeps the
|
|
118
121
|
// hint column visible without truncating common names.
|
|
119
122
|
const LIST_LABEL_COL_WIDTH = 28;
|
|
120
|
-
const PNT_MARKER = Symbol.for("navigate-tree.pnt-installed");
|
|
121
|
-
const PNTWC_MARKER = Symbol.for("navigate-tree.pntwc-installed");
|
|
122
123
|
const ORIG_PROMPT_KEY = Symbol.for("navigate-tree.orig-prompt");
|
|
123
124
|
|
|
124
125
|
// Two warnings: list-site (read-only path; warns about the next turn's
|
|
@@ -142,15 +143,8 @@ const REFLECTION_BOOTSTRAP_WARNING_REWIND =
|
|
|
142
143
|
interface PiInternals {
|
|
143
144
|
agent: {
|
|
144
145
|
state: {
|
|
145
|
-
systemPrompt: string;
|
|
146
146
|
messages: unknown[];
|
|
147
|
-
tools: unknown[];
|
|
148
147
|
};
|
|
149
|
-
prepareNextTurn?: unknown;
|
|
150
|
-
// Preferred over `prepareNextTurn` by `Agent.createLoopConfig`
|
|
151
|
-
// when set; pi's own AgentSession always sets it (via
|
|
152
|
-
// `_installAgentNextTurnRefresh`) since pi-coding-agent 0.80.3.
|
|
153
|
-
prepareNextTurnWithContext?: unknown;
|
|
154
148
|
};
|
|
155
149
|
sessionManager: SessionManager;
|
|
156
150
|
}
|
|
@@ -159,34 +153,6 @@ function asInternals(session: AgentSession): PiInternals {
|
|
|
159
153
|
return session as unknown as PiInternals;
|
|
160
154
|
}
|
|
161
155
|
|
|
162
|
-
type PntResult = {
|
|
163
|
-
context?: {
|
|
164
|
-
systemPrompt?: unknown;
|
|
165
|
-
messages?: unknown[];
|
|
166
|
-
tools?: unknown[];
|
|
167
|
-
[k: string]: unknown;
|
|
168
|
-
};
|
|
169
|
-
model?: unknown;
|
|
170
|
-
thinkingLevel?: unknown;
|
|
171
|
-
};
|
|
172
|
-
// pi 0.75.5 invokes `agent.prepareNextTurn(signal)` from
|
|
173
|
-
// `Agent.createLoopConfig` — a single AbortSignal argument. Since pi
|
|
174
|
-
// 0.80.3 the loop instead invokes
|
|
175
|
-
// `agent.prepareNextTurnWithContext(nextTurnContext, signal)` (and only
|
|
176
|
-
// falls back to `prepareNextTurn` when the WithContext field is unset).
|
|
177
|
-
// Both differ from the documented
|
|
178
|
-
// `AgentLoopConfig.prepareNextTurn(context: PrepareNextTurnContext)`
|
|
179
|
-
// shape, which `Agent` is bridging. We accept whatever pi passes and
|
|
180
|
-
// forward it verbatim to the prior wrapper so we don't fight a future
|
|
181
|
-
// signature alignment. Verified against pi-coding-agent 0.75.5 and
|
|
182
|
-
// 0.83.0; revisit if the call site changes.
|
|
183
|
-
type PntFn = (...args: unknown[]) => Promise<PntResult> | PntResult;
|
|
184
|
-
type MarkedPntFn = PntFn & {
|
|
185
|
-
[PNT_MARKER]?: boolean;
|
|
186
|
-
[PNTWC_MARKER]?: boolean;
|
|
187
|
-
__prior?: PntFn;
|
|
188
|
-
};
|
|
189
|
-
|
|
190
156
|
// =============================================================================
|
|
191
157
|
// Reflection bootstrap & in-loop refresh
|
|
192
158
|
// =============================================================================
|
|
@@ -226,126 +192,11 @@ function patchAgentSessionPrototype(): void {
|
|
|
226
192
|
// module and reflection finds nothing.
|
|
227
193
|
const patched = function (this: AgentSession, ...args: unknown[]) {
|
|
228
194
|
captureSession(this);
|
|
229
|
-
installPrepareNextTurn(this);
|
|
230
195
|
return orig.apply(this, args);
|
|
231
196
|
};
|
|
232
197
|
proto.prompt = patched;
|
|
233
198
|
}
|
|
234
199
|
|
|
235
|
-
/**
|
|
236
|
-
* Wire the in-flight agent loop to refresh its context from sessionManager
|
|
237
|
-
* between turns within the same prompt() call. Without this, the loop
|
|
238
|
-
* snapshots agent.state.messages once at prompt start and pushes new
|
|
239
|
-
* messages onto its own array — a rewind issued mid-loop doesn't reduce
|
|
240
|
-
* the next API call's size until the user sends a new prompt.
|
|
241
|
-
*
|
|
242
|
-
* Two hook fields, by pi version:
|
|
243
|
-
* • pi ≤0.80.2: `Agent.createLoopConfig` reads `agent.prepareNextTurn`.
|
|
244
|
-
* • pi ≥0.80.3: AgentSession's constructor installs its own
|
|
245
|
-
* `agent.prepareNextTurnWithContext` (`_installAgentNextTurnRefresh`),
|
|
246
|
-
* and `createLoopConfig` prefers that field over `prepareNextTurn`.
|
|
247
|
-
* Pi's wrapper refreshes systemPrompt/tools/model/thinkingLevel per
|
|
248
|
-
* turn but leaves `messages` stale — so wrapping only
|
|
249
|
-
* `prepareNextTurn` silently dead-ends (footer % and the wire context
|
|
250
|
-
* stay pre-rewind for the rest of the loop).
|
|
251
|
-
*
|
|
252
|
-
* We install on BOTH fields, each with the same marker/__prior chaining
|
|
253
|
-
* discipline: on new pi the WithContext wrapper chains pi's own (keeping
|
|
254
|
-
* its per-turn refreshes) and overrides only `messages`; on old pi the
|
|
255
|
-
* WithContext field is never read and `prepareNextTurn` does the work.
|
|
256
|
-
*
|
|
257
|
-
* The Agent class's `createLoopConfig` dereferences both fields at the
|
|
258
|
-
* closure call site, so the values here are read at every turn boundary.
|
|
259
|
-
* But it gates the closure on either field being truthy at config
|
|
260
|
-
* creation — so we have to set this BEFORE prompt() runs, hence wiring
|
|
261
|
-
* it from inside the prompt patch.
|
|
262
|
-
*/
|
|
263
|
-
function installPrepareNextTurn(session: AgentSession): void {
|
|
264
|
-
const internals = asInternals(session);
|
|
265
|
-
const agent = internals.agent;
|
|
266
|
-
if (!agent) return;
|
|
267
|
-
|
|
268
|
-
const sm = internals.sessionManager;
|
|
269
|
-
|
|
270
|
-
// If the existing hooks were installed by a previous load of THIS
|
|
271
|
-
// extension, recover the chains they captured (their `__prior`) so we
|
|
272
|
-
// don't strand other extensions' closures across /reload. Preserve any
|
|
273
|
-
// other extension's (or pi's own) hooks so we compose with them.
|
|
274
|
-
const existing = agent.prepareNextTurn as MarkedPntFn | undefined;
|
|
275
|
-
const prior: PntFn | undefined =
|
|
276
|
-
typeof existing === "function" && existing[PNT_MARKER]
|
|
277
|
-
? existing.__prior
|
|
278
|
-
: (existing as PntFn | undefined);
|
|
279
|
-
const existingWc = agent.prepareNextTurnWithContext as
|
|
280
|
-
| MarkedPntFn
|
|
281
|
-
| undefined;
|
|
282
|
-
const priorWc: PntFn | undefined =
|
|
283
|
-
typeof existingWc === "function" && existingWc[PNTWC_MARKER]
|
|
284
|
-
? existingWc.__prior
|
|
285
|
-
: (existingWc as PntFn | undefined);
|
|
286
|
-
|
|
287
|
-
const next: MarkedPntFn = async (...args: unknown[]) => {
|
|
288
|
-
let priorResult: PntResult | undefined;
|
|
289
|
-
if (typeof prior === "function") {
|
|
290
|
-
priorResult = await prior(...args);
|
|
291
|
-
}
|
|
292
|
-
// Pi's loop replaces context wholesale (`currentContext = ctx ??
|
|
293
|
-
// currentContext`), not field-merges — a prior wrapper that returns
|
|
294
|
-
// a partial context (e.g. only systemPrompt) would silently drop
|
|
295
|
-
// tools. Spread the prior first so its fields survive, then fall
|
|
296
|
-
// back to `agent.state` for any field the prior left undefined.
|
|
297
|
-
// `messages` is owned by this wrapper.
|
|
298
|
-
const priorContext = priorResult?.context;
|
|
299
|
-
return {
|
|
300
|
-
context: {
|
|
301
|
-
...priorContext,
|
|
302
|
-
systemPrompt: priorContext?.systemPrompt ?? agent.state.systemPrompt,
|
|
303
|
-
tools: priorContext?.tools ?? agent.state.tools,
|
|
304
|
-
messages: sm.buildSessionContext().messages,
|
|
305
|
-
},
|
|
306
|
-
model: priorResult?.model,
|
|
307
|
-
thinkingLevel: priorResult?.thinkingLevel,
|
|
308
|
-
};
|
|
309
|
-
};
|
|
310
|
-
next[PNT_MARKER] = true;
|
|
311
|
-
next.__prior = prior;
|
|
312
|
-
agent.prepareNextTurn = next;
|
|
313
|
-
|
|
314
|
-
const nextWc: MarkedPntFn = async (...args: unknown[]) => {
|
|
315
|
-
// On pi ≥0.80.3 the loop calls this as (nextTurnContext, signal),
|
|
316
|
-
// where nextTurnContext = { message, toolResults, context,
|
|
317
|
-
// newMessages }. Forward args verbatim to the prior (pi's own
|
|
318
|
-
// wrapper expects exactly this shape).
|
|
319
|
-
const turn = args[0] as { context?: PntResult["context"] } | undefined;
|
|
320
|
-
let priorResult: PntResult | undefined;
|
|
321
|
-
if (typeof priorWc === "function") {
|
|
322
|
-
priorResult = await priorWc(...args);
|
|
323
|
-
}
|
|
324
|
-
// Same wholesale-replacement rationale as the prepareNextTurn
|
|
325
|
-
// wrapper above. When no prior result exists, fall back to the
|
|
326
|
-
// turn's live context (mirrors pi's own wrapper, which does
|
|
327
|
-
// `previousSnapshot?.context ?? turn.context`) so systemPrompt and
|
|
328
|
-
// tools are never dropped. `messages` is owned by this wrapper —
|
|
329
|
-
// this override is the entire point of the hook on pi ≥0.80.3, whose
|
|
330
|
-
// own wrapper refreshes every other field but leaves messages stale.
|
|
331
|
-
const priorContext = priorResult?.context ?? turn?.context;
|
|
332
|
-
return {
|
|
333
|
-
...priorResult,
|
|
334
|
-
context: {
|
|
335
|
-
...priorContext,
|
|
336
|
-
systemPrompt: priorContext?.systemPrompt ?? agent.state.systemPrompt,
|
|
337
|
-
tools: priorContext?.tools ?? agent.state.tools,
|
|
338
|
-
messages: sm.buildSessionContext().messages,
|
|
339
|
-
},
|
|
340
|
-
model: priorResult?.model,
|
|
341
|
-
thinkingLevel: priorResult?.thinkingLevel,
|
|
342
|
-
};
|
|
343
|
-
};
|
|
344
|
-
nextWc[PNTWC_MARKER] = true;
|
|
345
|
-
nextWc.__prior = priorWc;
|
|
346
|
-
agent.prepareNextTurnWithContext = nextWc;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
200
|
function findOwningSession(sm: SessionManager): AgentSession | null {
|
|
350
201
|
for (const ref of sessionInstances) {
|
|
351
202
|
const s = ref.deref();
|
|
@@ -356,6 +207,78 @@ function findOwningSession(sm: SessionManager): AgentSession | null {
|
|
|
356
207
|
return null;
|
|
357
208
|
}
|
|
358
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Resolve the provider's `streamSimple` for summarization routing via the
|
|
212
|
+
* PUBLIC modelRegistry API (no reflection).
|
|
213
|
+
*
|
|
214
|
+
* Custom providers registered via `pi.registerProvider(name, { api:
|
|
215
|
+
* <custom-id>, streamSimple })` are composed into the ModelRuntime provider
|
|
216
|
+
* returned by `ctx.modelRegistry.getProvider(...)` — its `streamSimple`
|
|
217
|
+
* dispatches to the extension handler (provider-composer `streamWith`).
|
|
218
|
+
* Without passing it as `streamFn` to `generateBranchSummary`,
|
|
219
|
+
* completeSummarization falls back to the pi-ai compat registry (builtin
|
|
220
|
+
* apis only) and throws "No API provider registered for api: <custom-id>".
|
|
221
|
+
*/
|
|
222
|
+
function resolveProviderStreamFn(
|
|
223
|
+
modelRegistry: ModelRegistry,
|
|
224
|
+
providerId: string,
|
|
225
|
+
): { streamFn: StreamFn } | undefined {
|
|
226
|
+
try {
|
|
227
|
+
const provider = modelRegistry.getProvider(providerId);
|
|
228
|
+
return provider?.streamSimple
|
|
229
|
+
? { streamFn: provider.streamSimple as StreamFn }
|
|
230
|
+
: undefined;
|
|
231
|
+
} catch {
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Drop `null` header-deletion markers (pi 0.84+ `ProviderHeaders` can carry
|
|
238
|
+
* `string | null` values). Mirrors pi's own `withoutDeletedHeaders` in
|
|
239
|
+
* agent-session.js — `generateBranchSummary` expects `Record<string, string>`.
|
|
240
|
+
*/
|
|
241
|
+
function stripNullHeaders(
|
|
242
|
+
headers: Record<string, string | null> | undefined,
|
|
243
|
+
): Record<string, string> | undefined {
|
|
244
|
+
if (!headers) return undefined;
|
|
245
|
+
const entries = Object.entries(headers).filter((entry) => entry[1] !== null);
|
|
246
|
+
return entries.length > 0
|
|
247
|
+
? (Object.fromEntries(entries) as Record<string, string>)
|
|
248
|
+
: undefined;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Project the session tree's active branch into the messages the next LLM
|
|
253
|
+
* call should see — the public-API replacement for the deleted
|
|
254
|
+
* `prepareNextTurn` double-wrap (which rebuilt `messages` from
|
|
255
|
+
* `sessionManager.buildSessionContext()` between every turn).
|
|
256
|
+
*
|
|
257
|
+
* Registered as a `context` extension handler: pi fires the `context` event
|
|
258
|
+
* inside `streamAssistantResponse` before EVERY LLM call (wired through
|
|
259
|
+
* `Agent.transformContext` → `runner.emitContext`), so this projection is
|
|
260
|
+
* applied at the wire boundary itself rather than at a turn-boundary
|
|
261
|
+
* snapshot. The loop's own `currentContext.messages` is never mutated — the
|
|
262
|
+
* handler only replaces the clone that `convertToLlm` consumes.
|
|
263
|
+
*
|
|
264
|
+
* We always replace (no leaf-gating): every `appendMessage` advances the
|
|
265
|
+
* session leaf, so a "leaf changed since last turn" heuristic would fire on
|
|
266
|
+
* essentially every call anyway — and always-replace is byte-for-byte the
|
|
267
|
+
* behavior of the old wrapper (per-turn rebuild from the tree).
|
|
268
|
+
*
|
|
269
|
+
* The parameter is typed structurally because `ReadonlySessionManager` is
|
|
270
|
+
* NOT re-exported from the package barrel (only `SessionManager` and
|
|
271
|
+
* `sessionEntryToContextMessages` are public); `ctx.sessionManager` at the
|
|
272
|
+
* event site is `ReadonlySessionManager`, which has `buildContextEntries()`.
|
|
273
|
+
*/
|
|
274
|
+
export function buildContextMessages(sm: {
|
|
275
|
+
buildContextEntries(): SessionEntry[];
|
|
276
|
+
}): ReturnType<typeof sessionEntryToContextMessages> {
|
|
277
|
+
return sm
|
|
278
|
+
.buildContextEntries()
|
|
279
|
+
.flatMap((entry) => sessionEntryToContextMessages(entry));
|
|
280
|
+
}
|
|
281
|
+
|
|
359
282
|
function refreshAgentMessages(sm: SessionManager): boolean {
|
|
360
283
|
// Manually replicate the agent-state refresh that pi's
|
|
361
284
|
// commandCtx.navigateTree does after branchWithSummary. Returns true on
|
|
@@ -518,6 +441,17 @@ export default function (
|
|
|
518
441
|
const summarize = opts?.summarize ?? generateBranchSummary;
|
|
519
442
|
patchAgentSessionPrototype();
|
|
520
443
|
|
|
444
|
+
// Public context event: replace the wire messages with the session-tree
|
|
445
|
+
// projection before every LLM call. This is the public-API replacement for
|
|
446
|
+
// the deleted `agent.prepareNextTurnWithContext` per-turn refresh — it
|
|
447
|
+
// fires inside `streamAssistantResponse` (via `Agent.transformContext` →
|
|
448
|
+
// `runner.emitContext`), before EVERY LLM call including the turn right
|
|
449
|
+
// after a mid-loop rewind, so the rewound chain (branch_summary +
|
|
450
|
+
// synthetic assistant + tool_result) reaches the next API call.
|
|
451
|
+
pi.on("context", (_event, ctx) => ({
|
|
452
|
+
messages: buildContextMessages(ctx.sessionManager),
|
|
453
|
+
}));
|
|
454
|
+
|
|
521
455
|
pi.registerTool({
|
|
522
456
|
name: "navigate_tree",
|
|
523
457
|
label: "Navigate Tree",
|
|
@@ -787,11 +721,28 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
787
721
|
}
|
|
788
722
|
|
|
789
723
|
const result = await summarize(entries, {
|
|
790
|
-
|
|
724
|
+
// `auth.baseUrl` (OAuth/credential-derived endpoint, e.g.
|
|
725
|
+
// githubCopilotOAuth) must be applied to the model, mirroring pi's
|
|
726
|
+
// own `_getSummarizationRequestAuth` (`result.auth.baseUrl ?
|
|
727
|
+
// { ...model, baseUrl: result.auth.baseUrl } : model`).
|
|
728
|
+
model: auth.baseUrl
|
|
729
|
+
? { ...ctx.model, baseUrl: auth.baseUrl }
|
|
730
|
+
: ctx.model,
|
|
791
731
|
apiKey: auth.apiKey ?? "",
|
|
792
|
-
headers: auth.headers,
|
|
732
|
+
headers: stripNullHeaders(auth.headers),
|
|
733
|
+
...(auth.env ? { env: auth.env } : {}),
|
|
793
734
|
signal: signal ?? new AbortController().signal,
|
|
794
735
|
customInstructions: p.summaryFocus,
|
|
736
|
+
// Route through the composed provider's `streamSimple` (public
|
|
737
|
+
// modelRegistry API) instead of the pi-ai compat registry.
|
|
738
|
+
// Custom providers registered via `pi.registerProvider(name,
|
|
739
|
+
// { api: <custom-id>, streamSimple })` are NOT visible to the
|
|
740
|
+
// compat registry (which only knows builtin apis) — without this,
|
|
741
|
+
// rewind fails with "No API provider registered for api:
|
|
742
|
+
// <custom-id>" for any custom-api provider (e.g. commandcode 0.5.x
|
|
743
|
+
// with api "commandcode-custom").
|
|
744
|
+
...(resolveProviderStreamFn(ctx.modelRegistry, ctx.model.provider) ??
|
|
745
|
+
{}),
|
|
795
746
|
});
|
|
796
747
|
if (result.aborted) {
|
|
797
748
|
return toolError("Summarization aborted.");
|
|
@@ -1002,14 +953,9 @@ export const __testHooks = {
|
|
|
1002
953
|
buildSyntheticAssistant,
|
|
1003
954
|
findLabelHint,
|
|
1004
955
|
findLabeledEntry,
|
|
1005
|
-
|
|
956
|
+
buildContextMessages,
|
|
1006
957
|
refreshAgentMessages,
|
|
1007
958
|
captureSession,
|
|
1008
|
-
/** Symbols used to mark the wrappers installed by `installPrepareNextTurn`
|
|
1009
|
-
* (`PNT_MARKER` on `agent.prepareNextTurn`, `PNTWC_MARKER` on
|
|
1010
|
-
* `agent.prepareNextTurnWithContext` — the field pi ≥0.80.3 prefers). */
|
|
1011
|
-
PNT_MARKER,
|
|
1012
|
-
PNTWC_MARKER,
|
|
1013
959
|
/** Read-only view of captured-session ref count for reaping assertions. */
|
|
1014
960
|
sessionRefCount(): number {
|
|
1015
961
|
return sessionInstances.length;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cad0p/pi-tree-navigator",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "agent-callable session tree navigation for pi: anchor named milestones, rewind work into a branch summary, free up context for long autonomous sessions",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"typescript": "^5.8.0"
|
|
48
48
|
},
|
|
49
49
|
"peerDependencies": {
|
|
50
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
51
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
50
|
+
"@earendil-works/pi-agent-core": ">=0.81.0",
|
|
51
|
+
"@earendil-works/pi-coding-agent": ">=0.81.0",
|
|
52
52
|
"typebox": "^1.0.0"
|
|
53
53
|
},
|
|
54
54
|
"license": "MIT"
|