@cad0p/pi-tree-navigator 0.1.1 → 0.1.2-20260824.0
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 +15 -11
- package/extensions/navigate-tree/index.ts +220 -183
- 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,10 +93,14 @@ 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
|
|
|
102
|
+
- **Tiny rewinds are rejected by a minimum-savings floor.** A `rewind` whose measured savings falls below an internal floor (~4k tokens of apparent context freed) is refused with guidance listing the active anchors instead of executing — collapsing a near-empty segment burns a summarizer LLM call and can even grow live context once the summary and its synthetic assistant land on the kept chain. This pairs with anchoring early: anchor at the start of a stage, then rewind only once real work has accumulated above the anchor.
|
|
103
|
+
|
|
100
104
|
- **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.
|
|
101
105
|
|
|
102
106
|
- **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's `normalizeMessages` filter. Other providers may have different validation rules — untested.
|
|
@@ -110,13 +114,13 @@ The synthetic assistant we inject after each rewind carries the **post-rewind ch
|
|
|
110
114
|
## Development
|
|
111
115
|
|
|
112
116
|
```bash
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
+
pnpm install
|
|
118
|
+
pnpm test # helpers + dispatch / reflection bootstrap / salvage path
|
|
119
|
+
pnpm run lint # biome check extensions/
|
|
120
|
+
pnpm run typecheck # tsc --noEmit
|
|
117
121
|
```
|
|
118
122
|
|
|
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.
|
|
123
|
+
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
124
|
|
|
121
125
|
## License
|
|
122
126
|
|
|
@@ -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 {
|
|
@@ -64,7 +67,8 @@ const LABEL_PREFIX = "anchor:";
|
|
|
64
67
|
|
|
65
68
|
// ---------------------------------------------------------------------------
|
|
66
69
|
// Exported boundary constants below (MAX_SESSION_REFS, MAX_HINT_WALK_DEPTH,
|
|
67
|
-
// MIN_SUMMARY_FOCUS_LENGTH,
|
|
70
|
+
// MIN_SUMMARY_FOCUS_LENGTH, MIN_REWIND_SAVINGS_TOKENS,
|
|
71
|
+
// MAX_SYNTHETIC_FOCUS_LENGTH).
|
|
68
72
|
//
|
|
69
73
|
// Stability: these are internal tunables. Exported only so the test suite
|
|
70
74
|
// can pin boundary cases by constant rather than literal. Re-tuning is
|
|
@@ -92,6 +96,19 @@ export const MAX_HINT_WALK_DEPTH = 50;
|
|
|
92
96
|
// reliably loses continuity — raising it forces more useful focus text
|
|
93
97
|
// without inviting verbosity.
|
|
94
98
|
export const MIN_SUMMARY_FOCUS_LENGTH = 20;
|
|
99
|
+
// Floor on the *apparent* token savings (`beforeTokens − tokensAtTarget`) a
|
|
100
|
+
// `rewind` must clear to execute. Absolute threshold, deliberately NOT
|
|
101
|
+
// window-relative: % semantics drift with window size, so a fixed token bar
|
|
102
|
+
// is the only stable shape. Every rewind carries ≈350–900 tokens of fixed
|
|
103
|
+
// overhead (the branch_summary adds ~300–800 permanent tokens on the kept
|
|
104
|
+
// chain; the synthetic assistant re-emits ~50 on every subsequent turn), so
|
|
105
|
+
// apparent savings OVERSTATES true freed context. Empirically the degenerate
|
|
106
|
+
// anchor→immediate-rewind case measures ~100 apparent tokens while a healthy
|
|
107
|
+
// stage measures ~20k — 4k separates them cleanly (~200×) and roughly equals
|
|
108
|
+
// one 300-line source-file read (~5.9k tokens measured). Below the floor a
|
|
109
|
+
// rewind burns a summarizer LLM call, forks a junk branch_summary, consumes
|
|
110
|
+
// a milestone label, and GROWS live context.
|
|
111
|
+
export const MIN_REWIND_SAVINGS_TOKENS = 4000;
|
|
95
112
|
// Cap on the `summaryFocus` length stored in the synthetic assistant's
|
|
96
113
|
// arguments. The full focus is passed live to `generateBranchSummary`, so
|
|
97
114
|
// the summarizer always sees the original; we only need a trimmed copy in
|
|
@@ -117,8 +134,6 @@ const LIST_PCT_COL_WIDTH = 5;
|
|
|
117
134
|
// is 40, but the typical kebab-case name is 8–20 chars; 28 keeps the
|
|
118
135
|
// hint column visible without truncating common names.
|
|
119
136
|
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
137
|
const ORIG_PROMPT_KEY = Symbol.for("navigate-tree.orig-prompt");
|
|
123
138
|
|
|
124
139
|
// Two warnings: list-site (read-only path; warns about the next turn's
|
|
@@ -142,15 +157,8 @@ const REFLECTION_BOOTSTRAP_WARNING_REWIND =
|
|
|
142
157
|
interface PiInternals {
|
|
143
158
|
agent: {
|
|
144
159
|
state: {
|
|
145
|
-
systemPrompt: string;
|
|
146
160
|
messages: unknown[];
|
|
147
|
-
tools: unknown[];
|
|
148
161
|
};
|
|
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
162
|
};
|
|
155
163
|
sessionManager: SessionManager;
|
|
156
164
|
}
|
|
@@ -159,34 +167,6 @@ function asInternals(session: AgentSession): PiInternals {
|
|
|
159
167
|
return session as unknown as PiInternals;
|
|
160
168
|
}
|
|
161
169
|
|
|
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
170
|
// =============================================================================
|
|
191
171
|
// Reflection bootstrap & in-loop refresh
|
|
192
172
|
// =============================================================================
|
|
@@ -226,126 +206,11 @@ function patchAgentSessionPrototype(): void {
|
|
|
226
206
|
// module and reflection finds nothing.
|
|
227
207
|
const patched = function (this: AgentSession, ...args: unknown[]) {
|
|
228
208
|
captureSession(this);
|
|
229
|
-
installPrepareNextTurn(this);
|
|
230
209
|
return orig.apply(this, args);
|
|
231
210
|
};
|
|
232
211
|
proto.prompt = patched;
|
|
233
212
|
}
|
|
234
213
|
|
|
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
214
|
function findOwningSession(sm: SessionManager): AgentSession | null {
|
|
350
215
|
for (const ref of sessionInstances) {
|
|
351
216
|
const s = ref.deref();
|
|
@@ -356,6 +221,78 @@ function findOwningSession(sm: SessionManager): AgentSession | null {
|
|
|
356
221
|
return null;
|
|
357
222
|
}
|
|
358
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Resolve the provider's `streamSimple` for summarization routing via the
|
|
226
|
+
* PUBLIC modelRegistry API (no reflection).
|
|
227
|
+
*
|
|
228
|
+
* Custom providers registered via `pi.registerProvider(name, { api:
|
|
229
|
+
* <custom-id>, streamSimple })` are composed into the ModelRuntime provider
|
|
230
|
+
* returned by `ctx.modelRegistry.getProvider(...)` — its `streamSimple`
|
|
231
|
+
* dispatches to the extension handler (provider-composer `streamWith`).
|
|
232
|
+
* Without passing it as `streamFn` to `generateBranchSummary`,
|
|
233
|
+
* completeSummarization falls back to the pi-ai compat registry (builtin
|
|
234
|
+
* apis only) and throws "No API provider registered for api: <custom-id>".
|
|
235
|
+
*/
|
|
236
|
+
function resolveProviderStreamFn(
|
|
237
|
+
modelRegistry: ModelRegistry,
|
|
238
|
+
providerId: string,
|
|
239
|
+
): { streamFn: StreamFn } | undefined {
|
|
240
|
+
try {
|
|
241
|
+
const provider = modelRegistry.getProvider(providerId);
|
|
242
|
+
return provider?.streamSimple
|
|
243
|
+
? { streamFn: provider.streamSimple as StreamFn }
|
|
244
|
+
: undefined;
|
|
245
|
+
} catch {
|
|
246
|
+
return undefined;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Drop `null` header-deletion markers (pi 0.84+ `ProviderHeaders` can carry
|
|
252
|
+
* `string | null` values). Mirrors pi's own `withoutDeletedHeaders` in
|
|
253
|
+
* agent-session.js — `generateBranchSummary` expects `Record<string, string>`.
|
|
254
|
+
*/
|
|
255
|
+
function stripNullHeaders(
|
|
256
|
+
headers: Record<string, string | null> | undefined,
|
|
257
|
+
): Record<string, string> | undefined {
|
|
258
|
+
if (!headers) return undefined;
|
|
259
|
+
const entries = Object.entries(headers).filter((entry) => entry[1] !== null);
|
|
260
|
+
return entries.length > 0
|
|
261
|
+
? (Object.fromEntries(entries) as Record<string, string>)
|
|
262
|
+
: undefined;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Project the session tree's active branch into the messages the next LLM
|
|
267
|
+
* call should see — the public-API replacement for the deleted
|
|
268
|
+
* `prepareNextTurn` double-wrap (which rebuilt `messages` from
|
|
269
|
+
* `sessionManager.buildSessionContext()` between every turn).
|
|
270
|
+
*
|
|
271
|
+
* Registered as a `context` extension handler: pi fires the `context` event
|
|
272
|
+
* inside `streamAssistantResponse` before EVERY LLM call (wired through
|
|
273
|
+
* `Agent.transformContext` → `runner.emitContext`), so this projection is
|
|
274
|
+
* applied at the wire boundary itself rather than at a turn-boundary
|
|
275
|
+
* snapshot. The loop's own `currentContext.messages` is never mutated — the
|
|
276
|
+
* handler only replaces the clone that `convertToLlm` consumes.
|
|
277
|
+
*
|
|
278
|
+
* We always replace (no leaf-gating): every `appendMessage` advances the
|
|
279
|
+
* session leaf, so a "leaf changed since last turn" heuristic would fire on
|
|
280
|
+
* essentially every call anyway — and always-replace is byte-for-byte the
|
|
281
|
+
* behavior of the old wrapper (per-turn rebuild from the tree).
|
|
282
|
+
*
|
|
283
|
+
* The parameter is typed structurally because `ReadonlySessionManager` is
|
|
284
|
+
* NOT re-exported from the package barrel (only `SessionManager` and
|
|
285
|
+
* `sessionEntryToContextMessages` are public); `ctx.sessionManager` at the
|
|
286
|
+
* event site is `ReadonlySessionManager`, which has `buildContextEntries()`.
|
|
287
|
+
*/
|
|
288
|
+
export function buildContextMessages(sm: {
|
|
289
|
+
buildContextEntries(): SessionEntry[];
|
|
290
|
+
}): ReturnType<typeof sessionEntryToContextMessages> {
|
|
291
|
+
return sm
|
|
292
|
+
.buildContextEntries()
|
|
293
|
+
.flatMap((entry) => sessionEntryToContextMessages(entry));
|
|
294
|
+
}
|
|
295
|
+
|
|
359
296
|
function refreshAgentMessages(sm: SessionManager): boolean {
|
|
360
297
|
// Manually replicate the agent-state refresh that pi's
|
|
361
298
|
// commandCtx.navigateTree does after branchWithSummary. Returns true on
|
|
@@ -404,6 +341,49 @@ function estimateAtEntry(
|
|
|
404
341
|
).tokens;
|
|
405
342
|
}
|
|
406
343
|
|
|
344
|
+
/**
|
|
345
|
+
* Rejection copy for the rewind min-savings floor (#21). Reports the
|
|
346
|
+
* apparent savings plus every active anchor — chronological root→leaf,
|
|
347
|
+
* same walk order as `list`, including labelStart itself — with its
|
|
348
|
+
* cumulative context percentage, so the agent can pick a genuinely earlier
|
|
349
|
+
* anchor or keep working. Move-on-collision means the tool itself never
|
|
350
|
+
* creates duplicate anchor names; a manual `/label anchor:x` CAN put the
|
|
351
|
+
* same name on two entries — duplicates print twice here, exactly as
|
|
352
|
+
* `list` prints them today.
|
|
353
|
+
*
|
|
354
|
+
* Deliberately does NOT print the floor value anywhere: an agent-visible
|
|
355
|
+
* numeric bar invites grinding up to it instead of doing real work.
|
|
356
|
+
*/
|
|
357
|
+
function buildMinSavingsRejection(
|
|
358
|
+
labelStart: string,
|
|
359
|
+
labelEnd: string,
|
|
360
|
+
apparentSavings: number,
|
|
361
|
+
sm: SessionManager,
|
|
362
|
+
cw: number,
|
|
363
|
+
allEntries: SessionEntry[],
|
|
364
|
+
byId: Map<string, SessionEntry>,
|
|
365
|
+
): string {
|
|
366
|
+
const anchors: string[] = [];
|
|
367
|
+
let oldestAnchorName: string | null = null;
|
|
368
|
+
for (const e of sm.getBranch()) {
|
|
369
|
+
const lbl = sm.getLabel(e.id);
|
|
370
|
+
if (!lbl?.startsWith(LABEL_PREFIX)) continue;
|
|
371
|
+
const name = lbl.slice(LABEL_PREFIX.length);
|
|
372
|
+
if (!oldestAnchorName) oldestAnchorName = name;
|
|
373
|
+
anchors.push(
|
|
374
|
+
`'${name}' ${formatPct1(estimateAtEntry(allEntries, e.id, byId), cw)}`,
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
// cw=0 → percents fall back to formatPct1's k-format and the window
|
|
378
|
+
// suffix is omitted, mirroring `list`'s header behavior.
|
|
379
|
+
const suffix = cw > 0 ? ` of ${formatWindow(cw)}` : "";
|
|
380
|
+
const guidance =
|
|
381
|
+
oldestAnchorName === labelStart
|
|
382
|
+
? "No earlier anchors — keep working and rewind later once more has accumulated above it."
|
|
383
|
+
: "Rewind further back to actually free context, or keep working.";
|
|
384
|
+
return `Rewinding '${labelStart}' → '${labelEnd}' would free only ~${formatPct1(apparentSavings, 0)} tokens. ${guidance} Active anchors: ${anchors.join(" · ")}${suffix}`;
|
|
385
|
+
}
|
|
386
|
+
|
|
407
387
|
/**
|
|
408
388
|
* Walk parentId chain back from `fromId` and return a one-line preview of
|
|
409
389
|
* the first entry that has meaningful text content. Branch summaries are
|
|
@@ -518,12 +498,26 @@ export default function (
|
|
|
518
498
|
const summarize = opts?.summarize ?? generateBranchSummary;
|
|
519
499
|
patchAgentSessionPrototype();
|
|
520
500
|
|
|
501
|
+
// Public context event: replace the wire messages with the session-tree
|
|
502
|
+
// projection before every LLM call. This is the public-API replacement for
|
|
503
|
+
// the deleted `agent.prepareNextTurnWithContext` per-turn refresh — it
|
|
504
|
+
// fires inside `streamAssistantResponse` (via `Agent.transformContext` →
|
|
505
|
+
// `runner.emitContext`), before EVERY LLM call including the turn right
|
|
506
|
+
// after a mid-loop rewind, so the rewound chain (branch_summary +
|
|
507
|
+
// synthetic assistant + tool_result) reaches the next API call.
|
|
508
|
+
pi.on("context", (_event, ctx) => ({
|
|
509
|
+
messages: buildContextMessages(ctx.sessionManager),
|
|
510
|
+
}));
|
|
511
|
+
|
|
521
512
|
pi.registerTool({
|
|
522
513
|
name: "navigate_tree",
|
|
523
514
|
label: "Navigate Tree",
|
|
524
515
|
// Stateful: every action mutates SessionManager; concurrent calls would
|
|
525
516
|
// race on `leafId` / `labelsById` and produce an undefined tree.
|
|
526
517
|
executionMode: "sequential",
|
|
518
|
+
promptGuidelines: [
|
|
519
|
+
"navigate_tree: the further back you rewind, the more you free but the more collapses into the summary; pick the earliest anchor that still preserves what you need next.",
|
|
520
|
+
],
|
|
527
521
|
description: `Long-session context management via the pi session tree. Anchor named milestones, then collapse work between them into a model-generated summary while preserving the full history on a sibling branch. Despite the verb, \`rewind\` does not restore prior state — it forks a sibling branch from the anchor and continues forward from a model-generated summary; the abandoned subtree is preserved on disk but no longer on the active path.
|
|
528
522
|
|
|
529
523
|
Operations (set the \`action\` parameter):
|
|
@@ -661,7 +655,7 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
661
655
|
type: "text",
|
|
662
656
|
text:
|
|
663
657
|
`[anchor '${p.name}'] set at ${positionLine}${hintLine}\n\n` +
|
|
664
|
-
`
|
|
658
|
+
`Once real work has accumulated after this anchor, collapse it into a summary with: navigate_tree(action='rewind', labelStart='${p.name}', labelEnd='<milestone-name>', summaryFocus='<≥${MIN_SUMMARY_FOCUS_LENGTH}-char focus: latest user instruction + done + remaining>').`,
|
|
665
659
|
},
|
|
666
660
|
],
|
|
667
661
|
details: {
|
|
@@ -713,15 +707,13 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
713
707
|
);
|
|
714
708
|
}
|
|
715
709
|
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
}
|
|
724
|
-
|
|
710
|
+
// Token math hoisted ABOVE the model check / auth await (#21): these
|
|
711
|
+
// are pure SessionManager reads, and the min-savings floor below must
|
|
712
|
+
// be evaluable — and able to reject — before any auth resolution.
|
|
713
|
+
// `allEntries`/`byId` are hoisted out of the parented-assistant branch
|
|
714
|
+
// because the floor rejection copy reports per-anchor percentages from
|
|
715
|
+
// the same maps.
|
|
716
|
+
//
|
|
725
717
|
// The leaf at execute time is the assistant that just streamed the
|
|
726
718
|
// rewind tool call. Its `usage.input` is the *minimum* of recent API
|
|
727
719
|
// calls in this turn, so estimating from it understates what the
|
|
@@ -729,6 +721,9 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
729
721
|
// assistant's usage as baseline) so beforeTokens matches the value
|
|
730
722
|
// `list` would have reported on the previous turn.
|
|
731
723
|
const oldLeafEntry = sm.getEntry(oldLeaf);
|
|
724
|
+
const allEntries = sm.getEntries();
|
|
725
|
+
const byId = new Map<string, SessionEntry>();
|
|
726
|
+
for (const e of allEntries) byId.set(e.id, e);
|
|
732
727
|
let beforeTokens: number;
|
|
733
728
|
if (
|
|
734
729
|
oldLeafEntry &&
|
|
@@ -736,14 +731,12 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
736
731
|
oldLeafEntry.message.role === "assistant" &&
|
|
737
732
|
oldLeafEntry.parentId
|
|
738
733
|
) {
|
|
739
|
-
const allEntries = sm.getEntries();
|
|
740
|
-
const byId = new Map<string, SessionEntry>();
|
|
741
|
-
for (const e of allEntries) byId.set(e.id, e);
|
|
742
734
|
beforeTokens = estimateAtEntry(allEntries, oldLeafEntry.parentId, byId);
|
|
743
735
|
} else {
|
|
744
736
|
beforeTokens = estimateActiveBranchTokens(sm);
|
|
745
737
|
}
|
|
746
|
-
const
|
|
738
|
+
const tokensAtTarget = estimateAtEntry(allEntries, target, byId);
|
|
739
|
+
const contextWindow = ctx.model?.contextWindow ?? 0;
|
|
747
740
|
|
|
748
741
|
const { entries } = collectEntriesForBranchSummary(sm, oldLeaf, target);
|
|
749
742
|
if (entries.length === 0) {
|
|
@@ -786,12 +779,61 @@ Both \`name\` (anchor) and \`labelEnd\` (rewind) write into the same anchor name
|
|
|
786
779
|
}
|
|
787
780
|
}
|
|
788
781
|
|
|
782
|
+
// Minimum-savings floor (#21): refuse rewinds whose apparent savings
|
|
783
|
+
// is below MIN_REWIND_SAVINGS_TOKENS — collapsing a near-empty segment
|
|
784
|
+
// burns a summarizer LLM call, forks a junk branch_summary, consumes
|
|
785
|
+
// a milestone label, and grows live context. Placed AFTER the
|
|
786
|
+
// synthetic-boundary guard so the chained rewind-with-no-turns case
|
|
787
|
+
// keeps its specific diagnostic, and BEFORE the model/auth checks so
|
|
788
|
+
// the rejection fires without touching provider config.
|
|
789
|
+
const apparentSavings = beforeTokens - tokensAtTarget;
|
|
790
|
+
if (apparentSavings < MIN_REWIND_SAVINGS_TOKENS) {
|
|
791
|
+
return toolError(
|
|
792
|
+
buildMinSavingsRejection(
|
|
793
|
+
p.labelStart,
|
|
794
|
+
p.labelEnd,
|
|
795
|
+
apparentSavings,
|
|
796
|
+
sm,
|
|
797
|
+
contextWindow,
|
|
798
|
+
allEntries,
|
|
799
|
+
byId,
|
|
800
|
+
),
|
|
801
|
+
{ rejected: "min-savings", apparentSavings },
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
if (!ctx.model) {
|
|
806
|
+
return toolError("No model configured for summarization.");
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
|
|
810
|
+
if (!auth.ok) {
|
|
811
|
+
return toolError(`Auth resolution failed: ${auth.error}`);
|
|
812
|
+
}
|
|
813
|
+
|
|
789
814
|
const result = await summarize(entries, {
|
|
790
|
-
|
|
815
|
+
// `auth.baseUrl` (OAuth/credential-derived endpoint, e.g.
|
|
816
|
+
// githubCopilotOAuth) must be applied to the model, mirroring pi's
|
|
817
|
+
// own `_getSummarizationRequestAuth` (`result.auth.baseUrl ?
|
|
818
|
+
// { ...model, baseUrl: result.auth.baseUrl } : model`).
|
|
819
|
+
model: auth.baseUrl
|
|
820
|
+
? { ...ctx.model, baseUrl: auth.baseUrl }
|
|
821
|
+
: ctx.model,
|
|
791
822
|
apiKey: auth.apiKey ?? "",
|
|
792
|
-
headers: auth.headers,
|
|
823
|
+
headers: stripNullHeaders(auth.headers),
|
|
824
|
+
...(auth.env ? { env: auth.env } : {}),
|
|
793
825
|
signal: signal ?? new AbortController().signal,
|
|
794
826
|
customInstructions: p.summaryFocus,
|
|
827
|
+
// Route through the composed provider's `streamSimple` (public
|
|
828
|
+
// modelRegistry API) instead of the pi-ai compat registry.
|
|
829
|
+
// Custom providers registered via `pi.registerProvider(name,
|
|
830
|
+
// { api: <custom-id>, streamSimple })` are NOT visible to the
|
|
831
|
+
// compat registry (which only knows builtin apis) — without this,
|
|
832
|
+
// rewind fails with "No API provider registered for api:
|
|
833
|
+
// <custom-id>" for any custom-api provider (e.g. commandcode 0.5.x
|
|
834
|
+
// with api "commandcode-custom").
|
|
835
|
+
...(resolveProviderStreamFn(ctx.modelRegistry, ctx.model.provider) ??
|
|
836
|
+
{}),
|
|
795
837
|
});
|
|
796
838
|
if (result.aborted) {
|
|
797
839
|
return toolError("Summarization aborted.");
|
|
@@ -1002,14 +1044,9 @@ export const __testHooks = {
|
|
|
1002
1044
|
buildSyntheticAssistant,
|
|
1003
1045
|
findLabelHint,
|
|
1004
1046
|
findLabeledEntry,
|
|
1005
|
-
|
|
1047
|
+
buildContextMessages,
|
|
1006
1048
|
refreshAgentMessages,
|
|
1007
1049
|
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
1050
|
/** Read-only view of captured-session ref count for reaping assertions. */
|
|
1014
1051
|
sessionRefCount(): number {
|
|
1015
1052
|
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-20260824.0",
|
|
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"
|