pi-supernova 0.0.15 → 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/CHANGELOG.md +31 -0
- package/README.md +87 -33
- package/bottleneck.js +98 -97
- package/catalog.js +7 -32
- package/config.js +1 -2
- package/decode.js +61 -0
- package/evidence.js +14 -5
- package/format.js +16 -17
- package/guest-worker.js +29 -150
- package/host-bridge.js +152 -67
- package/index.js +82 -101
- package/ledger.js +73 -101
- package/omp-frame.js +0 -1
- package/package.json +7 -3
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +48 -118
- package/render.js +12 -13
- package/repo-index.js +16 -7
- package/runtime.js +204 -210
- package/snap.js +183 -214
- package/vfs.js +114 -70
- package/workspace.js +37 -33
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.2.0] - 2026-09-04
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- `read`, `write`, `edit`, and `bash` are the main interface. Source questions and directory selection use `read`; no separate search-tool discovery is needed.
|
|
10
|
+
- Internal adapters remain callable when the host hides their top-level tools. Explicit exclusions, delegated-tool permissions, and session guards still apply.
|
|
11
|
+
- Source selection favors declarations over callers. It reports ambiguous, missing, and incomplete results instead of arbitrary file choices.
|
|
12
|
+
- Bounded ripgrep searches replace whole-repository content loading for source questions. Large source files remain searchable; context and signatures have explicit limits.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Directory reads include staged entries before commit. Explicit test-directory searches include test files without extra query words.
|
|
17
|
+
|
|
18
|
+
## [0.1.0] - 2026-09-04
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Each program uses a separate worker and transaction. Late callbacks, idle worker errors, concurrent runs, startup cancellation, and startup deadlines no longer share state.
|
|
23
|
+
- Command deadlines stop descendants. Signal exits report failure. Native commits recover partial replacements; writes reject missing content and edits reject overlapping matches.
|
|
24
|
+
- Unified patches handle zero-context insertions, empty files, final-newline changes, and CRLF. Reads see external changes; evidence search includes staged files.
|
|
25
|
+
- OMP tool calls use the current session’s enabled, permission-aware registry. Discovery supplies complete schemas. Batch options, failure envelopes, scheduling, and call identifiers remain consistent.
|
|
26
|
+
- Output limits cover aggregate batch text, markers, and spill footers. Details remain valid JSON; logs report truncation. DataView ranges and own `__proto__` values survive serialization.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Acorn parses supported function and body forms. Return hints ignore comments and nested callbacks. Invalid collection arguments report errors.
|
|
31
|
+
- The result ledger checks text equality, retains explicit-read pins across aliases, expires source records, and accepts a zero window.
|
|
32
|
+
- Terminal width checks use Unicode graphemes. Expanded cards wrap all bounded result and log text without a preview-line limit.
|
|
33
|
+
- Pi 0.85.0 native adapters respect active-tool changes. Pi has no cross-extension execution API; the README states this limit.
|
|
34
|
+
- Live checks cover Pi 0.85.0 and OMP 18.1.10, including expanded terminal output.
|
|
35
|
+
|
|
5
36
|
## [0.0.15] - 2026-09-04
|
|
6
37
|
|
|
7
38
|
### Removed
|
package/README.md
CHANGED
|
@@ -5,14 +5,22 @@
|
|
|
5
5
|
[](https://nodejs.org)
|
|
6
6
|
[](https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/packages.md)
|
|
7
7
|
|
|
8
|
-
**CodeMode for [Pi](https://pi.dev) and [OMP](https://omp.sh).** One
|
|
8
|
+
**CodeMode for [Pi](https://pi.dev) and [OMP](https://omp.sh).** One program with `read`, `write`, `edit`, and `bash`. Search, source context, and result limits stay inside these commands.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
pi install npm:pi-supernova
|
|
12
12
|
omp install npm:pi-supernova
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
Restart the host after install. OMP uses the current session’s enabled tool registry, independent of extension load order.
|
|
16
|
+
|
|
17
|
+
Pi 0.85.0 supplies tool schemas but has no cross-extension execution API. Supernova uses native adapters and executors registered through its API instance.
|
|
18
|
+
|
|
19
|
+
An unrelated Pi extension’s tools are not callable through Supernova. OMP 18.1.10 supports those calls through its session registry.
|
|
20
|
+
|
|
21
|
+
Both hosts use the same runtime, transactions, and result limits. Hiding a top-level native tool does not remove its internal Supernova adapter.
|
|
22
|
+
|
|
23
|
+
Use `excludeTools` to disable an internal adapter. Delegated extension tools still require host permission and active status. Replaced or disposed sessions cannot call tools.
|
|
16
24
|
|
|
17
25
|
---
|
|
18
26
|
|
|
@@ -20,39 +28,63 @@ Load **before** other tool-owning packages so `registerTool` capture works. Rest
|
|
|
20
28
|
|
|
21
29
|
| | Stock multi-tool prompt | **pi-supernova** |
|
|
22
30
|
|--|-------------------------|------------------|
|
|
23
|
-
| Schema tax | Every tool in context |
|
|
31
|
+
| Schema tax | Every tool in context | Four known commands; optional host schemas on demand |
|
|
24
32
|
| Multi-step | Model stitches turns | One in-process program |
|
|
25
33
|
| Parallel reads | Ad hoc | `callMany` Auto / `parallel()` |
|
|
26
34
|
| Hosts | Separate packages | Same tarball for Pi **and** OMP |
|
|
27
35
|
|
|
28
|
-
Guest code runs as an `AsyncFunction` in a worker
|
|
36
|
+
Guest code runs as an `AsyncFunction` in a new worker for each program. It has the same trust level as host `bash`.
|
|
37
|
+
|
|
38
|
+
The host parses the bounded source with Acorn before execution. Tool calls use RPC. Deadlines and cancellation cover worker startup, tool discovery, and execution.
|
|
39
|
+
|
|
40
|
+
A completed worker cannot supply callbacks or globals to another program. Concurrent programs have separate transactions, traces, cancellation signals, and call budgets.
|
|
29
41
|
|
|
30
42
|
---
|
|
31
43
|
|
|
32
44
|
## Quick example
|
|
33
45
|
|
|
34
46
|
```js
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
const hit = JSON.parse(await read("src", {about: "validateJwtRefreshToken"}));
|
|
48
|
+
if (hit.status !== "found") return hit;
|
|
49
|
+
return await read(hit.path, {about: "expired tokens"});
|
|
50
|
+
```
|
|
38
51
|
|
|
39
|
-
|
|
40
|
-
const wave = await nova.callMany([
|
|
41
|
-
{ name: "read", args: { path: "a.ts" } },
|
|
42
|
-
{ name: "read", args: { path: "b.ts" } },
|
|
43
|
-
]);
|
|
52
|
+
No catalog lookup or separate search command is needed.
|
|
44
53
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
| Command | Result |
|
|
55
|
+
|---------|--------|
|
|
56
|
+
| `read(file, offset?, limit?)` | File text; line numbers start at one |
|
|
57
|
+
| `read(files)` | An array of file texts |
|
|
58
|
+
| `read(directory)` | Direct directory entries |
|
|
59
|
+
| `read("symbol or question")` | JSON text with source status, path, line, signature, and context |
|
|
60
|
+
| `read(path, {about: question})` | Relevant file bodies, or source selection inside a directory |
|
|
61
|
+
| `write(path, text)` | Write a file |
|
|
62
|
+
| `edit(path, oldText, newText)` | Post-edit lines, a structural check, and references |
|
|
63
|
+
| `bash(command, options?)` | Bounded command output; failure throws |
|
|
64
|
+
|
|
65
|
+
### Source selection
|
|
66
|
+
|
|
67
|
+
`read` reports `found`, `ambiguous`, `not_found`, or `incomplete`.
|
|
68
|
+
Only `found` selects a path. Uncertain results contain at most three candidates.
|
|
69
|
+
|
|
70
|
+
Identifier declarations take priority over callers and filenames. Natural-language selection uses text matches, not semantic inference. Use a symbol or a narrower directory when needed.
|
|
71
|
+
|
|
72
|
+
The confidence value is a ranking heuristic, not a measured probability. Duplicate declarations do not produce a confident winner.
|
|
73
|
+
|
|
74
|
+
Search uses ripgrep without loading every file into the text cache. It can locate declarations beyond the 512 KiB whole-file cache limit.
|
|
48
75
|
|
|
49
|
-
|
|
76
|
+
Each candidate contains up to seven source lines. Signatures and individual lines have a 240-character limit, with explicit truncation markers.
|
|
50
77
|
|
|
51
|
-
|
|
78
|
+
File-list and content-search output have separate 2,097,152-character budgets. Incomplete searches report their status instead of selecting a file.
|
|
52
79
|
|
|
53
|
-
|
|
80
|
+
The measured 10,001-file fixture returned a 199-character result. This is about 50 tokens at four characters per token, not an exact token count.
|
|
54
81
|
|
|
55
|
-
|
|
82
|
+
In the 23-question source check, 17 questions selected the correct file and six returned candidates. No result selected a wrong file.
|
|
83
|
+
All 15 identifier questions selected the correct file. Natural-language questions can still miss the intended file in their candidates.
|
|
84
|
+
|
|
85
|
+
Search scans disk on each question. Warm searches can be slower than the previous full-content cache, but do not retain every source file.
|
|
86
|
+
|
|
87
|
+
Already-seen result lines collapse to references. `read(path, firstLine, lineCount)` shows a range again. `edit` returns changed lines without a verification read.
|
|
56
88
|
|
|
57
89
|
The returned value is rendered as a compact JS literal (unquoted keys, one item per line only when a container exceeds 120 columns) and capped at `maxReturnChars`. Strings are returned raw. This costs ~43% fewer tokens than pretty JSON. Return small shaped values, not raw file dumps.
|
|
58
90
|
|
|
@@ -69,27 +101,36 @@ Unified Pi/OMP card: one aligned row per call (status · tool · duration · tar
|
|
|
69
101
|
╰───────────────────────────────────────────────────────────────╯
|
|
70
102
|
```
|
|
71
103
|
|
|
72
|
-
Multi-line commands show their first line
|
|
104
|
+
Multi-line commands show their first line and a hidden-line count. Expand the card to show the complete bounded return value and logs.
|
|
105
|
+
|
|
106
|
+
Expanded text wraps at the terminal width without a preview-line limit. Operation lists and mutation diffs keep their separate 24-row budgets.
|
|
73
107
|
|
|
74
108
|
---
|
|
75
109
|
|
|
76
|
-
##
|
|
110
|
+
## Optional APIs
|
|
77
111
|
|
|
78
112
|
| API | Role |
|
|
79
113
|
|-----|------|
|
|
80
114
|
| `nova.search(query, limit?)` | Thin catalog hits |
|
|
81
|
-
| `nova.describe(name)` |
|
|
115
|
+
| `nova.describe(name)` | Complete JSON input schema on demand; explicit failure when no usable schema is available |
|
|
82
116
|
| `nova.call(name, args)` | Host tool or native adapter |
|
|
83
117
|
| `nova.callMany([{name,args}])` | Auto parallel wave; iterable array with `.mode` / `.results` |
|
|
84
118
|
| `nova.evidence(query, {k?, path?, maxChars?})` | Top-K source spans (path, lines, verbatim text) that answer a question. Zero-token evidence selection after Zero-Mem; ~68% fewer tokens than reading the files |
|
|
85
|
-
| `read(path, {about})` | Whole-file outline with only the relevant bodies expanded; folded bodies show `line … N lines` |
|
|
86
119
|
| `nova.surface(path)` | Structural outline for a source file |
|
|
87
|
-
| `nova.snap(query, searchRoot?)` |
|
|
120
|
+
| `nova.snap(query, searchRoot?)` | Structured source-selection result; the same engine as a source question passed to `read` |
|
|
88
121
|
| `nova.has(name)` | Whether a catalog or native tool is callable (sync) |
|
|
89
|
-
| `parallel(thunks)` / `pipeline(items, …stages)` |
|
|
122
|
+
| `parallel(thunks)` / `pipeline(items, …stages)` | Array-based helpers; pipeline stages must be functions |
|
|
90
123
|
| `nova.speculate(fn)` | Counterfactual branch (rollback / commit) |
|
|
91
124
|
|
|
92
|
-
|
|
125
|
+
`nova.call` returns an explicit `{ok, value}` envelope. Convenience helpers throw when a host tool reports failure.
|
|
126
|
+
|
|
127
|
+
`callMany` runs known read-only tools concurrently. Unknown tools and mutating actions run in order, including LSP rename operations.
|
|
128
|
+
|
|
129
|
+
`nova.describe` preserves required fields, unions, enums, nested objects, and numeric constraints. OMP ArkType and Zod schemas convert to JSON Schema.
|
|
130
|
+
|
|
131
|
+
Source questions ignore hidden files by default. An explicit hidden directory includes its hidden files. Git metadata stays excluded.
|
|
132
|
+
|
|
133
|
+
An explicit test directory includes test files without extra query words. Directory reads include staged entries before commit.
|
|
93
134
|
|
|
94
135
|
---
|
|
95
136
|
|
|
@@ -112,11 +153,17 @@ Optional `~/.pi/agent/supernova.json` or `~/.omp/agent/supernova.json`
|
|
|
112
153
|
}
|
|
113
154
|
```
|
|
114
155
|
|
|
156
|
+
`maxCallResultChars` bounds text per call. A batch shares one text budget across all items; it does not repeat a joined copy.
|
|
157
|
+
|
|
158
|
+
Envelope fields and JSON encoding add overhead. Details have a separate 2,000-character JSON budget. Truncation flags cover returns, raw objects, and logs.
|
|
159
|
+
|
|
160
|
+
An optional `spillDir` retains complete truncated text. Inline spill footers and truncation markers fit within the configured text limit.
|
|
161
|
+
|
|
115
162
|
`seenWindow` is how many programs back the seen-ledger remembers (set 0 to disable collapsing). `maxHeapMb` caps the guest worker heap (V8 `resourceLimits` on Node) and arms a process-RSS watchdog that terminates a runaway program on both Node and Bun.
|
|
116
163
|
|
|
117
164
|
Defaults also set `excludeTools` (includes `supernova` and DCE helpers). An empty `"excludeTools": []` **replaces** those defaults, so omit the key unless you mean that.
|
|
118
165
|
|
|
119
|
-
Slash command `/supernova`: catalog size,
|
|
166
|
+
Slash command `/supernova`: callable catalog size, external tools, native adapters, and session token stats.
|
|
120
167
|
|
|
121
168
|
---
|
|
122
169
|
|
|
@@ -141,8 +188,8 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
|
|
|
141
188
|
|
|
142
189
|
| Symptom | Fix |
|
|
143
190
|
|---------|-----|
|
|
144
|
-
| `unknown tool "…"` |
|
|
145
|
-
| `Rendered line exceeds terminal width` |
|
|
191
|
+
| `unknown tool "…"` | Check `excludeTools` for native adapters. Enable delegated tools in the host. Restart after package changes. |
|
|
192
|
+
| `Rendered line exceeds terminal width` | Install the current package and restart so the Unicode width code reloads. |
|
|
146
193
|
| `callMany` / not iterable | ≥0.0.1; the return is an array with `.mode` / `.results` |
|
|
147
194
|
| Extension missing on OMP | `omp install npm:pi-supernova` (needs `"omp".extensions`) |
|
|
148
195
|
|
|
@@ -152,9 +199,16 @@ Pair with DCE last if you use it: `omp install npm:pi-deferred-context-engine`.
|
|
|
152
199
|
|
|
153
200
|
- Guest JS is **unsandboxed**. Adapter path jails are not a boundary against `import("node:fs")`. The worker only contains hangs, exits, and memory, not intent.
|
|
154
201
|
- Guest error messages carry `(line:col)` on Node; Bun's engine does not expose guest-relative positions.
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
202
|
+
|
|
203
|
+
## Transactions and file freshness
|
|
204
|
+
|
|
205
|
+
- `bash` and mutating host tools commit pending writes before execution. Later rollback cannot undo those changes or external effects.
|
|
206
|
+
- Native commits stage replacements and backups before installation. A commit failure restores earlier replacements; a recovery failure reports retained backup paths.
|
|
207
|
+
- A nested `nova.speculate` branch cannot call external mutators. Await each branch before returning.
|
|
208
|
+
- Native reads use current disk content unless a staged write replaces it. Evidence search includes new staged files.
|
|
209
|
+
- Workspace file-list updates use filesystem watchers. Without a working watcher, external new files can take 10 seconds to appear in indexed searches.
|
|
210
|
+
|
|
211
|
+
This is a pre-1.0 package. APIs and the terminal display can change between minor releases.
|
|
158
212
|
|
|
159
213
|
## Research and prior art
|
|
160
214
|
|
|
@@ -163,7 +217,7 @@ Supernova's retrieval and result shaping implement published methods. Where a pa
|
|
|
163
217
|
| Work | What we use it for | Where |
|
|
164
218
|
|------|--------------------|-------|
|
|
165
219
|
| **Zero-Mem: Zero-Token Memory Operations for LLM Agents**, Xiao, Zhu, Zhang, Chen, Hong, Zhuang, Zhang, Chen, Ouyang, Ren, Huang (arXiv:2607.29377) | `evidence(query)`: entity–context graph with co-occurrence weights (eq. 3–4), turn/window/episode hierarchy as line/span/file (eq. 5, 11), query profile and relational/local routing (eq. 6–7), lexical entity alignment and one propagation step (eq. 8–9), personalized PageRank over spans (eq. 10), per-view normalisation and ρ-weighted fusion (eq. 12–13), closure with bridges and neighbours (eq. 14), deterministic calibration (eq. 15). Top-K = 5 follows the paper's Top-5 ≈ Top-10 finding. | `evidence.js` |
|
|
166
|
-
| **Agent Zero Memory: Provenance-Aware Long-Term Memory for LLM Agents**, Zhu, Wu (arXiv:2608.29606) | Every returned unit carries provenance (path, line range, verbatim text); the L0→L1→L2 read discipline (`
|
|
220
|
+
| **Agent Zero Memory: Provenance-Aware Long-Term Memory for LLM Agents**, Zhu, Wu (arXiv:2608.29606) | Every returned unit carries provenance (path, line range, verbatim text); the L0→L1→L2 read discipline (`read(query)` → `read(path, {about})` → `read(path, offset, limit)`); the citation-lock idea that a model should only cite what it actually opened. | `evidence.js`, `outline.js`, tool guidance |
|
|
167
221
|
| **Harness-of-Harness: Multi-Day Autonomous Software Development with Continual Improvement**, Yan, Su, et al. (arXiv:2609.01481) | Progressive disclosure (index first, detail on demand) and carrying evidence forward instead of reconstructing it from code. | outline / result shaping |
|
|
168
222
|
| **Act More, Decide Less: Skill-Guided Adaptive Action Chunking for Long-Horizon LLM Agents**, Yang, Jin, Zhao, et al. (arXiv:2609.02042) | Framing: one supernova program is an action chunk (one model decision, many primitive actions, stop at the first failing one). | runtime design |
|
|
169
223
|
| **fff**, Dmitriy Kovalenko, MIT, <https://github.com/dmtrKovalenko/fff> | File search. We reimplemented fff's ranking in plain JavaScript after reading its Rust sources (`crates/fff-core/src/score.rs`, `dbs/frecency.rs`, `path_utils.rs`); the formulas and constants are fff's, the code is ours, and nothing runs out of process. Ported: typo-tolerant fuzzy path matching with boundary/consecutive/case bonuses and smart-case; exact-filename +40% and filename +20% bonuses; frecency boost `base·f/100` with fff's AI-mode decay (3-day half-life, 7-day window) and modification-recency steps (30s/5m/15m/1h/4h); git-modified +15%; directory-distance penalty from the current file (−1 per hop, floor −20); definition-first result hinting; fuzzy fallback on zero literal matches; weak-match cutoff; watcher-driven index refresh. Not ported: fff's SIMD/frizbee matcher (ours is an fzf-style greedy match with backward tightening), LMDB persistence (frecency is per session), and the MCP/Neovim surfaces. | `fuzzy.js`, `repo-index.js`, `host-bridge.js` |
|
package/bottleneck.js
CHANGED
|
@@ -4,120 +4,121 @@ import { randomUUID } from "node:crypto";
|
|
|
4
4
|
import { isString, isObject } from "./decode.js";
|
|
5
5
|
import { truncateChars, formatValue } from "./format.js";
|
|
6
6
|
|
|
7
|
-
function
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
serialized = isString(value) ? value : JSON.stringify(value);
|
|
11
|
-
} catch {
|
|
12
|
-
serialized = String(value);
|
|
13
|
-
}
|
|
14
|
-
if (serialized === undefined) serialized = "null";
|
|
15
|
-
return truncateChars(serialized, maxChars, label);
|
|
7
|
+
function json(value) {
|
|
8
|
+
try { return JSON.stringify(value) ?? "null"; } catch { return JSON.stringify(String(value)); }
|
|
16
9
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
if (!
|
|
20
|
-
return
|
|
21
|
-
.filter((part) => part && isObject(part) && part.type === "text" && isString(part.text))
|
|
22
|
-
.map((part) => part.text)
|
|
23
|
-
.join("\n");
|
|
10
|
+
function detailsOf(raw) {
|
|
11
|
+
const details = raw?.details;
|
|
12
|
+
if (!isString(details)) return details;
|
|
13
|
+
try { return JSON.parse(details); } catch { return details; }
|
|
24
14
|
}
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
export function hostResultFailed(raw) {
|
|
16
|
+
const details = detailsOf(raw);
|
|
17
|
+
return raw?.isError === true || details?.ok === false || (Number.isInteger(details?.exitCode) && details.exitCode !== 0);
|
|
18
|
+
}
|
|
19
|
+
function extractRawString(raw) {
|
|
27
20
|
if (raw == null) return "";
|
|
28
21
|
if (isString(raw)) return raw;
|
|
29
22
|
if (!isObject(raw)) return String(raw);
|
|
30
|
-
if (Array.isArray(raw.content)) return
|
|
23
|
+
if (Array.isArray(raw.content)) return raw.content.filter(part => part?.type === "text" && isString(part.text)).map(part => part.text).join("\n");
|
|
31
24
|
if (isString(raw.text)) return raw.text;
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function summarizeDetails(details, maxChars) {
|
|
36
|
-
return serializeBounded(details, maxChars, "details").text;
|
|
25
|
+
return json(raw);
|
|
37
26
|
}
|
|
38
27
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
28
|
+
/** Bound JSON before serialization; preserve small scalar fields such as exitCode. */
|
|
29
|
+
function summarizeDetails(value, budget = 2000) {
|
|
30
|
+
const encoded = json(value);
|
|
31
|
+
if (encoded.length <= budget) return encoded;
|
|
32
|
+
const snapshot = JSON.parse(encoded);
|
|
33
|
+
const fit = (input, limit) => {
|
|
34
|
+
const serialized = json(input);
|
|
35
|
+
if (serialized.length <= limit) return input;
|
|
36
|
+
if (isString(input)) {
|
|
37
|
+
let low = 0;
|
|
38
|
+
let high = Math.min(input.length, limit);
|
|
39
|
+
while (low < high) {
|
|
40
|
+
const mid = Math.ceil((low + high) / 2);
|
|
41
|
+
if (json(truncateChars(input, mid).text).length <= limit) low = mid;
|
|
42
|
+
else high = mid - 1;
|
|
43
|
+
}
|
|
44
|
+
return truncateChars(input, low).text;
|
|
45
|
+
}
|
|
46
|
+
if (!isObject(input) && !Array.isArray(input)) return null;
|
|
47
|
+
const out = Array.isArray(input) ? [] : { truncated: true };
|
|
48
|
+
const entries = Object.entries(input);
|
|
49
|
+
if (!Array.isArray(input)) entries.sort((a, b) => json(a[1]).length - json(b[1]).length);
|
|
50
|
+
for (const [key, child] of entries) {
|
|
51
|
+
const used = json(out).length;
|
|
52
|
+
const overhead = Array.isArray(out) ? 1 : json(key).length + 2;
|
|
53
|
+
const available = limit - used - overhead;
|
|
54
|
+
if (available < 4) break;
|
|
55
|
+
const bounded = fit(child, available);
|
|
56
|
+
if (Array.isArray(out)) out.push(bounded);
|
|
57
|
+
else Object.defineProperty(out, key, { value: bounded, enumerable: true, configurable: true });
|
|
58
|
+
if (json(out).length > limit) {
|
|
59
|
+
if (Array.isArray(out)) out.pop();
|
|
60
|
+
else delete out[key];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
};
|
|
65
|
+
return json(fit(snapshot, budget));
|
|
57
66
|
}
|
|
58
67
|
|
|
59
|
-
function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
68
|
+
function spill(fullText, config) {
|
|
69
|
+
if (!isString(config.spillDir) || !config.spillDir) return undefined;
|
|
70
|
+
fs.mkdirSync(config.spillDir, { recursive: true, mode: 0o700 });
|
|
71
|
+
const file = path.join(config.spillDir, Date.now() + "-" + randomUUID().slice(0, 8) + ".txt");
|
|
72
|
+
fs.writeFileSync(file, fullText, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
73
|
+
return file;
|
|
63
74
|
}
|
|
64
75
|
|
|
65
76
|
export function packageHostResult(raw, config) {
|
|
66
77
|
const maxChars = config.maxCallResultChars ?? 65536;
|
|
67
|
-
const
|
|
68
|
-
const
|
|
69
|
-
const
|
|
70
|
-
const text = extractRawString(raw, maxChars);
|
|
71
|
-
const items = batchItems(details, maxChars);
|
|
72
|
-
// Batch items travel as their own field; keep the details summary small and parseable.
|
|
73
|
-
const summarizedDetails =
|
|
74
|
-
details === undefined ? undefined : summarizeDetails(items ? { ...details, items: undefined } : details, 2000);
|
|
75
|
-
|
|
78
|
+
const details = detailsOf(raw);
|
|
79
|
+
const batch = details?.batch === true && Array.isArray(details.items) ? details.items : undefined;
|
|
80
|
+
const text = batch ? "" : extractRawString(raw);
|
|
76
81
|
const capped = truncateChars(text, maxChars, "host-result");
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
82
|
+
let truncated = capped.truncated || details?.outputTruncated === true;
|
|
83
|
+
const result = { ok: !hostResultFailed(raw), value: capped.text, truncated };
|
|
84
|
+
if (details !== undefined) result.details = summarizeDetails(batch ? { ...details, items: undefined } : details);
|
|
85
|
+
if (batch) {
|
|
86
|
+
let remaining = maxChars;
|
|
87
|
+
result.items = batch.map((item, index) => {
|
|
88
|
+
const bounded = truncateChars(item, Math.floor(remaining / (batch.length - index)), "host-result");
|
|
89
|
+
remaining -= bounded.text.length;
|
|
90
|
+
truncated ||= bounded.truncated;
|
|
91
|
+
return bounded.text;
|
|
92
|
+
});
|
|
85
93
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const maxLogLineChars = config.maxLogLineChars ?? 4096;
|
|
104
|
-
const clippedLogs = logLines.slice(0, maxLogLines).map((line) => {
|
|
105
|
-
const s = isString(line) ? line : String(line);
|
|
106
|
-
if (s.length <= maxLogLineChars) return s;
|
|
107
|
-
return s.slice(0, maxLogLineChars) + "…";
|
|
108
|
-
});
|
|
109
|
-
return { clippedLogs, logTruncated: logLines.length > maxLogLines };
|
|
94
|
+
result.truncated = truncated;
|
|
95
|
+
if (truncated) {
|
|
96
|
+
result.originalChars = batch ? batch.reduce((sum, item) => sum + String(item).length, 0) : text.length;
|
|
97
|
+
if (config.spillDir) {
|
|
98
|
+
const pointer = spill(batch ? batch.join("\n---\n") : text, config);
|
|
99
|
+
if (pointer) {
|
|
100
|
+
result.spill = pointer;
|
|
101
|
+
if (!batch) {
|
|
102
|
+
const footer = "\n[full output spilled to " + pointer + "]";
|
|
103
|
+
result.value = footer.length <= maxChars
|
|
104
|
+
? truncateChars(text, maxChars - footer.length, "host-result").text + footer
|
|
105
|
+
: capped.text;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return result;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
export function packageFinalReturn(value, logs, config) {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
114
|
+
const serialized = truncateChars(isString(value) ? value : formatValue(value), config.maxReturnChars ?? 32000, "return");
|
|
115
|
+
const maxLines = config.maxLogLines ?? 100;
|
|
116
|
+
let logTruncated = logs.length > maxLines;
|
|
117
|
+
const clipped = logs.slice(0, maxLines).map(line => {
|
|
118
|
+
const result = truncateChars(line, config.maxLogLineChars ?? 4096, "log");
|
|
119
|
+
logTruncated ||= result.truncated;
|
|
120
|
+
return result.text;
|
|
121
|
+
});
|
|
122
|
+
return { returnValue: serialized.truncated ? serialized.text : value, returnText: serialized.text,
|
|
123
|
+
returnTruncated: serialized.truncated, logs: clipped, logTruncated };
|
|
123
124
|
}
|
package/catalog.js
CHANGED
|
@@ -4,13 +4,13 @@ import { isString, isObject } from "./decode.js";
|
|
|
4
4
|
const NATIVE_TOOL_DEFINITIONS = [
|
|
5
5
|
{
|
|
6
6
|
name: "read",
|
|
7
|
-
description: "Read
|
|
7
|
+
description: "Read files, directories, or source questions. Directory questions use about. Source selection reports uncertainty instead of guessing.",
|
|
8
8
|
parameters: { type: "object", properties: {
|
|
9
|
-
path: { type: "string", description: "Workspace-relative file
|
|
9
|
+
path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], description: "Workspace-relative file or directory, source question, or array of paths" },
|
|
10
10
|
target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
|
|
11
11
|
offset: { type: "number", description: "One-based starting line" },
|
|
12
12
|
limit: { type: "number", description: "Maximum lines to return" },
|
|
13
|
-
about: { type: "string", description: "Question or symbol:
|
|
13
|
+
about: { type: "string", description: "Question or symbol: expand relevant file bodies, or select source inside a directory" },
|
|
14
14
|
} },
|
|
15
15
|
},
|
|
16
16
|
{
|
|
@@ -28,7 +28,7 @@ const NATIVE_TOOL_DEFINITIONS = [
|
|
|
28
28
|
parameters: { type: "object", properties: { path: { type: "string" }, patch: { type: "string" } }, required: ["patch"] },
|
|
29
29
|
},
|
|
30
30
|
{
|
|
31
|
-
name: "snap", description: "
|
|
31
|
+
name: "snap", description: "Select source with found, ambiguous, not_found, or incomplete status. The read command uses the same engine.",
|
|
32
32
|
parameters: { type: "object", properties: {
|
|
33
33
|
query: { type: "string", description: "Source concept to resolve" },
|
|
34
34
|
path: { type: "string", description: "Optional workspace search root; explicitly targeting a hidden directory includes its hidden files, but Git metadata is always excluded" },
|
|
@@ -110,6 +110,7 @@ function normalizeTool(tool) {
|
|
|
110
110
|
description,
|
|
111
111
|
descLower: description.toLowerCase(),
|
|
112
112
|
parameters: tool.parameters,
|
|
113
|
+
schemaError: tool.schemaError,
|
|
113
114
|
sourcePath: sourcePathOf(tool),
|
|
114
115
|
};
|
|
115
116
|
}
|
|
@@ -158,33 +159,6 @@ export function searchCatalog(catalog, query, limit = 12) {
|
|
|
158
159
|
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
function fieldSummary(key, schema, required) {
|
|
162
|
-
const s = schema && isObject(schema) ? schema : {};
|
|
163
|
-
// Only signal-bearing keys: `required:false` and empty descriptions cost tokens and say nothing.
|
|
164
|
-
return {
|
|
165
|
-
type: s.type || (Array.isArray(s.anyOf) ? "union" : "unknown"),
|
|
166
|
-
required: required.has(key) || undefined,
|
|
167
|
-
description: isString(s.description) ? s.description.slice(0, 120) : undefined,
|
|
168
|
-
};
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function schemaSummary(parameters) {
|
|
172
|
-
if (!parameters || !isObject(parameters)) return { type: "unknown" };
|
|
173
|
-
const props = parameters.properties;
|
|
174
|
-
if (!props || !isObject(props)) {
|
|
175
|
-
return {
|
|
176
|
-
type: parameters.type || "object",
|
|
177
|
-
note: "schema present (no enumerable properties)",
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
const required = new Set(Array.isArray(parameters.required) ? parameters.required : []);
|
|
181
|
-
const fields = {};
|
|
182
|
-
for (const [key, schema] of Object.entries(props)) {
|
|
183
|
-
fields[key] = fieldSummary(key, schema, required);
|
|
184
|
-
}
|
|
185
|
-
return { type: "object", fields };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
162
|
/** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
|
|
189
163
|
function editDistance(a, b) {
|
|
190
164
|
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...new Array(b.length).fill(0)]);
|
|
@@ -232,12 +206,13 @@ export function describeTool(catalog, name) {
|
|
|
232
206
|
if (!row) {
|
|
233
207
|
return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
|
|
234
208
|
}
|
|
209
|
+
if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
|
|
235
210
|
if (!row._described) {
|
|
236
211
|
row._described = {
|
|
237
212
|
ok: true,
|
|
238
213
|
name: row.name,
|
|
239
214
|
description: row.description,
|
|
240
|
-
parameters:
|
|
215
|
+
parameters: row.parameters,
|
|
241
216
|
sourcePath: row.sourcePath,
|
|
242
217
|
};
|
|
243
218
|
}
|
package/config.js
CHANGED
|
@@ -8,7 +8,7 @@ const require = createRequire(import.meta.url);
|
|
|
8
8
|
const DEFAULTS = require("./config.default.json");
|
|
9
9
|
|
|
10
10
|
const KNOWN_KEYS = new Set(Object.keys(DEFAULTS));
|
|
11
|
-
const NONNEGATIVE_INTEGER_KEYS = new Set(["maxLogLines"]);
|
|
11
|
+
const NONNEGATIVE_INTEGER_KEYS = new Set(["maxLogLines", "seenWindow"]);
|
|
12
12
|
const POSITIVE_INTEGER_KEYS = new Set([
|
|
13
13
|
"timeoutMs",
|
|
14
14
|
"maxCodeChars",
|
|
@@ -18,7 +18,6 @@ const POSITIVE_INTEGER_KEYS = new Set([
|
|
|
18
18
|
"maxLogLineChars",
|
|
19
19
|
"maxSearchResults",
|
|
20
20
|
"maxHeapMb",
|
|
21
|
-
"seenWindow",
|
|
22
21
|
]);
|
|
23
22
|
|
|
24
23
|
const VALIDATORS = {
|