pi-zip 0.2.0 → 0.2.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/README.md +1 -0
- package/package.json +1 -1
- package/src/index.ts +19 -2
- package/src/placeholder.ts +6 -3
- package/src/plan.ts +12 -4
- package/src/run.ts +12 -1
package/README.md
CHANGED
|
@@ -29,6 +29,7 @@ Known limits:
|
|
|
29
29
|
|
|
30
30
|
- **GLM** (automatic prefix cache that outlives its declared 5 minutes): v0.1 cost about 1.2× BC live. v0.2 learns the real cache lifetime and folds the previous turn after the declared TTL (offline 0.95–0.97× v0.1), but this was not verified live.
|
|
31
31
|
- **Long autonomous runs** (one prompt, hundreds of tool calls, e.g. sub-agents): roughly on par with BC, not better. Outputs are only folded inside a running turn once they are 60 requests old.
|
|
32
|
+
- **Tool allowlists** (`pi --tools read,bash`, and sub-agent launchers that pass one): Pi then hides `zip_recall`, so pi-zip folds only outputs the model can re-read (files, read-only commands) and the placeholder says to re-read. Add `zip_recall` to the list to get full folding.
|
|
32
33
|
- If you always answer within the cache lifetime, there is little to save, by design.
|
|
33
34
|
|
|
34
35
|
## What you see
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-zip",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Keeps long Pi sessions cheap without losing anything: folds old tool output only when the prompt cache has already gone cold, and every fold can be recalled byte for byte. Zero config.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
package/src/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// pi-zip: keeps long Pi sessions cheap without losing anything. Wiring only; the logic lives in the sibling modules.
|
|
2
2
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { registerZipCommand } from "./notice.ts";
|
|
4
|
+
import { RECALL_TOOL } from "./placeholder.ts";
|
|
4
5
|
import { registerRecallTool } from "./recall.ts";
|
|
5
6
|
import { Zip } from "./run.ts";
|
|
6
7
|
|
|
@@ -9,8 +10,24 @@ export default function piZip(pi: ExtensionAPI) {
|
|
|
9
10
|
const zip = new Zip(pi);
|
|
10
11
|
registerRecallTool(pi, zip); // stays registered even when off: earlier folds must stay recallable, and a tool-list change would bust the cache
|
|
11
12
|
registerZipCommand(pi, zip);
|
|
12
|
-
pi
|
|
13
|
-
|
|
13
|
+
// A tool allowlist (`pi --tools read,bash`, sub-agent launchers) replaces the whole selection and Pi then does not even register
|
|
14
|
+
// zip_recall; folds of outputs the model could not get back would be lost, so only rereadable outputs fold then (plan rereadOnly).
|
|
15
|
+
const checkRecall = () => {
|
|
16
|
+
try {
|
|
17
|
+
const active = pi.getActiveTools?.();
|
|
18
|
+
if (Array.isArray(active)) zip.setRecallOk(active.includes(RECALL_TOOL));
|
|
19
|
+
} catch {
|
|
20
|
+
/* never in the way */
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
pi.on("session_start", (_e, ctx) => {
|
|
24
|
+
checkRecall();
|
|
25
|
+
return zip.sessionStart(ctx);
|
|
26
|
+
});
|
|
27
|
+
pi.on("before_agent_start", (_e, ctx) => {
|
|
28
|
+
checkRecall();
|
|
29
|
+
return zip.beforeAgentStart(ctx);
|
|
30
|
+
});
|
|
14
31
|
pi.on("context_with_system", (e, ctx) => zip.context(e, ctx)); // the complete transcript: system messages stay where Pi put them
|
|
15
32
|
pi.on("turn_end", (e, ctx) => zip.turnEnd(e, ctx));
|
|
16
33
|
pi.on("agent_before_settle", (e, ctx) => zip.settle(e, ctx));
|
package/src/placeholder.ts
CHANGED
|
@@ -108,6 +108,7 @@ export function outcomeHint(text: string, tool: string, isError?: boolean): stri
|
|
|
108
108
|
export interface PlaceholderMeta {
|
|
109
109
|
turn?: number; // user turn the output belongs to (1-based, as seen in the projection when it was folded)
|
|
110
110
|
isError?: boolean;
|
|
111
|
+
noRecall?: boolean; // zip_recall is not declared to the model (`--tools` allowlist): point at re-reading instead
|
|
111
112
|
}
|
|
112
113
|
|
|
113
114
|
/** Placeholder text, or null when the output is too short to be worth folding. The text is stored with the fold (context_edit),
|
|
@@ -121,15 +122,17 @@ export function makePlaceholder(text: string, tool: string, args: string, handle
|
|
|
121
122
|
return (
|
|
122
123
|
`${PH_MARK} · ${tool}${args ? " " + args : ""}${meta.turn ? ` · turn ${meta.turn}` : ""}${outcome ? ` · ${outcome}` : ""} · ${text.length} chars, ${lines} lines · handle ${handle}]\n` +
|
|
123
124
|
(keys.length ? `key lines kept (original line numbers; up to ${keep}):\n${body}\n` : "") +
|
|
124
|
-
|
|
125
|
+
(!meta.noRecall
|
|
126
|
+
? `Original kept byte for byte, recallable even after summaries or compaction: ${RECALL_TOOL}("${handle}") (optional grep/range) is instant, free, no side effects; prefer it to re-running or re-reading (output may differ). Do not guess its content.`
|
|
127
|
+
: `Original not shown (${RECALL_TOOL} is not enabled in this session): re-read the file or re-run the read-only command if you need it. Do not guess its content.`)
|
|
125
128
|
);
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
/** Placeholder for a toolResult block (null for image results, entries without an id, or short outputs). */
|
|
129
|
-
export function makePlaceholderFor(b: Block, calls: Calls, keep = 8): string | null {
|
|
132
|
+
export function makePlaceholderFor(b: Block, calls: Calls, keep = 8, noRecall = false): string | null {
|
|
130
133
|
const content = b.msg.content;
|
|
131
134
|
if (Array.isArray(content) && content.some((c: Any) => c?.type !== "text")) return null;
|
|
132
135
|
if (!b.entryId) return null;
|
|
133
136
|
const call = calls.get(b.msg.toolCallId);
|
|
134
|
-
return makePlaceholder(textOf(content), call?.name ?? b.msg.toolName ?? "tool", call ? shortArgs(call.args) : "", handleFor(b.entryId), keep, { turn: b.userTurn, isError: !!b.msg.isError });
|
|
137
|
+
return makePlaceholder(textOf(content), call?.name ?? b.msg.toolName ?? "tool", call ? shortArgs(call.args) : "", handleFor(b.entryId), keep, { turn: b.userTurn, isError: !!b.msg.isError, noRecall });
|
|
135
138
|
}
|
package/src/plan.ts
CHANGED
|
@@ -282,6 +282,8 @@ export interface PlanOpts {
|
|
|
282
282
|
relax?: boolean; // default RELAX_PREV_TURN
|
|
283
283
|
minGain?: number;
|
|
284
284
|
inturnAge?: number; // default settings().inturnAge (PI_ZIP_INTURN_AGE, 60); 0 = outputs of the protected turns never fold on age
|
|
285
|
+
noSummary?: boolean; // a summary was already made while this cache stayed warm: no second one unless the context is at/above the compaction room
|
|
286
|
+
rereadOnly?: boolean; // zip_recall is not available to the model: fold only outputs that can be re-read (classify "rereadable")
|
|
285
287
|
pastTtl?: boolean; // a user return after the declared TTL: a warm plan may relax into the previous user turn too (RELAX_PREV_TURN)
|
|
286
288
|
}
|
|
287
289
|
|
|
@@ -366,7 +368,7 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
366
368
|
};
|
|
367
369
|
const addFold = (b: Block, trig: string): boolean => {
|
|
368
370
|
if (tokOverride.has(b.idx) || recalled.has(handleFor(b.entryId!))) return false; // F4: never refold what the model recalled
|
|
369
|
-
const ph = makePlaceholderFor(b, calls, keepLines);
|
|
371
|
+
const ph = makePlaceholderFor(b, calls, keepLines, !!o.rereadOnly);
|
|
370
372
|
if (ph === null) return false;
|
|
371
373
|
const phTok = tok4(ph);
|
|
372
374
|
if (!(phTok < 0.9 * b.tokens)) return false;
|
|
@@ -384,7 +386,7 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
384
386
|
const head = law.pr?.cls === "automatic" ? MIN_EXPECT / k : 0;
|
|
385
387
|
const start: number[] = [];
|
|
386
388
|
blocks.reduce((acc, b) => ((start[b.idx] = acc), acc + b.tokens), o.sys);
|
|
387
|
-
const foldable = (b: Block) => b.kind === "toolResult" && !b.edited && !!b.entryId && b.tokens > foldMin && !isRecall(b) && start[b.idx] >= head;
|
|
389
|
+
const foldable = (b: Block) => b.kind === "toolResult" && !b.edited && !!b.entryId && b.tokens > foldMin && !isRecall(b) && start[b.idx] >= head && (!o.rereadOnly || classify(b) === "rereadable");
|
|
388
390
|
const protectedTurn = (b: Block) => b.userTurn >= userTurns - PROTECT_USER_TURNS + 1;
|
|
389
391
|
const savings = () => folds.reduce((a, t) => a + t.entryTokens - t.phTokens, 0);
|
|
390
392
|
const cands = blocks.filter((b) => foldable(b) && !protectedTurn(b));
|
|
@@ -416,7 +418,13 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
416
418
|
}
|
|
417
419
|
}
|
|
418
420
|
let ctxAfterFolds = ctxEst - savings();
|
|
419
|
-
|
|
421
|
+
// what no summary can remove: the system prompt, the protected turns (after their folds) and the previous summary, which the next
|
|
422
|
+
// one carries forward; the cap is never chased below it (a context that is mostly this floor would be re-summarised for nothing)
|
|
423
|
+
const prevSum = blocks[0]?.kind === "summary" ? blocks[0].tokens : 0;
|
|
424
|
+
const floor = o.sys + prevSum + blocks.reduce((a, b) => a + (protectedTurn(b) ? (tokOverride.get(b.idx) ?? b.tokens) : 0), 0);
|
|
425
|
+
const target = Math.max(coldCap, floor + SUMMARY_FLOOR);
|
|
426
|
+
const atRoom = room !== null && k * ctxAfterFolds >= room;
|
|
427
|
+
const sumTrigger: "cold" | "valve" | null = ctxAfterFolds > target && (!o.noSummary || atRoom) ? trig : null;
|
|
420
428
|
let cutIdx: number | null = null;
|
|
421
429
|
let prefixTokens = 0;
|
|
422
430
|
let summaryTokensPlanned = 0;
|
|
@@ -435,7 +443,7 @@ export function planContext(entries: Any[], o: PlanOpts): PlanResult | null {
|
|
|
435
443
|
acc += tokOverride.get(i) ?? blocks[i].tokens;
|
|
436
444
|
}
|
|
437
445
|
const total = o.sys + acc;
|
|
438
|
-
const S = (x: number) => clamp(SUMMARY_RATIO * x, SUMMARY_FLOOR, SUMMARY_CAP);
|
|
446
|
+
const S = (x: number) => Math.max(clamp(SUMMARY_RATIO * x, SUMMARY_FLOOR, SUMMARY_CAP), prevSum); // the next summary carries the previous one forward
|
|
439
447
|
if (cuts.length) {
|
|
440
448
|
let pick = cuts[cuts.length - 1];
|
|
441
449
|
for (const c of cuts) if (total - pre[c] + S(pre[c]) <= coldCap) { pick = c; break; }
|
package/src/run.ts
CHANGED
|
@@ -92,6 +92,15 @@ export class Zip implements ZipControl {
|
|
|
92
92
|
|
|
93
93
|
active = () => !this.off && !this.conflict;
|
|
94
94
|
|
|
95
|
+
/** zip_recall declared to the model? Checked at session start and before every run (index.ts); a `--tools` allowlist hides it. */
|
|
96
|
+
recallOk = true;
|
|
97
|
+
/** A summary was persisted since the last cold run start: later warm plans make none unless at the compaction room. */
|
|
98
|
+
summarizedWarm = false;
|
|
99
|
+
setRecallOk(ok: boolean) {
|
|
100
|
+
if (ok !== this.recallOk) this.ledger({ type: "recall_available", ok });
|
|
101
|
+
this.recallOk = ok;
|
|
102
|
+
}
|
|
103
|
+
|
|
95
104
|
ledger(rec: Record<string, unknown>) {
|
|
96
105
|
const path = process.env.PI_ZIP_LEDGER; // test only
|
|
97
106
|
if (!path) return;
|
|
@@ -139,7 +148,7 @@ export class Zip implements ZipControl {
|
|
|
139
148
|
private opts(ctx: Any, pending: boolean, mode: "cold" | "warm", entries: Any[], pWarm = mode === "cold" ? 0 : 1): { o: PlanOpts; cal: Calibration } {
|
|
140
149
|
const sys = this.sysTokens(ctx);
|
|
141
150
|
const cal = calibrate(entries, sys);
|
|
142
|
-
return { cal, o: { mode, sys, k: cal.k, cwd: (ctx?.cwd as string) ?? process.cwd(), recalled: this.recalled, model: ctx?.model, promptPending: pending, reserve: this.reserve(ctx), steerIds: this.steerIds, law: this.law(ctx, pWarm), trace: [] } };
|
|
151
|
+
return { cal, o: { mode, sys, k: cal.k, cwd: (ctx?.cwd as string) ?? process.cwd(), recalled: this.recalled, model: ctx?.model, promptPending: pending, rereadOnly: !this.recallOk, noSummary: mode === "warm" && this.summarizedWarm, reserve: this.reserve(ctx), steerIds: this.steerIds, law: this.law(ctx, pWarm), trace: [] } };
|
|
143
152
|
}
|
|
144
153
|
|
|
145
154
|
private law(ctx: Any, pWarm: number): Law {
|
|
@@ -227,6 +236,7 @@ export class Zip implements ZipControl {
|
|
|
227
236
|
if (!this.prevKey) ({ key: this.prevKey, total: this.prevTotal } = lastPrompt(branch)); // fresh process: the session's newest response
|
|
228
237
|
const { cold, reason, ttl, pWarm: p, src, gapS, pastTtl } = detectCold(ctx.model, this.lastReqMs, branch, Date.now(), this.lastModelKey, (g, prior) => pWarm(this.ent, g, prior));
|
|
229
238
|
this.cold = cold;
|
|
239
|
+
if (cold) this.summarizedWarm = false; // a cold return may summarise again
|
|
230
240
|
this.pastTtl = pastTtl;
|
|
231
241
|
this.pWarm = p;
|
|
232
242
|
this.survSrc = src;
|
|
@@ -505,6 +515,7 @@ export class Zip implements ZipControl {
|
|
|
505
515
|
return undefined;
|
|
506
516
|
}
|
|
507
517
|
const bad = checkEdits(o.blocks, o.userTurns, live, cut);
|
|
518
|
+
if (!bad && cut) this.summarizedWarm = true; // one summary per warm stretch (two in a row cost two uncached calls; the second gains little)
|
|
508
519
|
if (bad) {
|
|
509
520
|
this.ledger({ type: "guard_drop", turnIndex: e.turnIndex, reason: bad, folds: live.length, source: plan.source });
|
|
510
521
|
plan.persisted = false; // keep the request-local view: never switch the fold set mid-run
|