pi-supernova 0.0.11 → 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/CHANGELOG.md +74 -3
- package/README.md +72 -21
- package/bottleneck.js +98 -97
- package/catalog.js +12 -36
- package/check.js +166 -0
- package/config.default.json +1 -0
- package/config.js +1 -2
- package/decode.js +57 -6
- package/evidence.js +27 -35
- package/format.js +16 -17
- package/fuzzy.js +182 -0
- package/guest-worker.js +38 -157
- package/host-bridge.js +246 -124
- package/index.js +95 -123
- package/ledger.js +150 -0
- package/omp-frame.js +7 -23
- package/outline.js +80 -0
- package/package.json +11 -2
- package/parallel.js +42 -34
- package/patch.js +62 -72
- package/render-measure.js +46 -144
- package/render.js +43 -158
- package/repo-index.js +106 -11
- package/runtime.js +204 -227
- package/search.js +141 -0
- package/snap.js +1 -6
- package/surface.js +22 -13
- package/vfs.js +114 -75
- package/workspace.js +51 -33
package/host-bridge.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
import * as fs from "node:fs/promises";
|
|
3
3
|
import * as path from "node:path";
|
|
4
|
-
import { packageHostResult } from "./bottleneck.js";
|
|
4
|
+
import { packageHostResult, hostResultFailed } from "./bottleneck.js";
|
|
5
5
|
import { isString, isNumber, isFunction, isObject } from "./decode.js";
|
|
6
6
|
import { isMutatingTool, runParallelWave } from "./parallel.js";
|
|
7
7
|
import { unknownToolMessage } from "./catalog.js";
|
|
@@ -9,10 +9,15 @@ import { extractStructuralSurface } from "./surface.js";
|
|
|
9
9
|
import { buildEditDiff, buildMultiEditDiff, buildPatchDiff, buildWriteDiff } from "./diff.js";
|
|
10
10
|
import { executeSnap } from "./snap.js";
|
|
11
11
|
import { selectEvidence } from "./evidence.js";
|
|
12
|
-
import { WorkspaceIndex
|
|
12
|
+
import { WorkspaceIndex } from "./repo-index.js";
|
|
13
|
+
import { outlineFile } from "./outline.js";
|
|
14
|
+
import { SeenLedger } from "./ledger.js";
|
|
15
|
+
import { quickCheck } from "./check.js";
|
|
16
|
+
import { declaredName } from "./repo-index.js";
|
|
13
17
|
import { CausalVfs } from "./vfs.js";
|
|
14
18
|
import { applyPatchToText } from "./patch.js";
|
|
15
|
-
import { resolveWorkspacePath, runCommand, clearPathCache } from "./workspace.js";
|
|
19
|
+
import { resolveWorkspacePath, runCommand, clearPathCache, relativeSlash } from "./workspace.js";
|
|
20
|
+
import { fuzzyFind, grepIndexed, listIndexed, listWithTools, rgGrepArgs } from "./search.js";
|
|
16
21
|
|
|
17
22
|
function textResult(text, details) {
|
|
18
23
|
return {
|
|
@@ -63,15 +68,6 @@ async function probeExistingFile(cwd, targetParam, vfs) {
|
|
|
63
68
|
}
|
|
64
69
|
}
|
|
65
70
|
|
|
66
|
-
function patchModeOf(params) {
|
|
67
|
-
return (
|
|
68
|
-
isString(params?.patch) ||
|
|
69
|
-
(params?.newText === undefined &&
|
|
70
|
-
isString(params?.oldText) &&
|
|
71
|
-
(params.oldText.includes("@@ -") || params.oldText.startsWith("---")))
|
|
72
|
-
);
|
|
73
|
-
}
|
|
74
|
-
|
|
75
71
|
function applyReplacements(target, content, requestedEdits) {
|
|
76
72
|
if (requestedEdits.length === 0) throw new Error("edit requires at least one replacement");
|
|
77
73
|
const matches = requestedEdits.map((replacement) => {
|
|
@@ -83,7 +79,7 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
83
79
|
if (index < 0) {
|
|
84
80
|
throw new Error(`edit target not found in ${target}: oldText must match the file byte-for-byte (read() it first; check whitespace and quotes)`);
|
|
85
81
|
}
|
|
86
|
-
if (content.indexOf(replacement.oldText, index +
|
|
82
|
+
if (content.indexOf(replacement.oldText, index + 1) >= 0) {
|
|
87
83
|
throw new Error(`edit target is not unique in ${target}: include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]`);
|
|
88
84
|
}
|
|
89
85
|
return { ...replacement, index, end: index + replacement.oldText.length };
|
|
@@ -115,84 +111,27 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
115
111
|
return `${entry.name}${isDir ? "/" : ""} (${typeLabel}${sizeSuffix})`;
|
|
116
112
|
}
|
|
117
113
|
|
|
118
|
-
function
|
|
119
|
-
const args = ["--line-number", "--no-heading", "--color", "never"];
|
|
120
|
-
if (params?.caseSensitive !== true) args.push("--ignore-case");
|
|
121
|
-
if (params?.glob) args.push("--glob", String(params.glob));
|
|
122
|
-
args.push("--", pattern, searchPath);
|
|
123
|
-
return args;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/** rg --files, then find(1) when rg is unavailable; both accept an optional glob/name pattern. */
|
|
127
|
-
async function listWithTools(searchDir, pattern, cwd, signal) {
|
|
128
|
-
const args = ["--files"];
|
|
129
|
-
if (pattern) args.push("-g", pattern);
|
|
130
|
-
const res = await runCommand(["rg", ...args, searchDir], { cwd, timeoutMs: 30_000, signal }).catch(() => null);
|
|
131
|
-
if (res && (res.exitCode === 0 || res.exitCode === 1)) return textResult(res.stdout, { via: "rg" });
|
|
132
|
-
const findArgs = [searchDir];
|
|
133
|
-
if (pattern) findArgs.push("-name", pattern);
|
|
134
|
-
const findRes = await runCommand(["find", ...findArgs], { cwd, timeoutMs: 30_000, signal });
|
|
135
|
-
return textResult(findRes.stdout, { via: "find" });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/** rg-compatible grep served from the index; null when the pattern or tree needs real rg. */
|
|
139
|
-
async function grepIndexed(index, pattern, params, searchPath, cwd) {
|
|
140
|
-
let regex;
|
|
141
|
-
try {
|
|
142
|
-
regex = new RegExp(pattern, params?.caseSensitive === true ? "" : "i");
|
|
143
|
-
} catch {
|
|
144
|
-
return null;
|
|
145
|
-
}
|
|
146
|
-
let files = await index.files(searchPath);
|
|
147
|
-
if (!index.canScan(files)) return null;
|
|
148
|
-
if (params?.glob) {
|
|
149
|
-
const matcher = globToRegExp(String(params.glob));
|
|
150
|
-
files = files.filter((f) => matcher.test(path.relative(cwd, f).split(path.sep).join("/")));
|
|
151
|
-
}
|
|
152
|
-
const rows = index.grep(files, regex, cwd);
|
|
153
|
-
return rows.length ? rows.join("\n") + "\n" : "";
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** rg --files [-g pattern] served from the index; null when the tree is too large. */
|
|
157
|
-
async function listIndexed(index, root, cwd, pattern) {
|
|
158
|
-
const files = await index.files(root);
|
|
159
|
-
if (!index.canScan(files)) return null;
|
|
160
|
-
const rel = files.map((f) => path.relative(cwd, f).split(path.sep).join("/"));
|
|
161
|
-
if (!pattern) return rel.length ? rel.join("\n") + "\n" : "";
|
|
162
|
-
let matcher;
|
|
163
|
-
try {
|
|
164
|
-
matcher = globToRegExp(pattern);
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
168
|
-
const hits = rel.filter((f) => matcher.test(f));
|
|
169
|
-
return hits.length ? hits.join("\n") + "\n" : "";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function createNativeAdapters(getCwd, vfs, config, index) {
|
|
114
|
+
function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
173
115
|
async function readAdapter(params, signal) {
|
|
116
|
+
signal?.throwIfAborted();
|
|
174
117
|
const cwd = getCwd();
|
|
175
118
|
const targetParam = params?.path ?? params?.target;
|
|
176
119
|
|
|
177
120
|
if (Array.isArray(targetParam)) {
|
|
178
121
|
const results = await Promise.all(
|
|
179
|
-
targetParam.map((p) => readAdapter({
|
|
122
|
+
targetParam.map((p) => readAdapter({ ...params, path: p }, signal)),
|
|
180
123
|
);
|
|
181
124
|
const items = results.map((r) => r.content[0].text);
|
|
182
|
-
return textResult(
|
|
125
|
+
return textResult("", { count: results.length, batch: true, items });
|
|
183
126
|
}
|
|
184
127
|
|
|
185
128
|
if (looksLikePath(targetParam)) {
|
|
186
129
|
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
187
|
-
|
|
188
|
-
return textResult(sliceLines(text, params?.offset, params?.limit), { path: targetPath });
|
|
130
|
+
return readFile(targetPath, params);
|
|
189
131
|
}
|
|
190
132
|
|
|
191
133
|
const existing = await probeExistingFile(cwd, targetParam, vfs);
|
|
192
|
-
if (existing)
|
|
193
|
-
const text = await vfs.read(existing);
|
|
194
|
-
return textResult(sliceLines(text, params?.offset, params?.limit), { path: existing });
|
|
195
|
-
}
|
|
134
|
+
if (existing) return readFile(existing, params);
|
|
196
135
|
|
|
197
136
|
if (isString(targetParam) && targetParam.trim()) {
|
|
198
137
|
try {
|
|
@@ -208,8 +147,133 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
208
147
|
}
|
|
209
148
|
|
|
210
149
|
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
211
|
-
|
|
212
|
-
|
|
150
|
+
return readFile(targetPath, params);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
154
|
+
async function readFile(targetPath, params) {
|
|
155
|
+
const cwd = getCwd();
|
|
156
|
+
const rel = relativeSlash(cwd, targetPath);
|
|
157
|
+
const text = await vfs.read(targetPath);
|
|
158
|
+
index.touch(rel);
|
|
159
|
+
if (isString(params?.about)) {
|
|
160
|
+
const pending = vfs.getOverlay(targetPath);
|
|
161
|
+
const entry = pending === undefined ? index.entry(targetPath) : WorkspaceIndex.fromText(targetPath, pending);
|
|
162
|
+
const outline = entry && outlineFile(entry, rel, params.about, outlineOptions(params, await referenceFinder(cwd, targetPath)));
|
|
163
|
+
if (outline) {
|
|
164
|
+
recordOutlineOrigins(rel, outline.text);
|
|
165
|
+
return textResult(outline.text, { path: targetPath, outline: true, expanded: outline.expanded, declarations: outline.declarations });
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const explicit = isNumber(params?.offset) || isNumber(params?.limit);
|
|
169
|
+
const firstLine = isNumber(params?.offset) ? Math.max(1, Math.floor(params.offset)) : 1;
|
|
170
|
+
const sliced = sliceLines(text, params?.offset, params?.limit);
|
|
171
|
+
ledger.recordOrigin(rel, firstLine, sliced.split("\n"), explicit);
|
|
172
|
+
return textResult(sliced, { path: targetPath });
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* The edit result answers the follow-ups a model would otherwise spend turns on: the post-edit
|
|
177
|
+
* lines with numbers (so no verification re-read), a quick structural check, and every other
|
|
178
|
+
* place a changed declaration is referenced (so callers are not forgotten).
|
|
179
|
+
*/
|
|
180
|
+
async function editSummary(cwd, target, original, updated, diff) {
|
|
181
|
+
const rel = relativeSlash(cwd, target);
|
|
182
|
+
const newLines = updated.split("\n");
|
|
183
|
+
const added = diff.lines.filter((l) => l.type === "add");
|
|
184
|
+
const first = added.length ? Math.max(1, added[0].lineNum - 2) : 1;
|
|
185
|
+
const last = added.length ? Math.min(newLines.length, added[added.length - 1].lineNum + 2) : Math.min(newLines.length, first + 6);
|
|
186
|
+
const window = [];
|
|
187
|
+
for (let l = first; l <= last && window.length < 40; l++) window.push(String(l).padStart(5) + " " + newLines[l - 1]);
|
|
188
|
+
ledger.recordOrigin(rel, first, newLines.slice(first - 1, first - 1 + window.length));
|
|
189
|
+
let out = `edited ${rel}:${first}–${first + window.length - 1}\n${window.join("\n")}`;
|
|
190
|
+
const check = quickCheck(updated, path.extname(target));
|
|
191
|
+
if (check && !check.ok) out += `\ncheck: ${check.message}`;
|
|
192
|
+
const refs = await changedDeclarationRefs(cwd, target, original, updated, diff);
|
|
193
|
+
if (refs) out += `\n${refs}`;
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async function changedDeclarationRefs(cwd, target, original, updated, diff) {
|
|
198
|
+
// Diff rows carry the replaced fragments; declarations live on whole file lines.
|
|
199
|
+
const oldLines = original.split("\n");
|
|
200
|
+
const newLines = updated.split("\n");
|
|
201
|
+
const names = new Set();
|
|
202
|
+
for (const l of diff.lines) {
|
|
203
|
+
if (l.type === "context") continue;
|
|
204
|
+
const name = declaredName((l.type === "remove" ? oldLines : newLines)[l.lineNum - 1] ?? "");
|
|
205
|
+
if (name) names.add(name);
|
|
206
|
+
}
|
|
207
|
+
if (names.size === 0) return "";
|
|
208
|
+
const find = await referenceFinder(cwd, target);
|
|
209
|
+
const parts = [];
|
|
210
|
+
for (const name of [...names].slice(0, 3)) {
|
|
211
|
+
const refs = find(name).filter((r) => !r.startsWith(relativeSlash(cwd, target) + ":"));
|
|
212
|
+
if (refs.length) parts.push(`${name} also referenced in ${refs.slice(0, 6).join(", ")}${refs.length > 6 ? " (+" + (refs.length - 6) + ")" : ""}`);
|
|
213
|
+
}
|
|
214
|
+
return parts.join("\n");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const SOURCE_REF = /((?:[\w.@-]+\/)*[\w.@-]+\.(?:m?[jt]sx?|c[jt]s|py|rs|go|java|kt|rb|php|c|cc|cpp|h|hpp|cs|swift|json|ya?ml|toml))(?::|\()(\d+)/g;
|
|
218
|
+
|
|
219
|
+
/** Source window (±2 lines, ► on the cited line) for one path:line, or null when it is outside the workspace/index. */
|
|
220
|
+
function sourceWindow(cwd, file, lineNo) {
|
|
221
|
+
const candidate = path.resolve(cwd, file);
|
|
222
|
+
if (!candidate.startsWith(path.resolve(cwd) + path.sep)) return null;
|
|
223
|
+
const entry = index.entry(candidate);
|
|
224
|
+
if (!entry) return null;
|
|
225
|
+
const { raw } = WorkspaceIndex.linesOf(entry);
|
|
226
|
+
if (lineNo < 1 || lineNo > raw.length) return null;
|
|
227
|
+
const rel = relativeSlash(cwd, candidate);
|
|
228
|
+
const start = Math.max(1, lineNo - 2);
|
|
229
|
+
const rows = [];
|
|
230
|
+
for (let l = start; l <= Math.min(raw.length, lineNo + 2); l++) rows.push((l === lineNo ? "►" : " ") + String(l).padStart(4) + " " + raw[l - 1]);
|
|
231
|
+
ledger.recordOrigin(rel, start, rows);
|
|
232
|
+
return rel + ":" + lineNo + "\n" + rows.join("\n");
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A failing command names path:line; the model wants those lines next. Attach them (≤4 sites). */
|
|
236
|
+
function sourceForReferences(cwd, output) {
|
|
237
|
+
const seen = new Set();
|
|
238
|
+
const blocks = [];
|
|
239
|
+
for (const m of output.matchAll(SOURCE_REF)) {
|
|
240
|
+
const key = m[1] + ":" + m[2];
|
|
241
|
+
if (seen.has(key)) continue;
|
|
242
|
+
seen.add(key);
|
|
243
|
+
const block = sourceWindow(cwd, m[1], Number(m[2]));
|
|
244
|
+
if (block) blocks.push(block);
|
|
245
|
+
if (blocks.length >= 4) break;
|
|
246
|
+
}
|
|
247
|
+
return blocks.length ? "\n--- source\n" + blocks.join("\n") : "";
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function outlineOptions(params, references) {
|
|
251
|
+
const options = { references };
|
|
252
|
+
if (params?.maxChars) options.maxChars = params.maxChars;
|
|
253
|
+
return options;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** Outline lines carry their own line numbers (" 330 text"); provenance follows them. */
|
|
257
|
+
function recordOutlineOrigins(rel, outlineText) {
|
|
258
|
+
for (const line of outlineText.split("\n")) {
|
|
259
|
+
const m = /^\s*(\d+) (.*)$/.exec(line);
|
|
260
|
+
if (m && !/ … \d+ lines$/.test(line)) ledger.recordOrigin(rel, Number(m[1]), [line]);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Where else a name appears (declaration line excluded), for outlines and edit results. */
|
|
265
|
+
async function referenceFinder(cwd, targetPath) {
|
|
266
|
+
const files = await index.files(cwd);
|
|
267
|
+
if (!index.canScan(files)) return () => [];
|
|
268
|
+
return (name, excludeLine) => {
|
|
269
|
+
if (!name || name.length < 3) return [];
|
|
270
|
+
const escaped = name.replace(/[$]/g, (c) => "\\" + c);
|
|
271
|
+
const regex = new RegExp("\\b" + escaped + "\\b");
|
|
272
|
+
return index
|
|
273
|
+
.grepRows(files, regex, cwd)
|
|
274
|
+
.filter((r) => !(r.line === excludeLine && r.rel === relativeSlash(cwd, targetPath)))
|
|
275
|
+
.map((r) => r.rel + ":" + r.line);
|
|
276
|
+
};
|
|
213
277
|
}
|
|
214
278
|
|
|
215
279
|
return {
|
|
@@ -218,12 +282,14 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
218
282
|
const cwd = getCwd();
|
|
219
283
|
const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
|
|
220
284
|
if (signal?.aborted) throw new Error("aborted");
|
|
221
|
-
|
|
285
|
+
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
286
|
+
const content = params.content;
|
|
222
287
|
let prevText = "";
|
|
223
288
|
try {
|
|
224
289
|
prevText = await vfs.read(target);
|
|
225
290
|
} catch {}
|
|
226
291
|
const { speculative } = await vfs.write(target, content);
|
|
292
|
+
index.touch(relativeSlash(cwd, target));
|
|
227
293
|
const diff = buildWriteDiff(target, prevText, content);
|
|
228
294
|
const tag = speculative ? " (speculative)" : "";
|
|
229
295
|
return textResult(`wrote ${target}${tag}`, { path: target, speculative, diff });
|
|
@@ -233,33 +299,19 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
233
299
|
const target = await resolveWorkspacePath(cwd, params?.path, "edit", false);
|
|
234
300
|
if (signal?.aborted) throw new Error("aborted");
|
|
235
301
|
|
|
236
|
-
if (patchModeOf(params)) {
|
|
237
|
-
const patchContent = params.patch || params.oldText;
|
|
238
|
-
const original = await vfs.read(target);
|
|
239
|
-
const { resultText, hunkCount } = applyPatchToText(original, patchContent);
|
|
240
|
-
const { speculative } = await vfs.write(target, resultText);
|
|
241
|
-
const diff = buildPatchDiff(target, patchContent);
|
|
242
|
-
const tag = speculative ? " (speculative)" : "";
|
|
243
|
-
return textResult(`applied ${hunkCount} hunk(s) to ${target}${tag}`, {
|
|
244
|
-
path: target,
|
|
245
|
-
hunks: hunkCount,
|
|
246
|
-
speculative,
|
|
247
|
-
diff,
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
302
|
const requestedEdits = Array.isArray(params?.edits)
|
|
252
303
|
? params.edits
|
|
253
304
|
: [{ oldText: params?.oldText, newText: params?.newText }];
|
|
254
305
|
const content = await vfs.read(target);
|
|
255
306
|
const { updated, matches } = applyReplacements(target, content, requestedEdits);
|
|
256
307
|
const { speculative } = await vfs.write(target, updated);
|
|
308
|
+
index.touch(relativeSlash(cwd, target));
|
|
257
309
|
const diff =
|
|
258
310
|
matches.length === 1
|
|
259
311
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
260
312
|
: buildMultiEditDiff(target, content, matches);
|
|
261
|
-
const
|
|
262
|
-
return textResult(
|
|
313
|
+
const summary = await editSummary(cwd, target, content, updated, diff);
|
|
314
|
+
return textResult(summary, { path: target, speculative, diff });
|
|
263
315
|
},
|
|
264
316
|
async apply_patch(params, signal) {
|
|
265
317
|
const cwd = getCwd();
|
|
@@ -316,7 +368,8 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
316
368
|
const options = {};
|
|
317
369
|
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
318
370
|
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
319
|
-
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), options });
|
|
371
|
+
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options });
|
|
372
|
+
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
320
373
|
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
321
374
|
},
|
|
322
375
|
async surface(params, signal) {
|
|
@@ -352,10 +405,11 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
352
405
|
index.invalidate();
|
|
353
406
|
}
|
|
354
407
|
const { stdout, stderr } = res;
|
|
355
|
-
|
|
408
|
+
let text = stdout && stderr ? stdout + (stdout.endsWith("\n") ? "" : "\n") + stderr : stdout || stderr;
|
|
409
|
+
if (res.exitCode !== 0) text += sourceForReferences(cwd, text);
|
|
356
410
|
return {
|
|
357
411
|
content: [{ type: "text", text }],
|
|
358
|
-
details: { exitCode: res.exitCode, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
412
|
+
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
359
413
|
isError: res.exitCode !== 0,
|
|
360
414
|
};
|
|
361
415
|
},
|
|
@@ -366,6 +420,7 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
366
420
|
const searchPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "grep", true) : cwd;
|
|
367
421
|
const indexed = await grepIndexed(index, pattern, params, searchPath, cwd);
|
|
368
422
|
if (indexed !== null) return textResult(indexed, { exitCode: indexed ? 0 : 1, via: "index" });
|
|
423
|
+
// Large tree: real rg keeps its own output format.
|
|
369
424
|
const res = await runCommand(["rg", ...rgGrepArgs(pattern, params, searchPath)], { cwd, timeoutMs: 30_000, signal });
|
|
370
425
|
if (res.exitCode !== 0 && res.exitCode !== 1) {
|
|
371
426
|
throw new Error(res.stderr.trim() || `rg exited ${res.exitCode}`);
|
|
@@ -376,6 +431,8 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
376
431
|
const cwd = getCwd();
|
|
377
432
|
const pattern = String(params?.pattern || "");
|
|
378
433
|
if (!pattern) throw new Error("glob requires pattern");
|
|
434
|
+
const fuzzy = await fuzzyFind(index, cwd, cwd, pattern);
|
|
435
|
+
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
379
436
|
const indexed = await listIndexed(index, cwd, cwd, pattern);
|
|
380
437
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
381
438
|
const rg = await runCommand(["rg", "--files", "-g", pattern], { cwd, timeoutMs: 30_000, signal }).catch(
|
|
@@ -398,6 +455,8 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
398
455
|
const pattern = params?.pattern || params?.glob;
|
|
399
456
|
if (signal?.aborted) throw new Error("aborted");
|
|
400
457
|
const globPattern = pattern ? String(pattern) : null;
|
|
458
|
+
const fuzzy = await fuzzyFind(index, searchDir, cwd, globPattern);
|
|
459
|
+
if (fuzzy !== null) return textResult(fuzzy, { via: "fuzzy" });
|
|
401
460
|
const indexed = await listIndexed(index, searchDir, cwd, globPattern);
|
|
402
461
|
if (indexed !== null) return textResult(indexed, { via: "index" });
|
|
403
462
|
return listWithTools(searchDir, globPattern, cwd, signal);
|
|
@@ -416,18 +475,24 @@ function createNativeAdapters(getCwd, vfs, config, index) {
|
|
|
416
475
|
};
|
|
417
476
|
}
|
|
418
477
|
|
|
419
|
-
export function createHostBridge({ pi, config, getCwd }) {
|
|
420
|
-
const index = new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
478
|
+
export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedger }) {
|
|
479
|
+
const index = registry?.index ?? new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
480
|
+
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ?? 40 });
|
|
421
481
|
const vfs = new CausalVfs(() => index.invalidate());
|
|
422
|
-
const executors = new Map();
|
|
423
|
-
const
|
|
482
|
+
const executors = registry?.executors ?? new Map();
|
|
483
|
+
const definitions = registry?.definitions ?? new Map();
|
|
484
|
+
const sharedRegistry = registry ?? { executors, definitions, index, callSeq: 0 };
|
|
485
|
+
let closed = false;
|
|
486
|
+
const natives = createNativeAdapters(getCwd, vfs, config, index, ledger);
|
|
424
487
|
let callCount = 0;
|
|
425
488
|
let activeCtx = null;
|
|
489
|
+
let hostSession = null;
|
|
490
|
+
let boundSessionId;
|
|
426
491
|
let activeSignal = undefined;
|
|
427
492
|
let trace = [];
|
|
428
493
|
let callListener = null;
|
|
429
494
|
|
|
430
|
-
if (pi && isFunction(pi.registerTool)) {
|
|
495
|
+
if (!registry && pi && isFunction(pi.registerTool)) {
|
|
431
496
|
const original = pi.registerTool.bind(pi);
|
|
432
497
|
const excluded = new Set(config.excludeTools || []);
|
|
433
498
|
pi.registerTool = (tool) => {
|
|
@@ -439,6 +504,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
439
504
|
!excluded.has(tool.name)
|
|
440
505
|
) {
|
|
441
506
|
executors.set(tool.name, tool.execute.bind(tool));
|
|
507
|
+
definitions.set(tool.name, tool);
|
|
442
508
|
}
|
|
443
509
|
return original(tool);
|
|
444
510
|
};
|
|
@@ -446,10 +512,52 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
446
512
|
|
|
447
513
|
function bindCallContext(ctx, signal) {
|
|
448
514
|
activeCtx = ctx || null;
|
|
515
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
516
|
+
boundSessionId = sessionId;
|
|
517
|
+
const registry = pi?.pi?.AgentRegistry?.global?.();
|
|
518
|
+
hostSession = sessionId && registry?.list
|
|
519
|
+
? registry.list().map(ref => ref.session).find(session => !session?.isDisposed && session?.sessionManager?.getSessionId?.() === sessionId) ?? null
|
|
520
|
+
: null;
|
|
449
521
|
activeSignal = signal;
|
|
522
|
+
vfs.signal = signal;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function hostTool(name) {
|
|
526
|
+
if (!hostSession) return undefined;
|
|
527
|
+
const metadata = definitions.get(name);
|
|
528
|
+
// Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
|
|
529
|
+
if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
|
|
530
|
+
return hostSession.getToolForEvalBridge?.(name);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function isCallable(name) {
|
|
534
|
+
if (name === "supernova" || (config.excludeTools ?? []).includes(name)) return false;
|
|
535
|
+
if (hostSession) {
|
|
536
|
+
if (hostSession.isDisposed || hostSession.sessionManager.getSessionId() !== boundSessionId) return false;
|
|
537
|
+
if (!hostSession.getEvalBridgeToolNames().includes(name) && definitions.has(name)) return false;
|
|
538
|
+
return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
|
|
539
|
+
}
|
|
540
|
+
if (definitions.has(name) && isFunction(pi?.getActiveTools) && !pi.getActiveTools().includes(name)) return false;
|
|
541
|
+
return executors.has(name) || Object.hasOwn(natives, name);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
function refreshTools() {
|
|
545
|
+
const tools = pi?.getAllTools?.() ?? [];
|
|
546
|
+
for (const tool of tools) {
|
|
547
|
+
if (!isString(tool?.name)) continue;
|
|
548
|
+
definitions.set(tool.name, { ...definitions.get(tool.name), ...tool });
|
|
549
|
+
if (!hostSession && isFunction(tool.execute)) executors.set(tool.name, tool.execute.bind(tool));
|
|
550
|
+
}
|
|
551
|
+
return [...definitions.values()].filter(tool => isCallable(tool.name));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function externalNames() {
|
|
555
|
+
return [...definitions.keys()].filter(name => !!hostTool(name) || executors.has(name));
|
|
450
556
|
}
|
|
451
557
|
|
|
452
558
|
function resetCallBudget() {
|
|
559
|
+
closed = false;
|
|
560
|
+
vfs.closed = false;
|
|
453
561
|
callCount = 0;
|
|
454
562
|
trace = [];
|
|
455
563
|
// Files may change between programs (editor, git); never serve a stale run.
|
|
@@ -465,10 +573,6 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
465
573
|
callListener = isFunction(fn) ? fn : null;
|
|
466
574
|
}
|
|
467
575
|
|
|
468
|
-
function hasExecutor(name) {
|
|
469
|
-
return executors.has(name) || Object.hasOwn(natives, name);
|
|
470
|
-
}
|
|
471
|
-
|
|
472
576
|
function beginSpeculation() {
|
|
473
577
|
return vfs.begin();
|
|
474
578
|
}
|
|
@@ -481,10 +585,6 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
481
585
|
return vfs.rollback();
|
|
482
586
|
}
|
|
483
587
|
|
|
484
|
-
function clearVfsCache() {
|
|
485
|
-
vfs.clear();
|
|
486
|
-
}
|
|
487
|
-
|
|
488
588
|
function resultDiff(response) {
|
|
489
589
|
let details = response?.details;
|
|
490
590
|
if (isString(details)) {
|
|
@@ -505,6 +605,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
505
605
|
}
|
|
506
606
|
|
|
507
607
|
function checkCallBudget(name) {
|
|
608
|
+
if (closed) throw new Error("program is already complete");
|
|
508
609
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
509
610
|
callCount += 1;
|
|
510
611
|
if (callCount > maxCalls) {
|
|
@@ -547,23 +648,35 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
547
648
|
|
|
548
649
|
async function invokeRaw(name, args) {
|
|
549
650
|
checkCallBudget(name);
|
|
651
|
+
const callId = ++sharedRegistry.callSeq;
|
|
550
652
|
assertCallableTarget(name);
|
|
653
|
+
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
551
654
|
|
|
552
655
|
const record = { name, args: args || {}, time: Date.now() };
|
|
553
656
|
trace.push(record);
|
|
554
657
|
notifyCall(record);
|
|
555
658
|
|
|
556
659
|
try {
|
|
557
|
-
const
|
|
660
|
+
const delegated = hostTool(name);
|
|
661
|
+
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
558
662
|
if (exec) {
|
|
559
663
|
const fallbackDiff = await writeFallbackDiff(name, args);
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
664
|
+
const mutating = isMutatingTool(name, config, args, definitions.get(name));
|
|
665
|
+
if (mutating) await vfs.prepareExternalMutation(name);
|
|
666
|
+
if (activeSignal?.aborted || closed) throw new Error("aborted");
|
|
667
|
+
if (!isCallable(name)) throw new Error("tool is no longer enabled in this session: " + name);
|
|
668
|
+
try {
|
|
669
|
+
const res = await exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, delegated
|
|
670
|
+
? { ...activeCtx, settings: hostSession.settings, toolNames: hostSession.getEvalBridgeToolNames(), autoApprove: false }
|
|
671
|
+
: activeCtx);
|
|
672
|
+
completeRecord(record, res, fallbackDiff);
|
|
673
|
+
return res;
|
|
674
|
+
} finally {
|
|
675
|
+
if (mutating) { vfs.invalidateCache(); index.invalidate(); clearPathCache(); }
|
|
676
|
+
}
|
|
564
677
|
}
|
|
565
678
|
|
|
566
|
-
const native = natives[name];
|
|
679
|
+
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
567
680
|
if (native) {
|
|
568
681
|
const res = await native(args || {}, activeSignal);
|
|
569
682
|
completeRecord(record, res);
|
|
@@ -582,7 +695,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
582
695
|
|
|
583
696
|
function finishRecord(record, res) {
|
|
584
697
|
record.ms = Date.now() - record.time;
|
|
585
|
-
record.ok = res
|
|
698
|
+
record.ok = !hostResultFailed(res);
|
|
586
699
|
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
587
700
|
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
588
701
|
}
|
|
@@ -594,14 +707,16 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
594
707
|
}
|
|
595
708
|
|
|
596
709
|
async function callMany(calls) {
|
|
597
|
-
|
|
710
|
+
if (!Array.isArray(calls)) throw new TypeError("nova.callMany requires an array");
|
|
711
|
+
const list = calls;
|
|
712
|
+
if (list.some(item => !isString(item?.name) || !item.name)) throw new TypeError("nova.callMany entries require a tool name");
|
|
598
713
|
const thunks = list.map((item) => {
|
|
599
714
|
const n = item?.name;
|
|
600
715
|
const a = item?.args;
|
|
601
716
|
return () => call(n, a);
|
|
602
717
|
});
|
|
603
718
|
const names = list.map((item) => item?.name).filter((n) => isString(n));
|
|
604
|
-
const wave = await runParallelWave(thunks, { names }, { mode: "auto", config });
|
|
719
|
+
const wave = await runParallelWave(thunks, { names, calls: list, definitions: names.map(name => definitions.get(name)) }, { mode: "auto", config });
|
|
605
720
|
// Return a results array that also carries .mode/.reason, and is directly
|
|
606
721
|
// iterable so `for (const r of await nova.callMany([...]))` works.
|
|
607
722
|
const results = Array.isArray(wave.results) ? wave.results.slice() : [];
|
|
@@ -615,20 +730,27 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
615
730
|
|
|
616
731
|
return {
|
|
617
732
|
executors,
|
|
733
|
+
definitions,
|
|
618
734
|
natives,
|
|
735
|
+
refreshTools,
|
|
736
|
+
isCallable,
|
|
737
|
+
externalNames,
|
|
738
|
+
supportsBatchRead: () => !hostTool("read") && !executors.has("read"),
|
|
739
|
+
fork(options) {
|
|
740
|
+
return createHostBridge({ pi, config, getCwd: options.getCwd, registry: sharedRegistry, ledger: ledger.fork() });
|
|
741
|
+
},
|
|
742
|
+
close() { closed = true; vfs.closed = true; },
|
|
619
743
|
bindCallContext,
|
|
620
744
|
resetCallBudget,
|
|
621
745
|
getTrace,
|
|
622
746
|
setCallListener,
|
|
623
|
-
hasExecutor,
|
|
624
747
|
beginSpeculation,
|
|
625
748
|
commitSpeculation,
|
|
626
749
|
rollbackSpeculation,
|
|
627
|
-
clearVfsCache,
|
|
628
750
|
getVfsCacheSize: () => vfs.getCacheSize(),
|
|
629
751
|
getOverlayDepth: () => vfs.getOverlayDepth(),
|
|
630
752
|
call,
|
|
631
753
|
callMany,
|
|
632
|
-
|
|
754
|
+
ledger,
|
|
633
755
|
};
|
|
634
756
|
}
|