pi-fold 1.0.1 → 1.0.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
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# pi-fold
|
|
2
2
|
|
|
3
|
+
[](https://doi.org/10.5281/zenodo.21856873)
|
|
4
|
+
|
|
3
5
|
Agent-governed lossless context folding for Pi. A session folds its own transcript instead of losing it: the stale end collapses into short briefs while the exact originals stay on disk, addressed and verified by SHA-256, one tool call away.
|
|
4
6
|
|
|
5
7
|
The full write-up, with interactive versions of every mechanism on this page and the complete measurement story, is at [shaneconner.com/projects/pi-fold](https://shaneconner.com/projects/pi-fold/).
|
|
@@ -28,7 +30,7 @@ Everything else follows from one invariant, and it is the reason several otherwi
|
|
|
28
30
|
|
|
29
31
|
**So the runtime may move a byte in the window only at a moment it is already rewriting the window**, which is a commit or a fold. Everything else is a pure append at the tail, and an appended byte is never later altered, shortened, removed, or repositioned. Stated plainly: never mutate the window, and never show something and then take it back. The second half is the one that costs features. Anything that displays state and then updates it in place is out, however useful the display was, because updating in place is a mid-prefix write with better manners.
|
|
30
32
|
|
|
31
|
-
**Marks are free, because a mark moves nothing.** Under `foldScheduling: "epoch"` a `fold` action records a pending mark and returns immediately. The mark lives in durable session state, outside the window; the projection stays byte-identical and the cached prefix survives. The agent can therefore mark as it works, the moment a span stops
|
|
33
|
+
**Marks are free, because a mark moves nothing.** Under `foldScheduling: "epoch"` a `fold` action records a pending mark and returns immediately. The mark lives in durable session state, outside the window; the projection stays byte-identical and the cached prefix survives. The agent can therefore mark as it works, the moment a span stops mattering, without weighing each fold against a cache invalidation.
|
|
32
34
|
|
|
33
35
|
**A commit is the one moment bytes move, and every accumulated decision lands at once.** It goes through the same preparation and commit machinery an immediate fold uses, so a committed mark and an immediate fold produce the identical fold record. Committing is the runtime's call, not a verb the agent can spend: measured 2026-08-07, an agent given the verb called it twice and the runtime correctly held it both times, which is surface without function.
|
|
34
36
|
|
|
@@ -50,6 +52,8 @@ One 64-stage staged assignment over the curl C repository: one Pi session per ru
|
|
|
50
52
|
|
|
51
53
|
There is one run per arm, and native did not finish: it ended its turn at stage 56 of 64. That leaves two pairings, and both are reported here rather than the flattering one.
|
|
52
54
|
|
|
55
|
+
The measurement is written up as a paper, with the figure sources, the redacted per-request ledgers, and the campaign log deposited beside it: [doi.org/10.5281/zenodo.21856873](https://doi.org/10.5281/zenodo.21856873). The story of how the campaign got here is on [Medium](https://medium.com/@shane.conner/compaction-doesnt-have-to-mean-starting-over-89c0b319d1a6).
|
|
56
|
+
|
|
53
57
|
### Same seed, both cut at stage 56
|
|
54
58
|
|
|
55
59
|
**Basis:** each arm is cut at the ledger record that delivered the stage 56 payload, and usage and cost are summed from the same billed records at or before that cut, including native's three compaction records, which all precede it. Same seed, same plan hash `6ed7c5f9e957`, both runs on 2026-08-07.
|
|
@@ -90,7 +94,7 @@ There is one run per arm, and native did not finish: it ended its turn at stage
|
|
|
90
94
|
|
|
91
95
|
**Where the cost numbers come from.** Cost is Pi's own per-request ledger accounting, not a rate card chosen for the writeup. Every billed record's cost equals that record's own usage priced at the tier Pi applied: base rates of $5.00/M input, $0.50/M cache read and $30.00/M output, and a long-context tier of $10.00/M, $1.00/M and $45.00/M on any request whose context exceeds the model's 272,000-token window. That reproduces the ledger to the cent on both arms, including pi-fold's complete-run total of $13.89. Reasoning tokens sit inside output and are not billed separately. Native's three compaction summarizations are themselves billed calls, $0.58 at stage 56 and $0.54 on the complete run.
|
|
92
96
|
|
|
93
|
-
**Caveats, because
|
|
97
|
+
**Caveats, because the numbers rest on them.**
|
|
94
98
|
|
|
95
99
|
- **Fresh input is essentially identical.** 0.95x at stage 56 and 1.06x on the complete pairing: neither runtime consistently reads less new material. The 6.4 million token gap at stage 56 is re-sent accumulated context, 6.29 million of it cache read alone, not a difference in how much material was consumed.
|
|
96
100
|
- **No wall-clock claim on the complete pairing.** pi-fold was 5.6% slower there, 34.2 minutes against 32.4. The wall-clock win belongs to the stage-56 pairing only.
|
|
@@ -116,7 +120,7 @@ Below 80% occupancy of the serving budget the runtime is quiet: nothing folds au
|
|
|
116
120
|
|
|
117
121
|
Chapter briefs are generated by the session's model by default, using one request per fold with a bounded input span, bounded output, and no tool calls. Every summarizer failure falls back to a deterministic brief; set `summarizer: "deterministic"` to disable model calls entirely. The default spends provider tokens, as native compaction does. A cheaper model from the same provider is the recommended summarizer: a session on GPT 5.6 Sol can delegate fold briefs to GPT 5.6 Luna at medium effort with `summarizer: { provider: "openai", model: "gpt-5.6-luna", effort: "medium" }`, using the provider id from your Pi model registry.
|
|
118
122
|
|
|
119
|
-
The serving budget is the total window less the output reservation actually in force, because the per-request max-input descriptor a provider advertises assumes a full reservation and understates the real ceiling. With no `providerTotalWindow` supplied the runtime falls back to the transport's descriptor, which is the
|
|
123
|
+
The serving budget is the total window less the output reservation actually in force, because the per-request max-input descriptor a provider advertises assumes a full reservation and understates the real ceiling. With no `providerTotalWindow` supplied the runtime falls back to the transport's descriptor, which is the conservative default for a model it has no total-window fact for; the descriptor is reported in status either way, so the gap stays auditable. A provider-window fence aborts an unsafe request before transmission if no lossless fold can make room, and says so out of band rather than by writing into the window.
|
|
120
124
|
|
|
121
125
|
By default, evidence ingestion writes read-only artifact files under the session directory's `pi-fold-evidence/`, sets them to mode `0444`, and enforces a 512 MB session cap. These immutable files provide exact-recovery anchors for oversized tool results. Set `evidenceIngestion: false` to disable the hook and all evidence writes.
|
|
122
126
|
|
|
@@ -162,7 +166,7 @@ pi-fold combines two recent lines of work in one mechanism. Lossless hierarchica
|
|
|
162
166
|
|
|
163
167
|
Context-Folding ([arXiv:2510.11967](https://arxiv.org/abs/2510.11967), ByteDance Seed) has the agent branch into a sub-trajectory and fold it on completion, with the behavior shaped by reinforcement learning; the fold keeps a summary of the outcome rather than a lossless path back. MemGPT and its successor [Letta](https://github.com/letta-ai/letta) are the canonical agent-held memory-verb systems: the agent edits its own memory blocks and archives while the window pages against external stores. Two things separate pi-fold from that line. First, what is governed: Letta manages traffic between the window and outside storage, and when its message queue overflows the in-window record becomes a lossy recursive summary; pi-fold restructures the transcript in place. Second, recovery: theirs is retrieval, a search that re-ingests whatever comes back as new tokens; ours is expansion, which restores the exact folded entries after SHA-256 verification.
|
|
164
168
|
|
|
165
|
-
The
|
|
169
|
+
The placement is narrow. The lossless DAG is shared ground. The distinguishing claim is agent curation, and the measured run never exercised it, so the numbers above belong to the fallback. The part that is new is stated here and not yet measured.
|
|
166
170
|
|
|
167
171
|
The design also pairs with Pi's session trees: a fold's collapsed-or-expanded state is a natural unit for curating exactly which context a subagent or workflow leg inherits.
|
|
168
172
|
|
|
@@ -1385,7 +1385,7 @@ export function registerActiveContext(pi: any, options: {
|
|
|
1385
1385
|
if (chars === null || chars < PROJECTION_CALIBRATION_MIN_CHARS) return;
|
|
1386
1386
|
if (!Number.isFinite(measurement.tokens) || measurement.tokens < PROJECTION_CALIBRATION_MIN_TOKENS) return;
|
|
1387
1387
|
// The signed error of the estimate we made for the projection this measurement
|
|
1388
|
-
// describes. This is the only
|
|
1388
|
+
// describes. This is the only direct measure of how wrong the fence can be.
|
|
1389
1389
|
// Only a CALIBRATED estimate's error belongs in the window. The bootstrap constant
|
|
1390
1390
|
// is wrong by design -- 76% high in rep12 -- and letting that one reading set the
|
|
1391
1391
|
// margin would keep every later request permanently inside it.
|
|
@@ -174,7 +174,7 @@ export function updateAdvisoryMilestone(
|
|
|
174
174
|
* Wording rule for every dosage below: report what is HAPPENING and name the actions
|
|
175
175
|
* that change it. The earlier phrasing asked the agent to fold proactively, which is
|
|
176
176
|
* advice it can only act on by interrupting its own task; the runtime folds either way,
|
|
177
|
-
* so the
|
|
177
|
+
* so the useful thing to say is what it will do and what reacting buys.
|
|
178
178
|
*/
|
|
179
179
|
function curationText(milestone: AdvisoryMilestone, percent: number, toolName: string): string {
|
|
180
180
|
if (milestone === "orientation") {
|
|
@@ -272,7 +272,7 @@ export function advanceCurationGate(input: {
|
|
|
272
272
|
/**
|
|
273
273
|
* The last-call notice.
|
|
274
274
|
*
|
|
275
|
-
* Shape rule,
|
|
275
|
+
* Shape rule, chosen deliberately: operational status plus available actions, with continuing
|
|
276
276
|
* work as the stated default. A message shaped like a question inviting a chat-style
|
|
277
277
|
* reply can make a single-turn agent emit a final answer and end its own run, so this
|
|
278
278
|
* never asks anything and never implies a reply is expected.
|
|
@@ -77,7 +77,7 @@ export function pendingMarks(state: Pick<ActiveContextState, "pendingMarks">): P
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/**
|
|
80
|
-
* Property ORDER
|
|
80
|
+
* Property ORDER matters here: the state digest is a stable stringify, so an
|
|
81
81
|
* appended key would replay as digest drift. Rebuild the tail of the record in the
|
|
82
82
|
* same order `parseActiveContextState` produces.
|
|
83
83
|
*/
|
|
@@ -195,7 +195,7 @@ export function surfacingLog(state: Pick<ActiveContextState, "surfacing">): Surf
|
|
|
195
195
|
}
|
|
196
196
|
|
|
197
197
|
/**
|
|
198
|
-
* Property ORDER
|
|
198
|
+
* Property ORDER matters here: the state digest is a stable stringify, so a
|
|
199
199
|
* `surfacing` key assigned onto a state that did not already carry one would land
|
|
200
200
|
* after `pendingMarks`/`advisory`/`prepared` and replay as digest drift. Rebuild the
|
|
201
201
|
* tail of the record in the same order `parseActiveContextState` produces.
|
package/package.json
CHANGED