@lienat/pi-jev-compaction 0.1.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/LICENSE +21 -0
- package/README.en.md +215 -0
- package/README.md +171 -0
- package/assets/tamarajtran-jev-compaction.gif +0 -0
- package/assets/tamarajtran-jev-compaction.mp4 +0 -0
- package/extensions/fast-jev-compaction.ts +94 -0
- package/node_modules/fast-jev-compaction/LICENSE +21 -0
- package/node_modules/fast-jev-compaction/README.md +276 -0
- package/node_modules/fast-jev-compaction/cli/jev-find.mjs +103 -0
- package/node_modules/fast-jev-compaction/cli/jev-qa.mjs +159 -0
- package/node_modules/fast-jev-compaction/dist/client.d.ts +21 -0
- package/node_modules/fast-jev-compaction/dist/client.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/client.js +26 -0
- package/node_modules/fast-jev-compaction/dist/client.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/compact.d.ts +30 -0
- package/node_modules/fast-jev-compaction/dist/compact.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/compact.js +234 -0
- package/node_modules/fast-jev-compaction/dist/compact.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/index.d.ts +7 -0
- package/node_modules/fast-jev-compaction/dist/index.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/index.js +7 -0
- package/node_modules/fast-jev-compaction/dist/index.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/messages.d.ts +6 -0
- package/node_modules/fast-jev-compaction/dist/messages.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/messages.js +7 -0
- package/node_modules/fast-jev-compaction/dist/messages.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/request.d.ts +20 -0
- package/node_modules/fast-jev-compaction/dist/request.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/request.js +51 -0
- package/node_modules/fast-jev-compaction/dist/request.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/state.d.ts +29 -0
- package/node_modules/fast-jev-compaction/dist/state.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/state.js +256 -0
- package/node_modules/fast-jev-compaction/dist/state.js.map +1 -0
- package/node_modules/fast-jev-compaction/dist/types.d.ts +178 -0
- package/node_modules/fast-jev-compaction/dist/types.d.ts.map +1 -0
- package/node_modules/fast-jev-compaction/dist/types.js +2 -0
- package/node_modules/fast-jev-compaction/dist/types.js.map +1 -0
- package/node_modules/fast-jev-compaction/package.json +39 -0
- package/package.json +42 -0
- package/src/adapter.ts +373 -0
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
[](LICENSE)
|
|
2
|
+
[](https://www.npmjs.com/package/jev-compact)
|
|
3
|
+
[](https://github.com/tamaratran/fast-jev-compaction)
|
|
4
|
+
[](#fork-additions-openrouter-only-setup)
|
|
5
|
+
|
|
6
|
+
# fast-jev-compaction
|
|
7
|
+
|
|
8
|
+
Claude Code plugin that replaces the compaction summary with Jev decisions:
|
|
9
|
+
every tool call and result is scored in one fast request, stale ones are
|
|
10
|
+
dropped or truncated, everything kept stays verbatim. Also usable as an npm
|
|
11
|
+
library.
|
|
12
|
+
|
|
13
|
+
## Fork additions: jev-compact + jev-gate (OpenRouter, no TypeSafe key)
|
|
14
|
+
|
|
15
|
+
This fork (github.com/aleksvega) adds two small CLI tools in `cli/` that run
|
|
16
|
+
Jev through **OpenRouter's `/api/alpha/decisions`** (model `typesafe/jev-1.13`),
|
|
17
|
+
so only an `OPENROUTER_API_KEY` is required — no TypeSafe API key. The upstream
|
|
18
|
+
library and Claude Code plugin below are unchanged; to point them at
|
|
19
|
+
OpenRouter instead, set `baseUrl` (library option) or `TYPESAFE_BASE_URL` +
|
|
20
|
+
`TYPESAFE_MODEL=typesafe/jev-1.13`.
|
|
21
|
+
|
|
22
|
+
**Setup**
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install && npm run build
|
|
26
|
+
export OPENROUTER_API_KEY="sk-or-..." # your key; never commit it
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**jev-compact — verbatim session compaction**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
node cli/jev-compact.mjs transcript.json -o dump.md
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Input: a JSON array of `{role, text, toolUses, toolResults}`. One batched Jev
|
|
36
|
+
request decides, per tool call, whether the call and/or its result must stay
|
|
37
|
+
verbatim; the rest is dropped. Typical run: 62→6 messages, 41K→4.6K chars,
|
|
38
|
+
~$0.00002, output begins with a stats line.
|
|
39
|
+
|
|
40
|
+
**jev-gate — confidence-gated pre-push guardrail**
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
python cli/jev-gate.py /path/to/repo # exit 0 = ALLOW, 1 = BLOCK
|
|
44
|
+
python cli/jev-gate.py /path/to/repo --threshold 0.9
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
One batched call (3 Noul + 1 Score over the last commit's diff): hardcoded
|
|
48
|
+
secrets, syntax errors, breaking changes, test-failure risk. Blocks at
|
|
49
|
+
probability ≥ threshold (default 0.85) or score ≥ 3; an API failure also
|
|
50
|
+
blocks (fail-safe). ~400 ms, ~$0.00005. Verified: clean diff → ALLOW;
|
|
51
|
+
diff containing an `sk-...` key → BLOCK at p=0.99.
|
|
52
|
+
|
|
53
|
+
MIT, upstream credit: [tamaratran/fast-jev-compaction](https://github.com/tamaratran/fast-jev-compaction).
|
|
54
|
+
|
|
55
|
+
## What and why
|
|
56
|
+
|
|
57
|
+
Most context compaction asks an LLM to summarize old turns. A summary is
|
|
58
|
+
lossy: a file path, exact error, constraint, or command can disappear even when
|
|
59
|
+
it matters later. This library never rewrites anything. It only deletes tool
|
|
60
|
+
calls and tool results Jev says are no longer needed, and it asks Jev while
|
|
61
|
+
showing it the whole conversation. User and assistant text stays verbatim and
|
|
62
|
+
in order.
|
|
63
|
+
|
|
64
|
+
The repository is both an npm package (`src/`) and a Claude Code plugin
|
|
65
|
+
(`hooks/`, `.claude-plugin/`) that uses the package to replace Claude Code's
|
|
66
|
+
built-in compaction summary with the original messages.
|
|
67
|
+
|
|
68
|
+
## How it works
|
|
69
|
+
|
|
70
|
+
1. Every `tool_use` is paired with its `tool_result` by `tool_use_id`. Calls in
|
|
71
|
+
the first message or in the newest `preserveRecentMessages` messages are
|
|
72
|
+
pinned and never touched.
|
|
73
|
+
2. The **state** sent to Jev is the whole conversation so far, oldest first,
|
|
74
|
+
with every tool result replaced by a short note (`ok, 4213 chars (omitted)`).
|
|
75
|
+
Tool inputs are included, texts are included, nothing is summarized.
|
|
76
|
+
3. The state is fitted into `maxStateTokens` (25k by default) in stages, each
|
|
77
|
+
applied only if the previous one was not enough: tool inputs truncated to
|
|
78
|
+
1000, then 200, then 60 characters; long texts abridged to head + tail,
|
|
79
|
+
oldest non-pinned messages first; old non-pinned messages collapsed to a
|
|
80
|
+
`[… N chars omitted …]` note; old tool calls reduced to one line each
|
|
81
|
+
(`t12 Read file_path=src/a.ts → ok 480ch`); old call-less messages left
|
|
82
|
+
out; runs of old call-only messages folded into one entry. If it still
|
|
83
|
+
does not fit, compaction throws. Tokens are estimated without a tokenizer (a
|
|
84
|
+
word per six letters, half a token per digit, ~one per other symbol),
|
|
85
|
+
calibrated to land a little above the counts Jev reports.
|
|
86
|
+
4. For every non-pinned call Jev gets two `noul` questions: should the **call**
|
|
87
|
+
stay (knowing it was made, with its input, still matters), and should the
|
|
88
|
+
**result** stay verbatim (its contents are still needed and re-running the
|
|
89
|
+
tool would not do).
|
|
90
|
+
5. Questions are split into as many requests as needed so state plus questions
|
|
91
|
+
stays under `maxRequestTokens` (30k by default, under Jev's 32k request
|
|
92
|
+
limit). The same full state is resent with every request; requests run
|
|
93
|
+
concurrently and their answers are merged.
|
|
94
|
+
6. Decisions per call, against `keepThreshold`:
|
|
95
|
+
- `keepResult ≥ threshold` → keep call and result;
|
|
96
|
+
- else `keepCall ≥ threshold` → keep the call, truncate the result to its
|
|
97
|
+
first `truncateHeadChars` characters plus a one-line note;
|
|
98
|
+
- else → remove the call together with its result.
|
|
99
|
+
7. The message list is rebuilt: a message that loses all its content is
|
|
100
|
+
removed, untouched messages are returned as the same objects, and no result
|
|
101
|
+
is ever left without its call.
|
|
102
|
+
|
|
103
|
+
Jev failures, malformed answers, a missing key, or a history that cannot be
|
|
104
|
+
fitted throw; the caller (or the Claude Code hook) decides what to fall back to.
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
### hermes-compact - OpenAI-chat transcripts (Hermes-compatible)
|
|
108
|
+
|
|
109
|
+
npm install -g jev-compact
|
|
110
|
+
OPENROUTER_API_KEY=... hermes-compact transcript.json -o compacted.json
|
|
111
|
+
|
|
112
|
+
Input: JSON array / {"messages":[...]} / JSONL of OpenAI-chat messages
|
|
113
|
+
(string or array content, nested or flat tool_calls). Output: compacted
|
|
114
|
+
transcript JSON with verbatim kept messages and stats. Adapter semantics
|
|
115
|
+
ported from [deadczarvc/hermes-jev-compaction](https://github.com/deadczarvc/hermes-jev-compaction) (MIT) - thanks! Difference: this build needs no
|
|
116
|
+
TypeSafe key (OpenRouter backend).
|
|
117
|
+
|
|
118
|
+
## Install and usage (upstream — TypeSafe endpoint)
|
|
119
|
+
|
|
120
|
+
The upstream library targets the official TypeSafe API. **This fork does not need
|
|
121
|
+
a TypeSafe key**: use the OpenRouter setup in the "Fork additions" section above
|
|
122
|
+
(`OPENROUTER_API_KEY` only, model `typesafe/jev-1.13:latest` via
|
|
123
|
+
`https://openrouter.ai/api/alpha/decisions`).
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npm install fast-jev-compaction
|
|
127
|
+
export TYPESAFE_API_KEY=... # only needed for the upstream/official endpoint
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
import { compactMessages, reductionRatio, type Message } from 'fast-jev-compaction';
|
|
132
|
+
|
|
133
|
+
const transcript: Message[] = [
|
|
134
|
+
{ role: 'user', text: 'Fix the failing test. Never edit src/generated.', toolUses: [] },
|
|
135
|
+
{
|
|
136
|
+
role: 'assistant',
|
|
137
|
+
text: '',
|
|
138
|
+
toolUses: [{ tool_use_id: 'toolu_1', tool: 'Read', input: { file_path: 'src/a.ts' } }],
|
|
139
|
+
},
|
|
140
|
+
{ role: 'user', text: '', toolUses: [], toolResults: [{ tool_use_id: 'toolu_1', text: '…file…' }] },
|
|
141
|
+
// …
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
const result = await compactMessages(transcript, { preserveRecentMessages: 4 });
|
|
145
|
+
console.log(result.messages, result.decisions, result.stats);
|
|
146
|
+
if (reductionRatio(result) < 0.25) {
|
|
147
|
+
// not worth it: keep the original transcript, or summarize instead
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
`Message` is a subset of Claude Code's `SessionMessage`, so a session transcript
|
|
152
|
+
can be passed in as is.
|
|
153
|
+
|
|
154
|
+
To bring your own transport, implement `JevAsker` (one `ask(state, questions)`
|
|
155
|
+
method) and call `compact(messages, asker, options)`; `buildJevRequest` and
|
|
156
|
+
`parseJevResponse` give you the HTTP request body and response validation.
|
|
157
|
+
The building blocks (`collectToolCalls`, `fitState`, `batchCalls`,
|
|
158
|
+
`decideCall`, `applyDecisions`) are exported too.
|
|
159
|
+
|
|
160
|
+
`apiKey` defaults to `process.env.TYPESAFE_API_KEY`. Never commit the key or
|
|
161
|
+
put it in a source file.
|
|
162
|
+
|
|
163
|
+
## Options
|
|
164
|
+
|
|
165
|
+
| Option | Default | Description |
|
|
166
|
+
| --- | --- | --- |
|
|
167
|
+
| `apiKey` | `TYPESAFE_API_KEY` | TypeSafe API key (`compactMessages`/`JevClient`) |
|
|
168
|
+
| `model` | `jev-latest` | Jev model name |
|
|
169
|
+
| `baseUrl` | `https://api.typesafe.ai/v1/systemone` | System One endpoint |
|
|
170
|
+
| `fetch` | native `fetch` | Injectable fetch implementation for tests |
|
|
171
|
+
| `goal` | last 3 user prompts | Ongoing task description included in the state |
|
|
172
|
+
| `keepThreshold` | `0.5` | Minimum keep probability for a call or result to stay |
|
|
173
|
+
| `preserveRecentMessages` | `6` | Newest messages never touched (the first is always kept) |
|
|
174
|
+
| `maxStateTokens` | `25000` | Estimated token ceiling for the state |
|
|
175
|
+
| `maxRequestTokens` | `30000` | Estimated ceiling for state plus one batch of questions |
|
|
176
|
+
| `truncateHeadChars` | `300` | Characters of a dropped tool result retained before its note |
|
|
177
|
+
|
|
178
|
+
`result.stats` reports message and character counts before and after, the
|
|
179
|
+
per-reason decision counts, the state size in estimated tokens, which fitting
|
|
180
|
+
stage was needed, and the number of requests.
|
|
181
|
+
|
|
182
|
+
## Limitations
|
|
183
|
+
|
|
184
|
+
- Only tool calls and results are candidates; text messages are never removed
|
|
185
|
+
or shortened in the output (they are only abridged in the state Jev sees).
|
|
186
|
+
- Token sizes are estimates from character counts, not a tokenizer.
|
|
187
|
+
- Calibration is at the request level; a probability is not a proof that a
|
|
188
|
+
result is safe to delete. The assistant can always re-run the tool.
|
|
189
|
+
- The full state is repeated with every request, so a history near the state
|
|
190
|
+
ceiling costs one request per handful of questions.
|
|
191
|
+
|
|
192
|
+
## Claude Code plugin
|
|
193
|
+
|
|
194
|
+
The repository root is a Claude Code function-hook plugin: `hooks/fast-jev.ts`
|
|
195
|
+
is a thin adapter that feeds `session.compact` transcripts through `src/` and
|
|
196
|
+
falls back to Claude Code's built-in summary on errors or insufficient
|
|
197
|
+
reduction. See [`hooks/README.md`](hooks/README.md) for configuration and the
|
|
198
|
+
Claude Code 2.1.274 type reference.
|
|
199
|
+
|
|
200
|
+
### Install in Claude Code
|
|
201
|
+
|
|
202
|
+
Function hooks are an early-access Claude Code feature (2.1.274+), so the
|
|
203
|
+
opt-in flag must be set wherever Claude Code runs, e.g. in `~/.claude/settings.json`:
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{ "env": { "CLAUDE_CODE_ENABLE_FUNCTION_HOOKS": "1", "TYPESAFE_API_KEY": "<your key>" } }
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Then add this repository as a plugin marketplace and install the plugin,
|
|
210
|
+
either from the shell or as slash commands inside a session:
|
|
211
|
+
|
|
212
|
+
```sh
|
|
213
|
+
claude plugin marketplace add tamaratran/fast-jev-compaction
|
|
214
|
+
claude plugin install fast-jev-compaction@fast-jev-compaction
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The install prompts for the plugin options (API key, thresholds, `truncateHeadChars`,
|
|
218
|
+
…); leave them at their defaults to use `TYPESAFE_API_KEY` from the environment.
|
|
219
|
+
Restart Claude Code or run `/reload-plugins`. From then on `/compact` (and
|
|
220
|
+
auto-compaction) goes through Jev: the toast reads
|
|
221
|
+
`fast-jev-compaction: kept N/M messages, no summary (…)` when the pruned history
|
|
222
|
+
replaced the built-in summary, or `fallback to built-in summary (…)` when Jev
|
|
223
|
+
could not remove enough (short sessions, or when it fails).
|
|
224
|
+
|
|
225
|
+
To run from a checkout without installing: `CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 claude --plugin-dir .`
|
|
226
|
+
from the repository root. No publishing step is required; the marketplace is
|
|
227
|
+
just the repo's `.claude-plugin/marketplace.json`.
|
|
228
|
+
|
|
229
|
+
## Development
|
|
230
|
+
|
|
231
|
+
```sh
|
|
232
|
+
npm install
|
|
233
|
+
npm run typecheck # library + hook
|
|
234
|
+
npm test
|
|
235
|
+
npm run build
|
|
236
|
+
npm run validate:plugin # claude plugin validate
|
|
237
|
+
TYPESAFE_API_KEY="$(cat ~/.typesafe_key)" npm run demo
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
The unit tests use a fake Jev and never contact TypeSafe. The demo is the live
|
|
241
|
+
network check.
|
|
242
|
+
|
|
243
|
+
## Animated demo (macOS)
|
|
244
|
+
|
|
245
|
+
`demo/JevDemo` is a small native SwiftUI app that plays a scripted, dramatized
|
|
246
|
+
version of the compaction flow inside a Claude Code-style terminal: the tool
|
|
247
|
+
calls of a canned transcript are scored, results and calls Jev lets go turn red
|
|
248
|
+
and collapse away, and the rest stays verbatim. It never calls the API; it
|
|
249
|
+
exists to be screen recorded.
|
|
250
|
+
|
|
251
|
+
```sh
|
|
252
|
+
demo/JevDemo/build.sh # builds demo/JevDemo/build/JevDemo.app and launches it
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
Press space in the app to replay from the start.
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
## jev-qa — быстрый QA-скан репозитория
|
|
259
|
+
|
|
260
|
+
Ищет ошибки кода за секунды: стадия 1 — бесплатные синтаксис-проверки (py_compile / node --check), стадия 2 — Jev-семантика по каждому файлу (баг, error-handling, security, logic — все вопросы одним пакетом, параллельно).
|
|
261
|
+
|
|
262
|
+
```bash
|
|
263
|
+
OPENROUTER_API_KEY=... jev-qa <repo> [--diff] [--out report.md] [--max N]
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Замер (our measurements): 15 файлов за 5.4 с, ~$0.001; файл с подсаженным багом — has_bug 0.95 / logic 0.90 против 0.2–0.6 у чистых.
|
|
267
|
+
|
|
268
|
+
### jev-find — natural-language file search
|
|
269
|
+
|
|
270
|
+
Find code by meaning, not by name: a Jev walker ensemble walks the repo and reports where walkers landed.
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
jev-find "where is authentication handled?" ./src --walkers 20
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
Cheap (~$0.0005/search) and fast (~1-2 s on small repos). Pattern credit: ellipsis-dev/blink.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* jev-find — natural-language file search with a Jev walker ensemble (blink pattern).
|
|
4
|
+
*
|
|
5
|
+
* OPENROUTER_API_KEY=... node cli/jev-find.mjs "where is authentication handled?" <dir> [--walkers 20] [--depth 10]
|
|
6
|
+
*
|
|
7
|
+
* N walkers start at <dir>. At every step Jev gets the query + the child list and
|
|
8
|
+
* returns a Choice over children (+ BACK). Walkers that reach a file stop there.
|
|
9
|
+
* Output: table of destination files with share of walkers.
|
|
10
|
+
*/
|
|
11
|
+
import { readdirSync, statSync, readFileSync } from 'node:fs';
|
|
12
|
+
import { join, relative } from 'node:path';
|
|
13
|
+
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const flag = (name, def) => {
|
|
16
|
+
const i = args.indexOf(name);
|
|
17
|
+
return i >= 0 ? parseInt(args[i + 1], 10) || def : def;
|
|
18
|
+
};
|
|
19
|
+
const query = args[0];
|
|
20
|
+
const root = args[1];
|
|
21
|
+
if (!query || !root || !statSync(root, { throwIfNoEntry: false })?.isDirectory()) {
|
|
22
|
+
console.error('usage: node cli/jev-find.mjs "<query>" <dir> [--walkers 20] [--depth 10]');
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
const W = flag('--walkers', 20);
|
|
26
|
+
const MAXD = flag('--depth', 10);
|
|
27
|
+
const SKIP = new Set(['node_modules', '.git', 'dist', '.venv', 'venv', '__pycache__', '.next', 'build', 'coverage', '.vercel']);
|
|
28
|
+
const MAX_CHILDREN = 28;
|
|
29
|
+
|
|
30
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
31
|
+
if (!apiKey) { console.error('OPENROUTER_API_KEY not set'); process.exit(1); }
|
|
32
|
+
|
|
33
|
+
async function jev(state, questions) {
|
|
34
|
+
const res = await fetch('https://openrouter.ai/api/alpha/decisions', {
|
|
35
|
+
method: 'POST',
|
|
36
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
37
|
+
body: JSON.stringify({ model: process.env.JEV_MODEL || 'typesafe/jev-1.13', state: state.slice(0, 20_000), questions }),
|
|
38
|
+
});
|
|
39
|
+
if (!res.ok) throw new Error(`Jev HTTP ${res.status}: ${(await res.text()).slice(0, 300)}`);
|
|
40
|
+
return (await res.json()).answers ?? (await res.json());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const childrenOf = (dir) =>
|
|
44
|
+
readdirSync(dir, { withFileTypes: true })
|
|
45
|
+
.filter((e) => !SKIP.has(e.name) && !e.name.startsWith('.'))
|
|
46
|
+
.map((e) => ({ name: e.name, path: join(dir, e.name), dir: e.isDirectory() }))
|
|
47
|
+
.slice(0, MAX_CHILDREN);
|
|
48
|
+
|
|
49
|
+
const t0 = Date.now();
|
|
50
|
+
let calls = 0;
|
|
51
|
+
const dest = new Map(); // file -> walker count
|
|
52
|
+
|
|
53
|
+
async function walk(dir, depth) {
|
|
54
|
+
if (depth > MAXD) return;
|
|
55
|
+
const kids = childrenOf(dir);
|
|
56
|
+
if (!kids.length) return;
|
|
57
|
+
const options = kids.map((k) => k.name).concat(['STOP_HERE']);
|
|
58
|
+
const criteria = {};
|
|
59
|
+
for (const k of kids) criteria[k.name] = k.dir ? `folder: ${k.name}/` : `file: ${k.name}`;
|
|
60
|
+
criteria.STOP_HERE = 'this folder already contains the answer as a whole';
|
|
61
|
+
calls++;
|
|
62
|
+
let choice;
|
|
63
|
+
try {
|
|
64
|
+
const a = await jev(
|
|
65
|
+
`Search query: "${query}"\nCurrent folder: ${relative(root, dir) || '.'}\nEntries:\n${kids.map((k) => {
|
|
66
|
+
if (!k.dir) {
|
|
67
|
+
let hint = '';
|
|
68
|
+
try { hint = readFileSync(k.path, 'utf8').slice(0, 400).split('\n').find((l) => l.trim()) || ''; } catch {}
|
|
69
|
+
return `- ${k.name} (file)${hint ? ` — ${hint.slice(0, 120)}` : ''}`;
|
|
70
|
+
}
|
|
71
|
+
let sub = '';
|
|
72
|
+
try { sub = readdirSync(k.path).filter((n) => !SKIP.has(n) && !n.startsWith('.')).slice(0, 12).join(', '); } catch {}
|
|
73
|
+
return `- ${k.name}/ (folder${sub ? `: ${sub}` : ''})`;
|
|
74
|
+
}).join('\n')}`,
|
|
75
|
+
{ next: { type: 'choice', criteria, instructions: 'Which entry should the walker descend into next to find what the query asks about? Pick a file when it looks like the answer itself, a folder to go deeper, or STOP_HERE if this folder contains the answer as a whole.' } },
|
|
76
|
+
);
|
|
77
|
+
choice = a.next?.choice;
|
|
78
|
+
// Sample per walker from Jev probabilities so the ensemble actually explores
|
|
79
|
+
const probs = a.next?.probabilities || {};
|
|
80
|
+
const entries = Object.entries(probs).filter(([k, p]) => typeof p === 'number' && p > 0 && kids.some((k2) => k2.name === k));
|
|
81
|
+
if (entries.length) {
|
|
82
|
+
let r = Math.random() * entries.reduce((s, [, p]) => s + p, 0);
|
|
83
|
+
for (const [k, p] of entries) { r -= p; if (r <= 0) { choice = k; break; } }
|
|
84
|
+
}
|
|
85
|
+
} catch (e) {
|
|
86
|
+
return; // walker dies, fail-open
|
|
87
|
+
}
|
|
88
|
+
if (!choice || choice === 'STOP_HERE') return;
|
|
89
|
+
const kid = kids.find((k) => k.name === choice);
|
|
90
|
+
if (!kid) return;
|
|
91
|
+
if (!kid.dir) { dest.set(kid.path, (dest.get(kid.path) || 0) + 1); return; }
|
|
92
|
+
await walk(kid.path, depth + 1);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
await Promise.all(Array.from({ length: W }, () => walk(root, 0)));
|
|
96
|
+
|
|
97
|
+
const ms = Date.now() - t0;
|
|
98
|
+
const rows = [...dest.entries()].sort((a, b) => b[1] - a[1]);
|
|
99
|
+
console.log(`\njev-find "${query}" — ${W} walkers, ${calls} Jev calls, ${(ms / 1000).toFixed(1)} s\n`);
|
|
100
|
+
if (!rows.length) { console.log('No walker reached a file. Try more walkers or a different query.'); process.exit(0); }
|
|
101
|
+
for (const [path, n] of rows.slice(0, 10)) {
|
|
102
|
+
console.log(` ${String(Math.round((n / W) * 100)).padStart(3)}% ${relative(root, path)}`);
|
|
103
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* jev-qa — fast code-error finder: free syntax stage + Jev semantic stage.
|
|
4
|
+
*
|
|
5
|
+
* OPENROUTER_API_KEY=... node cli/jev-qa.mjs <repo> [--diff] [--out report.md] [--max N]
|
|
6
|
+
*
|
|
7
|
+
* Stage 1 (free, instant): node --check for js/mjs, python -m py_compile for py.
|
|
8
|
+
* Stage 2 (Jev, ~$0.0002/file): per file, one request with parallel questions:
|
|
9
|
+
* has_bug (noul), severity (score low/medium/high), plus Noul for
|
|
10
|
+
* error-handling gaps, security risk, logic bug.
|
|
11
|
+
* Report: markdown, sorted by risk = severity * confidence.
|
|
12
|
+
*/
|
|
13
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
14
|
+
import { readFileSync, writeFileSync, existsSync, statSync, readdirSync } from 'node:fs';
|
|
15
|
+
import { join, relative, extname } from 'node:path';
|
|
16
|
+
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const repo = args[0];
|
|
19
|
+
const diffOnly = args.includes('--diff');
|
|
20
|
+
const outIdx = args.indexOf('--out');
|
|
21
|
+
const outFile = outIdx !== -1 ? args[outIdx + 1] : null;
|
|
22
|
+
const maxIdx = args.indexOf('--max');
|
|
23
|
+
const maxFiles = maxIdx !== -1 ? Number(args[maxIdx + 1]) : 40;
|
|
24
|
+
|
|
25
|
+
if (!repo || !existsSync(repo)) {
|
|
26
|
+
console.error('usage: node cli/jev-qa.mjs <repo> [--diff] [--out report.md] [--max N]');
|
|
27
|
+
process.exit(2);
|
|
28
|
+
}
|
|
29
|
+
const apiKey = process.env.OPENROUTER_API_KEY;
|
|
30
|
+
if (!apiKey) {
|
|
31
|
+
console.error('OPENROUTER_API_KEY is not set');
|
|
32
|
+
process.exit(2);
|
|
33
|
+
}
|
|
34
|
+
const MODEL = process.env.JEV_MODEL || 'typesafe/jev-1.13';
|
|
35
|
+
const BASE = (process.env.JEV_BASE_URL || 'https://openrouter.ai/api/alpha/decisions').replace(/\/$/, '');
|
|
36
|
+
|
|
37
|
+
const walk = (dir, skip = ['node_modules', '.git', 'dist', '.venv', 'venv', '__pycache__', '.next', 'build', 'coverage']) => {
|
|
38
|
+
const out = [];
|
|
39
|
+
for (const name of readdirSync(dir)) {
|
|
40
|
+
if (skip.some((s) => name === s || name.endsWith(s))) continue;
|
|
41
|
+
const p = join(dir, name);
|
|
42
|
+
const st = statSync(p, { throwIfNoEntry: false });
|
|
43
|
+
if (!st) continue;
|
|
44
|
+
if (st.isDirectory()) out.push(...walk(p, skip));
|
|
45
|
+
else if (/\.(py|js|mjs|ts|tsx)$/.test(name)) out.push(p);
|
|
46
|
+
}
|
|
47
|
+
return out;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
let files = walk(repo);
|
|
51
|
+
if (diffOnly) {
|
|
52
|
+
const git = (a) => spawnSync('git', a, { cwd: repo, encoding: 'utf8' }).stdout.trim().split('\n').filter(Boolean);
|
|
53
|
+
const changed = new Set([...git(['diff', '--name-only', 'HEAD']), ...git(['diff', '--name-only'])].map((f) => f.replace(/\\/g, '/')));
|
|
54
|
+
files = files.filter((f) => changed.has(relative(repo, f).replace(/\\/g, '/')));
|
|
55
|
+
if (!files.length) { console.log('jev-qa: no changed code files'); process.exit(0); }
|
|
56
|
+
}
|
|
57
|
+
files = files.slice(0, maxFiles);
|
|
58
|
+
|
|
59
|
+
// ---------- stage 1: free syntax checks ----------
|
|
60
|
+
const stage1 = [];
|
|
61
|
+
for (const f of files) {
|
|
62
|
+
const ext = extname(f);
|
|
63
|
+
let res = null;
|
|
64
|
+
try {
|
|
65
|
+
if (ext === '.py') {
|
|
66
|
+
const r = spawnSync('python', ['-m', 'py_compile', f], { encoding: 'utf8' });
|
|
67
|
+
res = r.status !== 0 ? r.stderr.split('\n').slice(-3).join(' ') : null;
|
|
68
|
+
} else if (['.js', '.mjs'].includes(ext)) {
|
|
69
|
+
const r = spawnSync('node', ['--check', f], { encoding: 'utf8' });
|
|
70
|
+
res = r.status !== 0 ? (r.stderr || r.stdout).split('\n').slice(0, 3).join(' ') : null;
|
|
71
|
+
} else if (['.ts', '.tsx'].includes(ext)) {
|
|
72
|
+
const tsc = spawnSync('npx', ['--no-install', 'tsc', '--noEmit', f], { encoding: 'utf8', cwd: repo });
|
|
73
|
+
res = tsc.status !== 0 && tsc.stdout ? tsc.stdout.split('\n').slice(0, 3).join(' ') : null;
|
|
74
|
+
}
|
|
75
|
+
} catch { /* checker missing — skip to stage 2 */ }
|
|
76
|
+
if (res) stage1.push({ file: relative(repo, f), error: res.trim() });
|
|
77
|
+
}
|
|
78
|
+
const stage1Files = new Set(stage1.map((s) => s.file));
|
|
79
|
+
|
|
80
|
+
// ---------- stage 2: Jev semantic pass ----------
|
|
81
|
+
async function jev(state, questions) {
|
|
82
|
+
const body = { model: MODEL, state, questions };
|
|
83
|
+
const res = await fetch(BASE, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { Authorization: `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
|
|
86
|
+
body: JSON.stringify(body),
|
|
87
|
+
});
|
|
88
|
+
if (!res.ok) throw new Error(`Jev HTTP ${res.status}: ${(await res.text()).slice(0, 200)}`);
|
|
89
|
+
return res.json();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const semantic = [];
|
|
93
|
+
const t0 = Date.now();
|
|
94
|
+
let calls = 0;
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
if (stage1Files.has(relative(repo, f))) continue; // already failed syntax; no tokens wasted
|
|
97
|
+
let code = '';
|
|
98
|
+
try { code = readFileSync(f, 'utf8'); } catch { continue; }
|
|
99
|
+
if (!code.trim() || code.length > 100_000) continue;
|
|
100
|
+
const started = Date.now();
|
|
101
|
+
let data;
|
|
102
|
+
try {
|
|
103
|
+
data = await jev(code.slice(0, 60_000), {
|
|
104
|
+
has_bug: { type: 'noul', instructions: 'Does this file contain a real bug or broken logic (not style)?' },
|
|
105
|
+
severity: { type: 'score', instructions: 'How severe is the worst real issue in this file? 1 = harmless, 5 = data loss or security hole', criteria: ['harmless', 'minor', 'user-facing malfunction', 'serious', 'critical: data loss/security'] },
|
|
106
|
+
error_handling_gap: { type: 'noul', instructions: 'Are there unhandled error paths or missing catch/validation around fallible operations?' },
|
|
107
|
+
security_risk: { type: 'noul', instructions: 'Is there an injection, secret leak, or unsafe shell/file handling?' },
|
|
108
|
+
logic_bug: { type: 'noul', instructions: 'Is there a logic error: wrong operator, inverted condition, wrong variable, off-by-one?' },
|
|
109
|
+
});
|
|
110
|
+
} catch (e) {
|
|
111
|
+
console.error('Jev failed on', relative(repo, f), e.message);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
calls++;
|
|
115
|
+
const a = data.answers || {};
|
|
116
|
+
const n = (k) => a[k]?.noul ?? a[k]?.probability ?? 0;
|
|
117
|
+
const sev = a.severity?.score ?? 0;
|
|
118
|
+
const conf = a.severity?.confidence ?? a.has_bug?.confidence ?? 0;
|
|
119
|
+
const risk = Number(sev) * Math.max(n('has_bug'), 0.5);
|
|
120
|
+
if (n('has_bug') > 0.5 || sev >= 1) {
|
|
121
|
+
semantic.push({
|
|
122
|
+
file: relative(repo, f),
|
|
123
|
+
has_bug: +n('has_bug').toFixed(2),
|
|
124
|
+
severity: sev,
|
|
125
|
+
confidence: +conf.toFixed(2),
|
|
126
|
+
risk: +risk.toFixed(2),
|
|
127
|
+
error_handling: +n('error_handling_gap').toFixed(2),
|
|
128
|
+
security: +n('security_risk').toFixed(2),
|
|
129
|
+
logic: +n('logic_bug').toFixed(2),
|
|
130
|
+
ms: Date.now() - started,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
semantic.sort((x, y) => y.risk - x.risk);
|
|
135
|
+
|
|
136
|
+
// ---------- report ----------
|
|
137
|
+
const lines = [`# jev-qa report — ${new Date().toISOString()}`, ''];
|
|
138
|
+
lines.push(`Scope: ${files.length} files · stage1 syntax errors: ${stage1.length} · stage2 flagged: ${semantic.length} · Jev calls: ${calls} in ${Date.now() - t0} ms`);
|
|
139
|
+
lines.push('');
|
|
140
|
+
if (stage1.length) {
|
|
141
|
+
lines.push('## Syntax errors (stage 1, free)');
|
|
142
|
+
for (const s of stage1) lines.push(`- **${s.file}** — \`${s.error}\``);
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
if (semantic.length) {
|
|
146
|
+
lines.push('## Semantic flags (stage 2, Jev) — sorted by risk');
|
|
147
|
+
lines.push('');
|
|
148
|
+
lines.push('| file | bug | sev | risk | err-handling | security | logic | ms |');
|
|
149
|
+
lines.push('|---|---|---|---|---|---|---|---|');
|
|
150
|
+
for (const s of semantic) {
|
|
151
|
+
lines.push(`| ${s.file} | ${s.has_bug} | ${s.severity} | ${s.risk} | ${s.error_handling} | ${s.security} | ${s.logic} | ${s.ms} |`);
|
|
152
|
+
}
|
|
153
|
+
} else if (!stage1.length) {
|
|
154
|
+
lines.push('No issues flagged.');
|
|
155
|
+
}
|
|
156
|
+
const report = lines.join('\n');
|
|
157
|
+
if (outFile) writeFileSync(outFile, report + '\n', 'utf8');
|
|
158
|
+
console.log(report);
|
|
159
|
+
console.log(`\njev-qa done: ${calls} Jev calls, ${Date.now() - t0} ms total${outFile ? ` → ${outFile}` : ''}`);
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { JevAsker, JevQuestions, JevResponse, JevState } from './types.js';
|
|
2
|
+
export interface JevClientOptions {
|
|
3
|
+
/** Defaults to `process.env.TYPESAFE_API_KEY`. */
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
/** Defaults to `jev-latest`. */
|
|
6
|
+
model?: string;
|
|
7
|
+
/** Defaults to the System One endpoint. */
|
|
8
|
+
baseUrl?: string;
|
|
9
|
+
/** Defaults to the global `fetch`. */
|
|
10
|
+
fetch?: typeof fetch;
|
|
11
|
+
}
|
|
12
|
+
/** Asks Jev over HTTP with the global `fetch` (or an injected one). */
|
|
13
|
+
export declare class JevClient implements JevAsker {
|
|
14
|
+
private readonly apiKey;
|
|
15
|
+
private readonly model;
|
|
16
|
+
private readonly baseUrl;
|
|
17
|
+
private readonly fetcher;
|
|
18
|
+
constructor(options?: JevClientOptions);
|
|
19
|
+
ask(state: JevState, questions: JevQuestions): Promise<JevResponse>;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,YAAY,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAEhF,MAAM,WAAW,gBAAgB;IAC/B,kDAAkD;IAClD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gCAAgC;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2CAA2C;IAC3C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,sCAAsC;IACtC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAC;CACtB;AAED,uEAAuE;AACvE,qBAAa,SAAU,YAAW,QAAQ;IACxC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAe;gBAE3B,OAAO,GAAE,gBAAqB;IAOpC,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,YAAY,GAAG,OAAO,CAAC,WAAW,CAAC;CAc1E"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { buildJevRequest, parseJevResponse } from './request.js';
|
|
2
|
+
/** Asks Jev over HTTP with the global `fetch` (or an injected one). */
|
|
3
|
+
export class JevClient {
|
|
4
|
+
apiKey;
|
|
5
|
+
model;
|
|
6
|
+
baseUrl;
|
|
7
|
+
fetcher;
|
|
8
|
+
constructor(options = {}) {
|
|
9
|
+
this.apiKey = options.apiKey ?? process.env.TYPESAFE_API_KEY ?? '';
|
|
10
|
+
this.model = options.model;
|
|
11
|
+
this.baseUrl = options.baseUrl;
|
|
12
|
+
this.fetcher = options.fetch ?? fetch;
|
|
13
|
+
}
|
|
14
|
+
async ask(state, questions) {
|
|
15
|
+
if (!this.apiKey)
|
|
16
|
+
throw new Error('TYPESAFE_API_KEY is not configured');
|
|
17
|
+
const request = buildJevRequest({ apiKey: this.apiKey, model: this.model, baseUrl: this.baseUrl }, state, questions);
|
|
18
|
+
const response = await this.fetcher(request.url, {
|
|
19
|
+
method: request.method,
|
|
20
|
+
headers: request.headers,
|
|
21
|
+
body: request.body,
|
|
22
|
+
});
|
|
23
|
+
return parseJevResponse(response.status, response.ok, await response.text());
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAcjE,uEAAuE;AACvE,MAAM,OAAO,SAAS;IACH,MAAM,CAAS;IACf,KAAK,CAAqB;IAC1B,OAAO,CAAqB;IAC5B,OAAO,CAAe;IAEvC,YAAY,UAA4B,EAAE;QACxC,IAAI,CAAC,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,EAAE,CAAC;QACnE,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC;IACxC,CAAC;IAED,KAAK,CAAC,GAAG,CAAC,KAAe,EAAE,SAAuB;QAChD,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;QACxE,MAAM,OAAO,GAAG,eAAe,CAC7B,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,EACjE,KAAK,EACL,SAAS,CACV,CAAC;QACF,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,EAAE;YAC/C,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,OAAO,EAAE,OAAO,CAAC,OAAO;YACxB,IAAI,EAAE,OAAO,CAAC,IAAI;SACnB,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,EAAE,EAAE,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IAC/E,CAAC;CACF"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { CallAnswer, CallDecision, CompactOptions, CompactResult, JevAsker, JevQuestions, Message, ResolvedCompactOptions, ToolCall } from './types.js';
|
|
2
|
+
export declare const DEFAULT_OPTIONS: ResolvedCompactOptions;
|
|
3
|
+
export declare function resolveOptions(options?: CompactOptions): ResolvedCompactOptions;
|
|
4
|
+
/** The two `noul` questions asked about one call: keep the call, keep its result. */
|
|
5
|
+
export declare function questionsFor(call: ToolCall): JevQuestions;
|
|
6
|
+
/**
|
|
7
|
+
* Splits the candidate calls into batches whose questions, together with the
|
|
8
|
+
* (always complete) state, fit one request.
|
|
9
|
+
*/
|
|
10
|
+
export declare function batchCalls(calls: readonly ToolCall[], stateTokens: number, options: Pick<ResolvedCompactOptions, 'maxRequestTokens'>): ToolCall[][];
|
|
11
|
+
export declare function decideCall(call: Pick<ToolCall, 'id' | 'tool' | 'pinned'>, answer: CallAnswer, options: Pick<ResolvedCompactOptions, 'keepThreshold'>): CallDecision;
|
|
12
|
+
/**
|
|
13
|
+
* Rebuilds the conversation from the decisions. A dropped call disappears
|
|
14
|
+
* together with its result; a dropped result keeps a bounded head and note.
|
|
15
|
+
* Messages that lose all their content are removed; untouched messages are
|
|
16
|
+
* returned as the same objects they came in as.
|
|
17
|
+
*/
|
|
18
|
+
export declare function applyDecisions(messages: readonly Message[], decisions: readonly CallDecision[], calls: readonly ToolCall[], headChars: number): Message[];
|
|
19
|
+
/** Characters of text, tool input and tool output a message holds. */
|
|
20
|
+
export declare function messageChars(message: Message): number;
|
|
21
|
+
export declare function reductionRatio(result: Pick<CompactResult, 'stats'>): number;
|
|
22
|
+
/**
|
|
23
|
+
* Compacts a transcript by asking Jev, for every tool call outside the pinned
|
|
24
|
+
* first and newest messages, whether the call and whether its result must
|
|
25
|
+
* stay. The whole history (results omitted, fitted into `maxStateTokens`) is
|
|
26
|
+
* sent as state with every batch of questions. Throws when Jev fails or the
|
|
27
|
+
* history cannot be fitted; the caller decides whether to fall back.
|
|
28
|
+
*/
|
|
29
|
+
export declare function compact(messages: readonly Message[], asker: JevAsker, options?: CompactOptions): Promise<CompactResult>;
|
|
30
|
+
//# sourceMappingURL=compact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact.d.ts","sourceRoot":"","sources":["../src/compact.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,UAAU,EACV,YAAY,EACZ,cAAc,EACd,aAAa,EAEb,QAAQ,EACR,YAAY,EACZ,OAAO,EACP,sBAAsB,EACtB,QAAQ,EAET,MAAM,YAAY,CAAC;AAEpB,eAAO,MAAM,eAAe,EAAE,sBAO7B,CAAC;AASF,wBAAgB,cAAc,CAAC,OAAO,GAAE,cAAmB,GAAG,sBAAsB,CAoBnF;AAED,qFAAqF;AACrF,wBAAgB,YAAY,CAAC,IAAI,EAAE,QAAQ,GAAG,YAAY,CAWzD;AAED;;;GAGG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,kBAAkB,CAAC,GACxD,QAAQ,EAAE,EAAE,CAsBd;AAED,wBAAgB,UAAU,CACxB,IAAI,EAAE,IAAI,CAAC,QAAQ,EAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC,EAC9C,MAAM,EAAE,UAAU,EAClB,OAAO,EAAE,IAAI,CAAC,sBAAsB,EAAE,eAAe,CAAC,GACrD,YAAY,CAUd;AA4BD;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,SAAS,EAAE,SAAS,YAAY,EAAE,EAClC,KAAK,EAAE,SAAS,QAAQ,EAAE,EAC1B,SAAS,EAAE,MAAM,GAChB,OAAO,EAAE,CAuEX;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAWrD;AAED,wBAAgB,cAAc,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,EAAE,OAAO,CAAC,GAAG,MAAM,CAG3E;AAMD;;;;;;GAMG;AACH,wBAAsB,OAAO,CAC3B,QAAQ,EAAE,SAAS,OAAO,EAAE,EAC5B,KAAK,EAAE,QAAQ,EACf,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,aAAa,CAAC,CAgDxB"}
|