pi-context-guard-jev 0.1.0 → 0.3.1
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 +98 -253
- package/docs/CACHE.md +139 -3
- package/docs/DESIGN.md +263 -39
- package/docs/JEV.md +95 -27
- package/docs/REFERENCE.md +164 -0
- package/docs/RESULTS.md +146 -0
- package/docs/TESTING.md +116 -27
- package/package.json +2 -2
- package/src/cache-pin.ts +170 -0
- package/src/checkpoint.ts +459 -0
- package/src/config.ts +63 -2
- package/src/decide.ts +74 -0
- package/src/distill.ts +9 -2
- package/src/images.ts +89 -0
- package/src/index.ts +299 -58
- package/src/items.ts +446 -0
- package/src/process.ts +213 -0
- package/src/recall.ts +98 -6
- package/src/render.ts +2 -0
- package/src/run.ts +143 -32
- package/src/size.ts +114 -0
- package/src/stats.ts +77 -15
- package/src/types.ts +9 -0
package/README.md
CHANGED
|
@@ -1,294 +1,139 @@
|
|
|
1
1
|
# pi-context-guard-jev
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
A [Pi](https://github.com/earendil-works/pi) extension that removes tool output your agent no longer needs
|
|
4
|
+
from its context. The Jev classifier decides which lines to keep. Kept lines are copied verbatim, and the
|
|
5
|
+
model can `recall` anything that was removed.
|
|
5
6
|
|
|
6
|
-
, the next
|
|
23
|
-
request carried **9.2k instead of 35.3k tokens** after reading a 164 KB file, **11.5k instead of 34.3k**
|
|
24
|
-
after a code search, and **5.7k instead of 7.5k** after a failing test run. Every follow-up was still
|
|
25
|
-
answered correctly.
|
|
26
|
-
- **Verbatim, never rewritten.** Jev is a classifier: it answers yes/no and multiple-choice questions and
|
|
27
|
-
does not write text. Kept lines are copied byte for byte, and each removed stretch becomes one line that
|
|
28
|
-
says what was there (`[… 79 lines omitted: matches in dist/core/tools/grep.js, …]`). Unlike a summary,
|
|
29
|
-
nothing the model sees afterwards is paraphrased or invented.
|
|
30
|
-
- **Nothing is lost.** The original stays in the session file. The model can `recall` it (whole, by
|
|
31
|
-
regex, or by line range), and `/tree` back to before the edit restores it.
|
|
32
|
-
- **Judged against the work, not by size.** Truncation and size limits can't tell a needed line from
|
|
33
|
-
noise. Jev sees the question, the final answer and the earlier conversation, and decides chunk by chunk,
|
|
34
|
-
so lines the ongoing task needs survive even when this run's question was about something else. A
|
|
35
|
-
failed command the agent already got past is removed outright.
|
|
36
|
-
- **Cheap and quick.** $0.0004–0.003 and 0.6–1 s per qualifying run (measured). It runs once, after the
|
|
37
|
-
run has finished, never while the agent works.
|
|
38
|
-
- **Claude and OpenAI, cache-aware.** Tested with Claude Sonnet 5.5 and GPT-6 Luna. On Anthropic it adds a
|
|
39
|
-
cache breakpoint at the previous question, so the request after an edit still reads the conversation
|
|
40
|
-
up to there from cache.
|
|
41
|
-
- **You can see what it does.** The footer shows `🛡 −10.1k · 1`. `/guard` lists every decision with Jev's
|
|
42
|
-
probabilities, and `/guard off` turns it off for good.
|
|
43
|
-
|
|
44
|
-
**Trade-offs:**
|
|
45
|
-
- **It sends data to a third party.** Tool output goes to OpenRouter and TypeSafe ([Privacy](#privacy)).
|
|
46
|
-
- **It can cost a little more in short sessions.** The first request after an edit rewrites the prompt
|
|
47
|
-
cache, so the first gain is context room; the bill shrinks only in longer sessions
|
|
48
|
-
([Examples](#examples-measured)).
|
|
49
|
-
- **Follow-ups may need `recall`.** A question that goes somewhere new can need something that was
|
|
50
|
-
removed.
|
|
51
|
-
|
|
52
|
-
## How it works
|
|
53
|
-
|
|
54
|
-
1. **A run finishes.** When Pi is about to settle a completed run (`agent_before_settle`), the extension
|
|
55
|
-
looks at that run only: everything after the last user message.
|
|
56
|
-
2. **It picks candidates.** These are text-only tool results of at least 4,000 characters, including the
|
|
57
|
-
output of failed commands. The run is
|
|
58
|
-
skipped if the candidates add up to less than 8,000 characters.
|
|
59
|
-
3. **It chunks each output.** grep-style output is grouped by file. Other output is cut into windows of
|
|
60
|
-
roughly 0.8–4k characters, with breaks at blank lines where possible.
|
|
61
|
-
4. **It sends one Jev request per segment** (up to 32,000 characters and 40 chunks). Each request asks three
|
|
62
|
-
kinds of question about the user's question and the final answer, and, when there is one, the earlier
|
|
63
|
-
conversation of the session (so output that the ongoing work still needs is kept even if this run's
|
|
64
|
-
question was about something else):
|
|
65
|
-
- `keep_whole`: is all of this output still needed?
|
|
66
|
-
- `focus`: does the answer use most of it, none of it, or a few chunks (and if so, which one matters most)?
|
|
67
|
-
- one yes/no question per chunk: is this chunk needed?
|
|
68
|
-
5. **It writes a `context_edit` per result.** The edit keeps the needed chunks verbatim, replaces each run of
|
|
69
|
-
removed chunks with an omission line, and starts with a header that tells the model how to
|
|
70
|
-
`recall` the full output. If the kept part would still be more than 60% of the original, the result is
|
|
71
|
-
left as it is.
|
|
72
|
-
|
|
73
|
-
Errors, timeouts and unclear answers always mean *keep*. For the full algorithm, see
|
|
74
|
-
[docs/DESIGN.md](docs/DESIGN.md).
|
|
7
|
+

|
|
75
8
|
|
|
76
9
|
## Install
|
|
77
10
|
|
|
78
11
|
```bash
|
|
79
12
|
pi install npm:pi-context-guard-jev
|
|
80
|
-
# or from GitHub
|
|
81
|
-
pi install git:github.com/alandarev/pi-context-guard-jev
|
|
82
|
-
# or, for development, load it for one session from a checkout
|
|
83
|
-
pi -e ./src/index.ts
|
|
84
13
|
```
|
|
85
14
|
|
|
86
|
-
|
|
15
|
+
Requires Pi ≥ 1.0.3 and an OpenRouter credential (`OPENROUTER_API_KEY` or `/login`). Once it is ready, the
|
|
16
|
+
footer shows `🛡 0 saved`.
|
|
87
17
|
|
|
88
|
-
|
|
89
|
-
- An OpenRouter credential (`OPENROUTER_API_KEY` or `/login`) for the default classifier,
|
|
90
|
-
`openrouter/~typesafe/jev-latest`. Any classifier model in Pi's catalog can be set with the `model`
|
|
91
|
-
setting, but only the default has been tested.
|
|
18
|
+
## What it removes
|
|
92
19
|
|
|
93
|
-
|
|
20
|
+
| Item | Rule | The model then sees |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| Large tool output (≥ 4k chars) | Jev keeps the chunks the work still needs | The kept lines, verbatim, and one line per removed stretch |
|
|
23
|
+
| Small tool output (400–4k chars) | Jev: is it still needed? | A one-line note |
|
|
24
|
+
| Earlier exchange about another task | Jev: is it still relevant? | A one-line note on its prompt |
|
|
25
|
+
| Image in a tool result | After 3 turns (no Jev call) | The result's text and a one-line note |
|
|
94
26
|
|
|
95
|
-
|
|
27
|
+
The extension runs when a run finishes. In long runs it also runs mid-run, once enough old output has piled
|
|
28
|
+
up. An edit makes the next request rewrite part of the prompt cache, so mid-run edits and edits to earlier
|
|
29
|
+
exchanges wait until the smaller context pays for that. It leaves alone images you send, results of `edit`
|
|
30
|
+
and `write`, and the last two exchanges before your current prompt.
|
|
96
31
|
|
|
97
|
-
|
|
98
|
-
each Jev request sends the following to OpenRouter, which forwards it to TypeSafe:
|
|
32
|
+
## Example
|
|
99
33
|
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
- the assistant's notes written during the run (up to 2,000 characters),
|
|
103
|
-
- excerpts of the earlier conversation in this session: the latest compaction or branch summary (up to
|
|
104
|
-
2,000 characters), the first prompt (up to 1,000), and the last 3 earlier exchanges, each one prompt (up to
|
|
105
|
-
800) and the last assistant text after it (up to 1,200). Tool calls and tool outputs of earlier runs are not
|
|
106
|
-
included. Set `historyExchanges` to `0` to send none of it,
|
|
107
|
-
- the tool name and arguments,
|
|
108
|
-
- the full text of the large tool output being judged. This can include source code, logs or anything else
|
|
109
|
-
the tools returned.
|
|
34
|
+
The agent ran `rg -n -i compact dist docs` (50 KB) to find where Pi decides to auto-compact. After the
|
|
35
|
+
run, the output that later requests carry starts like this:
|
|
110
36
|
|
|
111
|
-
|
|
37
|
+
```text
|
|
38
|
+
[context-guard] Distilled the output of bash `rg -n -i compact dist docs`: kept 141 of 431 lines (15.1k of 50.4k chars); the rest was judged not needed for the answer. Full output: recall({"entryId":"1704787f"}).
|
|
39
|
+
[… 79 lines omitted: matches in dist/core/tools/grep.js, dist/core/session-manager.js, dist/core/export-html/template.js, dist/core/export-html/template.css, dist/core/sdk.js and 5 more …]
|
|
40
|
+
dist/core/compaction/compaction.d.ts:25: /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
|
41
|
+
dist/core/compaction/compaction.d.ts:28:export interface CompactionSettings {
|
|
42
|
+
dist/core/compaction/compaction.d.ts:33:export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
43
|
+
dist/core/compaction/compaction.d.ts:54:/** Estimate projected context without trusting usage captured before a later edit or compaction. */
|
|
44
|
+
dist/core/compaction/compaction.d.ts:57: * Check if compaction should trigger based on context usage.
|
|
45
|
+
dist/core/compaction/compaction.d.ts:59:export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
46
|
+
[… 44 lines omitted: matches in dist/core/compaction/compaction.d.ts, dist/core/compaction/branch-summarization.d.ts, dist/core/compaction/index.d.ts, dist/core/session-manager.d.ts, dist/core/cache-stats.js and 2 more …]
|
|
47
|
+
…
|
|
48
|
+
```
|
|
112
49
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
<https://openrouter.ai/docs/guides/privacy/data-collection>.
|
|
50
|
+
With the matches it kept, the model then answered which compaction settings a user can change and their
|
|
51
|
+
defaults correctly, without `recall`.
|
|
116
52
|
|
|
117
|
-
|
|
53
|
+
Small outputs, old exchanges and images leave one line each:
|
|
118
54
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
| `🛡 guard off` | Turned off with `/guard off` or `"enabled": false` |
|
|
125
|
-
| `🛡 no openrouter key` | No credential for the classifier's provider (the provider name is filled in) |
|
|
126
|
-
| `🛡 Jev model not found` | The configured `model` is not in Pi's classifier catalog |
|
|
55
|
+
```text
|
|
56
|
+
[context-guard] Omitted the output of bash `npm test` (1.2k chars): judged no longer needed. Full output: recall({"entryId":"…"}).
|
|
57
|
+
[context-guard] Omitted an earlier exchange judged unrelated to the current work: "which API clients retry on 503?" (6 messages, ~2.2k tokens). Full exchange: recall({"entryId":"…"}).
|
|
58
|
+
[context-guard] Removed 1 image from this output (1280×960; ~1.6k tokens), 4 turns old. recall({"entryId":"…"}) shows it again.
|
|
59
|
+
```
|
|
127
60
|
|
|
128
|
-
|
|
129
|
-
**on the current branch right now**: original length minus replacement length, for every distilled result
|
|
130
|
-
that is still in the context. It goes down after `/tree` to a point before an edit, and after compaction
|
|
131
|
-
removes distilled results. It is not a measured token count and not a cumulative total.
|
|
61
|
+
## Results
|
|
132
62
|
|
|
133
|
-
|
|
63
|
+
Context size of the next request, with the guard on and off:
|
|
134
64
|
|
|
135
|
-
|
|
|
136
|
-
|
|
137
|
-
|
|
|
138
|
-
|
|
|
139
|
-
|
|
|
65
|
+
| Task | Model | On | Off |
|
|
66
|
+
|---|---|---|---|
|
|
67
|
+
| Code search (50 KB `rg`), then a follow-up question | Claude Sonnet 5.5 | 15.5k | 28.2k |
|
|
68
|
+
| The same | GPT-6 Luna | 11.5k | 34.3k |
|
|
69
|
+
| Reading a 164 KB file, then a question about a detail | GPT-6 Luna | 9.2k | 35.3k |
|
|
70
|
+
| Failing test log, then "now fix it" | GPT-6 Luna | 5.7k | 7.5k |
|
|
71
|
+
| Autonomous run fixing 10 bugs (peak over the run) | GPT-6 Luna | 45k | 68–88k |
|
|
72
|
+
| Six screenshots, then a question about one of them | Claude Sonnet 5.5 | 9.4k | ~14.1k |
|
|
140
73
|
|
|
141
|
-
|
|
74
|
+
Every follow-up was answered correctly, and every task was finished. The screenshot row's "off" value is
|
|
75
|
+
the request before the images were removed. Jev costs $0.0003–0.007 and takes 0.6–2 s per pass. These are
|
|
76
|
+
single runs, not a benchmark. Details: [docs/RESULTS.md](docs/RESULTS.md).
|
|
142
77
|
|
|
143
|
-
##
|
|
78
|
+
## Use
|
|
144
79
|
|
|
145
|
-
|
|
146
|
-
`Full output: recall({"entryId":"…"}).` The model can call:
|
|
80
|
+
It works on its own; there is nothing to call.
|
|
147
81
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
82
|
+
- **Footer:** `🛡 −4.2k · 3` means about 4.2k tokens are kept out of context right now, by 3 edits. After a
|
|
83
|
+
compaction, `🛡 0 · Σ−79k` shows the total saved in the session.
|
|
84
|
+
- **`/guard`:** shows every decision of the last pass, with Jev's probabilities. `/guard off` and
|
|
85
|
+
`/guard on` are saved for later sessions. `/guard reload` re-reads the settings.
|
|
86
|
+
- **`recall`:** the model calls it when it needs something removed: the whole original, lines that match a
|
|
87
|
+
regex, or a line range. You can also ask for it ("recall the full test log").
|
|
88
|
+
- **Undo:** use `/tree` to go back to before an edit; the original is still in the session file.
|
|
154
89
|
|
|
155
|
-
|
|
90
|
+
## Configure
|
|
156
91
|
|
|
157
|
-
|
|
92
|
+
Optional. Put the settings in `~/.pi/agent/context-guard.json`:
|
|
158
93
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"excludeTools": ["edit", "write", "bash"],
|
|
97
|
+
"imageKeepTurns": 5,
|
|
98
|
+
"historyExchanges": 0
|
|
99
|
+
}
|
|
100
|
+
```
|
|
162
101
|
|
|
163
102
|
| Key | Default | Meaning |
|
|
164
103
|
|---|---|---|
|
|
165
104
|
| `enabled` | `true` | Master switch (`/guard on` and `/guard off` write it) |
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `noneThreshold` | `0.5` | Remove a segment entirely if `focus` picks "none" with at least this probability and no chunk passes |
|
|
173
|
-
| `maxKeepRatio` | `0.6` | Skip the edit if the kept chunks are more than this fraction of the original |
|
|
174
|
-
| `keepCitedFiles` | `false` | Also keep grep chunks for files that the final answer names (see DESIGN.md for why it is off) |
|
|
175
|
-
| `timeoutMs` | `8000` | Total time budget for all Jev requests of one run; unfinished parts are kept |
|
|
176
|
-
| `concurrency` | `6` | Jev requests in parallel |
|
|
177
|
-
| `maxSegmentChars` | `32000` | Most characters of one output per Jev request (keeps the worst-case request under Jev's 32k-token context, see docs/JEV.md) |
|
|
178
|
-
| `maxChunksPerSegment` | `40` | Most chunks per Jev request |
|
|
179
|
-
| `excludeTools` | `["edit", "write"]` | Tools whose results are never distilled |
|
|
180
|
-
| `distillErrors` | `true` | Also distill error results, such as the log of a failing test or build; Jev is told the call failed, and that its details are usually no longer needed once the agent got past it |
|
|
181
|
-
| `historyExchanges` | `3` | Earlier exchanges (prompt + last assistant text) sent to Jev so it judges relevance against the ongoing work; `0` sends no earlier conversation at all (integer 0–10) |
|
|
182
|
-
| `pinAnthropicCache` | `true` | Add an Anthropic cache breakpoint at the previous user question ([docs/CACHE.md](docs/CACHE.md)) |
|
|
183
|
-
|
|
184
|
-
Example:
|
|
185
|
-
|
|
186
|
-
```json
|
|
187
|
-
{ "minResultChars": 6000, "excludeTools": ["edit", "write", "bash"] }
|
|
188
|
-
```
|
|
105
|
+
| `excludeTools` | `["edit", "write"]` | Tools whose output is never touched |
|
|
106
|
+
| `imageKeepTurns` | `3` | Remove images from tool results after this many turns; `0` turns this off |
|
|
107
|
+
| `pruneExchanges` | `true` | Omit earlier exchanges about other tasks |
|
|
108
|
+
| `midRun` | `true` | Also run during long runs, not only at the end |
|
|
109
|
+
| `historyExchanges` | `3` | Earlier exchanges sent to Jev as context; `0` sends none |
|
|
110
|
+
| `model` | `openrouter/~typesafe/jev-latest` | Classifier model |
|
|
189
111
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
- user and assistant messages, custom messages
|
|
193
|
-
- tool results that contain images
|
|
194
|
-
- error results, if `distillErrors` is `false`
|
|
195
|
-
- results of excluded tools (`edit`, `write` by default)
|
|
196
|
-
- results shorter than `minResultChars`, and runs below `minRunChars`
|
|
197
|
-
- aborted or failed runs
|
|
198
|
-
- results that are already distilled, or that another extension already edited
|
|
199
|
-
- single lines too long for one Jev request (kept as they are)
|
|
200
|
-
- earlier runs: only the run that just finished is edited
|
|
201
|
-
|
|
202
|
-
## Limitations
|
|
203
|
-
|
|
204
|
-
- **Lossy by design.** Relevance is judged against the run's question and answer and the earlier
|
|
205
|
-
conversation (the ongoing task), so a follow-up that goes somewhere new may need something that was
|
|
206
|
-
removed. The header and omission lines say what is missing, and `recall` returns it.
|
|
207
|
-
- **Latency.** Settling waits for Jev. This adds about 0.5–1.2 s after each qualifying run (measured), and
|
|
208
|
-
never more than `timeoutMs` (8 s).
|
|
209
|
-
- **Cost.** Each qualifying run costs a few Jev requests, measured at under $0.001 per run. The first prompt
|
|
210
|
-
after an edit also has a one-time partial prompt-cache miss ([docs/CACHE.md](docs/CACHE.md)).
|
|
211
|
-
- **Steering.** A message sent during a run starts a new span. Tool results from before it are never
|
|
212
|
-
distilled.
|
|
213
|
-
|
|
214
|
-
## Examples (measured)
|
|
215
|
-
|
|
216
|
-
Real runs on public code with Pi 1.0.3: each example ran once with the guard on and once with it off
|
|
217
|
-
(`--no-guard`), from the same prompts. These are single runs, not a benchmark: the main model takes a
|
|
218
|
-
different path each time. Reproduce with `node test/e2e/examples.mjs` ([docs/TESTING.md](docs/TESTING.md)).
|
|
219
|
-
|
|
220
|
-
**A. Searching a real codebase.** In a copy of the published Pi package (`dist/` without source maps and
|
|
221
|
-
the minified bundle, plus `docs/`), the agent was asked where Pi decides to auto-compact, starting with
|
|
222
|
-
`rg -n -i compact dist docs`. Pi's bash tool returned the last 50 KB of that search. Jev kept the matches
|
|
223
|
-
in the compaction module, the settings and the decision points in `agent-session.js`, and dropped matches
|
|
224
|
-
in the grep tool, HTML export templates, the SDK and so on. Then the agent was asked "which of those
|
|
225
|
-
settings can a user change, and what are their defaults?".
|
|
226
|
-
- Claude Sonnet 5.5: 50,413 → 16,210 chars (141 of 431 lines kept). The first turn-2 request carried
|
|
227
|
-
**15.5k tokens instead of 28.2k**. The answer was correct (`enabled` true, `reserveTokens` 16384,
|
|
228
|
-
`keepRecentTokens` 20000), without `recall`.
|
|
229
|
-
- GPT-6 Luna: 51,216 → 13,023 chars (107 of 385 lines), and a 5.4k read of `compaction.js` → 1.8k; two
|
|
230
|
-
smaller outputs were left whole. Turn 2 carried **11.5k tokens instead of 34.3k** and was correct, without
|
|
231
|
-
`recall`.
|
|
232
|
-
|
|
233
|
-
**C. Reading a 164 KB source file.** The agent read `dist/core/agent-session.js` (3,535 lines) in
|
|
234
|
-
seven 500-line pieces and explained how a prompt reaches the model. Jev kept 45–164 lines of each of the first
|
|
235
|
-
four pieces and removed the last three completely (each became a one-line stub). The follow-up asked for a
|
|
236
|
-
detail the explanation never mentioned: the exact error thrown when a prompt arrives during compaction.
|
|
237
|
-
That line was among the kept ones, so the agent quoted it exactly without `recall`. The turn-2 request
|
|
238
|
-
carried **9.2k tokens instead of 35.3k**.
|
|
239
|
-
|
|
240
|
-
**B. A failing test run.** In a copy of this repository with one injected off-by-one bug, the agent ran
|
|
241
|
-
the unit tests (a 15.8k-character log, 3 failures among 116 tests) and reported the cause. Jev kept the
|
|
242
|
-
3 failures with their assertion messages and the failing-test summary, and dropped the passing tests:
|
|
243
|
-
15.8k → 8.6k chars. Turn 2 ("now fix it") carried **5.7k tokens instead of 7.5k**, and both runs fixed
|
|
244
|
-
the bug. (In the first run of this example, failing commands were never distilled; `distillErrors` was
|
|
245
|
-
added because of it.)
|
|
246
|
-
|
|
247
|
-
What the model sees after distilling (example A, Claude; the first lines of the real replacement):
|
|
112
|
+
All settings: [docs/REFERENCE.md](docs/REFERENCE.md#configuration).
|
|
248
113
|
|
|
249
|
-
|
|
250
|
-
[context-guard] Distilled the output of bash `rg -n -i compact dist docs`: kept 141 of 431 lines (15.1k of 50.4k chars); the rest was judged not needed for the answer. Full output: recall({"entryId":"1704787f"}).
|
|
251
|
-
[… 79 lines omitted: matches in dist/core/tools/grep.js, dist/core/session-manager.js, dist/core/export-html/template.js, dist/core/export-html/template.css, dist/core/sdk.js and 5 more …]
|
|
252
|
-
dist/core/compaction/compaction.d.ts:25: /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
|
253
|
-
dist/core/compaction/compaction.d.ts:28:export interface CompactionSettings {
|
|
254
|
-
dist/core/compaction/compaction.d.ts:33:export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
255
|
-
dist/core/compaction/compaction.d.ts:54:/** Estimate projected context without trusting usage captured before a later edit or compaction. */
|
|
256
|
-
dist/core/compaction/compaction.d.ts:57: * Check if compaction should trigger based on context usage.
|
|
257
|
-
dist/core/compaction/compaction.d.ts:59:export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
258
|
-
[… 44 lines omitted: matches in dist/core/compaction/compaction.d.ts, dist/core/compaction/branch-summarization.d.ts, dist/core/compaction/index.d.ts, dist/core/session-manager.d.ts, dist/core/cache-stats.js and 2 more …]
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
| Example | Model | Tool output before → after | Jev | Next request's context, on / off | Main-model cost of turn 2, on / off | Follow-up |
|
|
262
|
-
|---|---|---|---|---|---|---|
|
|
263
|
-
| A. code search | Claude Sonnet 5.5 | 50.4k → 16.2k chars | 3 requests, 0.8 s, $0.0012 | 15.5k / 28.2k tokens | $0.033 / $0.016 | correct, no `recall` |
|
|
264
|
-
| A. code search | GPT-6 Luna | 56.6k → 14.8k chars (2 outputs; 2 more, 15k, left whole) | 5 requests, 0.6 s, $0.0015 | 11.5k / 34.3k tokens | $0.0012 / $0.0005 | correct, no `recall` |
|
|
265
|
-
| C. 164 KB file | GPT-6 Luna | 163k → 26k chars (7 reads) | 7 requests, 0.9 s, $0.0027 | 9.2k / 35.3k tokens | $0.0007 / $0.0005 | correct, no `recall` |
|
|
266
|
-
| B. failing test log | GPT-6 Luna | 15.8k → 8.6k chars | 1 request, 0.8 s, $0.0004 | 5.7k / 7.5k tokens | $0.0022 / $0.0016 | fixed in both runs |
|
|
267
|
-
|
|
268
|
-
**When it doesn't help.**
|
|
114
|
+
## Privacy
|
|
269
115
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
At the per-token prices in these runs, the smaller context earns that back after about 7 more requests
|
|
276
|
-
(Claude, A), about 3 (GPT, A) or about 1 (GPT, C). Before that, the benefit is a smaller context window,
|
|
277
|
-
not a lower bill. Anthropic requests also get a cache breakpoint at the previous question, which raised
|
|
278
|
-
the first request's cache read from 1,693 to 5,810 tokens in a long run ([docs/CACHE.md](docs/CACHE.md)).
|
|
279
|
-
- **Small or fully needed outputs are left whole.** In A, GPT's 9k-character read of `agent-session.js` was
|
|
280
|
-
`not-worth` (too much of it was needed) and a 6k narrowed search was judged needed whole.
|
|
116
|
+
Jev runs on OpenRouter and TypeSafe. Each pass sends them your prompt, the agent's answer or progress
|
|
117
|
+
notes, short excerpts of earlier messages, and the text of the tool outputs being judged. That text can
|
|
118
|
+
include source code and logs. Images, `edit` and `write` results, and outputs under 400 chars are never
|
|
119
|
+
sent. Turn the extension off with `/guard off`. The full list of what is sent:
|
|
120
|
+
[docs/REFERENCE.md](docs/REFERENCE.md#data-sent-to-jev).
|
|
281
121
|
|
|
282
|
-
|
|
283
|
-
[docs/TESTING.md](docs/TESTING.md#results-so-far-pi-103).
|
|
122
|
+
## Trade-offs
|
|
284
123
|
|
|
285
|
-
|
|
124
|
+
- **The first request after an edit costs more.** The prompt cache has to be rewritten from the edit
|
|
125
|
+
onward. In short sessions you gain context room, not a lower bill.
|
|
126
|
+
- **Lossy by design.** A follow-up that goes somewhere new may need `recall` (the model sees what was
|
|
127
|
+
removed and how to get it back).
|
|
128
|
+
- **It adds 0.5–2 s** after a run, or mid-run while a checkpoint runs (at most 8 s).
|
|
286
129
|
|
|
287
|
-
|
|
288
|
-
- [docs/CACHE.md](docs/CACHE.md): prompt-cache impact (Anthropic, OpenAI) and the cost model
|
|
289
|
-
- [docs/JEV.md](docs/JEV.md): the Jev questions, probe results and threshold choices
|
|
290
|
-
- [docs/TESTING.md](docs/TESTING.md): unit tests, live end-to-end tests, manual TUI check
|
|
130
|
+
## More
|
|
291
131
|
|
|
292
|
-
|
|
132
|
+
- [docs/REFERENCE.md](docs/REFERENCE.md): every setting, status, command and `recall` parameter
|
|
133
|
+
- [docs/RESULTS.md](docs/RESULTS.md): measured runs, including where it does not help
|
|
134
|
+
- [docs/DESIGN.md](docs/DESIGN.md): how it works
|
|
135
|
+
- [docs/CACHE.md](docs/CACHE.md): prompt-cache costs on Anthropic and OpenAI
|
|
136
|
+
- [docs/JEV.md](docs/JEV.md): the Jev questions and threshold choices
|
|
137
|
+
- [docs/TESTING.md](docs/TESTING.md): unit and live tests
|
|
293
138
|
|
|
294
|
-
MIT
|
|
139
|
+
MIT license.
|
package/docs/CACHE.md
CHANGED
|
@@ -21,7 +21,8 @@ runs, not averages.
|
|
|
21
21
|
Consecutive `tool_use` blocks count as one position, and so do consecutive `tool_result` blocks.
|
|
22
22
|
- Entries with a longer TTL must come before entries with a shorter TTL.
|
|
23
23
|
- Reading an entry refreshes its TTL. The docs do **not** say that reading a longer prefix refreshes a
|
|
24
|
-
shorter entry that was written separately
|
|
24
|
+
shorter entry that was written separately; measured below, it does
|
|
25
|
+
([Mid-run checkpoints](#mid-run-checkpoints)).
|
|
25
26
|
- Pricing relative to base input: 5-minute write 1.25×, 1-hour write 2×, read 0.1× (0.05× on Opus 5.5).
|
|
26
27
|
- The minimum cacheable prompt is 512 tokens on Sonnet/Opus 5.5.
|
|
27
28
|
|
|
@@ -97,8 +98,10 @@ had more than 4 breakpoints.
|
|
|
97
98
|
|
|
98
99
|
Over WebSocket, Pi's `openai-codex` transport sends only the new input items with `previous_response_id`, as
|
|
99
100
|
long as the input only grows. After an edit, the input is no longer an extension of the previous one, so
|
|
100
|
-
Pi sends the full input. The server-side continuation is lost for that request, and
|
|
101
|
-
|
|
101
|
+
Pi sends the full input. The server-side continuation is lost for that request, and in every measurement the
|
|
102
|
+
cache read was at most the 2,560-token stable prefix, **wherever the first edited item was** (also when it
|
|
103
|
+
was 18 turns into a run; see [Mid-run checkpoints](#mid-run-checkpoints)). This happens once, on the first
|
|
104
|
+
request after the edit.
|
|
102
105
|
|
|
103
106
|
### Measured
|
|
104
107
|
|
|
@@ -112,6 +115,139 @@ GPT-6 Luna (openai-codex), badge scenario, turn 2 first request:
|
|
|
112
115
|
|
|
113
116
|
The pin does not apply to OpenAI.
|
|
114
117
|
|
|
118
|
+
## Mid-run checkpoints
|
|
119
|
+
|
|
120
|
+
A checkpoint edits outputs from earlier in the *current* run, so the next request cannot read the run from
|
|
121
|
+
cache beyond the first edited output. Two questions decide the design: does a cache entry at the
|
|
122
|
+
unchanged prefix still exist when the checkpoint comes, possibly many minutes after it was written; and
|
|
123
|
+
how much does the one-time rewrite cost against the per-request saving.
|
|
124
|
+
|
|
125
|
+
### Do cache entries on the read path stay alive? (measured)
|
|
126
|
+
|
|
127
|
+
`node test/e2e/ttl-probe.mjs` (Claude Sonnet 5.5, `pi-claude-auth`). The model `cat`s a ~31k-token file
|
|
128
|
+
(entry A ends at that result, written once as Pi's rolling breakpoint), then makes further requests; at the
|
|
129
|
+
end, the request after A is changed so that only A can be read.
|
|
130
|
+
|
|
131
|
+
| Arm | Between writing A and the test request | Test request |
|
|
132
|
+
|---|---|---|
|
|
133
|
+
| refresh | 8 requests in 7.6 min, each marking A (5m) and reading the longer prefix | cacheRead **31,001** (A alive) |
|
|
134
|
+
| control | the same 8 requests, A **not** marked again | cacheRead **30,999** (A alive) |
|
|
135
|
+
| ttl1h | as refresh, A at `ttl: "1h"` | cacheRead 31,001; but switching the earlier breakpoints to 1h was itself a full miss (cacheRead 0 → 31,001 written at the 1h price) |
|
|
136
|
+
| idle (5m) | **no request for 362 s** (Pi's cache warming stopped) | cacheRead **1,431**, cacheWrite 29,633 (A expired) |
|
|
137
|
+
| idle1h | no request for 365 s, A at 1h | cacheRead 30,977 (A alive) |
|
|
138
|
+
|
|
139
|
+
So the 5-minute TTL is real (idle), but **every request that reads a longer prefix keeps the shorter entries
|
|
140
|
+
on its path alive** (control). During an active run (requests seconds apart, and Pi's own cache warming,
|
|
141
|
+
setting `cacheWarming: "streaming"` by default, during long tool calls), every turn boundary that was once
|
|
142
|
+
the rolling breakpoint stays readable. A 1-hour TTL is not needed and would cost a full rewrite at 2× once.
|
|
143
|
+
The extension therefore keeps Pi's TTL (5m by default). Pi's warming requests go through
|
|
144
|
+
`before_provider_request` (seen in the capture), so they carry the extension's breakpoints too.
|
|
145
|
+
|
|
146
|
+
What the lookback limit still breaks: the request after an edit has its rolling breakpoint at the end; the
|
|
147
|
+
first edited output is usually many more than 20 positions back. So every Anthropic request keeps the
|
|
148
|
+
previous-question pin (the floor: system prompt and question; with `pi-claude-auth`, the relocated system
|
|
149
|
+
prompt sits in that same user message), and the first request after any edits (checkpoint, run end, or
|
|
150
|
+
another extension; the `context_edit` entries after the last answered assistant message) gets two more
|
|
151
|
+
breakpoints (`cacheAnchors` in `src/checkpoint.ts`):
|
|
152
|
+
|
|
153
|
+
- a **read point**, only at a cache entry this process saw itself write: a `tool_result` block that carried
|
|
154
|
+
a breakpoint in a request it sent, to the same model, still on the branch, and with no `context_edit` at
|
|
155
|
+
or before it and no compaction or branch summary since, written or read through within the last 5
|
|
156
|
+
minutes. A response refreshes an entry when its `cacheRead` shows the request read clearly past it: at
|
|
157
|
+
least the run's baseline (the full input of the run's first request) plus the characters from the
|
|
158
|
+
question to the entry divided by 1.5, which overestimates the tokens (code ≈ 2.3 characters per token).
|
|
159
|
+
This follows the control arm above. The latest trusted entry before the first edited entry is used. The
|
|
160
|
+
log lives in memory and is empty after a reload, so a read point is never inferred;
|
|
161
|
+
- a **write anchor** at the end of the first edited tool-result batch, so the edited prefix gets an entry
|
|
162
|
+
that the next checkpoint can read. It enters the log only if it was really in the request.
|
|
163
|
+
|
|
164
|
+
Budget: Pi uses 3 (API key: tools, system, rolling) or 4 breakpoints (OAuth: plus the identity
|
|
165
|
+
`system[0]`; `pi-claude-auth`: tools, `system[1]`, question, rolling). To make room, the identity breakpoint
|
|
166
|
+
(covered by `system[1]`) and then the tools breakpoint (covered by the system breakpoint) are dropped. If
|
|
167
|
+
that is not enough, the write anchor goes first, then the read point; the question stays. Marks use the
|
|
168
|
+
rolling breakpoint's TTL and are skipped if they would break the TTL order. Only breakpoints actually left in
|
|
169
|
+
the payload are logged, so an anchor dropped for the budget is never treated as written.
|
|
170
|
+
|
|
171
|
+
### The first request after a checkpoint (measured)
|
|
172
|
+
|
|
173
|
+
Claude Sonnet 5.5, long scenario with settings that force checkpoints into a short run
|
|
174
|
+
(`midRunBreakEven: false`, `midRunMinAgeTurns: 2`, `midRunBatchChars: 20000`), with `pi-claude-auth` and
|
|
175
|
+
`test/e2e/plain-test-runs.ts`:
|
|
176
|
+
|
|
177
|
+
| Configuration | Checkpoint | First edited output | Next request |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| default anchors | 1st (after request 3) | the run's first output | cacheRead **5,832** (system prompt + question, via the question pin), cacheWrite 10,867 |
|
|
180
|
+
| default anchors | 2nd (after request 8) | 3 turns into the run | cacheRead **16,699** (read at the entry written after the 1st checkpoint), cacheWrite 22,778 |
|
|
181
|
+
| default anchors | 3rd (after request 10) | 5 turns into the run | cacheRead **16,699**, cacheWrite 20,008 |
|
|
182
|
+
| `pinAnthropicCache: false` | 1st | the run's first output | cacheRead 5,832, cacheWrite 29,570 |
|
|
183
|
+
| `pinAnthropicCache: false` | 2nd, 3rd, 4th | 2–6 turns into the run | cacheRead **5,832** each time, cacheWrite 41,162 / 32,183 / 33,872 |
|
|
184
|
+
|
|
185
|
+
Without the guard's breakpoints, every request after a checkpoint read only the system-and-question entry
|
|
186
|
+
and rewrote the whole run. With them, the later checkpoints read up to the entry written by the first
|
|
187
|
+
request after the first checkpoint. The first checkpoint of a run usually edits the run's first output, so
|
|
188
|
+
only the system prompt and question can be read then. When a read point is placed, the write anchor often
|
|
189
|
+
does not fit the budget (question, read point, `system[1]` and rolling take all four slots).
|
|
190
|
+
|
|
191
|
+
GPT-6 Luna (openai-codex), long scenario, 8 checkpoints in 5 runs (default batch and age settings), at
|
|
192
|
+
requests 13–30: the first request after every checkpoint read **2,560 or 0** tokens from cache (17k–29k
|
|
193
|
+
uncached), against 35k–72k read on the request before it. That is the transport, not the position of the
|
|
194
|
+
edit.
|
|
195
|
+
|
|
196
|
+
### Economics and the break-even rule
|
|
197
|
+
|
|
198
|
+
At the prices in these runs (Sonnet 5.5: input $2/M, 5m write $2.5/M, read $0.2/M; GPT-6 Luna: input
|
|
199
|
+
$0.10/M, cached $0.01/M), a checkpoint costs once, and saves on every later request:
|
|
200
|
+
|
|
201
|
+
- OpenAI Codex: the whole context is sent uncached once: extra ≈ 0.9 × context × input price.
|
|
202
|
+
- Anthropic: everything after the read point is written again: extra ≈ (1.25 − 0.1) × rewritten tokens.
|
|
203
|
+
- Saving per later request: the removed tokens × the cache-read price. Jev removed 60–90% of checkpoint
|
|
204
|
+
outputs in the runs.
|
|
205
|
+
|
|
206
|
+
Measured (GPT, a run with two checkpoints): checkpoint 1 removed 57k characters; the next request was 17,144 tokens uncached
|
|
207
|
+
(≈ $0.0015 extra) and every later request carried about 19k tokens less (≈ $0.0002 saved each): break-even
|
|
208
|
+
after about 8 requests, and the run went on for 12 more. Checkpoint 2 needed about 9 requests and the run
|
|
209
|
+
ended 2 requests later. On Anthropic the ratio of write to read price is larger (12.5:1 vs 10:1).
|
|
210
|
+
|
|
211
|
+
So checkpoints pay for themselves only when the run goes on for roughly 8–30 more requests. The
|
|
212
|
+
**break-even rule** (`midRunBreakEven`, on) skips a checkpoint unless
|
|
213
|
+
`pending chars × turns so far ≥ factor × rewrite chars` (OpenAI: factor 13, rewrite = whole context;
|
|
214
|
+
Anthropic: factor 16, rewrite = context after the read point the next request would get, or after the
|
|
215
|
+
question when no logged entry is trusted, or the whole context with pinning off; other providers: factor
|
|
216
|
+
16, rewrite = context from the first eligible output), assuming the run goes on
|
|
217
|
+
about as long as it has run so far. Context size is measured in text characters (`src/size.ts`): an image
|
|
218
|
+
counts as 4 characters per token (Pi's chars ÷ 4; Anthropic: width × height / 750 tokens), not its base64 length, and a
|
|
219
|
+
thinking signature counts at half its length. Counting the base64 once blocked every checkpoint in a long
|
|
220
|
+
Claude run with screenshots (rewrite estimated at 2–22M characters for a 150–370k-token context).
|
|
221
|
+
In the measured Claude runs (8–14 requests) it never allowed a
|
|
222
|
+
checkpoint; in the GPT runs (26–37 requests) it allowed one or two. Turn it off to checkpoint purely for
|
|
223
|
+
context room.
|
|
224
|
+
|
|
225
|
+
## Old exchanges
|
|
226
|
+
|
|
227
|
+
An omitted exchange comes before the current question, so the first request after it cannot read the
|
|
228
|
+
question's cache entry: that prefix contains the edit. `cacheAnchors` handles this like any other edit: the
|
|
229
|
+
read point is the latest trusted logged entry before the earliest edited entry, and the question pin is
|
|
230
|
+
still placed (it writes a new entry for the requests that follow). Without such an entry, only the tools and
|
|
231
|
+
system prompt are read. The break-even estimate (`anthropicRewriteChars`) follows the same rule: with an edit
|
|
232
|
+
at or before the question and no trusted read point, the rewrite is the whole context.
|
|
233
|
+
|
|
234
|
+
Measured (`topics` scenario, Claude Sonnet 5.5, first request of prompt 5 after the first exchange was
|
|
235
|
+
omitted at the end of prompt 4 with `exchangeBreakEven: false`): cacheRead 1,760 (tools and system),
|
|
236
|
+
cacheWrite 6,280, against cacheRead 12,376 and cacheWrite 438 with the guard off. With the default gate the
|
|
237
|
+
omission was deferred and prompt 5 read 11,049 tokens from cache. On OpenAI Codex any edit makes the next
|
|
238
|
+
request read at most the 2,560-token stable prefix (see above).
|
|
239
|
+
|
|
240
|
+
So each omission costs one rewrite of everything after the omitted exchange on the next request (Pi's
|
|
241
|
+
default `cacheWarming: "streaming"` keeps the cache warm between prompts that come within the TTL) and saves
|
|
242
|
+
the exchange on every request after it. At run end, omissions therefore go through a break-even gate
|
|
243
|
+
(`exchangeBreakEven`): their marginal rewrite (beyond what the pass's other edits rewrite anyway) must pay
|
|
244
|
+
off over as many later requests as the session has had so far, and they must save at least
|
|
245
|
+
`exchangeMinSavingChars` (8,000 characters); otherwise they wait and accumulate. On OpenAI Codex a pass that
|
|
246
|
+
edits anything else already costs the whole context, so exchange omissions in that pass are free. Mid-run, exchanges join the checkpoint batch only when the batch with them pays off; their early
|
|
247
|
+
position makes the rewrite most of the context, so with factor 16 the batch must reach about
|
|
248
|
+
1/16 of the context per turn so far (for example, old exchanges that are half of the context pay off from
|
|
249
|
+
about turn 32).
|
|
250
|
+
|
|
115
251
|
## Cost model
|
|
116
252
|
|
|
117
253
|
All units are input-token equivalents, with a 5-minute TTL (write 1.25×, read 0.1×).
|