pi-jev-lens 0.4.0 → 0.5.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 +21 -13
- package/index.ts +26 -61
- package/package.json +7 -24
- package/src/ledger.ts +1 -1
- package/src/policy.ts +3 -3
- package/STATUS.md +0 -395
- package/src/classifier.ts +0 -112
- package/src/config.ts +0 -185
- package/src/health.ts +0 -38
- package/src/presend.ts +0 -217
- package/src/shell-display.ts +0 -90
- package/src/text.ts +0 -68
- package/src/treesitter.ts +0 -220
- package/src/types.ts +0 -36
- package/src/views.ts +0 -611
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# pi-jev-lens
|
|
2
2
|
|
|
3
|
+
The pi extension of [jev-lens](https://github.com/dizk/jev-lens). The core that builds and chooses views is the
|
|
4
|
+
`jev-lens` package in the same repository; a Claude Code plugin uses the same core.
|
|
5
|
+
|
|
3
6
|
Your coding agent reads a 600-line file to change one function. jev-lens sends the model the outline of the file and
|
|
4
7
|
that one function. The agent can ask for the rest with the `recall` tool, and the footer of the result tells it so.
|
|
5
8
|
|
|
@@ -67,8 +70,7 @@ Three results shaped the design:
|
|
|
67
70
|
Use pi 0.84.3 or newer. The extension uses pi's built-in tool renderers for results that it does not compress.
|
|
68
71
|
|
|
69
72
|
```sh
|
|
70
|
-
pi install npm:pi-jev-lens
|
|
71
|
-
pi install git:github.com/dizk/pi-jev-lens # or from GitHub
|
|
73
|
+
pi install npm:pi-jev-lens
|
|
72
74
|
```
|
|
73
75
|
|
|
74
76
|
jev needs a TypeSafe API key. You can get one at [console.typesafe.ai](https://console.typesafe.ai). jev-lens looks
|
|
@@ -95,9 +97,9 @@ The mock can compress results without API calls. It is not the jev model.
|
|
|
95
97
|
For development, clone the repository and load it directly:
|
|
96
98
|
|
|
97
99
|
```sh
|
|
98
|
-
git clone https://github.com/dizk/
|
|
100
|
+
git clone https://github.com/dizk/jev-lens.git && cd jev-lens && npm install
|
|
99
101
|
echo 'TYPESAFE_API_KEY=...' > .env
|
|
100
|
-
pi -e ./index.ts
|
|
102
|
+
pi -e ./packages/pi/index.ts
|
|
101
103
|
```
|
|
102
104
|
|
|
103
105
|
## How it works
|
|
@@ -236,7 +238,7 @@ node --import tsx eval/presend-replay.ts <session dir> # replay your own pi ses
|
|
|
236
238
|
node --import tsx eval/bench/autoresearch.ts --iterations 8 # a researcher model tunes the prompts and thresholds
|
|
237
239
|
```
|
|
238
240
|
|
|
239
|
-
STATUS.md is the research log. It lists every variant that we tried, its numbers, and why the defaults are what they
|
|
241
|
+
[STATUS.md](../../STATUS.md) at the repository root is the research log. It lists every variant that we tried, its numbers, and why the defaults are what they
|
|
240
242
|
are. In short: new code-built views moved the numbers, prompt wording did not, and the small holdout was wrong about
|
|
241
243
|
code until the slice was five times larger.
|
|
242
244
|
|
|
@@ -258,17 +260,21 @@ never removes a tool result. It only rewrites it.
|
|
|
258
260
|
|
|
259
261
|
## Using this as a reference
|
|
260
262
|
|
|
261
|
-
The pieces do not depend on pi
|
|
263
|
+
The pieces that do not depend on pi live in the `jev-lens` core package (`packages/core`), which any agent can use
|
|
264
|
+
as a library; its README shows the API. What is where:
|
|
262
265
|
|
|
263
266
|
| piece | file | depends on |
|
|
264
267
|
|---|---|---|
|
|
265
|
-
|
|
|
266
|
-
|
|
|
267
|
-
|
|
|
268
|
-
| the
|
|
269
|
-
| the
|
|
268
|
+
| the pre-send pipeline: candidates, jev's choice, expansion, footer | `packages/core/src/lens.ts` | the rows below |
|
|
269
|
+
| candidate views | `packages/core/src/views.ts` | nothing. The async code views can load `treesitter.ts` |
|
|
270
|
+
| tree-sitter blocks and signatures | `packages/core/src/treesitter.ts` | `web-tree-sitter`, `@vscode/tree-sitter-wasm`, `@binclusive/tree-sitter-kotlin-wasm` |
|
|
271
|
+
| the jev questions, the state shape, the decision rule, block expansion | `packages/core/src/presend.ts` | `@typesafe-ai/sdk` |
|
|
272
|
+
| the parser for bash display commands | `packages/core/src/shell-display.ts` | nothing |
|
|
273
|
+
| the recall tool's slicing | `packages/core/src/recall.ts` | nothing |
|
|
274
|
+
| the wiring for pi (tool_result, the recall tool, the UI) | `packages/pi/index.ts`, `packages/pi/src/ui.ts` | pi |
|
|
275
|
+
| the wiring for Claude Code (PostToolUse hook, MCP server) | `packages/claude-code/src/` | Claude Code |
|
|
270
276
|
| the benchmark and the metrics on real trajectories | `eval/presend-score.ts`, `eval/bench/` | run `eval/bench/fetch.sh` first |
|
|
271
|
-
| post-send decisions and the frozen ledger | `src/classifier.ts`, `src/policy.ts`, `src/ledger.ts` | `@typesafe-ai/sdk` |
|
|
277
|
+
| post-send decisions and the frozen ledger (pi only) | `packages/core/src/classifier.ts`, `packages/pi/src/policy.ts`, `packages/pi/src/ledger.ts` | `@typesafe-ai/sdk` |
|
|
272
278
|
|
|
273
279
|
The sequence is this. When a large tool result arrives, code builds the views from the text. jev says which view is
|
|
274
280
|
enough and whether the exact text is needed. Code applies the selection policy. If needed, a second request expands
|
|
@@ -277,9 +283,11 @@ full text stays in the details of the result.
|
|
|
277
283
|
|
|
278
284
|
## Contributing and license
|
|
279
285
|
|
|
280
|
-
Issues and pull requests are welcome at [github.com/dizk/
|
|
286
|
+
Issues and pull requests are welcome at [github.com/dizk/jev-lens](https://github.com/dizk/jev-lens).
|
|
281
287
|
`npm test` runs the unit tests with the mock classifier. `npm run typecheck` runs tsc. A change to how views are
|
|
282
288
|
built or chosen must come with benchmark numbers. If the change touches code views, you must use the 500-trajectory
|
|
283
289
|
slice.
|
|
284
290
|
|
|
291
|
+
For npm publication through GitHub Releases, see [Release to npm](../../docs/releasing.md).
|
|
292
|
+
|
|
285
293
|
MIT, see LICENSE.
|
package/index.ts
CHANGED
|
@@ -18,18 +18,15 @@ import { Type } from "typebox";
|
|
|
18
18
|
import { createBashToolDefinition, createFindToolDefinition, createGrepToolDefinition, createLsToolDefinition, createReadToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { Text } from "@earendil-works/pi-tui";
|
|
20
20
|
import { ComparisonResult, comparisonHint, listLines, savingsLine, type CompressedRecord } from "./src/ui.ts";
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
import { Health } from "./src/health.ts";
|
|
21
|
+
import {
|
|
22
|
+
buildItemState, contentText, createPresend, describeToolCall, estimateTokensOfText, Health, JevClassifier, keyFilePath,
|
|
23
|
+
Lens, loadConfigWithVariant, MockClassifier, promptsWithVariant, RECALL_DESCRIPTION, RECALL_PARAM_DESCRIPTIONS, recallMissText, sliceRecall, storeKey, toolCallsOf,
|
|
24
|
+
type CallStats, type Classifier, type Decision,
|
|
25
|
+
} from "jev-lens";
|
|
27
26
|
import { SecretInput } from "./src/secret-input.ts";
|
|
28
27
|
import { commandCompletions, commandHelp } from "./src/commands.ts";
|
|
29
28
|
import { ENTRY_TYPE, rebuildLedger } from "./src/ledger.ts";
|
|
30
29
|
import { applyLedger, decideBucket, pendingPrunable, shouldApplyPending } from "./src/policy.ts";
|
|
31
|
-
import { contentText, describeToolCall, estimateTokensOfText, toolCallsOf, truncate } from "./src/text.ts";
|
|
32
|
-
import type { CallStats, Decision } from "./src/types.ts";
|
|
33
30
|
|
|
34
31
|
interface PendingResult {
|
|
35
32
|
message: AgentMessage & { role: "toolResult" };
|
|
@@ -38,18 +35,18 @@ interface PendingResult {
|
|
|
38
35
|
|
|
39
36
|
export default function (pi: ExtensionAPI) {
|
|
40
37
|
const { cfg, variant } = loadConfigWithVariant();
|
|
41
|
-
const prompts
|
|
38
|
+
const prompts = promptsWithVariant(variant);
|
|
42
39
|
const viewParams = variant.views ?? {};
|
|
43
40
|
let keySource = process.env.TYPESAFE_API_KEY === cfg.apiKey && cfg.apiKey ? "env" : cfg.apiKey ? keyFilePath() : "none";
|
|
44
|
-
let usingMock = cfg
|
|
41
|
+
let { presend, mock: usingMock } = createPresend(cfg, prompts);
|
|
45
42
|
let classifier: Classifier = usingMock ? new MockClassifier() : new JevClassifier(cfg);
|
|
46
|
-
let
|
|
43
|
+
let lens = new Lens({ cfg, presend, viewParams });
|
|
47
44
|
/** Switch from the mock to jev once a key is available (from `/jev-lens key`), without a restart. */
|
|
48
45
|
const useKey = (apiKey: string) => {
|
|
49
46
|
cfg.apiKey = apiKey;
|
|
50
|
-
usingMock = cfg
|
|
47
|
+
({ presend, mock: usingMock } = createPresend(cfg, prompts));
|
|
51
48
|
classifier = usingMock ? new MockClassifier() : new JevClassifier(cfg);
|
|
52
|
-
|
|
49
|
+
lens = new Lens({ cfg, presend, viewParams });
|
|
53
50
|
};
|
|
54
51
|
/** Full text of compressed tool results, by toolCallId, for the recall tool (also persisted in result details). */
|
|
55
52
|
const fullOutputs = new Map<string, { text: string; toolName: string; args: unknown; view: string }>();
|
|
@@ -365,38 +362,25 @@ export default function (pi: ExtensionAPI) {
|
|
|
365
362
|
if (tokens < cfg.presendMinTokens) return;
|
|
366
363
|
if (event.content.some((c) => c.type === "image")) return;
|
|
367
364
|
presendTotals.considered++;
|
|
368
|
-
const started = Date.now();
|
|
369
365
|
try {
|
|
370
|
-
const
|
|
371
|
-
const cands = await buildCandidatesAsync(event.toolName, event.input, text, terms, viewParams);
|
|
366
|
+
const out = await lens.compress({ toolCallId: event.toolCallId, toolName: event.toolName, args: event.input, text, isError: event.isError, context: { firstUser, latestUser, agentText: lastAssistantText } }, signal);
|
|
372
367
|
if (epoch !== generation || signal.aborted) return;
|
|
373
|
-
if (
|
|
368
|
+
if (out.reason === "no-candidates") {
|
|
374
369
|
log({ event: "presend", id: event.toolCallId, tool: event.toolName, tokens, view: "full", reason: "no-candidates" });
|
|
375
370
|
return;
|
|
376
371
|
}
|
|
377
|
-
const totalLines = text.split("\n").length;
|
|
378
|
-
const state = buildPresendState(cfg, { firstUser, latestUser, agentText: lastAssistantText, toolName: event.toolName, args: event.input, isError: event.isError, cands, totalLines, totalChars: text.length });
|
|
379
|
-
const answer = await presend.choose(state, cands.views.map((v) => v.kind), signal);
|
|
380
|
-
if (epoch !== generation || signal.aborted) return;
|
|
381
|
-
let view = decideView(answer, cands, cfg);
|
|
382
|
-
let expanded: number[] | undefined;
|
|
383
|
-
if (view.kind !== "full") {
|
|
384
|
-
const above = cands.kind === "command" ? cfg.presendSectionExpandAbove : cfg.presendExpandAbove;
|
|
385
|
-
const ex = await expandRelevantBlocks(presend, state, text, cands, view, above, signal, cands.blocks, cfg.presendSectionFloor);
|
|
386
|
-
if (epoch !== generation || signal.aborted) return;
|
|
387
|
-
if (ex) { view = ex.view; expanded = ex.probs.map((p, i) => (p > above ? i : -1)).filter((i) => i >= 0); }
|
|
388
|
-
}
|
|
389
372
|
health.success("presend");
|
|
390
373
|
status(ctx);
|
|
391
|
-
|
|
392
|
-
|
|
374
|
+
const view = out.view!, answer = out.answer!;
|
|
375
|
+
log({ event: "presend", id: event.toolCallId, tool: event.toolName, kind: out.kind, tokens, view: view.kind, viewTokens: out.sentTokens, chosen: answer.choice, needsFull: answer.needsFull, p: answer.probabilities, confidence: answer.confidence, expanded: out.expanded, candidates: out.candidates, ms: out.ms });
|
|
376
|
+
if (!out.compressed) return;
|
|
393
377
|
presendTotals.compressed++;
|
|
394
|
-
presendTotals.tokensSaved += tokens -
|
|
378
|
+
presendTotals.tokensSaved += tokens - out.sentTokens;
|
|
395
379
|
fullOutputs.set(event.toolCallId, { text, toolName: event.toolName, args: event.input, view: view.kind });
|
|
396
|
-
remember({ id: event.toolCallId, toolName: event.toolName, args: event.input, kind:
|
|
397
|
-
const details = { ...((event.details as object) ?? {}), jevLens: { full: text, view: view.kind, kind:
|
|
380
|
+
remember({ id: event.toolCallId, toolName: event.toolName, args: event.input, kind: out.kind!, view: view.kind, tokensBefore: tokens, tokensAfter: out.sentTokens, full: text, sent: view.text, included: view.included, needsFull: answer.needsFull, pFull: answer.probabilities.full, recalls: 0, at: Date.now() });
|
|
381
|
+
const details = { ...((event.details as object) ?? {}), jevLens: { full: text, view: view.kind, kind: out.kind, args: event.input, p: answer.probabilities, needsFull: answer.needsFull, included: view.included } };
|
|
398
382
|
status(ctx);
|
|
399
|
-
return { content: [{ type: "text", text:
|
|
383
|
+
return { content: [{ type: "text", text: out.text }], details };
|
|
400
384
|
} catch (err) {
|
|
401
385
|
if (epoch !== generation || signal.aborted) return;
|
|
402
386
|
health.failure("presend", err);
|
|
@@ -409,11 +393,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
409
393
|
pi.registerTool({
|
|
410
394
|
name: "recall",
|
|
411
395
|
label: "Recall",
|
|
412
|
-
description:
|
|
396
|
+
description: RECALL_DESCRIPTION,
|
|
413
397
|
parameters: Type.Object({
|
|
414
|
-
id: Type.String({ description:
|
|
415
|
-
lines: Type.Optional(Type.String({ description:
|
|
416
|
-
pattern: Type.Optional(Type.String({ description:
|
|
398
|
+
id: Type.String({ description: RECALL_PARAM_DESCRIPTIONS.id }),
|
|
399
|
+
lines: Type.Optional(Type.String({ description: RECALL_PARAM_DESCRIPTIONS.lines })),
|
|
400
|
+
pattern: Type.Optional(Type.String({ description: RECALL_PARAM_DESCRIPTIONS.pattern })),
|
|
417
401
|
}),
|
|
418
402
|
async execute(_toolCallId, params) {
|
|
419
403
|
presendTotals.recalls++;
|
|
@@ -421,28 +405,9 @@ export default function (pi: ExtensionAPI) {
|
|
|
421
405
|
if (rec) rec.recalls++;
|
|
422
406
|
const hit = fullOutputs.get(params.id);
|
|
423
407
|
log({ event: "recall", id: params.id, found: !!hit, lines: params.lines, pattern: params.pattern });
|
|
424
|
-
if (!hit) return { content: [{ type: "text", text:
|
|
425
|
-
const
|
|
426
|
-
|
|
427
|
-
if (params.lines) {
|
|
428
|
-
const m = params.lines.match(/^(\d+)\s*-\s*(\d+)$/);
|
|
429
|
-
if (!m) return { content: [{ type: "text", text: "lines must look like 120-180" }], details: { id: params.id, lines: 0 } };
|
|
430
|
-
const a = Math.max(1, Number(m[1])), b = Math.min(all.length, Number(m[2]));
|
|
431
|
-
idx = idx.filter((i) => i + 1 >= a && i + 1 <= b);
|
|
432
|
-
}
|
|
433
|
-
if (params.pattern) {
|
|
434
|
-
let test: (l: string) => boolean;
|
|
435
|
-
const rx = params.pattern.match(/^\/(.*)\/([a-z]*)$/);
|
|
436
|
-
if (rx) { const re = new RegExp(rx[1], rx[2].includes("i") ? rx[2] : rx[2] + "i"); test = (l) => re.test(l); }
|
|
437
|
-
else { const needle = params.pattern.toLowerCase(); test = (l) => l.toLowerCase().includes(needle); }
|
|
438
|
-
const keep = new Set<number>();
|
|
439
|
-
for (const i of idx) if (test(all[i])) for (let j = Math.max(0, i - 2); j <= Math.min(all.length - 1, i + 2); j++) keep.add(j);
|
|
440
|
-
idx = idx.filter((i) => keep.has(i));
|
|
441
|
-
}
|
|
442
|
-
const width = String(all.length).length;
|
|
443
|
-
const body = idx.length === all.length ? hit.text : idx.map((i) => `${String(i + 1).padStart(width)}│ ${all[i]}`).join("\n");
|
|
444
|
-
const header = idx.length === all.length ? "" : `[${idx.length} of ${all.length} lines from ${hit.toolName} ${truncate(JSON.stringify(hit.args ?? {}), 80)}]\n`;
|
|
445
|
-
return { content: [{ type: "text", text: header + body }], details: { id: params.id, lines: idx.length } };
|
|
408
|
+
if (!hit) return { content: [{ type: "text", text: recallMissText(params.id) }], details: { id: params.id, lines: 0 } };
|
|
409
|
+
const slice = sliceRecall(hit, params);
|
|
410
|
+
return { content: [{ type: "text", text: slice.text }], details: { id: params.id, lines: slice.count } };
|
|
446
411
|
},
|
|
447
412
|
});
|
|
448
413
|
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-jev-lens",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "pi extension that compresses large tool results before they reach the model: jev picks the view (outline, relevant blocks, sections, signals, testlog), full text stays recallable",
|
|
5
5
|
"author": "Didrik Rognstad",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "git+https://github.com/dizk/
|
|
9
|
+
"url": "git+https://github.com/dizk/jev-lens.git",
|
|
10
|
+
"directory": "packages/pi"
|
|
10
11
|
},
|
|
11
|
-
"homepage": "https://github.com/dizk/
|
|
12
|
+
"homepage": "https://github.com/dizk/jev-lens#readme",
|
|
12
13
|
"bugs": {
|
|
13
|
-
"url": "https://github.com/dizk/
|
|
14
|
+
"url": "https://github.com/dizk/jev-lens/issues"
|
|
14
15
|
},
|
|
15
16
|
"keywords": [
|
|
16
17
|
"pi-package",
|
|
@@ -31,37 +32,19 @@
|
|
|
31
32
|
"index.ts",
|
|
32
33
|
"src/",
|
|
33
34
|
"README.md",
|
|
34
|
-
"LICENSE"
|
|
35
|
-
"STATUS.md"
|
|
35
|
+
"LICENSE"
|
|
36
36
|
],
|
|
37
|
-
"scripts": {
|
|
38
|
-
"test": "vitest run",
|
|
39
|
-
"replay": "node --import tsx eval/replay.ts",
|
|
40
|
-
"typecheck": "tsc --noEmit"
|
|
41
|
-
},
|
|
42
37
|
"pi": {
|
|
43
38
|
"extensions": [
|
|
44
39
|
"./index.ts"
|
|
45
40
|
]
|
|
46
41
|
},
|
|
47
42
|
"dependencies": {
|
|
48
|
-
"
|
|
49
|
-
"@typesafe-ai/sdk": "^0.6.0",
|
|
50
|
-
"@vscode/tree-sitter-wasm": "^0.3.1",
|
|
51
|
-
"web-tree-sitter": "^0.27.0"
|
|
43
|
+
"jev-lens": "^0.5.0"
|
|
52
44
|
},
|
|
53
45
|
"peerDependencies": {
|
|
54
46
|
"@earendil-works/pi-coding-agent": ">=0.84.3",
|
|
55
47
|
"@earendil-works/pi-tui": "*",
|
|
56
48
|
"typebox": "*"
|
|
57
|
-
},
|
|
58
|
-
"devDependencies": {
|
|
59
|
-
"@types/node": "^26.6.1",
|
|
60
|
-
"tsx": "^4.23.13",
|
|
61
|
-
"typescript": "^7.0.2",
|
|
62
|
-
"vitest": "^5.0.1",
|
|
63
|
-
"@earendil-works/pi-coding-agent": "^0.84.3",
|
|
64
|
-
"@earendil-works/pi-tui": "^0.84.3",
|
|
65
|
-
"typebox": "^1.3.33"
|
|
66
49
|
}
|
|
67
50
|
}
|
package/src/ledger.ts
CHANGED
package/src/policy.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AgentMessage, ToolResultMessage } from "./pi-types.ts";
|
|
2
|
-
import type { Config } from "
|
|
3
|
-
import type { Bucket, Decision } from "
|
|
4
|
-
import { contentText, estimateTokensOfText } from "
|
|
2
|
+
import type { Config } from "jev-lens";
|
|
3
|
+
import type { Bucket, Decision } from "jev-lens";
|
|
4
|
+
import { contentText, estimateTokensOfText } from "jev-lens";
|
|
5
5
|
|
|
6
6
|
export const STUB_PREFIX = "[jev-lens pruned:";
|
|
7
7
|
|