pi-context-guard-jev 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +198 -55
- package/docs/CACHE.md +135 -3
- package/docs/DESIGN.md +218 -30
- package/docs/JEV.md +95 -27
- package/docs/TESTING.md +95 -25
- package/package.json +2 -2
- package/src/cache-pin.ts +170 -0
- package/src/checkpoint.ts +436 -0
- package/src/config.ts +56 -2
- package/src/decide.ts +74 -0
- package/src/distill.ts +9 -2
- package/src/index.ts +243 -55
- package/src/items.ts +447 -0
- package/src/process.ts +176 -0
- package/src/recall.ts +63 -4
- package/src/run.ts +78 -27
- package/src/stats.ts +35 -5
- package/src/types.ts +7 -0
package/docs/DESIGN.md
CHANGED
|
@@ -9,26 +9,33 @@ evidence behind the thresholds are in [JEV.md](JEV.md). Cache behaviour is in [C
|
|
|
9
9
|
|---|---|
|
|
10
10
|
| `src/index.ts` | The only module that uses the Pi API: hooks, `recall` tool, `/guard` command, status bar, classifier lookup |
|
|
11
11
|
| `src/config.ts` | `DEFAULT_CONFIG`, validation (`normalizeConfig`), load/save of `context-guard.json`, `provider/id` parsing |
|
|
12
|
-
| `src/run.ts` | `collectRun`: finds the run that just finished, its question, answer, notes and candidate tool results |
|
|
12
|
+
| `src/run.ts` | `collectRun`: finds the run that just finished, its question, answer, notes and candidate tool results; the shared span and candidate rules |
|
|
13
|
+
| `src/checkpoint.ts` | Mid-run checkpoints: eligible outputs (`collectCheckpoint`), superseded detection, the memo and pending edits from the branch, the read point, the break-even rule |
|
|
13
14
|
| `src/chunk.ts` | `chunkOutput` (grep-aware chunking) and `segmentChunks` (grouping chunks into Jev requests) |
|
|
14
|
-
| `src/decide.ts` | `buildRequest` (the Jev request) and `interpret` (answers → keep set); `citedFiles` |
|
|
15
|
+
| `src/decide.ts` | `buildRequest` (the run-end Jev request), `buildCheckpointRequest` (the mid-run one) and `interpret` (answers → keep set); `citedFiles` |
|
|
15
16
|
| `src/distill.ts` | `distillRun`: chunk, run Jev requests in parallel within a time budget, decide per result, build `context_edit` drafts |
|
|
17
|
+
| `src/items.ts` | Whole items: small outputs (`collectSmall`) and old exchanges (`collectExchanges`), their Jev requests, stubs and edits, and `judgeItems` (batched, parallel, time budget) |
|
|
18
|
+
| `src/process.ts` | `processItems`: large outputs, small outputs and old exchanges of one pass, judged in parallel and merged into one list of edits and records |
|
|
16
19
|
| `src/render.ts` | The replacement text: header, verbatim chunks, omission lines; the `[context-guard]` marker |
|
|
17
|
-
| `src/cache-pin.ts` | Anthropic
|
|
20
|
+
| `src/cache-pin.ts` | Anthropic breakpoints: the previous user question, and the read point after edits (`placeGuardBreakpoints`) |
|
|
18
21
|
| `src/stats.ts` | Savings and Jev usage computed from the session; status-bar text |
|
|
19
|
-
| `src/recall.ts` | The text returned by `recall` |
|
|
22
|
+
| `src/recall.ts` | The text returned by `recall` (a tool output, or the transcript of an omitted exchange) |
|
|
20
23
|
| `src/types.ts` | Loose structural types for the Pi objects used, so the pure modules can be tested without Pi |
|
|
21
24
|
|
|
22
25
|
Everything except `index.ts` is pure. `distillRun` gets the classifier as an injected function.
|
|
23
26
|
|
|
24
|
-
## The
|
|
27
|
+
## The boundaries: `agent_before_settle` and `turn_end`
|
|
25
28
|
|
|
26
|
-
|
|
29
|
+
Run-end distillation happens in `agent_before_settle`, and only when `event.outcome === "completed"` and
|
|
30
|
+
the guard is on. Long runs also get mid-run checkpoints at `turn_end` (see
|
|
31
|
+
[Mid-run checkpoints](#mid-run-checkpoints)); everything below about drafts applies to both.
|
|
27
32
|
|
|
28
33
|
Why there:
|
|
29
34
|
|
|
30
35
|
- The final answer exists, so relevance can be judged against it. Pruning during a run would take raw
|
|
31
36
|
data away from the model while it still needs it, and every edit would break the prompt cache again.
|
|
37
|
+
Mid-run checkpoints accept those costs only for outputs that are several turns old, in rare batches,
|
|
38
|
+
and only when the break-even rule says they pay off.
|
|
32
39
|
- A boundary handler can return session entry drafts. A `context_edit` draft is append-only: it changes
|
|
33
40
|
only what the model sees from then on. The raw entry stays in the session file, the UI and exports, and
|
|
34
41
|
`ctx.sessionManager.getEntry(id)` still returns it. That is what `recall` reads.
|
|
@@ -45,9 +52,11 @@ Hooks used:
|
|
|
45
52
|
| Hook | Use |
|
|
46
53
|
|---|---|
|
|
47
54
|
| `session_start` | Load settings, check the classifier model and credential, draw the status |
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
50
|
-
| `
|
|
55
|
+
| `turn_end` | Mid-run checkpoint, when a batch is due |
|
|
56
|
+
| `agent_before_settle` | Distill the finished run (skipping outputs judged mid-run) |
|
|
57
|
+
| `before_provider_request` | Anthropic breakpoints (only for `anthropic-messages` models) |
|
|
58
|
+
| `session_tree`, `session_compact` | Redraw the status (checkpoint state is read from the branch anyway) |
|
|
59
|
+
| `agent_settled` | Redraw the status |
|
|
51
60
|
|
|
52
61
|
## Algorithm
|
|
53
62
|
|
|
@@ -186,6 +195,168 @@ In this order:
|
|
|
186
195
|
usually already cleared at this boundary, so Esc does not cancel it; the time budget is the limit.
|
|
187
196
|
- An exception from the whole pass shows a warning and leaves the run untouched.
|
|
188
197
|
|
|
198
|
+
## Items: what one pass judges
|
|
199
|
+
|
|
200
|
+
Every pass (run end, or a mid-run checkpoint) collects three kinds of item and judges them in parallel
|
|
201
|
+
(`processItems`); their edits and results go into one record.
|
|
202
|
+
|
|
203
|
+
| Kind | What | How it is judged | Dropped item becomes |
|
|
204
|
+
|---|---|---|---|
|
|
205
|
+
| `large` | Tool results of the current run of at least `minResultChars` (4,000), under the candidate rules above | Chunked, one request per segment (above) | The kept chunks verbatim with omission lines |
|
|
206
|
+
| `small` | Tool results of the current run from `smallResultMinChars` (400) up to `minResultChars`, under the same rules | Whole: one bool per item, many items per request | A one-line stub: `[context-guard] Omitted the output of bash \`…\` (1.2k chars): judged no longer needed. Full output: recall({"entryId":"…"}).` |
|
|
207
|
+
| `exchange` | Old exchanges (below) | Whole: one bool per exchange, several per request | A stub on the prompt, every other entry omitted |
|
|
208
|
+
|
|
209
|
+
**Small outputs** (`collectSmall`). An output whose stub would not be shorter is skipped. Each item carries
|
|
210
|
+
the tool label, `status: failed` for error results, `superseded` when a later call made it out of date
|
|
211
|
+
(`supersededBy`, as for large outputs mid-run), and the output (at most 4,000 characters). The question is
|
|
212
|
+
"will the agent still need the output in item_N?" mid-run, or "does item_N contain anything that
|
|
213
|
+
final_answer relies on, or that a likely follow-up would need?" at run end; both say that an output marked
|
|
214
|
+
superseded is out of date. An item is kept when P ≥ `smallKeepThreshold` (0.45; probe in
|
|
215
|
+
[JEV.md](JEV.md#small-outputs)). Mid-run, small outputs follow the same age rule and memo as large ones; at
|
|
216
|
+
run end every small output without the marker is judged, including those kept at a checkpoint.
|
|
217
|
+
|
|
218
|
+
**Old exchanges** (`collectExchanges`). An exchange is a user prompt and every entry up to the next one:
|
|
219
|
+
assistant messages, tool calls and results, custom messages such as subagent notices. Eligible are
|
|
220
|
+
exchanges before the current run's user message, without the last `keepRecentExchanges` (2) before it,
|
|
221
|
+
that completed (their last assistant message has no tool calls, so a span cut by a steering message is
|
|
222
|
+
never one), that are not already omitted (their prompt shows the stub), that were not judged during the
|
|
223
|
+
current run (`exchangeMemo`: exchange results in records after the last user message, except deferred
|
|
224
|
+
ones), and that can be recovered and are worth it: no message with an image, no entry whose model-visible
|
|
225
|
+
content differs from the raw entry for any reason other than our own marker (another extension's edit,
|
|
226
|
+
or an omission by someone else, which Pi keeps in the projection as an entry with no messages), at least
|
|
227
|
+
2,000 characters of editable content, and a stub shorter than that content. A kept exchange is
|
|
228
|
+
judged again in a later run, against that run's work. Compaction and branch summaries are never part of an
|
|
229
|
+
exchange; entries Pi cannot edit (system messages, `!` shell executions) stay in place.
|
|
230
|
+
|
|
231
|
+
Jev sees the current work (`user_question`, `earlier_conversation`, and `agent_progress` mid-run or
|
|
232
|
+
`final_answer` at run end) and, per exchange, the prompt (at most 600 characters), the last assistant text
|
|
233
|
+
(800), the tool calls as `toolLabel()`s (at most 20) and the size. The question: "Is exchange_N still
|
|
234
|
+
relevant to the current work? Relevant means the agent may need its details: the same files, task,
|
|
235
|
+
decisions or facts." An exchange is omitted only when P < `exchangeOmitThreshold` (0.2; probe in
|
|
236
|
+
[JEV.md](JEV.md#old-exchanges)). Then the prompt entry gets the stub
|
|
237
|
+
`[context-guard] Omitted an earlier exchange judged unrelated to the current work: "<first 120 characters of the prompt>" (N messages, ~Mk tokens). Full exchange: recall({"entryId":"<prompt entry id>"}).`
|
|
238
|
+
and every other editable entry gets `replacement: null`, which omits it from model context. The record's
|
|
239
|
+
result for the exchange lists exactly those entry ids (`omitted`), and `recall` on the stub shows exactly
|
|
240
|
+
the prompt and those entries. Tool calls and
|
|
241
|
+
their results are omitted together. The resulting shapes (a stub user message right before the next user
|
|
242
|
+
message; spans without assistant and tool-result messages) are accepted by Anthropic (with
|
|
243
|
+
`pi-claude-auth`) and OpenAI Codex, checked live with `test/e2e/omit-probe.ts` and the `topics` scenario.
|
|
244
|
+
|
|
245
|
+
**Run-end break-even for exchanges** (`exchangeBreakEven`, on). After the pass is judged, its exchange
|
|
246
|
+
omissions are checked: their marginal cache cost is the rewrite with them minus the rewrite the pass's
|
|
247
|
+
other edits cause anyway (`rewriteCharsFor`, with the trusted read point and floor rules of
|
|
248
|
+
[CACHE.md](CACHE.md); on OpenAI Codex any edit costs the whole context, so exchanges ride along for free
|
|
249
|
+
when the pass edits anything else). If that cost is above zero, the omissions must save at least
|
|
250
|
+
`exchangeMinSavingChars` (8,000) and pass `paysOff` with as many later requests as the session has had so
|
|
251
|
+
far (model-visible assistant messages). Otherwise they are deferred: their edits are dropped, their results
|
|
252
|
+
say `kept` / `deferred`, and they are judged again at later run ends against the work of that run, so
|
|
253
|
+
deferred exchanges accumulate until their total pays off. `exchangeBreakEven: false` always omits
|
|
254
|
+
unrelated exchanges.
|
|
255
|
+
|
|
256
|
+
**Batching.** Whole items are grouped into requests of at most `maxSegmentChars` characters of item text and
|
|
257
|
+
`maxChunksPerSegment` (40) items, under the same `timeoutMs` budget; the three kinds run at the same time,
|
|
258
|
+
and one shared limiter keeps at most `concurrency` (6) Jev requests in flight across all of them. An item without an
|
|
259
|
+
answer (error, timeout) is kept and asked again next time.
|
|
260
|
+
|
|
261
|
+
## Mid-run checkpoints
|
|
262
|
+
|
|
263
|
+
A 30–120 minute autonomous run would otherwise carry every tool output until it ends; Pi compacts only
|
|
264
|
+
near the window minus 16k tokens. Checkpoints judge older outputs while the run is still going.
|
|
265
|
+
|
|
266
|
+
**Hook (verified on Pi 1.0.3).** `turn_end` is an actionable boundary (`TurnEndEvent extends
|
|
267
|
+
BoundaryState`). `agent-session.js` dispatches it from `agent.finishTurn`, commits the returned drafts
|
|
268
|
+
(`_commitBoundaryDrafts` → `_refreshFinalizedContext`), and only then runs `prepareNextTurn`, which rebuilds
|
|
269
|
+
the messages from the session projection and runs the turn-end compaction check
|
|
270
|
+
(`_compactBeforeNextAssistantResponse`). So a `context_edit` returned at `turn_end` reaches the very next
|
|
271
|
+
provider request, before any compaction decision; the e2e runs confirm it (the next request's context
|
|
272
|
+
drops by the removed amount). The runner replaces the draft list as for `agent_before_settle`, so the
|
|
273
|
+
handler returns `[...event.entries, ...edits, record]`, and it never asks for `continue`.
|
|
274
|
+
|
|
275
|
+
**When.** At the end of a turn whose assistant message has tool calls (the final turn of a run is left to
|
|
276
|
+
run-end distillation), with `outcome: "completed"` and `midRun` on:
|
|
277
|
+
|
|
278
|
+
1. **Eligible items:** large outputs (`collectCheckpoint`) and small outputs (`collectSmall`) of the current
|
|
279
|
+
run whose call was made at least `midRunMinAgeTurns` (4) turns before the current turn (turn = assistant
|
|
280
|
+
message index in the run) and that are not in the memo, and old exchanges not judged during this run.
|
|
281
|
+
2. **Batch:** their total must reach `midRunBatchChars` (60,000; `midRunBatchCharsOpenAI` for
|
|
282
|
+
`openai-codex` models), so edits come in rare batches.
|
|
283
|
+
3. **Break-even** (`midRunBreakEven`): the checkpoint must be likely to pay for its one-time cache
|
|
284
|
+
rewrite: `pending chars × turns so far ≥ factor × rewrite chars`, with factor 13 and the whole context
|
|
285
|
+
for OpenAI Codex; factor 16 for Anthropic, where the rewrite is the context after the read point the
|
|
286
|
+
next request would get (the latest trusted entry before the first eligible output, `trustedReadPoint`)
|
|
287
|
+
or, without one, after the question (the floor; with `pinAnthropicCache` off, the whole context); and
|
|
288
|
+
factor 16 and the context from the first eligible output on for other providers
|
|
289
|
+
(derivation in `checkpoint.ts` and [CACHE.md](CACHE.md#mid-run-checkpoints)). "Turns so far" stands in
|
|
290
|
+
for the turns still to come. The rewrite starts at the earliest edited entry: an old exchange comes
|
|
291
|
+
before the question, so with exchanges in the batch the Anthropic rewrite is everything after the latest
|
|
292
|
+
trusted entry before that exchange, or the whole context. If the whole batch does not pay off, the rule
|
|
293
|
+
is tried for the tool outputs alone, and the exchanges wait for run end.
|
|
294
|
+
|
|
295
|
+
Then all eligible outputs go to `distillRun` with the checkpoint request builder and
|
|
296
|
+
`chunkKeepThreshold = midRunChunkKeepThreshold` (0.6), under the same `timeoutMs` budget, abort signal and
|
|
297
|
+
keep-on-error rules, with the busy status and working message shown.
|
|
298
|
+
|
|
299
|
+
**The checkpoint request** (`buildCheckpointRequest`; separate from the run-end request): there is no final
|
|
300
|
+
answer yet. The state holds `situation` (the agent is still working; this is an earlier call's output),
|
|
301
|
+
`earlier_conversation`, `user_question`, `agent_progress` (`latest_note`: the latest assistant text, at
|
|
302
|
+
most 2,000 characters; `earlier_notes`: earlier text of the run, clipped from the start to 2,000),
|
|
303
|
+
`later_tool_calls` (labels of the calls made after this output, oldest first; above 30 the first and last
|
|
304
|
+
15 with a gap note), `superseded` when it applies, and then tool, status, arguments, size and chunks as at
|
|
305
|
+
run end. The questions ask whether the agent will still need the output (or chunk) to finish
|
|
306
|
+
`user_question`; out-of-date lines and lines it has already acted on and moved past are not needed. The
|
|
307
|
+
probe behind the wording and the threshold is in [JEV.md](JEV.md#checkpoint-wording).
|
|
308
|
+
|
|
309
|
+
**Superseded** (`supersededBy`), judged from the later tool calls: a `read` whose file was later changed by
|
|
310
|
+
`edit`/`write` ("this file was changed after this read (edit src/a.ts, 3 turns later)") or read again with
|
|
311
|
+
the same offset and limit; a command that ran again (whitespace and a trailing `2>&1` normalized); any other
|
|
312
|
+
tool called again with identical arguments.
|
|
313
|
+
|
|
314
|
+
**State from the branch.** The memo and the pending edits are derived, at that moment, from the entries
|
|
315
|
+
persisted on the active branch (`ctx.sessionManager.getBranch()`). A boundary's drafts are not final: a
|
|
316
|
+
later handler can replace the list, and one invalid draft makes Pi discard all of them; state taken from
|
|
317
|
+
the returned drafts would then skip those outputs forever. Reading the branch also makes `/tree`,
|
|
318
|
+
compaction and reloads correct without any rebuild step. The only in-memory state is the log of cache
|
|
319
|
+
entries this process wrote (below), which only ever adds a read point.
|
|
320
|
+
|
|
321
|
+
**Memo.** Each output is judged mid-run at most once. The memo is the set of output results in the mid-run
|
|
322
|
+
records on the branch, except those Jev never answered for (`timeout`, `error`, `aborted`), which the next
|
|
323
|
+
checkpoint asks again. A "keep" is therefore not re-asked at every checkpoint. Old exchanges have their own
|
|
324
|
+
memo per run (see Items).
|
|
325
|
+
|
|
326
|
+
**Run end judges kept outputs again.** An output kept at a checkpoint (whole, or `not-worth`) is a
|
|
327
|
+
candidate again at run end, this time with the final answer: files the agent was still editing are usually not
|
|
328
|
+
needed once the task is done. Distilled outputs carry the marker and are skipped by the candidate rules.
|
|
329
|
+
|
|
330
|
+
**Anthropic breakpoints** (`placeGuardBreakpoints`). Every Anthropic request gets the previous-question
|
|
331
|
+
pin (the floor; with `pi-claude-auth` that block also carries the relocated system prompt). On the first
|
|
332
|
+
request after **pending edits** (`context_edit` entries on the branch after the last answered assistant
|
|
333
|
+
message, whether from a checkpoint, from run end on the next prompt, or from another extension; a failed or
|
|
334
|
+
aborted response does not count, since its retry repeats the request), `cacheAnchors` adds up to two more:
|
|
335
|
+
|
|
336
|
+
- **Read point**, only at a cache entry this process saw itself write. After placing its breakpoints, the
|
|
337
|
+
extension records every `tool_result` block that carries a breakpoint in the payload (Pi's rolling one
|
|
338
|
+
and its own): entry id, model, time, and the last branch entry at that moment. An entry is trusted only
|
|
339
|
+
if the same model wrote it, or read through it, within the TTL (5 minutes), the entry and that last
|
|
340
|
+
branch entry are still on the branch, and since then no `context_edit` touched anything at or before it
|
|
341
|
+
and no compaction or branch summary was appended. The latest trusted entry before the first edited entry
|
|
342
|
+
is the read point. The log is cleared on `session_start` (and so on reload); anything uncertain falls
|
|
343
|
+
back to the question pin.
|
|
344
|
+
- **Refresh on read-through** (`message_end`, `refreshOnReadThrough`): reading a longer prefix keeps the
|
|
345
|
+
entries on its path alive (CACHE.md), so a response moves a logged entry's time to its request's time
|
|
346
|
+
when the request went to the same model, the entry's prefix is unchanged, and `cacheRead` reaches past
|
|
347
|
+
the entry with a margin: at least the run's baseline (the full input of its first request: system prompt,
|
|
348
|
+
tools, question) plus the projected characters from the question to the entry divided by 1.5. Code and
|
|
349
|
+
logs run at about 2.3 characters per token and prose at about 4, so this overestimates the tokens up to
|
|
350
|
+
the entry. Without a baseline, or with any doubt, nothing is refreshed.
|
|
351
|
+
- **Write anchor:** the end of the tool-result batch holding the first edited entry, so the edited prefix
|
|
352
|
+
gets an entry that a later checkpoint can read. It counts as written only once it was seen in a request
|
|
353
|
+
the extension sent (the same log).
|
|
354
|
+
|
|
355
|
+
Priority under the 4-breakpoint budget: after dropping Pi's redundant identity and tools breakpoints, the
|
|
356
|
+
question comes first, then the read point, then the write anchor. Cache-warming replays add no assistant
|
|
357
|
+
message, so they get the same breakpoints. Measured effect and the TTL findings:
|
|
358
|
+
[CACHE.md](CACHE.md#mid-run-checkpoints).
|
|
359
|
+
|
|
189
360
|
## Replacement format (`render.ts`)
|
|
190
361
|
|
|
191
362
|
Kept chunks are copied verbatim, in order. Each run of removed chunks becomes one omission line. For
|
|
@@ -208,21 +379,27 @@ The draft is `{ type: "context_edit", targetId, replacement: { content: [{ type:
|
|
|
208
379
|
|
|
209
380
|
## The `context-guard` record
|
|
210
381
|
|
|
211
|
-
Each run that made at least one Jev request also appends a custom entry
|
|
382
|
+
Each run (and each mid-run checkpoint) that made at least one Jev request also appends a custom entry
|
|
383
|
+
(`customType: "context-guard"`):
|
|
212
384
|
|
|
213
385
|
| Field | Meaning |
|
|
214
386
|
|---|---|
|
|
215
387
|
| `v` | Format version, `1` |
|
|
388
|
+
| `phase` | `"mid-run"` for checkpoints, `"run-end"` otherwise (a record without it counts as run end) |
|
|
216
389
|
| `model` | Classifier used |
|
|
217
390
|
| `savedChars` | Characters removed by this run's edits |
|
|
218
391
|
| `requests`, `inputTokens`, `costUsd`, `ms` | Jev usage for the run |
|
|
219
392
|
| `timedOut` | The time budget ran out |
|
|
220
|
-
| `results[]` | One per
|
|
393
|
+
| `results[]` | One per item: `kind` (`large`, `small`, `exchange`; absent means large), `entryId` (for an exchange, its prompt entry), `tool`, `label`, `outcome` (`distilled`, `removed`, `kept`), `reason`, `beforeChars`, `afterChars`, `jev` |
|
|
221
394
|
|
|
222
395
|
`reason` is one of `chunks`, `none-needed`, `not-worth`, `all-chunks`, `whole-needed`, `whole-chosen`,
|
|
223
|
-
`no-answer`, `error` or `timeout` (several are joined with commas)
|
|
396
|
+
`no-answer`, `error` or `timeout` (several are joined with commas) for large outputs, `needed` or
|
|
397
|
+
`not-needed` for small outputs, and `relevant`, `unrelated` or `deferred` for exchanges (an omitted exchange's
|
|
398
|
+
result also has `omitted`, the entry ids that got `replacement: null`). Whole items have the trace
|
|
399
|
+
`p.06` (P of the bool). `jev` holds one trace per segment, such as
|
|
224
400
|
`kw.15 focus=chunk_4:.52 keep 9/29` or `kw.82 focus=whole:.71 keep all (whole-needed)`. That is
|
|
225
|
-
P(`keep_whole`), the focus choice and its probability, and the result. `/guard` prints these for the last
|
|
401
|
+
P(`keep_whole`), the focus choice and its probability, and the result. `/guard` prints these for the last
|
|
402
|
+
record (run or checkpoint) and counts the checkpoints.
|
|
226
403
|
|
|
227
404
|
The extension does not write Jev's usage into Pi's own usage entries. It is recorded only here.
|
|
228
405
|
|
|
@@ -231,48 +408,59 @@ The extension does not write Jev's usage into Pi's own usage entries. It is reco
|
|
|
231
408
|
Nothing is kept in memory. Every redraw recomputes from the session:
|
|
232
409
|
|
|
233
410
|
- **Savings:** Pi's session projection (`buildSessionProjection()`) is checked for tool results whose
|
|
234
|
-
model-visible text starts with `[context-guard]` while the raw entry does not
|
|
235
|
-
minus visible length.
|
|
411
|
+
model-visible text starts with `[context-guard]` while the raw entry does not; each one adds raw length
|
|
412
|
+
minus visible length. A prompt that shows the exchange stub adds the raw length of its exchange (the
|
|
413
|
+
prompt and the entries up to the next user message that the model no longer sees: Pi keeps an omitted
|
|
414
|
+
entry in the projection with no messages) minus the stub.
|
|
415
|
+
Tokens are estimated as characters ÷ 4.
|
|
236
416
|
- **Jev usage:** the `context-guard` records on the active branch (`getBranch()`) are summed.
|
|
237
417
|
|
|
238
418
|
So the numbers follow branches, `/tree`, reloads and compaction.
|
|
239
419
|
|
|
240
|
-
The ready-state footer is `🛡 −4.2k · 1`: ≈ tokens kept out of context · distilled
|
|
420
|
+
The ready-state footer is `🛡 −4.2k · 1`: ≈ tokens kept out of context · distilled outputs plus omitted
|
|
421
|
+
exchanges (the first part
|
|
241
422
|
in the theme's success colour, the rest dimmed). Before anything is distilled it shows `🛡 0 saved`.
|
|
242
423
|
|
|
243
424
|
## Recall
|
|
244
425
|
|
|
245
426
|
`recall` reads the raw entry with `ctx.sessionManager.getEntry(entryId)`. `context_edit` never changes that
|
|
246
|
-
entry.
|
|
427
|
+
entry. For a tool result it returns the output; for a user prompt (the stub of an omitted exchange) it
|
|
428
|
+
returns a transcript of the raw exchange from the branch: `## user`, `## assistant` with the text and
|
|
429
|
+
`[tool call] name {arguments}` lines, `## tool result (tool)` and `## custom message (type)` sections, for
|
|
430
|
+
exactly the entries the latest record lists as `omitted` (without such a record, up to the next user
|
|
431
|
+
message). The tool returns the whole text, or only lines that match `pattern` (with line numbers), or an
|
|
247
432
|
`offset`/`limit` window. Output is capped at 2,000 lines and 50 KB, with a continuation note; a single
|
|
248
433
|
line longer than 50 KB is cut with a `[line N cut at 50KB]` note. `pattern` is a JavaScript regex of at most
|
|
249
434
|
500 characters, matched against the first 4,000 characters of each line inside a `node:vm` sandbox with a
|
|
250
435
|
1-second timeout, so a catastrophic pattern returns an error instead of freezing Pi. An id that is
|
|
251
|
-
|
|
436
|
+
neither a tool result nor a user prompt on the branch gives an error that tells the model to use the id
|
|
437
|
+
from a `[context-guard]` note.
|
|
252
438
|
|
|
253
439
|
## Pitfalls
|
|
254
440
|
|
|
255
|
-
- **Never
|
|
256
|
-
|
|
257
|
-
- **Never
|
|
258
|
-
|
|
441
|
+
- **Never advance state on returned drafts.** A later handler may replace them, or Pi may reject the list;
|
|
442
|
+
read state back from the branch.
|
|
443
|
+
- **Never omit half of a tool call.** `replacement: null` on a tool result alone would leave its `tool_use`
|
|
444
|
+
without a result, and providers reject the request. A single output that is not needed is replaced with
|
|
445
|
+
a one-line stub; `null` is only used for whole exchanges, which omit the assistant message with the call
|
|
446
|
+
and its results together.
|
|
447
|
+
- **Never replace an assistant message's content.** A replacement turns the content into one text block,
|
|
448
|
+
which drops thinking, signatures and tool calls. Assistant messages are only omitted, as part of a whole
|
|
449
|
+
exchange.
|
|
259
450
|
- **Steering messages move the span.** A user message sent during a run starts a new span. Tool results
|
|
260
451
|
from before it are never candidates, now or later. That is the safe direction.
|
|
261
452
|
- **`keepCitedFiles` is off by default.** Answers also name files to rule them out. In a live run, Claude's
|
|
262
453
|
answer said `test/fixtures/entities.json` was *not* a usage, and the option kept 40 lines of noise from
|
|
263
454
|
that file. See [JEV.md](JEV.md).
|
|
264
|
-
- **
|
|
265
|
-
point on
|
|
455
|
+
- **Earlier runs are only edited as whole exchanges.** Editing single outputs of older runs would break the
|
|
456
|
+
prompt cache from that point on for little gain; an old exchange is omitted as a whole, once, when it is
|
|
457
|
+
unrelated to the current work, and only its first later request pays the rewrite.
|
|
266
458
|
- **Don't distill from a `context` hook.** Output that varies between requests makes every request miss the
|
|
267
459
|
cache.
|
|
268
460
|
|
|
269
461
|
## Open questions
|
|
270
462
|
|
|
271
|
-
1.
|
|
272
|
-
If not, warm requests do not carry the question breakpoint. Not checked.
|
|
273
|
-
2. Should the pin go on the last unedited tool result instead of the question? That would keep more of the
|
|
274
|
-
run cached on the next prompt. Not tried.
|
|
275
|
-
3. Other classifier entries (e.g. `typesafe/jev-latest` with a 64k context) and non-code outputs have
|
|
463
|
+
1. Other classifier entries (e.g. `typesafe/jev-latest` with a 64k context) and non-code outputs have
|
|
276
464
|
not been tested.
|
|
277
|
-
|
|
465
|
+
2. The OpenAI Codex WebSocket transport loses its delta continuation after an edit
|
|
278
466
|
([CACHE.md](CACHE.md#openai)). It is not clear whether anything can be done about that from an extension.
|
package/docs/JEV.md
CHANGED
|
@@ -53,7 +53,7 @@ question per chunk separated them clearly: cited files 0.90–0.93, noise 0.14
|
|
|
53
53
|
A 30k-character grep with 28 chunks and 30 questions: **13,155 input tokens, 745 ms, $0.00055**. Code is
|
|
54
54
|
about 2.3 characters per token.
|
|
55
55
|
|
|
56
|
-
**Worst-case budget probe** (
|
|
56
|
+
**Worst-case budget probe** (with earlier conversation in the state): every history part at its
|
|
57
57
|
clip limit (2,000-character summary, 1,000-character first request, 800 + 1,200 characters per exchange),
|
|
58
58
|
a 4,000-character question, a 6,000-character answer, 2,000 characters of notes, and one 40-chunk segment of
|
|
59
59
|
31,848 characters of dense `rg` output over Pi's `dist/core`:
|
|
@@ -64,8 +64,8 @@ a 4,000-character question, a 6,000-character answer, 2,000 characters of notes,
|
|
|
64
64
|
| 10 (maximum) | 83,214 | **23,619** | 799 ms | $0.00099 |
|
|
65
65
|
|
|
66
66
|
Both fit Jev's 32k-token context with room to spare (the target was ≤ 28k), so `maxSegmentChars` is
|
|
67
|
-
**32,000
|
|
68
|
-
|
|
67
|
+
**32,000**, which keeps the worst case well below 32k with history included, and `maxChunksPerSegment`
|
|
68
|
+
is 40. Nested objects in the state (`earlier_conversation`) are accepted.
|
|
69
69
|
|
|
70
70
|
In the live end-to-end runs, one qualifying run took 2 Jev requests: 550 ms and $0.00083 (GPT-6 Luna run), and
|
|
71
71
|
1.18 s and $0.00084 (Claude run).
|
|
@@ -98,14 +98,14 @@ noise. It is off by default.
|
|
|
98
98
|
### Small reads are not worth it
|
|
99
99
|
|
|
100
100
|
`read` results of about 2.3k characters always ended as `not-worth`: Jev kept 2 of 3 chunks. So
|
|
101
|
-
`minResultChars`
|
|
101
|
+
`minResultChars` is 4,000.
|
|
102
102
|
|
|
103
103
|
### Earlier conversation (`historyExchanges`)
|
|
104
104
|
|
|
105
105
|
Without history, Jev only sees the run's own question and answer. A broad search in a session whose goal was
|
|
106
106
|
stated earlier ("I'm about to migrate every API client to a shared retry helper") then loses exactly the
|
|
107
107
|
lines the ongoing work needs, because this run's question ("where is StatusBadge used?") does not mention
|
|
108
|
-
them. So the state
|
|
108
|
+
them. So the state carries `earlier_conversation`: the latest compaction/branch summary, the first
|
|
109
109
|
prompt and the last 3 exchanges (prompt + last assistant text), clipped (docs/DESIGN.md).
|
|
110
110
|
|
|
111
111
|
**Wording.** Offline A/B on the real turn-2 run of the e2e `history` scenario (Claude's `rg -n -i status`,
|
|
@@ -126,28 +126,93 @@ still borderline (the relevant chunks sit around the threshold), so history make
|
|
|
126
126
|
what the ongoing work needs, not reliably all of it; `recall` remains the safety net.
|
|
127
127
|
|
|
128
128
|
**Live A/B** (`node test/e2e/run-e2e.mjs --scenario history`, with and without `--config
|
|
129
|
-
'{"historyExchanges":0}'`). Turn 1 states the retry-migration goal,
|
|
130
|
-
turn 3 asks which clients retry on 429 without a new search (9 of
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
| Model, history |
|
|
134
|
-
|
|
135
|
-
| GPT-6 Luna, on |
|
|
136
|
-
| GPT-6 Luna, off |
|
|
137
|
-
| Claude Sonnet 5.5, on | 1 |
|
|
138
|
-
| Claude Sonnet 5.5, off |
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
129
|
+
'{"historyExchanges":0}'`), with the wording `src/decide.ts` sends. Turn 1 states the retry-migration goal,
|
|
130
|
+
turn 2 is the broad StatusBadge search, turn 3 asks which clients retry on 429 without a new search (9 of
|
|
131
|
+
the 10 clients do); one run per cell:
|
|
132
|
+
|
|
133
|
+
| Model, history | src/api retry lines kept in turn 2 (of 80; with 429) | Turn-2 output, chars before → after | Jev | Turn 3 used `recall` | Turn 3 answer |
|
|
134
|
+
|---|---|---|---|---|---|
|
|
135
|
+
| GPT-6 Luna, on | **72 (21)** | 36,113 → 12,402 | 568 ms | yes | correct |
|
|
136
|
+
| GPT-6 Luna, off | 0 | 36,113 → 1,753 | 564 ms | yes | correct |
|
|
137
|
+
| Claude Sonnet 5.5, on | 5 (1) | 22,641 → 2,103 (Claude ran `rg … \| head -200`) | 555 ms | yes | missed `ticketStatus` (cut by Claude's own `head -200`) |
|
|
138
|
+
| Claude Sonnet 5.5, off | 0 | 36,113 → 2,638 | 547 ms | yes | correct list; headline says "eight" |
|
|
139
|
+
|
|
140
|
+
With history, GPT kept 72 of the 80 retry lines (9 of the 10 clients), at the cost of keeping 12.4k instead
|
|
141
|
+
of ~1.8k characters; Claude ran a truncated search and kept 5. A round with the second wording of the
|
|
142
|
+
table above kept no retry lines with history. History did not change turn-3 behaviour: the models used
|
|
143
|
+
`recall` either way, and the answers were about as good. Where it pays off is the context the model already
|
|
144
|
+
has after turn 2. Single runs per cell; the variance between runs is large.
|
|
145
|
+
|
|
146
|
+
### Checkpoint wording
|
|
147
|
+
|
|
148
|
+
Mid-run checkpoints ask a different question: there is no final answer, so Jev judges whether the agent
|
|
149
|
+
will still need an output to finish the task (`buildCheckpointRequest`; state and wording in
|
|
150
|
+
[DESIGN.md](DESIGN.md#mid-run-checkpoints)). Probed offline on real mid-run states from two no-guard runs
|
|
151
|
+
of the e2e `long` scenario (`fix all failing tests`), two repetitions per variant, reporting the share of
|
|
152
|
+
the output's characters that would be kept at chunk thresholds 0.5 / 0.55 / 0.6:
|
|
153
|
+
|
|
154
|
+
| Output (state) | What should happen | base wording | "acted" wording |
|
|
155
|
+
|---|---|---|---|
|
|
156
|
+
| GPT, 6 old `npm test` logs, each rerun later (`superseded`) | mostly go | 0.5: 15–69%, 0.6: **8–36%** | 0.5: 15–45%, 0.6: 15–30% |
|
|
157
|
+
| Claude, first `npm test` log (still the only full failure list; later runs were filtered) | keep the unresolved failures | 0.5: 73–80%, 0.6: 35–41% | 0.5: 80–87%, 0.6: 33–46% |
|
|
158
|
+
| Claude, `cat` of 5 source files, **all fixed since** | go | 0.5: 49–62%, 0.6: **11%** | 11% at every threshold |
|
|
159
|
+
| Claude, the same `cat` one turn after it ran, **files not fixed yet** | stay | 0.5: 100%, 0.6: **78%** | 0.5: 90%, 0.6: **10%** |
|
|
160
|
+
|
|
161
|
+
- *base*: "To finish user_question, will the agent still need the lines in chunk_N? Lines that are out of
|
|
162
|
+
date or that the agent has already acted on and moved past are not needed."
|
|
163
|
+
- *acted*: "Will the agent need to look at chunk_N again before it finishes user_question? Say no if
|
|
164
|
+
later_tool_calls show the agent already acted on it, or if superseded says a newer version exists."
|
|
165
|
+
It dropped the in-use files (10%) and was rejected.
|
|
166
|
+
- A stronger `superseded` sentence ("the agent still sees that newer output…"), with the acted wording, did not help (15–65% at 0.5).
|
|
167
|
+
|
|
168
|
+
Mid-run probabilities are compressed (per-chunk 0.14–0.76, against 0.14–0.93 at run end). At 0.5,
|
|
169
|
+
superseded logs and finished files kept 50–70%, which `maxKeepRatio` (0.6) would then turn into "not
|
|
170
|
+
worth it", so most checkpoints would edit nothing. **Base wording at 0.6** separated the cases: in-use
|
|
171
|
+
files 78%, finished files 11%, superseded logs 8–36%. So `midRunChunkKeepThreshold` defaults to 0.6, the
|
|
172
|
+
same as run end, not lower: the brief proposed starting at 0.5 to keep more, and the probe showed that
|
|
173
|
+
0.5 keeps nearly everything.
|
|
174
|
+
|
|
175
|
+
### Old exchanges
|
|
176
|
+
|
|
177
|
+
`buildExchangeRequest` asks one bool per exchange: "Is exchange_N still relevant to the current work
|
|
178
|
+
(user_question and what the agent is doing for it)? Relevant means the agent may need its details: the
|
|
179
|
+
same files, task, decisions or facts." Probed with `node test/e2e/probe-items.mjs` (2 repetitions):
|
|
180
|
+
|
|
181
|
+
- three hand-written sessions (`test/e2e/probe-data/exchanges.json`) modeled on the shape of real long
|
|
182
|
+
main-thread sessions (a prompt, a final answer, tool-call labels, a size; 5 old exchanges each, mixed
|
|
183
|
+
relevant and unrelated to the current task);
|
|
184
|
+
- the eligible exchanges at prompts 4 and 5 of two no-guard runs of the `topics` scenario (GPT and Claude,
|
|
185
|
+
public test repo): the first exchange (API retries) is unrelated to prompt 4 (`StatusBadge`) and relevant
|
|
186
|
+
to prompt 5 (its follow-up).
|
|
187
|
+
|
|
188
|
+
| Label | P(relevant), 42 answers |
|
|
189
|
+
|---|---|
|
|
190
|
+
| relevant (16) | 0.47–0.96 (the weakest: "add a disk-usage alert for the backup volume" for a failing-backup task, 0.47–0.49; "how is contextTokens estimated with edits" for a compaction-settings task, 0.68) |
|
|
191
|
+
| unrelated (26) | 0.03–0.10 |
|
|
192
|
+
|
|
193
|
+
| Omit when P < | Accuracy | Relevant exchanges omitted | Unrelated exchanges omitted |
|
|
194
|
+
|---|---|---|---|
|
|
195
|
+
| 0.1 – 0.4 | 42/42 | 0/16 | 26/26 |
|
|
196
|
+
| 0.5 | 41/42 | 1/16 | 26/26 |
|
|
197
|
+
|
|
198
|
+
`exchangeOmitThreshold` is 0.2: omit only when Jev is clearly sure, with a wide margin to the weakest
|
|
199
|
+
relevant exchange (0.47). The sessions are synthetic or generated, so real sessions may be less clear-cut;
|
|
200
|
+
the margin is meant to absorb that. Repetitions agreed within 0.03.
|
|
201
|
+
|
|
202
|
+
### Small outputs
|
|
203
|
+
|
|
204
|
+
`buildSmallRequest` asks one bool per output ("will the agent still need the output in item_N?" mid-run;
|
|
205
|
+
"does item_N contain anything that final_answer relies on, or that a likely follow-up would need?" at run
|
|
206
|
+
end), with `superseded` shown when a later call made the output out of date.
|
|
207
|
+
|
|
208
|
+
| Probe | Needed | Not needed |
|
|
209
|
+
|---|---|---|
|
|
210
|
+
| Two hand-written mid-run states on the `long` fixture's code (`test/e2e/probe-data/small.json`), 24 answers | 0.59–0.74 | 0.06–0.32 |
|
|
211
|
+
| A `long` run of GPT-6 Luna (public fixture), mid-run at request 12 and at run end: reads of source files | not superseded: 0.48–0.68 | superseded (the file was edited afterwards): 0.19–0.41 |
|
|
212
|
+
|
|
213
|
+
At 0.6 (the chunk threshold) 2 of 10 needed outputs would be dropped; at 0.45 none, and every superseded or
|
|
214
|
+
unneeded output goes. `smallKeepThreshold` is 0.45. Without the `superseded` hint, Jev kept reads of files
|
|
215
|
+
the agent had already edited (0.49–0.86), so the hint is part of the request.
|
|
151
216
|
|
|
152
217
|
## The final question set
|
|
153
218
|
|
|
@@ -175,5 +240,8 @@ answer did not quote but that a natural next question would need.
|
|
|
175
240
|
| `noneThreshold` | 0.5 | Remove a whole output only if `none` wins and no chunk passes its own question |
|
|
176
241
|
| `maxKeepRatio` | 0.6 | If most of the output is kept anyway, the saving does not justify the cache miss and the lost context |
|
|
177
242
|
| `minResultChars` | 4,000 | Reads around 2.3k characters were never worth an edit |
|
|
243
|
+
| `smallKeepThreshold` | 0.45 | Small-output probe above: needed outputs ≥ 0.48, superseded or unneeded ≤ 0.41 |
|
|
244
|
+
| `exchangeOmitThreshold` | 0.2 | Exchange probe above: unrelated ≤ 0.10, relevant ≥ 0.47; omit only when Jev is clearly sure |
|
|
245
|
+
| `midRunChunkKeepThreshold` | 0.6 | Checkpoint probe above: 0.5 kept 50–100% of everything; 0.6 kept in-use files and dropped finished ones |
|
|
178
246
|
|
|
179
247
|
When an answer is missing, the request fails or the budget runs out, the extension keeps everything.
|