pi-jev-lens 0.4.1 → 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 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 # from npm
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/pi-jev-lens.git && cd pi-jev-lens && npm install
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. You can lift them into another agent:
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
- | candidate views | `src/views.ts` | nothing. The async code views can load `src/treesitter.ts` |
266
- | tree-sitter blocks and signatures | `src/treesitter.ts` | `web-tree-sitter`, `@vscode/tree-sitter-wasm`, `@binclusive/tree-sitter-kotlin-wasm` |
267
- | the jev questions, the state shape, the decision rule, block expansion | `src/presend.ts` | `@typesafe-ai/sdk` |
268
- | the parser for bash display commands | `src/shell-display.ts` | nothing |
269
- | the hook wiring for pi (tool_result, the recall tool, the UI) | `index.ts`, `src/ui.ts` | pi |
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,11 +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/pi-jev-lens](https://github.com/dizk/pi-jev-lens).
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
 
285
- For npm publication through GitHub Releases, see [Release to npm](docs/releasing.md).
291
+ For npm publication through GitHub Releases, see [Release to npm](../../docs/releasing.md).
286
292
 
287
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 { TypeSafeClient } from "@typesafe-ai/sdk";
22
- import { buildItemState, JevClassifier, MockClassifier, type Classifier } from "./src/classifier.ts";
23
- import { buildPresendState, decideView, DEFAULT_PROMPTS, expandRelevantBlocks, JevPresend, MockPresend, type PresendClassifier, type PromptVariant } from "./src/presend.ts";
24
- import { buildCandidatesAsync, extractTerms, footer } from "./src/views.ts";
25
- import { keyFilePath, loadConfigWithVariant, storeKey, type Config } from "./src/config.ts";
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: PromptVariant = { ...DEFAULT_PROMPTS, ...((variant.prompts ?? {}) as Partial<PromptVariant>), viewDescriptions: { ...DEFAULT_PROMPTS.viewDescriptions, ...(((variant.prompts ?? {}) as Partial<PromptVariant>).viewDescriptions ?? {}) } };
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.forceMock || !cfg.apiKey;
41
+ let { presend, mock: usingMock } = createPresend(cfg, prompts);
45
42
  let classifier: Classifier = usingMock ? new MockClassifier() : new JevClassifier(cfg);
46
- let presend: PresendClassifier = usingMock ? new MockPresend() : new JevPresend(new TypeSafeClient({ apiKey: cfg.apiKey }), cfg.model, prompts);
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.forceMock;
47
+ ({ presend, mock: usingMock } = createPresend(cfg, prompts));
51
48
  classifier = usingMock ? new MockClassifier() : new JevClassifier(cfg);
52
- presend = usingMock ? new MockPresend() : new JevPresend(new TypeSafeClient({ apiKey }), cfg.model, prompts);
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 terms = extractTerms(latestUser, lastAssistantText, JSON.stringify(event.input ?? {}));
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 (cands.views.length < 2) {
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
- log({ event: "presend", id: event.toolCallId, tool: event.toolName, kind: cands.kind, tokens, view: view.kind, viewTokens: estimateTokensOfText(view.text), chosen: answer.choice, needsFull: answer.needsFull, p: answer.probabilities, confidence: answer.confidence, expanded, candidates: cands.views.map((v) => `${v.kind}:${v.chars}`), ms: Date.now() - started });
392
- if (view.kind === "full") return;
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 - estimateTokensOfText(view.text);
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: cands.kind, view: view.kind, tokensBefore: tokens, tokensAfter: estimateTokensOfText(view.text), full: text, sent: view.text, included: view.included, needsFull: answer.needsFull, pFull: answer.probabilities.full, recalls: 0, at: Date.now() });
397
- const details = { ...((event.details as object) ?? {}), jevLens: { full: text, view: view.kind, kind: cands.kind, args: event.input, p: answer.probabilities, needsFull: answer.needsFull, included: view.included } };
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: view.text + footer(view, event.toolCallId, totalLines) }], details };
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: "Return the full output of an earlier tool call that jev-lens showed in a reduced view (or that was pruned). Pass the id from the [jev-lens: ...] note. Optionally restrict to a line range \"a-b\" or to lines matching a pattern (case-insensitive substring or /regex/).",
396
+ description: RECALL_DESCRIPTION,
413
397
  parameters: Type.Object({
414
- id: Type.String({ description: "toolCallId from the jev-lens note" }),
415
- lines: Type.Optional(Type.String({ description: "Line range like 120-180 (1-based, inclusive)" })),
416
- pattern: Type.Optional(Type.String({ description: "Only lines matching this substring or /regex/, with 2 lines of context" })),
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: `No stored output for id ${params.id}. Re-run the original tool instead.` }], details: { id: params.id, lines: 0 } };
425
- const all = hit.text.split("\n");
426
- let idx = all.map((_, i) => i);
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.4.1",
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/pi-jev-lens.git"
9
+ "url": "git+https://github.com/dizk/jev-lens.git",
10
+ "directory": "packages/pi"
10
11
  },
11
- "homepage": "https://github.com/dizk/pi-jev-lens#readme",
12
+ "homepage": "https://github.com/dizk/jev-lens#readme",
12
13
  "bugs": {
13
- "url": "https://github.com/dizk/pi-jev-lens/issues"
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
- "@binclusive/tree-sitter-kotlin-wasm": "^0.1.0",
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
@@ -1,4 +1,4 @@
1
- import type { Decision } from "./types.ts";
1
+ import type { Decision } from "jev-lens";
2
2
 
3
3
  export const ENTRY_TYPE = "jev-lens";
4
4
 
package/src/policy.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { AgentMessage, ToolResultMessage } from "./pi-types.ts";
2
- import type { Config } from "./config.ts";
3
- import type { Bucket, Decision } from "./types.ts";
4
- import { contentText, estimateTokensOfText } from "./text.ts";
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