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
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# Reference
|
|
2
|
+
|
|
3
|
+
Everything you can see, call and configure. How it works inside: [DESIGN.md](DESIGN.md).
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Pi β₯ 1.0.3
|
|
8
|
+
- An OpenRouter credential (`OPENROUTER_API_KEY` or `/login`) for the default classifier,
|
|
9
|
+
`openrouter/~typesafe/jev-latest`. Any classifier model in Pi's catalog can be set with the `model`
|
|
10
|
+
setting, but only the default has been tested.
|
|
11
|
+
|
|
12
|
+
If the model or credential is missing, the extension does nothing and the status bar says so.
|
|
13
|
+
|
|
14
|
+
## Status bar
|
|
15
|
+
|
|
16
|
+
| Text | Meaning |
|
|
17
|
+
|---|---|
|
|
18
|
+
| `π‘ 0 saved` | On and ready; nothing is distilled on this branch yet |
|
|
19
|
+
| `π‘ 0 Β· Ξ£β79k` | A compaction dropped the earlier edits from context: nothing is kept out right now, about 79k tokens were saved on this branch in total |
|
|
20
|
+
| `π‘ distillingβ¦` | Jev is judging the outputs of the run that just finished, or of a mid-run checkpoint |
|
|
21
|
+
| `π‘ β4.2k Β· 1` | β tokens kept out of context Β· items: about 4.2k tokens are currently kept out of context, by 1 distilled or omitted tool output, output without its images, or old exchange |
|
|
22
|
+
| `π‘ β4.2k Β· 1 Β· Ξ£β83k` | The same, after a compaction: Ξ£ is the total saved on this branch, including edits the compaction dropped |
|
|
23
|
+
| `π‘ guard off` | Turned off with `/guard off` or `"enabled": false` |
|
|
24
|
+
| `π‘ no openrouter key` | No credential for the classifier's provider (the provider name is filled in) |
|
|
25
|
+
| `π‘ Jev model not found` | The configured `model` is not in Pi's classifier catalog |
|
|
26
|
+
|
|
27
|
+
The token figure is Pi's usual estimate, characters Γ· 4. It counts what is kept out of the model's context
|
|
28
|
+
**on the current branch right now**: original size minus replacement size, for every distilled output,
|
|
29
|
+
removed image and omitted exchange that is still in the context (an image counts as about width Γ height Γ· 750
|
|
30
|
+
tokens). It goes down after `/tree` to a point before an edit, and after compaction removes distilled
|
|
31
|
+
results; then Ξ£ shows the total of every pass on the branch so far. It is not a measured token count.
|
|
32
|
+
|
|
33
|
+
## `/guard` command
|
|
34
|
+
|
|
35
|
+
| Command | Effect |
|
|
36
|
+
|---|---|
|
|
37
|
+
| `/guard` or `/guard status` | Shows whether the guard is on, the model, the savings on this branch (distilled outputs and omitted exchanges), Jev runs and mid-run checkpoints, requests and cost, whether checkpoints and exchange pruning are on, and the last run's or checkpoint's result for each item (large, small, exchange), including Jev traces such as `kw.15 focus=chunk_4:.52 keep 9/29` or `p.06` |
|
|
38
|
+
| `/guard on` / `/guard off` | Turns the guard on or off and saves `enabled` to the settings file |
|
|
39
|
+
| `/guard reload` | Re-reads the settings file and shows any warnings again |
|
|
40
|
+
|
|
41
|
+
Settings are read at session start and on `/guard reload`.
|
|
42
|
+
|
|
43
|
+
## `recall` tool
|
|
44
|
+
|
|
45
|
+
Every distilled output and omitted exchange starts with `[context-guard]` and ends with
|
|
46
|
+
`recall({"entryId":"β¦"})`. For an omitted exchange, recall returns the whole exchange as a transcript
|
|
47
|
+
(prompt, replies, tool calls and results, custom messages). The model can call:
|
|
48
|
+
|
|
49
|
+
| Parameter | Meaning |
|
|
50
|
+
|---|---|
|
|
51
|
+
| `entryId` (required) | The entry id from the header |
|
|
52
|
+
| `pattern` | JavaScript regex (β€ 500 chars, 1 s limit); returns only matching lines, prefixed with their line numbers |
|
|
53
|
+
| `offset` | First line to return, 1-based. When `pattern` is set, this counts matching lines. |
|
|
54
|
+
| `limit` | Most lines to return |
|
|
55
|
+
|
|
56
|
+
Like Pi's `read` tool, the output is capped at 2,000 lines and 50 KB, and a note says how to continue.
|
|
57
|
+
|
|
58
|
+
## Configuration
|
|
59
|
+
|
|
60
|
+
The settings file is `~/.pi/agent/context-guard.json` (inside Pi's agent directory), or the path in
|
|
61
|
+
`PI_CONTEXT_GUARD_CONFIG`. Every key is optional. Values that are missing, the wrong type or out of range
|
|
62
|
+
fall back to the default.
|
|
63
|
+
|
|
64
|
+
| Key | Default | Meaning |
|
|
65
|
+
|---|---|---|
|
|
66
|
+
| `enabled` | `true` | Master switch (`/guard on` and `/guard off` write it) |
|
|
67
|
+
| `model` | `"openrouter/~typesafe/jev-latest"` | Classifier as `provider/id`; the first `/` separates the provider |
|
|
68
|
+
| `minResultChars` | `4000` | Tool results from this size are chunked; shorter ones are judged whole (see `smallResultMinChars`) |
|
|
69
|
+
| `minRunChars` | `8000` | Distill a run's large outputs only if they add up to at least this (small outputs and exchanges are judged regardless) |
|
|
70
|
+
| `keepWholeThreshold` | `0.7` | Keep a segment untouched if P(`keep_whole`) is at least this |
|
|
71
|
+
| `focusWholeThreshold` | `0.6` | Keep a segment untouched if `focus` picks "whole" with at least this probability |
|
|
72
|
+
| `chunkKeepThreshold` | `0.6` | Keep a chunk if P(chunk needed) is at least this |
|
|
73
|
+
| `noneThreshold` | `0.5` | Remove a segment entirely if `focus` picks "none" with at least this probability and no chunk passes |
|
|
74
|
+
| `maxKeepRatio` | `0.6` | Skip the edit if the kept chunks are more than this fraction of the original |
|
|
75
|
+
| `keepCitedFiles` | `false` | Also keep grep chunks for files that the final answer names (see DESIGN.md for why it is off) |
|
|
76
|
+
| `timeoutMs` | `8000` | Total time budget for all Jev requests of one run; unfinished parts are kept |
|
|
77
|
+
| `concurrency` | `6` | Jev requests in parallel |
|
|
78
|
+
| `maxSegmentChars` | `32000` | Most characters of one output per Jev request (keeps the worst-case request under Jev's 32k-token context, see [JEV.md](JEV.md)) |
|
|
79
|
+
| `maxChunksPerSegment` | `40` | Most chunks per Jev request |
|
|
80
|
+
| `excludeTools` | `["edit", "write"]` | Tools whose results are never distilled |
|
|
81
|
+
| `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 |
|
|
82
|
+
| `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) |
|
|
83
|
+
| `pinAnthropicCache` | `true` | Anthropic cache breakpoints: at the previous user question on every request, and on the first request after context edits a read point (at a cache entry this process wrote) and a write anchor ([CACHE.md](CACHE.md)) |
|
|
84
|
+
| `midRun` | `true` | Mid-run checkpoints during long runs ([RESULTS.md](RESULTS.md#long-autonomous-runs)) |
|
|
85
|
+
| `midRunMinAgeTurns` | `4` | Judge an output mid-run only once it is at least this many turns old (integer 1β100) |
|
|
86
|
+
| `midRunBatchChars` | `60000` | Run a checkpoint only when the not-yet-judged eligible outputs add up to this many characters |
|
|
87
|
+
| `midRunBatchCharsOpenAI` | `60000` | The same, for `openai-codex` models |
|
|
88
|
+
| `midRunChunkKeepThreshold` | `0.6` | Keep a chunk at a checkpoint if P(still needed) is at least this (probe in [JEV.md](JEV.md)) |
|
|
89
|
+
| `midRunBreakEven` | `true` | Skip a checkpoint unless its one-time prompt-cache rewrite is likely to pay off; `false` checkpoints purely for context room |
|
|
90
|
+
| `smallResultMinChars` | `400` | Tool results from this size up to `minResultChars` are judged as whole items; `0` turns this off |
|
|
91
|
+
| `smallKeepThreshold` | `0.45` | Keep a small output if P(still needed) is at least this (probe in [JEV.md](JEV.md)) |
|
|
92
|
+
| `pruneExchanges` | `true` | Judge old exchanges and omit the unrelated ones |
|
|
93
|
+
| `keepRecentExchanges` | `2` | The last this many exchanges before the current prompt are never omitted |
|
|
94
|
+
| `exchangeOmitThreshold` | `0.2` | Omit an exchange only if P(still relevant) is below this (probe in [JEV.md](JEV.md)) |
|
|
95
|
+
| `exchangeBreakEven` | `true` | At run end, omit unrelated exchanges only when the cache rewrite they cause pays off; deferred ones are judged again at later run ends and omitted once their total pays off. `false` always omits them |
|
|
96
|
+
| `exchangeMinSavingChars` | `8000` | A run end omits exchanges only if they save at least this many characters together (when they cost a cache rewrite) |
|
|
97
|
+
| `imageKeepTurns` | `3` | Remove images from tool results once this many assistant messages have followed them (`recall` shows them again); images in user messages are kept; `0` turns this off |
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
|
|
101
|
+
```json
|
|
102
|
+
{ "minResultChars": 6000, "excludeTools": ["edit", "write", "bash"] }
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Data sent to Jev
|
|
106
|
+
|
|
107
|
+
**This extension sends data to a third party, and it is on by default.** After every run that qualifies,
|
|
108
|
+
each Jev request sends the following to OpenRouter, which forwards it to TypeSafe:
|
|
109
|
+
|
|
110
|
+
- the user's question (up to 4,000 characters),
|
|
111
|
+
- the final answer (up to 6,000 characters),
|
|
112
|
+
- the assistant's notes written during the run (up to 2,000 characters),
|
|
113
|
+
- excerpts of the earlier conversation in this session: the latest compaction or branch summary (up to
|
|
114
|
+
2,000 characters), the first prompt (up to 1,000), and the last 3 earlier exchanges, each one prompt
|
|
115
|
+
(up to 800) and the last assistant text after it (up to 1,200). Tool calls and tool outputs of earlier runs
|
|
116
|
+
are not included in this part. Set `historyExchanges` to `0` to send none of it,
|
|
117
|
+
- the tool name and arguments,
|
|
118
|
+
- the full text of the large tool output being judged, and of small outputs (from 400 characters; up to
|
|
119
|
+
4,000 characters each). This can include source code, logs or anything else the tools returned,
|
|
120
|
+
- excerpts of old exchanges being judged: each prompt (up to 600 characters), the last assistant text (up to
|
|
121
|
+
800), the tool calls as short labels (tool name and the command or path, up to 80 characters each) and the
|
|
122
|
+
size. Set `pruneExchanges` to `false` to send none of it.
|
|
123
|
+
|
|
124
|
+
Never sent: tool outputs below `smallResultMinChars` (400 characters), results of excluded tools (`edit`,
|
|
125
|
+
`write`), images (never sent: they are removed by age), the text of outputs with images, old exchanges with entries another extension changed,
|
|
126
|
+
exchanges below 2,000 characters, and the last `keepRecentExchanges` (2) exchanges before the current
|
|
127
|
+
prompt. Large outputs are sent only when the run's large outputs add up to `minRunChars` (8,000); small
|
|
128
|
+
outputs and old exchanges are sent at every run end and every mid-run checkpoint that has any.
|
|
129
|
+
|
|
130
|
+
`/guard off` turns the extension off and saves that choice in the settings file, so it stays off in later
|
|
131
|
+
sessions. See OpenRouter's data policy:
|
|
132
|
+
<https://openrouter.ai/docs/guides/privacy/data-collection>.
|
|
133
|
+
|
|
134
|
+
## What it never touches
|
|
135
|
+
|
|
136
|
+
- messages of the current run other than tool results, and the last `keepRecentExchanges` exchanges before
|
|
137
|
+
the current prompt
|
|
138
|
+
- compaction and branch summaries, exchanges that did not finish (the last reply has tool calls), and
|
|
139
|
+
messages Pi cannot edit (system messages, `!` shell executions), which stay in place inside an omitted
|
|
140
|
+
exchange
|
|
141
|
+
- images in your own messages; images in tool results younger than `imageKeepTurns` (3) turns
|
|
142
|
+
- the text of tool results that contain images (only the images are removed, by age)
|
|
143
|
+
- error results, if `distillErrors` is `false`
|
|
144
|
+
- results of excluded tools (`edit`, `write` by default)
|
|
145
|
+
- results shorter than `smallResultMinChars` (400), and large results of runs below `minRunChars`
|
|
146
|
+
- aborted or failed runs
|
|
147
|
+
- results that are already distilled, or that another extension already edited
|
|
148
|
+
- single lines too long for one Jev request (kept as they are)
|
|
149
|
+
- tool outputs of earlier runs one by one: earlier runs are only touched as whole exchanges (and their old
|
|
150
|
+
images)
|
|
151
|
+
|
|
152
|
+
## Limitations
|
|
153
|
+
|
|
154
|
+
- **Lossy by design.** Relevance is judged against the run's question and answer and the earlier
|
|
155
|
+
conversation (the ongoing task), so a follow-up that goes somewhere new may need something that was
|
|
156
|
+
removed. The header and omission lines say what is missing, and `recall` returns it. An earlier task
|
|
157
|
+
that comes back after several unrelated ones may have been omitted as a whole exchange.
|
|
158
|
+
- **Latency.** Settling waits for Jev. This adds about 0.5β1.2 s after each qualifying run (measured), and
|
|
159
|
+
never more than `timeoutMs` (8 s). A mid-run checkpoint pauses the run the same way.
|
|
160
|
+
- **Cost.** Each qualifying run costs a few Jev requests, measured at under $0.001 per run. The first prompt
|
|
161
|
+
after an edit also has a one-time partial prompt-cache miss ([CACHE.md](CACHE.md)).
|
|
162
|
+
- **Steering.** A message sent during a run joins the run's question. Outputs from before it can be distilled,
|
|
163
|
+
but the first edit before it rewrites the whole prompt cache once (the question pin moves to the steering
|
|
164
|
+
message).
|
package/docs/RESULTS.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# Measured results
|
|
2
|
+
|
|
3
|
+
Single runs on Pi 1.0.3, not a benchmark: the main model takes a different path each time. Costs are what Pi
|
|
4
|
+
reports at list price. How to reproduce them: [TESTING.md](TESTING.md).
|
|
5
|
+
|
|
6
|
+
## Examples
|
|
7
|
+
|
|
8
|
+
Real runs on public code: each example ran once with the guard on and once with it off (`--no-guard`), from
|
|
9
|
+
the same prompts. Reproduce with `node test/e2e/examples.mjs`.
|
|
10
|
+
Examples AβC judge large outputs only (their numbers match `smallResultMinChars: 0`); example D shows the
|
|
11
|
+
default configuration and `exchangeBreakEven: false`.
|
|
12
|
+
|
|
13
|
+
**A. Searching a real codebase.** In a copy of the published Pi package (`dist/` without source maps and
|
|
14
|
+
the minified bundle, plus `docs/`), the agent was asked where Pi decides to auto-compact, starting with
|
|
15
|
+
`rg -n -i compact dist docs`. Pi's bash tool returned the last 50 KB of that search. Jev kept the matches
|
|
16
|
+
in the compaction module, the settings and the decision points in `agent-session.js`, and dropped matches
|
|
17
|
+
in the grep tool, HTML export templates, the SDK and so on. Then the agent was asked "which of those
|
|
18
|
+
settings can a user change, and what are their defaults?".
|
|
19
|
+
- Claude Sonnet 5.5: 50,413 β 16,210 chars (141 of 431 lines kept). The first turn-2 request carried
|
|
20
|
+
**15.5k tokens instead of 28.2k**. The answer was correct (`enabled` true, `reserveTokens` 16384,
|
|
21
|
+
`keepRecentTokens` 20000), without `recall`.
|
|
22
|
+
- GPT-6 Luna: 51,216 β 13,023 chars (107 of 385 lines), and a 5.4k read of `compaction.js` β 1.8k; two
|
|
23
|
+
smaller outputs were left whole. Turn 2 carried **11.5k tokens instead of 34.3k** and was correct, without
|
|
24
|
+
`recall`.
|
|
25
|
+
|
|
26
|
+
**C. Reading a 164 KB source file.** The agent read `dist/core/agent-session.js` (3,535 lines) in
|
|
27
|
+
seven 500-line pieces and explained how a prompt reaches the model. Jev kept 45β164 lines of each of the first
|
|
28
|
+
four pieces and removed the last three completely (each became a one-line stub). The follow-up asked for a
|
|
29
|
+
detail the explanation never mentioned: the exact error thrown when a prompt arrives during compaction.
|
|
30
|
+
That line was among the kept ones, so the agent quoted it exactly without `recall`. The turn-2 request
|
|
31
|
+
carried **9.2k tokens instead of 35.3k**.
|
|
32
|
+
|
|
33
|
+
**B. A failing test run.** In a copy of this repository with one injected off-by-one bug, the agent ran
|
|
34
|
+
the unit tests (a 15.8k-character log, 3 failures among 116 tests) and reported the cause. Jev kept the
|
|
35
|
+
3 failures with their assertion messages and the failing-test summary, and dropped the passing tests:
|
|
36
|
+
15.8k β 8.6k chars. Turn 2 ("now fix it") carried **5.7k tokens instead of 7.5k**, and both runs fixed
|
|
37
|
+
the bug. Failing commands are distilled because `distillErrors` is on by default.
|
|
38
|
+
|
|
39
|
+
What the model sees after distilling (example A, Claude; the first lines of the real replacement):
|
|
40
|
+
|
|
41
|
+
```text
|
|
42
|
+
[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"}).
|
|
43
|
+
[β¦ 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 β¦]
|
|
44
|
+
dist/core/compaction/compaction.d.ts:25: /** Extension-specific data (e.g., ArtifactIndex, version markers for structured compaction) */
|
|
45
|
+
dist/core/compaction/compaction.d.ts:28:export interface CompactionSettings {
|
|
46
|
+
dist/core/compaction/compaction.d.ts:33:export declare const DEFAULT_COMPACTION_SETTINGS: CompactionSettings;
|
|
47
|
+
dist/core/compaction/compaction.d.ts:54:/** Estimate projected context without trusting usage captured before a later edit or compaction. */
|
|
48
|
+
dist/core/compaction/compaction.d.ts:57: * Check if compaction should trigger based on context usage.
|
|
49
|
+
dist/core/compaction/compaction.d.ts:59:export declare function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean;
|
|
50
|
+
[β¦ 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 β¦]
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
| Example | Model | Tool output before β after | Jev | Next request's context, on / off | Main-model cost of turn 2, on / off | Follow-up |
|
|
54
|
+
|---|---|---|---|---|---|---|
|
|
55
|
+
| 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` |
|
|
56
|
+
| 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` |
|
|
57
|
+
| 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` |
|
|
58
|
+
| 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 |
|
|
59
|
+
| D. five topics | Claude Sonnet 5.5, `exchangeBreakEven: false` | 1 old exchange omitted (8.8k chars) | 2 requests, $0.0003 | 8.0k / 12.8k tokens (prompt 5) | $0.017 / $0.006 (prompt 5's first request) | correct, searched again |
|
|
60
|
+
|
|
61
|
+
**D. Several topics in one session.** In the generated test repo, five prompts in a row: which API
|
|
62
|
+
clients retry on 503; a README paragraph for `StatusBar`; a count of HTTP 500 lines in a log; where
|
|
63
|
+
`StatusBadge` is used; and then "back to the clients that retry on 503: which of them also retry on 502?"
|
|
64
|
+
(`node test/e2e/run-e2e.mjs --scenario topics --turns 5`). The README exchange is under 2,000 characters
|
|
65
|
+
and is never judged; the last 2 exchanges before each prompt are never judged either.
|
|
66
|
+
|
|
67
|
+
| Model, configuration | First exchange at the end of prompt 4 | At the end of prompt 5 | Prompt 5 | Context of prompt 5's first request | Main-model cost | Jev |
|
|
68
|
+
|---|---|---|---|---|---|---|
|
|
69
|
+
| GPT-6 Luna, default | judged unrelated (P = 0.07), **deferred**: too small to pay for the cache rewrite in a 4-prompt session | relevant (P = 0.95), kept | correct, no `recall` | 7.5k, 6,656 read from cache | $0.0018 | $0.0005 |
|
|
70
|
+
| Claude Sonnet 5.5, default | unrelated (P = 0.07), deferred | relevant (P = 0.97), kept | correct, no `recall` | 11.5k, 11,049 read from cache | $0.073 | $0.0003 |
|
|
71
|
+
| Claude Sonnet 5.5, `exchangeBreakEven: false` | unrelated (P = 0.08), **omitted** (8.8k characters, 5 entries) | β | correct, searched again | 8.0k: 1,760 read, 6,280 written | $0.087 | $0.0003 |
|
|
72
|
+
| Claude Sonnet 5.5, guard off | β | β | β | 12.8k | $0.083 | β |
|
|
73
|
+
|
|
74
|
+
In this short session the gate kept the first exchange, which the follow-up then needed. With the gate off,
|
|
75
|
+
the omission made the next request rewrite the cache from the omitted exchange on (it comes before the
|
|
76
|
+
question pin) and the model searched again; the smaller context pays off only in longer sessions.
|
|
77
|
+
|
|
78
|
+
**When it doesn't help.**
|
|
79
|
+
|
|
80
|
+
- **The first request after an edit costs more, not less.** The prompt cache only matches up to the
|
|
81
|
+
first changed message, so the next request writes the shorter history again instead of reading the long
|
|
82
|
+
one from cache. In A that made Claude's turn-2 request cost $0.033 instead of $0.016, and GPT's $0.0012
|
|
83
|
+
instead of $0.0005; OpenAI read nothing from cache on that request (in the fixture runs it read 2,560
|
|
84
|
+
tokens instead of 13,824: a partial miss).
|
|
85
|
+
At the per-token prices in these runs, the smaller context earns that back after about 7 more requests
|
|
86
|
+
(Claude, A), about 3 (GPT, A) or about 1 (GPT, C). Before that, the benefit is a smaller context window,
|
|
87
|
+
not a lower bill. Anthropic requests also get a cache breakpoint at the previous question; in a run with
|
|
88
|
+
11 sequential tool calls it raised the first request's cache read from 1,693 to 5,810 tokens compared
|
|
89
|
+
with `pinAnthropicCache: false` ([CACHE.md](CACHE.md)).
|
|
90
|
+
- **Small or fully needed outputs are left whole.** In A, GPT's 9k-character read of `agent-session.js` was
|
|
91
|
+
`not-worth` (too much of it was needed) and a 6k narrowed search was judged needed whole.
|
|
92
|
+
|
|
93
|
+
Numbers on the generated StatusBadge test repo (96% removed, `recall` of a removed line) are in
|
|
94
|
+
[TESTING.md](TESTING.md#results-pi-103).
|
|
95
|
+
|
|
96
|
+
## Long autonomous runs
|
|
97
|
+
|
|
98
|
+
**Measured** (Pi 1.0.3, e2e scenario `long`: "fix all failing tests" in a generated project with 10 bugs,
|
|
99
|
+
the full suite after every fix; single runs, not a benchmark; costs are what Pi reports at list price):
|
|
100
|
+
|
|
101
|
+
| Main model | Configuration | Requests | Peak context | Main-model cost | Checkpoints (Jev) | Suite green |
|
|
102
|
+
|---|---|---|---|---|---|---|
|
|
103
|
+
| GPT-6 Luna | guard off | 32 / 37 | 68.4k / 87.7k | $0.020 / $0.030 | β | yes / yes |
|
|
104
|
+
| GPT-6 Luna | `midRun: false` (run end only) | 26 | 86.3k | $0.022 | β (run end $0.006) | yes |
|
|
105
|
+
| GPT-6 Luna | default | 29 | **45.0k** | $0.021 | 2 at requests 16 and 24, 0.8 s each, removed 62k and 60k chars ($0.006 with run end) | yes |
|
|
106
|
+
| GPT-6 Luna | `smallResultMinChars: 0` (large outputs only) | 31 | 72.4k | $0.024 | 1 at request 19, 1.0 s, removed 154k chars ($0.007 with run end) | yes |
|
|
107
|
+
| Claude Sonnet 5.5 | guard off | 14 | 89.8k | $0.41 | β | yes |
|
|
108
|
+
| Claude Sonnet 5.5 | `smallResultMinChars: 0` | 8 | 65.7k | $0.24 | none: too short to pay off (run end $0.003) | yes |
|
|
109
|
+
| Claude Sonnet 5.5 | forced: `midRunBreakEven: false`, `midRunMinAgeTurns: 2`, `midRunBatchChars: 20000`, `smallResultMinChars: 0` | 12 | 43.3k | $0.34 | 3 ($0.0032 with run end) | yes |
|
|
110
|
+
|
|
111
|
+
Context per request (every 5th request), GPT-6 Luna:
|
|
112
|
+
|
|
113
|
+
| Request | 1 | 6 | 11 | 16 | 21 | 26 | 31 | 36 |
|
|
114
|
+
|---|---|---|---|---|---|---|---|---|
|
|
115
|
+
| guard off (37 requests) | 3.0k | 14.6k | 29.3k | 41.4k | 52.1k | 62.5k | 73.3k | 83.4k |
|
|
116
|
+
| default (29 requests) | 3.2k | 17.0k | 35.3k | 41.0k | 39.3k | 28.9k | β | β |
|
|
117
|
+
|
|
118
|
+
At the default run's first checkpoint, Jev distilled four superseded test logs (for example 30,164 β 8,163)
|
|
119
|
+
and replaced 4 of 7 small reads of files the agent had already edited with one-line notes (for example
|
|
120
|
+
3,386 β 145); the next request carried 20.1k instead of 41.0k tokens. No run needed `recall`.
|
|
121
|
+
|
|
122
|
+
**Trade-offs:**
|
|
123
|
+
- **OpenAI Codex reads nothing from cache after any edit** (2,560 or 0 tokens on the next request, after
|
|
124
|
+
every checkpoint measured, wherever the edit was). Each checkpoint re-sends the context uncached once and
|
|
125
|
+
pays back after about 8β10 later requests. In these 26β37-request runs the main-model cost came out
|
|
126
|
+
about the same with and without checkpoints, plus about $0.005 of Jev per run; the gain is a smaller
|
|
127
|
+
context.
|
|
128
|
+
- **On Anthropic a checkpoint rewrites the cache after the latest entry the next request can read** (at worst after the question; writing costs 12.5Γ the read price).
|
|
129
|
+
In the forced run, the requests after the three checkpoints read 5.8k / 16.7k / 16.7k tokens and wrote
|
|
130
|
+
10.9k / 22.8k / 20.0k. After the first checkpoint of a run only the system prompt and question can be
|
|
131
|
+
read, because the run's first output is usually the first one edited. Claude solved the task in 8β14
|
|
132
|
+
requests, too few for a checkpoint to pay off, so with the default break-even rule it never
|
|
133
|
+
checkpointed; forced checkpoints in such short runs cost more than they saved. Checkpoints are meant
|
|
134
|
+
for runs that go on for dozens of requests.
|
|
135
|
+
- **Latency mid-run:** a checkpoint pauses the run for the Jev requests (0.5β2.2 s measured, at most
|
|
136
|
+
`timeoutMs`).
|
|
137
|
+
- Details: [CACHE.md](CACHE.md#mid-run-checkpoints), [DESIGN.md](DESIGN.md#mid-run-checkpoints),
|
|
138
|
+
wording probe in [JEV.md](JEV.md#checkpoint-wording).
|
|
139
|
+
|
|
140
|
+
## Old images
|
|
141
|
+
|
|
142
|
+
Claude Sonnet 5.5 with `pi-claude-auth`, 2026-10-08: six 1200Γ800 PNGs, read one per message, then a
|
|
143
|
+
follow-up about the second one. The run end removed the four images that were 3β6 turns old, with no Jev
|
|
144
|
+
request (19.9k chars β 5k tokens). The next request carried 2 images instead of 6: 9.4k input tokens
|
|
145
|
+
instead of about 14.1k. Told not to read the file again, the model called `recall`, got the image back and
|
|
146
|
+
read the code number in it correctly.
|
package/docs/TESTING.md
CHANGED
|
@@ -13,15 +13,19 @@ These need Node β₯ 22.18, which runs the `.ts` files directly. There is no netw
|
|
|
13
13
|
|
|
14
14
|
| File | Covers |
|
|
15
15
|
|---|---|
|
|
16
|
-
| `
|
|
16
|
+
| `checkpoint.test.ts` | Mid-run eligibility and age, the memo and candidate rules, later calls, notes, superseded detection (edits, re-reads, reruns), cache anchors (read point only from logged entries of the same model within the TTL, invalidated by later edits and compaction, parallel-call batches never split, early return without pending edits), refresh on read-through (same model, cacheRead clearly past the entry, unchanged prefix; trusted after 5 minutes once refreshed), the run baseline, the Anthropic rewrite estimate (from the trusted read point, else from the question), pending edits, the memo from records, the break-even rule, context size with summaries |
|
|
17
|
+
| `items.test.ts` | Small outputs (size window, candidate rules, age, memo, stub length, superseded), exchange spans (custom messages, completed exchanges only, the current run and the recent exchanges never touched, already omitted, memo; images, foreign edits and foreign omissions skipped, our own markers allowed; system, `!` and custom messages; a compaction that cut an exchange; minimum size), stubs and edits, the small and exchange requests in both phases, batching, the shared limiter, `judgeItems` (parallel limit, failures keep, time budget), `processItems` (stubs, omissions, records with kinds, one `concurrency` across all kinds) |
|
|
18
|
+
| `run.test.ts` | Run span (including image-only prompts), question/answer/notes, candidate rules (errors, images, excluded tools, short, already-distilled and other-extension-edited results), steering messages (continue the run, join the question); history (last N exchanges, first request, compaction/branch summaries, image-only and steering prompts, clipping, `historyExchanges` 0) |
|
|
17
19
|
| `chunk.test.ts` | grep detection, grouping by file, context lines and `--`, `-digits-` file names, split/merge rules, plain-text windows, segment limits |
|
|
18
|
-
| `decide.test.ts` |
|
|
20
|
+
| `decide.test.ts` | Checkpoint request shape and wording; run-end request shape, with and without `earlier_conversation` (state order, omitted parts, history wording vs. the unchanged single-run wording); the decision order (error, missing answers, `keep_whole`, strong/weak `whole`, chunks, `none`); `citedFiles` |
|
|
19
21
|
| `distill.test.ts` | Skips, edits, `removed`, `not-worth`, failures, time budget (including a classifier that never answers and late answers), parent abort, concurrency limit, multi-segment outputs, oversize segments never sent, `keepCitedFiles`, usage sums |
|
|
20
22
|
| `render.test.ts` | Labels, verbatim chunks with omission lines, `read` offsets, file lists, full removal |
|
|
21
|
-
| `cache-pin.test.ts` | API-key and OAuth payloads, trailing system messages, TTL selection and `ttl-conflict`, string content, tool-result-only messages, over-budget restore |
|
|
22
|
-
| `stats.test.ts` | Savings from the projection, run records, status texts and colours, cost format |
|
|
23
|
+
| `cache-pin.test.ts` | Question pin: API-key and OAuth payloads, trailing system messages, TTL selection and `ttl-conflict`, string content, tool-result-only messages, over-budget restore. Anchors: question > read > write priority, read anchor inside a batch of tool_result blocks, breakpoint budget (identity and tools breakpoints dropped, then write, then read; the question survives a foreign breakpoint), not found / last message / already marked, normalized tool ids, TTL order, `markedToolResults` |
|
|
24
|
+
| `stats.test.ts` | Savings from the projection (images by pixels), run records (checkpoints counted), lifetime total and `compacted`, status texts (`π‘ 0 Β· Ξ£β79k`) and colours, cost format |
|
|
25
|
+
| `images.test.ts` | Old images: age rule, keep-turns 0/1/3, stub text and sizes, user images / already removed / foreign-edited results skipped |
|
|
26
|
+
| `size.test.ts` | Image sizes from PNG/JPEG/GIF/WebP headers, image tokens by pixels (not base64 length), thinking and tool-call sizes, `contextChars` with screenshots |
|
|
23
27
|
| `recall.test.ts` | Original output, pattern (length cap, 4,000-char match window, catastrophic-regex timeout), offset/limit, line and byte caps, oversized lines |
|
|
24
|
-
| `config.test.ts` | Defaults, validation and ranges (incl. `historyExchanges` 0β10 integer), `provider/id` parsing, load/save round trip |
|
|
28
|
+
| `config.test.ts` | Defaults, validation and ranges (incl. `historyExchanges` 0β10 integer and the `midRun*` settings), `provider/id` parsing, load/save round trip |
|
|
25
29
|
| `extension.test.ts` | The real entry point (see below) |
|
|
26
30
|
|
|
27
31
|
`extension.test.ts` loads `src/index.ts` through **Pi's own extension loader** (`loadExtensions` from the
|
|
@@ -29,7 +33,31 @@ These need Node β₯ 22.18, which runs the `.ts` files directly. There is no netw
|
|
|
29
33
|
`PI_CODING_AGENT_DIR` and `PI_CONTEXT_GUARD_CONFIG` to a temporary directory, so `/guard on|off` never touches the real
|
|
30
34
|
`~/.pi/agent/context-guard.json`. It checks that the handlers, tool and command are registered, that drafts
|
|
31
35
|
are appended after other extensions' drafts, that unfinished runs and a missing model are ignored, that the
|
|
32
|
-
pin applies only to `anthropic-messages`, that `recall` works, and that `/guard off|on` persists.
|
|
36
|
+
pin applies only to `anthropic-messages`, that `recall` works, and that `/guard off|on` persists. For
|
|
37
|
+
`turn_end` it checks that the final turn is skipped, that a batch below `midRunBatchChars` and a checkpoint
|
|
38
|
+
that would not break even are skipped, that a due checkpoint appends edits and a `mid-run` record after other
|
|
39
|
+
drafts (never `continue`), that nothing is judged twice once the record is committed, that drafts a later
|
|
40
|
+
handler dropped (or Pi rejected) are judged again, that an unanswered output (error) is asked again, that the
|
|
41
|
+
memo follows `/tree`, and that run end judges outputs kept mid-run again. On a `pi-claude-auth`-shaped
|
|
42
|
+
payload (system prompt moved into the first user message) it checks the question pin on every request; on
|
|
43
|
+
the first request after edits a write anchor, and a read point only at an entry this process sent earlier
|
|
44
|
+
(also on a warming replay); a read point after run-end edits; and no read point for another model, after
|
|
45
|
+
the TTL (mocked clock), after a reload, for an anchor dropped for the budget, or for history made with
|
|
46
|
+
pinning off. Through `message_end` it checks that a response that read through a logged entry keeps it
|
|
47
|
+
usable past 5 minutes, and that a short read or another model's response does not. For whole items it
|
|
48
|
+
checks that run end omits an unrelated old exchange (stub on the prompt, `null` for the rest) and stubs a
|
|
49
|
+
small output, that the next run judges the next eligible exchange, that `recall` on the stub returns the
|
|
50
|
+
whole exchange, that `pruneExchanges: false` and `smallResultMinChars: 0` turn them off, and that the
|
|
51
|
+
break-even rule drops exchanges from a checkpoint batch that would not pay off with them. The run-end
|
|
52
|
+
exchange gate: a small omission is deferred and the accumulated batch is omitted at a later run end; a big
|
|
53
|
+
early exchange in a short session is deferred, and omitted with `exchangeBreakEven: false`; on OpenAI Codex
|
|
54
|
+
an exchange rides along when the pass edits something else and is deferred when it does not. Old images: run
|
|
55
|
+
end removes images 3+ turns old with no Jev request (also with the model missing), `recall` returns them as
|
|
56
|
+
image blocks (a pattern search only names them), nothing is removed twice; a checkpoint fills its batch with
|
|
57
|
+
old images alone and still obeys break-even, `imageKeepTurns: 0` turns it off; images of an earlier run are
|
|
58
|
+
deferred by the run-end gate and removed with `exchangeBreakEven: false`; `recall` of an omitted exchange
|
|
59
|
+
attaches user images and names tool-result images. The fake session uses Pi's projection shape (an omitted
|
|
60
|
+
entry stays with no messages).
|
|
33
61
|
|
|
34
62
|
Test through Pi's loader (or `pi -e`), not a plain Node import of the extension. Plain Node imports of
|
|
35
63
|
`@earendil-works/pi-coding-agent` can fail with `ERR_PACKAGE_PATH_NOT_EXPORTED`, even though Pi loads the
|
|
@@ -41,7 +69,7 @@ This uses a real Pi, a real main model and real Jev requests. **It costs money o
|
|
|
41
69
|
main-model requests per turn, plus a few Jev requests (under $0.001 per distilled run).
|
|
42
70
|
|
|
43
71
|
```bash
|
|
44
|
-
node test/e2e/run-e2e.mjs --model <provider/id> [--ext <path>]... [--scenario badge|long|
|
|
72
|
+
node test/e2e/run-e2e.mjs --model <provider/id> [--ext <path>]... [--scenario badge|sequential|history|long|topics] \
|
|
45
73
|
[--config '<JSON>'] [--no-guard] [--turns N] [--thinking level]
|
|
46
74
|
```
|
|
47
75
|
|
|
@@ -49,7 +77,7 @@ node test/e2e/run-e2e.mjs --model <provider/id> [--ext <path>]... [--scenario ba
|
|
|
49
77
|
|---|---|---|
|
|
50
78
|
| `--model` | (required) | Main model, e.g. `openai-codex/gpt-6-luna` or `anthropic/claude-sonnet-5-5` |
|
|
51
79
|
| `--ext` | none | Extra extension to load; can be repeated (e.g. an auth extension) |
|
|
52
|
-
| `--scenario` | `badge` | `badge`: 3 turns about `StatusBadge` usages. `
|
|
80
|
+
| `--scenario` | `badge` | `badge`: 3 turns about `StatusBadge` usages. `sequential`: 2 turns with 10+ sequential tool calls, for the question pin. `history`: 3 turns for the earlier-conversation feature. `long`: one autonomous "fix all failing tests" run, for mid-run checkpoints. `topics`: 5 prompts on different topics, the last a follow-up on the first, for old exchanges (both below; use `--turns 5`). |
|
|
53
81
|
| `--config` | `{}` | context-guard settings for this run, e.g. `'{"pinAnthropicCache":false}'` |
|
|
54
82
|
| `--no-guard` | off | Control run without context-guard |
|
|
55
83
|
| `--turns` | `3` | Turns to run (capped by the scenario) |
|
|
@@ -80,7 +108,10 @@ How it works:
|
|
|
80
108
|
| turn 2 first request reads cache | `cacheRead` > 0 on the first request after the edit |
|
|
81
109
|
| turn 3 quotes the removed line exactly | The model quoted line 3 of `src/api/jobStatus.ts` from the "first search" |
|
|
82
110
|
| turn 3 used recall (line was distilled away) | Only when that line is missing from turn 3's payload: the model must have called `recall` |
|
|
83
|
-
|
|
|
111
|
+
| sequential: turn 1 produced context edits / 10+ tool calls / 10+ model requests | The sequential scenario really was long and sequential |
|
|
112
|
+
| long: the suite is green afterwards | `npm test` passes in the fixture after the run |
|
|
113
|
+
| long: at least one mid-run checkpoint (forced) | Only with `midRunBreakEven: false` in `--config`: a `mid-run` record was written (a default run may rightly make none) |
|
|
114
|
+
| long: request after the checkpoint at request N reads the cached prefix | Anthropic: the first request after each checkpoint reads at least what the run's first request read and wrote (system prompt and question) |
|
|
84
115
|
| history: turn 2 produced context edits | The broad search in turn 2 was distilled |
|
|
85
116
|
| history: turn 3 names exactly the clients that retry on 429 | The answer names the 9 clients whose fixture file has `=== 429`; a client named only to say it does *not* retry is allowed |
|
|
86
117
|
| anthropic requests stay within 4 breakpoints | For `anthropic/β¦` models: the pin never exceeds the limit |
|
|
@@ -93,6 +124,59 @@ summary has a `history` block: `keptApiRetryLines` / `kept429Lines` (src/api ret
|
|
|
93
124
|
distilled text), the turn-2 Jev results and ms, turn 3's tool calls and whether it used `recall`, and the
|
|
94
125
|
clients turn 3 named. Results are in [JEV.md](JEV.md#earlier-conversation-historyexchanges).
|
|
95
126
|
|
|
127
|
+
**`long` scenario.** `test/e2e/make-long-fixture.mjs` generates a small Node project (`ledgerly`: money
|
|
128
|
+
in integer cents, business-day calendars, CSV, pricing, a ledger report; 182 tests). Ten bugs sit in
|
|
129
|
+
different modules, in up to three layers: an import-time `ReferenceError` hides a whole test file, and two
|
|
130
|
+
tests fail first because of one bug and then, once it is fixed, because of another. A full `npm test` prints
|
|
131
|
+
12k characters when green and 25β30k with the failures, so every rerun is a large, quickly superseded tool
|
|
132
|
+
output. The prompt asks the agent to fix one bug at a time and run the plain suite after every fix, without
|
|
133
|
+
asking anything. (`--fixed` writes the bug-free version, `--bugs 1,4,β¦` a subset.) GPT-6 Luna needs 26β37
|
|
134
|
+
requests; Claude Sonnet 5.5 needs 8β14, because it batches several fixes per command and pipes test output
|
|
135
|
+
into `grep`/`head`. Load `--ext test/e2e/plain-test-runs.ts` for Claude to strip a pipe after `npm test`
|
|
136
|
+
(it logs each rewrite to stderr), so its suite runs return full logs like GPT's.
|
|
137
|
+
|
|
138
|
+
The summary gets a `long` block: per-request contexts (input + cacheRead + cacheWrite) and a sparkline,
|
|
139
|
+
totals of input/cacheRead/cacheWrite/output and the main-model cost Pi reports, every `context-guard`
|
|
140
|
+
record with its phase, saved characters, Jev ms and cost, and the request before and after it (context,
|
|
141
|
+
cacheRead, cacheWrite), whether the suite is green, `recall` calls, and repeated non-suite tool calls (lost
|
|
142
|
+
outputs searched again). Results are in [RESULTS.md](RESULTS.md#long-autonomous-runs) and in
|
|
143
|
+
[CACHE.md](CACHE.md#mid-run-checkpoints).
|
|
144
|
+
|
|
145
|
+
**`topics` scenario.** Five prompts in the generated StatusBadge repo: which API clients retry on 503; a
|
|
146
|
+
README paragraph for `StatusBar`; a count of HTTP 500 log lines; where `StatusBadge` is used; and "back to
|
|
147
|
+
the API clients that retry on 503: which of them also retry on 502?". The summary gets a `topics` block:
|
|
148
|
+
the context of each prompt's first request and its usage, the exchange results (phase, prompt, outcome,
|
|
149
|
+
P), the small-output results, the last prompt's tool calls and whether it used `recall`, and whether its
|
|
150
|
+
answer names exactly the clients whose file has both `=== 503` and `=== 502` (a crude check: it fails on
|
|
151
|
+
answers that also list clients to say they do not qualify). Results are in [RESULTS.md](RESULTS.md#examples) (example D) and in
|
|
152
|
+
[CACHE.md](CACHE.md#old-exchanges).
|
|
153
|
+
|
|
154
|
+
**API-shape probe.** `CG_OMIT_AT_TURN=2 node test/e2e/run-e2e.mjs --model β¦ --no-guard --ext
|
|
155
|
+
test/e2e/omit-probe.ts` omits the session's first exchange after turn 2 the way the guard does, so turn 3
|
|
156
|
+
sends a stub user message right before the next user message, without the omitted assistant and
|
|
157
|
+
tool-result messages. Both GPT-6 Luna and Claude Sonnet 5.5 (`pi-claude-auth`) accepted it and answered.
|
|
158
|
+
|
|
159
|
+
**Old images, live** (2026-10-08, Claude Sonnet 5.5 with `pi-claude-auth`, ad-hoc): six 1200Γ800 PNGs read
|
|
160
|
+
one per message, then a follow-up about the second one. Run end removed the four images 3β6 turns old with
|
|
161
|
+
no Jev request (record `requests: 0`, 19.9k chars β 5k tokens); the next request carried 2 images instead
|
|
162
|
+
of 6 (input 14.1k β 9.5k tokens). Told not to read the file again, the model called
|
|
163
|
+
`recall({"entryId":β¦})`, got the image back and read the code in it correctly.
|
|
164
|
+
|
|
165
|
+
**Session replay.** `node test/e2e/replay-session.ts <session.jsonl> [--from N] [--all] [--summary]` rebuilds Pi's
|
|
166
|
+
projection at every turn of a real session file and prints what the mid-run gates decide (run start, eligible
|
|
167
|
+
large and small outputs, old exchanges, batch size, break-even) and the run-end candidates. No model calls,
|
|
168
|
+
nothing leaves the machine. Without the live cache log it uses the question pin, so its break-even is the
|
|
169
|
+
conservative case.
|
|
170
|
+
|
|
171
|
+
**Whole-item probe.** `node test/e2e/probe-items.mjs [--reps 2] [--sessions <topics no-guard run dir>]β¦`
|
|
172
|
+
sends the hand-written exchanges and small outputs in `test/e2e/probe-data/` (and the `topics` exchanges)
|
|
173
|
+
to Jev and prints P per item and the accuracy per threshold (results in [JEV.md](JEV.md#old-exchanges)).
|
|
174
|
+
|
|
175
|
+
**Anthropic TTL experiment.** `node test/e2e/ttl-probe.mjs [--arms refresh,control,ttl1h,idle,idle1h]`
|
|
176
|
+
runs `test/e2e/ttl-probe.ts` with Claude: does a cache entry stay alive while later requests read a longer
|
|
177
|
+
prefix, and does it expire when nothing reads it for 6 minutes? About 9 minutes and roughly $0.9 at list price for all five arms.
|
|
178
|
+
Results: [CACHE.md](CACHE.md#do-cache-entries-on-the-read-path-stay-alive-measured).
|
|
179
|
+
|
|
96
180
|
With the guard on, the badge scenario has 9 checks, plus 1 for Anthropic models. Most guard checks are
|
|
97
181
|
skipped with `--no-guard`.
|
|
98
182
|
|
|
@@ -118,30 +202,37 @@ the runner disables installed extensions:
|
|
|
118
202
|
|
|
119
203
|
```bash
|
|
120
204
|
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth
|
|
121
|
-
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth --scenario
|
|
122
|
-
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth --scenario
|
|
205
|
+
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth --scenario sequential
|
|
206
|
+
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth --scenario sequential --config '{"pinAnthropicCache":false}'
|
|
123
207
|
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna
|
|
124
208
|
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --no-guard
|
|
125
209
|
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --scenario history
|
|
126
210
|
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --scenario history --config '{"historyExchanges":0}'
|
|
211
|
+
# mid-run checkpoints (up to 4 in parallel)
|
|
212
|
+
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --scenario long --no-guard
|
|
213
|
+
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --scenario long --config '{"midRun":false}'
|
|
214
|
+
node test/e2e/run-e2e.mjs --model openai-codex/gpt-6-luna --scenario long
|
|
215
|
+
node test/e2e/run-e2e.mjs --model anthropic/claude-sonnet-5-5 --ext ~/.pi/agent/npm/node_modules/pi-claude-auth --ext test/e2e/plain-test-runs.ts --scenario long
|
|
127
216
|
```
|
|
128
217
|
|
|
129
|
-
### Results
|
|
218
|
+
### Results (Pi 1.0.3)
|
|
130
219
|
|
|
131
220
|
| Run | Result |
|
|
132
221
|
|---|---|
|
|
133
222
|
| GPT-6 Luna, badge | 36,113 β 1,501 chars, 2 Jev requests, 550 ms, $0.00083; turn 3 used `recall` with a regex; 9/9 checks pass |
|
|
134
223
|
| Claude Sonnet 5.5, badge | 36,113 β 3,066 chars, 2 Jev requests, 1.18 s, $0.00084; turn 2 cacheRead 5,777 / cacheWrite 2,036; turn 3 used `recall`; 10/10 checks pass |
|
|
135
|
-
| Claude Sonnet 5.5,
|
|
224
|
+
| Claude Sonnet 5.5, sequential, with / without pin | first request after the run: cacheRead 5,810 / 1,693, cacheWrite 8,913 / 13,068 |
|
|
136
225
|
| GPT-6 Luna, badge, with / without guard | turn 2 cacheRead 2,560 / 13,824 |
|
|
137
|
-
| GPT-6 Luna, badge,
|
|
226
|
+
| GPT-6 Luna, badge, with history (default) | 36,113 β ~2.3k chars; 9/9 checks pass (a run in parallel with 4 others had one OpenAI cache miss on turn 2: 8/9) |
|
|
138
227
|
| GPT-6 Luna / Claude Sonnet 5.5, history on / off | See [JEV.md](JEV.md#earlier-conversation-historyexchanges): 2 rounds Γ 4 runs |
|
|
228
|
+
| GPT-6 Luna / Claude Sonnet 5.5, long (mid-run checkpoints) | See [RESULTS.md](RESULTS.md#long-autonomous-runs); all suites green, no `recall` |
|
|
229
|
+
| GPT-6 Luna / Claude Sonnet 5.5, topics, guard on / off | See [RESULTS.md](RESULTS.md#examples), example D |
|
|
139
230
|
|
|
140
231
|
See [CACHE.md](CACHE.md) for what the cache numbers mean.
|
|
141
232
|
|
|
142
233
|
## Real-world examples (`examples.mjs`)
|
|
143
234
|
|
|
144
|
-
The
|
|
235
|
+
The examples in [RESULTS.md](RESULTS.md#examples) come from `test/e2e/examples.mjs`. It runs the same prompts on
|
|
145
236
|
real public code twice, with context-guard on and with it off (`--no-guard`), and compares the first
|
|
146
237
|
request of the follow-up turn.
|
|
147
238
|
|
|
@@ -159,7 +250,7 @@ node test/e2e/examples.mjs --jobs 2
|
|
|
159
250
|
|
|
160
251
|
Each run gets its own workspace, session id and `PI_CONTEXT_GUARD_CONFIG` file (defaults). The Pi package is
|
|
161
252
|
public (npm); nothing private is sent to OpenRouter. Example B also records the test log's size
|
|
162
|
-
(`testLogChars`, which must be β₯ 8,000 to be a fair example; it was 14.
|
|
253
|
+
(`testLogChars`, which must be β₯ 8,000 to be a fair example; it was 14.4β15.8k).
|
|
163
254
|
|
|
164
255
|
Output: `test/e2e/out/examples-<stamp>/` with `results.md` (the table), `results.json`, and one directory per
|
|
165
256
|
run (`A-claude-on`, β¦) holding `summary.json` (per turn: tool calls, tool-result sizes, edits with kept/total
|
|
@@ -171,17 +262,15 @@ model saw.
|
|
|
171
262
|
Pi from list prices (most of it Claude in example A; GPT runs used a subscription). About 5 minutes with 4
|
|
172
263
|
parallel jobs.
|
|
173
264
|
|
|
174
|
-
**Findings
|
|
175
|
-
|
|
176
|
-
- Next-request context, on / off: A Claude 15.5k / 28.2k tokens, A GPT 11.5k / 34.3k, C GPT 9.2k / 35.3k
|
|
177
|
-
all follow-ups correct, none needed `recall`.
|
|
178
|
-
- The first request after an edit
|
|
179
|
-
- B
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
named two line numbers past the file lines (e.g. "lines 466β502" of a 1β500 read). Fixed: ranges are now
|
|
184
|
-
clamped to the file lines and a removed note gets no omission line.
|
|
265
|
+
**Findings** (details in [RESULTS.md](RESULTS.md#examples)):
|
|
266
|
+
|
|
267
|
+
- Next-request context, on / off: A Claude 15.5k / 28.2k tokens, A GPT 11.5k / 34.3k, C GPT 9.2k / 35.3k,
|
|
268
|
+
B GPT 5.7k / 7.5k; all follow-ups correct, none needed `recall`.
|
|
269
|
+
- The first request after an edit costs more with the guard on (cache rewrite): A Claude $0.033 / $0.016.
|
|
270
|
+
- B: the failing test log (an error result, distilled because `distillErrors` is on) went 15.8k β 8.6k
|
|
271
|
+
chars, keeping the 3 failures with their assertion messages.
|
|
272
|
+
- C: `read` outputs end with Pi's "[Showing lines β¦]" note; omission ranges are clamped to the file lines
|
|
273
|
+
and a removed note gets no omission line.
|
|
185
274
|
|
|
186
275
|
## Manual TUI check (tmux)
|
|
187
276
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-context-guard-jev",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.1",
|
|
4
|
+
"description": "Pi extension that removes tool output the agent no longer needs from its context. The Jev classifier keeps the needed lines verbatim; stale small outputs, unrelated old exchanges and old screenshots become one-line notes. Everything can be recalled.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "alandarev",
|