pi-supernova 0.0.15 → 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 +18 -0
- package/README.md +42 -11
- package/bottleneck.js +98 -97
- package/catalog.js +4 -29
- 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 +95 -22
- package/index.js +69 -88
- package/ledger.js +73 -101
- package/omp-frame.js +0 -1
- package/package.json +6 -2
- 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 +2 -1
- package/runtime.js +204 -210
- package/vfs.js +114 -70
- package/workspace.js +37 -33
package/guest-worker.js
CHANGED
|
@@ -1,95 +1,15 @@
|
|
|
1
1
|
import { parentPort } from "node:worker_threads";
|
|
2
2
|
import { parallel as runParallel, pipeline as runPipeline } from "./parallel.js";
|
|
3
|
-
import { isString,
|
|
3
|
+
import { isString, isObject, toPlain } from "./decode.js";
|
|
4
|
+
import { truncateChars } from "./format.js";
|
|
4
5
|
|
|
5
6
|
// Guest programs run here, off the host thread. The host can terminate() this
|
|
6
7
|
// worker mid-loop, so a runaway "while (true) {}" or process.exit() in guest
|
|
7
8
|
// code cannot take the harness down with it.
|
|
8
9
|
|
|
9
10
|
const AsyncFunction = Object.getPrototypeOf(async () => {}).constructor;
|
|
10
|
-
const
|
|
11
|
-
const COMPILED_CACHE_MAX = 256;
|
|
12
|
-
const PARAMS = [
|
|
13
|
-
"nova", "console", "parallel", "pipeline",
|
|
14
|
-
"read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate",
|
|
15
|
-
];
|
|
16
|
-
// V8 and JSC both place the body on the line after the synthesized header.
|
|
11
|
+
const PARAMS = ["nova", "console", "parallel", "pipeline", "read", "write", "edit", "patch", "surface", "snap", "evidence", "bash", "exec", "speculate"];
|
|
17
12
|
const BODY_LINE_OFFSET = 2;
|
|
18
|
-
|
|
19
|
-
function skipLeadingComments(src) {
|
|
20
|
-
let i = 0;
|
|
21
|
-
for (;;) {
|
|
22
|
-
while (/\s/.test(src[i])) i++;
|
|
23
|
-
if (src.startsWith("//", i)) {
|
|
24
|
-
const nl = src.indexOf("\n", i);
|
|
25
|
-
if (nl < 0) return src.length;
|
|
26
|
-
i = nl + 1;
|
|
27
|
-
} else if (src.startsWith("/*", i)) {
|
|
28
|
-
const end = src.indexOf("*/", i + 2);
|
|
29
|
-
if (end < 0) return src.length;
|
|
30
|
-
i = end + 2;
|
|
31
|
-
} else {
|
|
32
|
-
return i;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function skipString(src, j, quote) {
|
|
38
|
-
for (j++; j < src.length && src[j] !== quote; j++) if (src[j] === "\\") j++;
|
|
39
|
-
return j;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function skipBalancedParens(src, i) {
|
|
43
|
-
let depth = 0;
|
|
44
|
-
for (let j = i; j < src.length; j++) {
|
|
45
|
-
const ch = src[j];
|
|
46
|
-
if (ch === "(") {
|
|
47
|
-
depth++;
|
|
48
|
-
continue;
|
|
49
|
-
}
|
|
50
|
-
if (ch === ")") {
|
|
51
|
-
depth--;
|
|
52
|
-
if (depth === 0) return j + 1;
|
|
53
|
-
continue;
|
|
54
|
-
}
|
|
55
|
-
if ("\"'\u0060".includes(ch)) j = skipString(src, j, ch);
|
|
56
|
-
}
|
|
57
|
-
return -1;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** True when src (after comments) is a single arrow or function expression. */
|
|
61
|
-
function isFunctionExpression(src) {
|
|
62
|
-
let i = skipLeadingComments(src);
|
|
63
|
-
const rest = src.slice(i);
|
|
64
|
-
if (/^(async\s+)?function\b/.test(rest)) return true;
|
|
65
|
-
const asyncMatch = /^async\s+/.exec(rest);
|
|
66
|
-
if (asyncMatch) i += asyncMatch[0].length;
|
|
67
|
-
if (/^[A-Za-z_$][\w$]*\s*=>/.test(src.slice(i))) return true;
|
|
68
|
-
if (src[i] !== "(") return false;
|
|
69
|
-
const after = skipBalancedParens(src, i);
|
|
70
|
-
if (after < 0) return false;
|
|
71
|
-
return /^\s*=>/.test(src.slice(after));
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function wrapBody(code) {
|
|
75
|
-
const trimmed = String(code).trim();
|
|
76
|
-
if (!trimmed) throw new Error("code must be a non-empty string");
|
|
77
|
-
if (isFunctionExpression(trimmed)) return "const __fn = (" + trimmed + ");\nreturn await __fn();";
|
|
78
|
-
return trimmed;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function compile(code) {
|
|
82
|
-
const body = wrapBody(code);
|
|
83
|
-
let compiled = compiledCache.get(body);
|
|
84
|
-
if (compiled) return compiled;
|
|
85
|
-
compiled = new AsyncFunction(...PARAMS, body);
|
|
86
|
-
if (compiledCache.size >= COMPILED_CACHE_MAX) {
|
|
87
|
-
compiledCache.delete(compiledCache.keys().next().value);
|
|
88
|
-
}
|
|
89
|
-
compiledCache.set(body, compiled);
|
|
90
|
-
return compiled;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
13
|
/** Best-effort guest line:col from an error stack (V8 "<anonymous>:L:C", JSC "eval code").*/
|
|
94
14
|
function guestLocation(err) {
|
|
95
15
|
const stack = String(err?.stack);
|
|
@@ -100,64 +20,8 @@ function guestLocation(err) {
|
|
|
100
20
|
return { line, col: Number(m[2]) };
|
|
101
21
|
}
|
|
102
22
|
|
|
103
|
-
const MAX_DEPTH = 64;
|
|
104
|
-
const MAX_TYPED_ARRAY = 4096;
|
|
105
|
-
|
|
106
|
-
function plainFromBinary(value) {
|
|
107
|
-
const bytes = value.byteLength;
|
|
108
|
-
if (value instanceof ArrayBuffer) value = new Uint8Array(value);
|
|
109
|
-
if (value.length > MAX_TYPED_ARRAY) return "[" + value.constructor.name + " " + bytes + " bytes]";
|
|
110
|
-
return Array.from(value, (x) => (typeof x === "bigint" ? x.toString() + "n" : x));
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
function plainFromMap(value, seen, depth) {
|
|
114
|
-
const allStringKeys = [...value.keys()].every((k) => typeof k === "string");
|
|
115
|
-
if (!allStringKeys) return [...value].map(([k, v]) => [toPlain(k, seen, depth + 1), toPlain(v, seen, depth + 1)]);
|
|
116
|
-
const out = {};
|
|
117
|
-
for (const [k, v] of value) out[k] = toPlain(v, seen, depth + 1);
|
|
118
|
-
return out;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function plainFromCollection(value, seen, depth) {
|
|
122
|
-
if (Array.isArray(value)) return value.map((x) => toPlain(x, seen, depth + 1));
|
|
123
|
-
if (value instanceof Set) return [...value].map((x) => toPlain(x, seen, depth + 1));
|
|
124
|
-
if (value instanceof Map) return plainFromMap(value, seen, depth);
|
|
125
|
-
const out = {};
|
|
126
|
-
for (const k of Object.keys(value)) out[k] = toPlain(value[k], seen, depth + 1);
|
|
127
|
-
return out;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/** Convert any guest value to structured-clone-safe, JSON-shaped data. */
|
|
131
|
-
function toPlain(value, seen = new Set(), depth = 0) {
|
|
132
|
-
if (value === null || value === undefined) return value;
|
|
133
|
-
const t = typeof value;
|
|
134
|
-
if (t === "string" || t === "number" || t === "boolean") return value;
|
|
135
|
-
if (t === "bigint") return value.toString() + "n";
|
|
136
|
-
if (t === "function") return "[Function" + (value.name ? " " + value.name : "") + "]";
|
|
137
|
-
if (t === "symbol") return value.toString();
|
|
138
|
-
if (depth > MAX_DEPTH) return "[Depth]";
|
|
139
|
-
if (seen.has(value)) return "[Circular]";
|
|
140
|
-
if (value instanceof Date) return Number.isNaN(value.getTime()) ? "Invalid Date" : value.toISOString();
|
|
141
|
-
if (value instanceof RegExp) return value.toString();
|
|
142
|
-
if (value instanceof Error) {
|
|
143
|
-
const out = { name: value.name, message: value.message };
|
|
144
|
-
if (value.cause !== undefined) out.cause = toPlain(value.cause, seen, depth + 1);
|
|
145
|
-
return out;
|
|
146
|
-
}
|
|
147
|
-
if (value instanceof Promise) return "[Promise]";
|
|
148
|
-
if (ArrayBuffer.isView(value) || value instanceof ArrayBuffer) return plainFromBinary(value);
|
|
149
|
-
if (isFunction(value.toJSON)) return toPlain(value.toJSON(), seen, depth + 1);
|
|
150
|
-
seen.add(value);
|
|
151
|
-
try {
|
|
152
|
-
return plainFromCollection(value, seen, depth);
|
|
153
|
-
} finally {
|
|
154
|
-
seen.delete(value);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
// ---- RPC to the host thread ----
|
|
159
|
-
|
|
160
23
|
let activeRunId = 0;
|
|
24
|
+
let runActive = false;
|
|
161
25
|
let rpcSeq = 0;
|
|
162
26
|
const pendingRpc = new Map();
|
|
163
27
|
|
|
@@ -165,12 +29,13 @@ function post(msg) {
|
|
|
165
29
|
parentPort.postMessage(msg);
|
|
166
30
|
}
|
|
167
31
|
|
|
168
|
-
function
|
|
32
|
+
function callRpc(runId, method, args) {
|
|
33
|
+
if (!runActive || runId !== activeRunId) return Promise.reject(new Error("program is already complete"));
|
|
169
34
|
return new Promise((resolve, reject) => {
|
|
170
35
|
const id = ++rpcSeq;
|
|
171
36
|
pendingRpc.set(id, { resolve, reject });
|
|
172
37
|
try {
|
|
173
|
-
post({ op: "rpc", id, runId
|
|
38
|
+
post({ op: "rpc", id, runId, method, args });
|
|
174
39
|
} catch (err) {
|
|
175
40
|
pendingRpc.delete(id);
|
|
176
41
|
reject(new Error("nova." + method + " arguments are not transferable: " + err?.message));
|
|
@@ -179,6 +44,7 @@ function rpc(method, args) {
|
|
|
179
44
|
}
|
|
180
45
|
|
|
181
46
|
function unwrapValue(res) {
|
|
47
|
+
if (res?.ok === false) throw new Error(String(res.value ?? res.error ?? "host tool failed"));
|
|
182
48
|
if ("value" in Object(res)) return res.value;
|
|
183
49
|
return res;
|
|
184
50
|
}
|
|
@@ -201,7 +67,8 @@ function leanEnvelope(res) {
|
|
|
201
67
|
return res;
|
|
202
68
|
}
|
|
203
69
|
|
|
204
|
-
function buildGuestApi(available) {
|
|
70
|
+
function buildGuestApi(available, batchRead, runId) {
|
|
71
|
+
const rpc = (method, args) => callRpc(runId, method, args);
|
|
205
72
|
const availableSet = new Set(available);
|
|
206
73
|
const nova = {
|
|
207
74
|
search: (query, limit) => rpc("search", [query, limit]),
|
|
@@ -238,7 +105,9 @@ function buildGuestApi(available) {
|
|
|
238
105
|
const readArgs = (p, a, b) => (isObject(a) && !Array.isArray(a) ? { path: p, ...a } : { path: p, offset: a, limit: b });
|
|
239
106
|
const read = async (p, a, b) => {
|
|
240
107
|
if (Array.isArray(p)) {
|
|
108
|
+
if (!batchRead) return Promise.all(p.map(item => read(item, a, b)));
|
|
241
109
|
const res = await nova.call("read", readArgs(p, a, b));
|
|
110
|
+
unwrapValue(res);
|
|
242
111
|
if (Array.isArray(res?.items)) return res.items;
|
|
243
112
|
// Captured host executor without batch support: fan out.
|
|
244
113
|
return Promise.all(p.map((item) => read(item, a, b)));
|
|
@@ -277,8 +146,13 @@ function buildGuestApi(available) {
|
|
|
277
146
|
|
|
278
147
|
function makeConsole(runId, limits) {
|
|
279
148
|
let count = 0;
|
|
149
|
+
let truncated = false;
|
|
150
|
+
const markTruncated = () => {
|
|
151
|
+
if (!truncated) post({ op: "logTruncated", runId });
|
|
152
|
+
truncated = true;
|
|
153
|
+
};
|
|
280
154
|
const emit = (...args) => {
|
|
281
|
-
if (count >= limits.maxLogLines) return;
|
|
155
|
+
if (count >= limits.maxLogLines) { markTruncated(); return; }
|
|
282
156
|
count++;
|
|
283
157
|
const line = args
|
|
284
158
|
.map((a) => {
|
|
@@ -290,30 +164,34 @@ function makeConsole(runId, limits) {
|
|
|
290
164
|
}
|
|
291
165
|
})
|
|
292
166
|
.join(" ");
|
|
293
|
-
|
|
167
|
+
const clipped = truncateChars(line, limits.maxLogLineChars, "log");
|
|
168
|
+
if (clipped.truncated) markTruncated();
|
|
169
|
+
post({ op: "log", runId, line: clipped.text, truncated: clipped.truncated });
|
|
294
170
|
};
|
|
295
171
|
return { log: emit, warn: emit, error: emit, info: emit, debug: emit };
|
|
296
172
|
}
|
|
297
173
|
|
|
298
174
|
function postFailure(runId, err, location) {
|
|
175
|
+
runActive = false;
|
|
299
176
|
const message = err instanceof Error ? err.message : String(err);
|
|
300
177
|
post({ op: "error", runId, message, location });
|
|
301
178
|
}
|
|
302
179
|
|
|
303
180
|
async function handleRun(msg) {
|
|
304
|
-
const { runId,
|
|
181
|
+
const { runId, prepared, limits, available, batchRead = true } = msg;
|
|
305
182
|
activeRunId = runId;
|
|
183
|
+
runActive = true;
|
|
306
184
|
let compiled;
|
|
307
185
|
try {
|
|
308
|
-
compiled =
|
|
186
|
+
compiled = { fn: new AsyncFunction(...PARAMS, prepared.body), hasReturn: prepared.hasReturn };
|
|
309
187
|
} catch (err) {
|
|
310
188
|
postFailure(runId, err);
|
|
311
189
|
return;
|
|
312
190
|
}
|
|
313
|
-
const api = buildGuestApi(available);
|
|
191
|
+
const api = buildGuestApi(available, batchRead, runId);
|
|
314
192
|
const scopedConsole = makeConsole(runId, limits);
|
|
315
193
|
try {
|
|
316
|
-
const value = await compiled(
|
|
194
|
+
const value = await compiled.fn(
|
|
317
195
|
api.nova, scopedConsole, runParallel, runPipeline,
|
|
318
196
|
api.read, api.write, api.edit, api.patch, api.surface, api.snap, api.evidence, api.bash, api.exec, api.speculate,
|
|
319
197
|
);
|
|
@@ -324,7 +202,8 @@ async function handleRun(msg) {
|
|
|
324
202
|
} catch (err) {
|
|
325
203
|
plain = "[unserializable: " + (err?.message || err) + "]";
|
|
326
204
|
}
|
|
327
|
-
|
|
205
|
+
runActive = false;
|
|
206
|
+
post({ op: "done", runId, value: plain, undefinedReturn: value === undefined && !compiled.hasReturn, hasReturn: compiled.hasReturn });
|
|
328
207
|
} catch (err) {
|
|
329
208
|
if (runId !== activeRunId) return;
|
|
330
209
|
postFailure(runId, err, guestLocation(err));
|
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";
|
|
@@ -79,7 +79,7 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
79
79
|
if (index < 0) {
|
|
80
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)`);
|
|
81
81
|
}
|
|
82
|
-
if (content.indexOf(replacement.oldText, index +
|
|
82
|
+
if (content.indexOf(replacement.oldText, index + 1) >= 0) {
|
|
83
83
|
throw new Error(`edit target is not unique in ${target}: include more surrounding lines in oldText, or pass edits:[{oldText,newText},…]`);
|
|
84
84
|
}
|
|
85
85
|
return { ...replacement, index, end: index + replacement.oldText.length };
|
|
@@ -113,15 +113,16 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
113
113
|
|
|
114
114
|
function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
115
115
|
async function readAdapter(params, signal) {
|
|
116
|
+
signal?.throwIfAborted();
|
|
116
117
|
const cwd = getCwd();
|
|
117
118
|
const targetParam = params?.path ?? params?.target;
|
|
118
119
|
|
|
119
120
|
if (Array.isArray(targetParam)) {
|
|
120
121
|
const results = await Promise.all(
|
|
121
|
-
targetParam.map((p) => readAdapter({
|
|
122
|
+
targetParam.map((p) => readAdapter({ ...params, path: p }, signal)),
|
|
122
123
|
);
|
|
123
124
|
const items = results.map((r) => r.content[0].text);
|
|
124
|
-
return textResult(
|
|
125
|
+
return textResult("", { count: results.length, batch: true, items });
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
if (looksLikePath(targetParam)) {
|
|
@@ -281,7 +282,8 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
281
282
|
const cwd = getCwd();
|
|
282
283
|
const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
|
|
283
284
|
if (signal?.aborted) throw new Error("aborted");
|
|
284
|
-
|
|
285
|
+
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
286
|
+
const content = params.content;
|
|
285
287
|
let prevText = "";
|
|
286
288
|
try {
|
|
287
289
|
prevText = await vfs.read(target);
|
|
@@ -308,7 +310,6 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
308
310
|
matches.length === 1
|
|
309
311
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
310
312
|
: buildMultiEditDiff(target, content, matches);
|
|
311
|
-
const tag = speculative ? " (speculative)" : "";
|
|
312
313
|
const summary = await editSummary(cwd, target, content, updated, diff);
|
|
313
314
|
return textResult(summary, { path: target, speculative, diff });
|
|
314
315
|
},
|
|
@@ -367,7 +368,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
367
368
|
const options = {};
|
|
368
369
|
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
369
370
|
if (Number.isInteger(params?.maxChars) && params.maxChars > 0) options.maxChars = params.maxChars;
|
|
370
|
-
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 });
|
|
371
372
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
372
373
|
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
373
374
|
},
|
|
@@ -408,7 +409,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
408
409
|
if (res.exitCode !== 0) text += sourceForReferences(cwd, text);
|
|
409
410
|
return {
|
|
410
411
|
content: [{ type: "text", text }],
|
|
411
|
-
details: { exitCode: res.exitCode, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
412
|
+
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
412
413
|
isError: res.exitCode !== 0,
|
|
413
414
|
};
|
|
414
415
|
},
|
|
@@ -474,19 +475,24 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
474
475
|
};
|
|
475
476
|
}
|
|
476
477
|
|
|
477
|
-
export function createHostBridge({ pi, config, getCwd }) {
|
|
478
|
-
const index = new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
479
|
-
const ledger = new SeenLedger({ window: config.seenWindow ?? 40 });
|
|
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 });
|
|
480
481
|
const vfs = new CausalVfs(() => index.invalidate());
|
|
481
|
-
const executors = new Map();
|
|
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;
|
|
482
486
|
const natives = createNativeAdapters(getCwd, vfs, config, index, ledger);
|
|
483
487
|
let callCount = 0;
|
|
484
488
|
let activeCtx = null;
|
|
489
|
+
let hostSession = null;
|
|
490
|
+
let boundSessionId;
|
|
485
491
|
let activeSignal = undefined;
|
|
486
492
|
let trace = [];
|
|
487
493
|
let callListener = null;
|
|
488
494
|
|
|
489
|
-
if (pi && isFunction(pi.registerTool)) {
|
|
495
|
+
if (!registry && pi && isFunction(pi.registerTool)) {
|
|
490
496
|
const original = pi.registerTool.bind(pi);
|
|
491
497
|
const excluded = new Set(config.excludeTools || []);
|
|
492
498
|
pi.registerTool = (tool) => {
|
|
@@ -498,6 +504,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
498
504
|
!excluded.has(tool.name)
|
|
499
505
|
) {
|
|
500
506
|
executors.set(tool.name, tool.execute.bind(tool));
|
|
507
|
+
definitions.set(tool.name, tool);
|
|
501
508
|
}
|
|
502
509
|
return original(tool);
|
|
503
510
|
};
|
|
@@ -505,10 +512,52 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
505
512
|
|
|
506
513
|
function bindCallContext(ctx, signal) {
|
|
507
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;
|
|
508
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));
|
|
509
556
|
}
|
|
510
557
|
|
|
511
558
|
function resetCallBudget() {
|
|
559
|
+
closed = false;
|
|
560
|
+
vfs.closed = false;
|
|
512
561
|
callCount = 0;
|
|
513
562
|
trace = [];
|
|
514
563
|
// Files may change between programs (editor, git); never serve a stale run.
|
|
@@ -556,6 +605,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
556
605
|
}
|
|
557
606
|
|
|
558
607
|
function checkCallBudget(name) {
|
|
608
|
+
if (closed) throw new Error("program is already complete");
|
|
559
609
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
560
610
|
callCount += 1;
|
|
561
611
|
if (callCount > maxCalls) {
|
|
@@ -598,23 +648,35 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
598
648
|
|
|
599
649
|
async function invokeRaw(name, args) {
|
|
600
650
|
checkCallBudget(name);
|
|
651
|
+
const callId = ++sharedRegistry.callSeq;
|
|
601
652
|
assertCallableTarget(name);
|
|
653
|
+
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
602
654
|
|
|
603
655
|
const record = { name, args: args || {}, time: Date.now() };
|
|
604
656
|
trace.push(record);
|
|
605
657
|
notifyCall(record);
|
|
606
658
|
|
|
607
659
|
try {
|
|
608
|
-
const
|
|
660
|
+
const delegated = hostTool(name);
|
|
661
|
+
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
609
662
|
if (exec) {
|
|
610
663
|
const fallbackDiff = await writeFallbackDiff(name, args);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
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
|
+
}
|
|
615
677
|
}
|
|
616
678
|
|
|
617
|
-
const native = natives[name];
|
|
679
|
+
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
618
680
|
if (native) {
|
|
619
681
|
const res = await native(args || {}, activeSignal);
|
|
620
682
|
completeRecord(record, res);
|
|
@@ -633,7 +695,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
633
695
|
|
|
634
696
|
function finishRecord(record, res) {
|
|
635
697
|
record.ms = Date.now() - record.time;
|
|
636
|
-
record.ok = res
|
|
698
|
+
record.ok = !hostResultFailed(res);
|
|
637
699
|
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
638
700
|
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
639
701
|
}
|
|
@@ -645,14 +707,16 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
645
707
|
}
|
|
646
708
|
|
|
647
709
|
async function callMany(calls) {
|
|
648
|
-
|
|
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");
|
|
649
713
|
const thunks = list.map((item) => {
|
|
650
714
|
const n = item?.name;
|
|
651
715
|
const a = item?.args;
|
|
652
716
|
return () => call(n, a);
|
|
653
717
|
});
|
|
654
718
|
const names = list.map((item) => item?.name).filter((n) => isString(n));
|
|
655
|
-
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 });
|
|
656
720
|
// Return a results array that also carries .mode/.reason, and is directly
|
|
657
721
|
// iterable so `for (const r of await nova.callMany([...]))` works.
|
|
658
722
|
const results = Array.isArray(wave.results) ? wave.results.slice() : [];
|
|
@@ -666,7 +730,16 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
666
730
|
|
|
667
731
|
return {
|
|
668
732
|
executors,
|
|
733
|
+
definitions,
|
|
669
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; },
|
|
670
743
|
bindCallContext,
|
|
671
744
|
resetCallBudget,
|
|
672
745
|
getTrace,
|