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/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";
|
|
@@ -55,15 +55,15 @@ function looksLikePath(target) {
|
|
|
55
55
|
);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
async function
|
|
59
|
-
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read",
|
|
60
|
-
if (vfs.getOverlay(targetPath) !== undefined || vfs.cache.has(targetPath)) return targetPath;
|
|
58
|
+
async function probeExistingPath(cwd, targetParam, vfs) {
|
|
59
|
+
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", true);
|
|
60
|
+
if (vfs.getOverlay(targetPath) !== undefined || vfs.cache.has(targetPath)) return { path: targetPath, directory: false };
|
|
61
61
|
try {
|
|
62
62
|
const st = await fs.stat(targetPath);
|
|
63
|
-
|
|
64
|
-
return targetPath;
|
|
63
|
+
return { path: targetPath, directory: st.isDirectory() };
|
|
65
64
|
} catch (err) {
|
|
66
65
|
if (err?.code !== "ENOENT" && err?.code !== "ENOTDIR") throw err;
|
|
66
|
+
if (vfs.getOverlayPaths().some(file => file.startsWith(targetPath + path.sep))) return { path: targetPath, directory: true };
|
|
67
67
|
return null;
|
|
68
68
|
}
|
|
69
69
|
}
|
|
@@ -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 };
|
|
@@ -96,6 +96,11 @@ function applyReplacements(target, content, requestedEdits) {
|
|
|
96
96
|
return { updated, matches };
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
function formatDirectoryEntry(name, type, size = 0) {
|
|
100
|
+
const sizeSuffix = size ? `, ${size} bytes` : "";
|
|
101
|
+
return `${name}${type === "dir" ? "/" : ""} (${type}${sizeSuffix})`;
|
|
102
|
+
}
|
|
103
|
+
|
|
99
104
|
async function formatLsEntry(dirPath, entry) {
|
|
100
105
|
const isDir = entry.isDirectory();
|
|
101
106
|
const isSym = entry.isSymbolicLink();
|
|
@@ -107,46 +112,55 @@ async function formatLsEntry(dirPath, entry) {
|
|
|
107
112
|
size = st.size;
|
|
108
113
|
}
|
|
109
114
|
} catch {}
|
|
110
|
-
|
|
111
|
-
return `${entry.name}${isDir ? "/" : ""} (${typeLabel}${sizeSuffix})`;
|
|
115
|
+
return formatDirectoryEntry(entry.name, typeLabel, size);
|
|
112
116
|
}
|
|
113
117
|
|
|
114
118
|
function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
115
|
-
async function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const items = results.map((r) => r.content[0].text);
|
|
124
|
-
return textResult(items.join("\n---\n"), { count: results.length, batch: true, items });
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (looksLikePath(targetParam)) {
|
|
128
|
-
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
129
|
-
return readFile(targetPath, params);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const existing = await probeExistingFile(cwd, targetParam, vfs);
|
|
133
|
-
if (existing) return readFile(existing, params);
|
|
119
|
+
async function sourceRead(query, searchDir, signal) {
|
|
120
|
+
const cwd = getCwd();
|
|
121
|
+
const includeHidden = path.relative(cwd, searchDir).split(path.sep)
|
|
122
|
+
.some(segment => segment.startsWith(".") && segment.length > 1);
|
|
123
|
+
const result = await executeSnap({ query, searchDir, root: cwd, includeHidden, index,
|
|
124
|
+
overlayText: p => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), signal });
|
|
125
|
+
return textResult(JSON.stringify(result, null, 2), { ...result, isSnap: true });
|
|
126
|
+
}
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
128
|
+
async function readDirectory(dirPath, signal) {
|
|
129
|
+
signal?.throwIfAborted();
|
|
130
|
+
const rows = new Map();
|
|
131
|
+
for (const file of vfs.getOverlayPaths()) {
|
|
132
|
+
const relative = path.relative(dirPath, file);
|
|
133
|
+
if (!relative || relative === ".." || relative.startsWith(".." + path.sep) || path.isAbsolute(relative)) continue;
|
|
134
|
+
const [name, child] = relative.split(path.sep);
|
|
135
|
+
rows.set(name, child === undefined
|
|
136
|
+
? formatDirectoryEntry(name, "file", Buffer.byteLength(vfs.getOverlay(file), "utf8"))
|
|
137
|
+
: formatDirectoryEntry(name, "dir"));
|
|
138
|
+
}
|
|
139
|
+
let entries;
|
|
140
|
+
try { entries = await fs.readdir(dirPath, { withFileTypes: true }); } catch (error) {
|
|
141
|
+
if (error.code !== "ENOENT" || rows.size === 0) throw error;
|
|
142
|
+
entries = [];
|
|
143
|
+
}
|
|
144
|
+
for (const entry of entries) if (!rows.has(entry.name)) rows.set(entry.name, await formatLsEntry(dirPath, entry));
|
|
145
|
+
return textResult([...rows.values()].join("\n"), { path: dirPath, count: rows.size });
|
|
146
|
+
}
|
|
147
147
|
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
async function readAdapter(params, signal) {
|
|
149
|
+
signal?.throwIfAborted();
|
|
150
|
+
const cwd = getCwd();
|
|
151
|
+
const targetParam = params?.path ?? params?.target;
|
|
152
|
+
if (Array.isArray(targetParam)) {
|
|
153
|
+
const results = await Promise.all(targetParam.map(p => readAdapter({ ...params, path: p }, signal)));
|
|
154
|
+
return textResult("", { count: results.length, batch: true, items: results.map(r => r.content[0].text) });
|
|
155
|
+
}
|
|
156
|
+
const existing = await probeExistingPath(cwd, targetParam, vfs);
|
|
157
|
+
if (existing) {
|
|
158
|
+
if (!existing.directory) return readFile(existing.path, params);
|
|
159
|
+
return isString(params?.about) ? sourceRead(params.about, existing.path, signal) : readDirectory(existing.path, signal);
|
|
160
|
+
}
|
|
161
|
+
if (!looksLikePath(targetParam)) return sourceRead(targetParam, cwd, signal);
|
|
162
|
+
const targetPath = await resolveWorkspacePath(cwd, targetParam, "read", false);
|
|
163
|
+
return readFile(targetPath, params);
|
|
150
164
|
}
|
|
151
165
|
|
|
152
166
|
/** Plain text, a line window, or (with `about`) a relevance-folded outline of the whole file. */
|
|
@@ -281,7 +295,8 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
281
295
|
const cwd = getCwd();
|
|
282
296
|
const target = await resolveWorkspacePath(cwd, params?.path, "write", false);
|
|
283
297
|
if (signal?.aborted) throw new Error("aborted");
|
|
284
|
-
|
|
298
|
+
if (!isString(params?.content)) throw new Error("write requires string content");
|
|
299
|
+
const content = params.content;
|
|
285
300
|
let prevText = "";
|
|
286
301
|
try {
|
|
287
302
|
prevText = await vfs.read(target);
|
|
@@ -308,7 +323,6 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
308
323
|
matches.length === 1
|
|
309
324
|
? buildEditDiff(target, content, matches[0].oldText, matches[0].newText)
|
|
310
325
|
: buildMultiEditDiff(target, content, matches);
|
|
311
|
-
const tag = speculative ? " (speculative)" : "";
|
|
312
326
|
const summary = await editSummary(cwd, target, content, updated, diff);
|
|
313
327
|
return textResult(summary, { path: target, speculative, diff });
|
|
314
328
|
},
|
|
@@ -356,6 +370,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
356
370
|
index,
|
|
357
371
|
overlayText: (p) => vfs.getOverlay(p),
|
|
358
372
|
pendingPaths: vfs.getOverlayPaths(),
|
|
373
|
+
signal,
|
|
359
374
|
});
|
|
360
375
|
return textResult(JSON.stringify(res, null, 2), res);
|
|
361
376
|
},
|
|
@@ -367,7 +382,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
367
382
|
const options = {};
|
|
368
383
|
if (Number.isInteger(params?.k) && params.k > 0) options.k = params.k;
|
|
369
384
|
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 });
|
|
385
|
+
const res = await selectEvidence({ query: params.query, root: cwd, searchDir, index, overlayText: (p) => vfs.getOverlay(p), pendingPaths: vfs.getOverlayPaths(), options });
|
|
371
386
|
for (const span of res.spans) ledger.recordOrigin(span.path, span.lines[0], span.text.split("\n"));
|
|
372
387
|
return textResult(JSON.stringify(res), { route: res.route, count: res.spans.length });
|
|
373
388
|
},
|
|
@@ -408,7 +423,7 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
408
423
|
if (res.exitCode !== 0) text += sourceForReferences(cwd, text);
|
|
409
424
|
return {
|
|
410
425
|
content: [{ type: "text", text }],
|
|
411
|
-
details: { exitCode: res.exitCode, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
426
|
+
details: { exitCode: res.exitCode, signal: res.signal, outputTruncated: res.outputTruncated, transactionBarrier },
|
|
412
427
|
isError: res.exitCode !== 0,
|
|
413
428
|
};
|
|
414
429
|
},
|
|
@@ -463,30 +478,29 @@ function createNativeAdapters(getCwd, vfs, config, index, ledger) {
|
|
|
463
478
|
async ls(params, signal) {
|
|
464
479
|
const cwd = getCwd();
|
|
465
480
|
const dirPath = params?.path ? await resolveWorkspacePath(cwd, params.path, "ls", true) : cwd;
|
|
466
|
-
|
|
467
|
-
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
468
|
-
const lines = [];
|
|
469
|
-
for (const entry of entries) {
|
|
470
|
-
lines.push(await formatLsEntry(dirPath, entry));
|
|
471
|
-
}
|
|
472
|
-
return textResult(lines.join("\n"), { path: dirPath, count: entries.length });
|
|
481
|
+
return readDirectory(dirPath, signal);
|
|
473
482
|
},
|
|
474
483
|
};
|
|
475
484
|
}
|
|
476
485
|
|
|
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 });
|
|
486
|
+
export function createHostBridge({ pi, config, getCwd, registry, ledger: runLedger }) {
|
|
487
|
+
const index = registry?.index ?? new WorkspaceIndex((argv, opts) => runCommand(argv, opts));
|
|
488
|
+
const ledger = runLedger ?? new SeenLedger({ window: config.seenWindow ?? 40 });
|
|
480
489
|
const vfs = new CausalVfs(() => index.invalidate());
|
|
481
|
-
const executors = new Map();
|
|
490
|
+
const executors = registry?.executors ?? new Map();
|
|
491
|
+
const definitions = registry?.definitions ?? new Map();
|
|
492
|
+
const sharedRegistry = registry ?? { executors, definitions, index, callSeq: 0 };
|
|
493
|
+
let closed = false;
|
|
482
494
|
const natives = createNativeAdapters(getCwd, vfs, config, index, ledger);
|
|
483
495
|
let callCount = 0;
|
|
484
496
|
let activeCtx = null;
|
|
497
|
+
let hostSession = null;
|
|
498
|
+
let boundSessionId;
|
|
485
499
|
let activeSignal = undefined;
|
|
486
500
|
let trace = [];
|
|
487
501
|
let callListener = null;
|
|
488
502
|
|
|
489
|
-
if (pi && isFunction(pi.registerTool)) {
|
|
503
|
+
if (!registry && pi && isFunction(pi.registerTool)) {
|
|
490
504
|
const original = pi.registerTool.bind(pi);
|
|
491
505
|
const excluded = new Set(config.excludeTools || []);
|
|
492
506
|
pi.registerTool = (tool) => {
|
|
@@ -498,6 +512,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
498
512
|
!excluded.has(tool.name)
|
|
499
513
|
) {
|
|
500
514
|
executors.set(tool.name, tool.execute.bind(tool));
|
|
515
|
+
definitions.set(tool.name, tool);
|
|
501
516
|
}
|
|
502
517
|
return original(tool);
|
|
503
518
|
};
|
|
@@ -505,10 +520,56 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
505
520
|
|
|
506
521
|
function bindCallContext(ctx, signal) {
|
|
507
522
|
activeCtx = ctx || null;
|
|
523
|
+
const sessionId = ctx?.sessionManager?.getSessionId?.();
|
|
524
|
+
boundSessionId = sessionId;
|
|
525
|
+
const registry = pi?.pi?.AgentRegistry?.global?.();
|
|
526
|
+
hostSession = sessionId && registry?.list
|
|
527
|
+
? registry.list().map(ref => ref.session).find(session => !session?.isDisposed && session?.sessionManager?.getSessionId?.() === sessionId) ?? null
|
|
528
|
+
: null;
|
|
508
529
|
activeSignal = signal;
|
|
530
|
+
vfs.signal = signal;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function hostTool(name) {
|
|
534
|
+
if (!hostSession) return undefined;
|
|
535
|
+
const metadata = definitions.get(name);
|
|
536
|
+
// Keep Supernova's transactional adapters for ordinary built-ins. Respect overrides.
|
|
537
|
+
if (Object.hasOwn(natives, name) && metadata?.sourceInfo?.source === "builtin") return undefined;
|
|
538
|
+
return hostSession.getToolForEvalBridge?.(name);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function isCallable(name) {
|
|
542
|
+
if (name === "supernova" || (config.excludeTools ?? []).includes(name)) return false;
|
|
543
|
+
if (hostSession && (hostSession.isDisposed || hostSession.sessionManager.getSessionId() !== boundSessionId)) return false;
|
|
544
|
+
// An internal adapter belongs to Supernova, not the host's visible tool list.
|
|
545
|
+
const nativeOwned = Object.hasOwn(natives, name) && !executors.has(name)
|
|
546
|
+
&& (!hostSession || !definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin");
|
|
547
|
+
if (nativeOwned) return true;
|
|
548
|
+
if (hostSession) {
|
|
549
|
+
if (!hostSession.getEvalBridgeToolNames().includes(name) && definitions.has(name)) return false;
|
|
550
|
+
return !!hostTool(name) || (Object.hasOwn(natives, name) && (!definitions.has(name) || definitions.get(name).sourceInfo?.source === "builtin"));
|
|
551
|
+
}
|
|
552
|
+
if (definitions.has(name) && isFunction(pi?.getActiveTools) && !pi.getActiveTools().includes(name)) return false;
|
|
553
|
+
return executors.has(name) || Object.hasOwn(natives, name);
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function refreshTools() {
|
|
557
|
+
const tools = pi?.getAllTools?.() ?? [];
|
|
558
|
+
for (const tool of tools) {
|
|
559
|
+
if (!isString(tool?.name)) continue;
|
|
560
|
+
definitions.set(tool.name, { ...definitions.get(tool.name), ...tool });
|
|
561
|
+
if (!hostSession && isFunction(tool.execute)) executors.set(tool.name, tool.execute.bind(tool));
|
|
562
|
+
}
|
|
563
|
+
return [...definitions.values()].filter(tool => isCallable(tool.name));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function externalNames() {
|
|
567
|
+
return [...definitions.keys()].filter(name => !!hostTool(name) || executors.has(name));
|
|
509
568
|
}
|
|
510
569
|
|
|
511
570
|
function resetCallBudget() {
|
|
571
|
+
closed = false;
|
|
572
|
+
vfs.closed = false;
|
|
512
573
|
callCount = 0;
|
|
513
574
|
trace = [];
|
|
514
575
|
// Files may change between programs (editor, git); never serve a stale run.
|
|
@@ -556,6 +617,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
556
617
|
}
|
|
557
618
|
|
|
558
619
|
function checkCallBudget(name) {
|
|
620
|
+
if (closed) throw new Error("program is already complete");
|
|
559
621
|
const maxCalls = config.maxBridgeCalls ?? 256;
|
|
560
622
|
callCount += 1;
|
|
561
623
|
if (callCount > maxCalls) {
|
|
@@ -598,23 +660,35 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
598
660
|
|
|
599
661
|
async function invokeRaw(name, args) {
|
|
600
662
|
checkCallBudget(name);
|
|
663
|
+
const callId = ++sharedRegistry.callSeq;
|
|
601
664
|
assertCallableTarget(name);
|
|
665
|
+
if (!isCallable(name)) throw new Error(unknownToolMessage(name, [...definitions.keys(), ...Object.keys(natives)].filter(isCallable)));
|
|
602
666
|
|
|
603
667
|
const record = { name, args: args || {}, time: Date.now() };
|
|
604
668
|
trace.push(record);
|
|
605
669
|
notifyCall(record);
|
|
606
670
|
|
|
607
671
|
try {
|
|
608
|
-
const
|
|
672
|
+
const delegated = hostTool(name);
|
|
673
|
+
const exec = delegated ? delegated.execute.bind(delegated) : hostSession ? undefined : executors.get(name);
|
|
609
674
|
if (exec) {
|
|
610
675
|
const fallbackDiff = await writeFallbackDiff(name, args);
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
676
|
+
const mutating = isMutatingTool(name, config, args, definitions.get(name));
|
|
677
|
+
if (mutating) await vfs.prepareExternalMutation(name);
|
|
678
|
+
if (activeSignal?.aborted || closed) throw new Error("aborted");
|
|
679
|
+
if (!isCallable(name)) throw new Error("tool is no longer enabled in this session: " + name);
|
|
680
|
+
try {
|
|
681
|
+
const res = await exec(`supernova:${name}:${callId}`, args || {}, activeSignal, undefined, delegated
|
|
682
|
+
? { ...activeCtx, settings: hostSession.settings, toolNames: hostSession.getEvalBridgeToolNames(), autoApprove: false }
|
|
683
|
+
: activeCtx);
|
|
684
|
+
completeRecord(record, res, fallbackDiff);
|
|
685
|
+
return res;
|
|
686
|
+
} finally {
|
|
687
|
+
if (mutating) { vfs.invalidateCache(); index.invalidate(); clearPathCache(); }
|
|
688
|
+
}
|
|
615
689
|
}
|
|
616
690
|
|
|
617
|
-
const native = natives[name];
|
|
691
|
+
const native = Object.hasOwn(natives, name) ? natives[name] : undefined;
|
|
618
692
|
if (native) {
|
|
619
693
|
const res = await native(args || {}, activeSignal);
|
|
620
694
|
completeRecord(record, res);
|
|
@@ -633,7 +707,7 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
633
707
|
|
|
634
708
|
function finishRecord(record, res) {
|
|
635
709
|
record.ms = Date.now() - record.time;
|
|
636
|
-
record.ok = res
|
|
710
|
+
record.ok = !hostResultFailed(res);
|
|
637
711
|
const exitCode = isObject(res?.details) ? res.details.exitCode : undefined;
|
|
638
712
|
if (Number.isInteger(exitCode) && exitCode !== 0) record.exitCode = exitCode;
|
|
639
713
|
}
|
|
@@ -645,14 +719,16 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
645
719
|
}
|
|
646
720
|
|
|
647
721
|
async function callMany(calls) {
|
|
648
|
-
|
|
722
|
+
if (!Array.isArray(calls)) throw new TypeError("nova.callMany requires an array");
|
|
723
|
+
const list = calls;
|
|
724
|
+
if (list.some(item => !isString(item?.name) || !item.name)) throw new TypeError("nova.callMany entries require a tool name");
|
|
649
725
|
const thunks = list.map((item) => {
|
|
650
726
|
const n = item?.name;
|
|
651
727
|
const a = item?.args;
|
|
652
728
|
return () => call(n, a);
|
|
653
729
|
});
|
|
654
730
|
const names = list.map((item) => item?.name).filter((n) => isString(n));
|
|
655
|
-
const wave = await runParallelWave(thunks, { names }, { mode: "auto", config });
|
|
731
|
+
const wave = await runParallelWave(thunks, { names, calls: list, definitions: names.map(name => definitions.get(name)) }, { mode: "auto", config });
|
|
656
732
|
// Return a results array that also carries .mode/.reason, and is directly
|
|
657
733
|
// iterable so `for (const r of await nova.callMany([...]))` works.
|
|
658
734
|
const results = Array.isArray(wave.results) ? wave.results.slice() : [];
|
|
@@ -666,7 +742,16 @@ export function createHostBridge({ pi, config, getCwd }) {
|
|
|
666
742
|
|
|
667
743
|
return {
|
|
668
744
|
executors,
|
|
745
|
+
definitions,
|
|
669
746
|
natives,
|
|
747
|
+
refreshTools,
|
|
748
|
+
isCallable,
|
|
749
|
+
externalNames,
|
|
750
|
+
supportsBatchRead: () => !hostTool("read") && !executors.has("read"),
|
|
751
|
+
fork(options) {
|
|
752
|
+
return createHostBridge({ pi, config, getCwd: options.getCwd, registry: sharedRegistry, ledger: ledger.fork() });
|
|
753
|
+
},
|
|
754
|
+
close() { closed = true; vfs.closed = true; },
|
|
670
755
|
bindCallContext,
|
|
671
756
|
resetCallBudget,
|
|
672
757
|
getTrace,
|
package/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { isString, isFunction
|
|
2
|
+
import { isString, isFunction } from "./decode.js";
|
|
3
3
|
import { buildCatalog, searchCatalog, describeTool, mergeNativeToolDefinitions } from "./catalog.js";
|
|
4
4
|
import { loadConfig } from "./config.js";
|
|
5
5
|
import { createHostBridge } from "./host-bridge.js";
|
|
@@ -82,22 +82,22 @@ function errorText(outcome, call) {
|
|
|
82
82
|
|
|
83
83
|
function successText(outcome, call) {
|
|
84
84
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
85
|
-
const hint = outcome.undefinedReturn ? " (no return statement; add
|
|
85
|
+
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
86
86
|
return `ok #${call} ${outcome.wallMs}ms${truncated}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
87
87
|
}
|
|
88
|
-
const TOOL_DESCRIPTION = `Run one JavaScript program
|
|
88
|
+
const TOOL_DESCRIPTION = `Run one JavaScript program with four familiar commands: read, write, edit, bash. Use an async body or arrow. Return a small value; strings stay raw.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
read(path|paths, offset?, limit?) → text
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
nova.search(query) → [{name, description}] · nova.describe(name) → parameters · nova.has(name) sync
|
|
98
|
-
parallel(thunks) · pipeline(items, ...stages)
|
|
90
|
+
Native commands (async):
|
|
91
|
+
read(path|paths, offset?, limit?) → file text or text[]; read(directory) → directory entries
|
|
92
|
+
read("symbol or question") → JSON text with source location and context; no separate search tool needed
|
|
93
|
+
read(path, {about: question}) → relevant file bodies, or source selection inside a directory
|
|
94
|
+
write(path, text) → write a file
|
|
95
|
+
edit(path, oldText, newText) → post-edit lines, checks, and references; no verification read needed
|
|
96
|
+
bash(command, {cwd?, timeoutMs?}) → bounded output; throws on non-zero exit
|
|
99
97
|
|
|
100
|
-
|
|
98
|
+
Source selection reports found, ambiguous, not_found, or incomplete. Only found selects a path. Narrow the directory for uncertain results.
|
|
99
|
+
Optional composition: parallel(thunks), pipeline(items, ...stages), nova.call(name, args), nova.callMany(calls). Use nova.search/describe only for other host tools.
|
|
100
|
+
Already-seen lines collapse to references; read(path, firstLine, lineCount) shows them again. console.log is captured.`;
|
|
101
101
|
|
|
102
102
|
export default function piSupernova(pi) {
|
|
103
103
|
const config = loadConfig();
|
|
@@ -111,50 +111,38 @@ export default function piSupernova(pi) {
|
|
|
111
111
|
getCwd: () => cwd,
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
-
function refreshCatalog() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
114
|
+
function refreshCatalog(target = bridge) {
|
|
115
|
+
const tools = target.refreshTools();
|
|
116
|
+
const discoverable = mergeNativeToolDefinitions(tools, target.externalNames())
|
|
117
|
+
.filter(tool => target.isCallable(tool.name))
|
|
118
|
+
.map(tool => {
|
|
119
|
+
const schema = tool.parameters;
|
|
120
|
+
try {
|
|
121
|
+
if (isFunction(schema?.toJsonSchema)) return { ...tool, parameters: schema.toJsonSchema() };
|
|
122
|
+
if (schema && !schema.type && pi.zod?.toJSONSchema && (schema._zod || schema._def)) {
|
|
123
|
+
return { ...tool, parameters: pi.zod.toJSONSchema(schema, { io: "input" }) };
|
|
124
|
+
}
|
|
125
|
+
return tool;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
return { ...tool, parameters: undefined, schemaError: error.message };
|
|
128
|
+
}
|
|
129
|
+
});
|
|
124
130
|
catalog = buildCatalog(discoverable, config.excludeTools || []);
|
|
125
131
|
return catalog;
|
|
126
132
|
}
|
|
127
133
|
|
|
128
|
-
function makeNovaApi() {
|
|
134
|
+
function makeNovaApi(runBridge, runCatalog, cancel) {
|
|
129
135
|
return {
|
|
130
|
-
async
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return bridge.call(name, args);
|
|
141
|
-
},
|
|
142
|
-
async callMany(calls) {
|
|
143
|
-
return bridge.callMany(calls);
|
|
144
|
-
},
|
|
145
|
-
speculateBegin() {
|
|
146
|
-
bridge.beginSpeculation();
|
|
147
|
-
},
|
|
148
|
-
async speculateCommit() {
|
|
149
|
-
await bridge.commitSpeculation();
|
|
150
|
-
},
|
|
151
|
-
speculateRollback() {
|
|
152
|
-
bridge.rollbackSpeculation();
|
|
153
|
-
},
|
|
154
|
-
names() {
|
|
155
|
-
const cat = catalog.length ? catalog : refreshCatalog();
|
|
156
|
-
return [...new Set([...cat.map((t) => t.name), ...bridge.executors.keys(), ...Object.keys(bridge.natives)])];
|
|
157
|
-
},
|
|
136
|
+
search: async (query, limit) => searchCatalog(runCatalog, query, Number.isInteger(limit) ? limit : config.maxSearchResults),
|
|
137
|
+
describe: async (name) => describeTool(runCatalog, name),
|
|
138
|
+
call: (name, args) => runBridge.call(name, args),
|
|
139
|
+
callMany: (calls) => runBridge.callMany(calls),
|
|
140
|
+
speculateBegin: () => runBridge.beginSpeculation(),
|
|
141
|
+
speculateCommit: () => runBridge.commitSpeculation(),
|
|
142
|
+
speculateRollback: () => runBridge.rollbackSpeculation(),
|
|
143
|
+
names: () => runCatalog.map(tool => tool.name),
|
|
144
|
+
batchRead: runBridge.supportsBatchRead(),
|
|
145
|
+
cancel,
|
|
158
146
|
};
|
|
159
147
|
}
|
|
160
148
|
|
|
@@ -162,9 +150,9 @@ export default function piSupernova(pi) {
|
|
|
162
150
|
name: "supernova",
|
|
163
151
|
label: "Supernova",
|
|
164
152
|
description: TOOL_DESCRIPTION,
|
|
165
|
-
promptSnippet: "
|
|
153
|
+
promptSnippet: "Use read, write, edit, and bash in one program",
|
|
166
154
|
promptGuidelines: [
|
|
167
|
-
"Use
|
|
155
|
+
"Use read, write, edit, and bash inside supernova. Start with read(question), or read(directory, {about: question}) for scoped source selection. Read file bodies with read(file, {about: question}). Check source selection status before using its path. Return a compact value.",
|
|
168
156
|
],
|
|
169
157
|
parameters: Type.Object({
|
|
170
158
|
code: Type.String({ description: "JavaScript program: async body or arrow function." }),
|
|
@@ -177,70 +165,62 @@ export default function piSupernova(pi) {
|
|
|
177
165
|
renderCall: renderSupernovaCall,
|
|
178
166
|
renderResult: renderSupernovaResult,
|
|
179
167
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
180
|
-
|
|
168
|
+
const runCwd = ctx?.cwd || cwd;
|
|
181
169
|
const runController = new AbortController();
|
|
182
170
|
const abortRun = () => runController.abort(signal?.reason);
|
|
183
171
|
if (signal?.aborted) abortRun();
|
|
184
172
|
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
173
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd });
|
|
174
|
+
runBridge.bindCallContext(ctx, runController.signal);
|
|
175
|
+
runBridge.resetCallBudget();
|
|
185
176
|
|
|
186
|
-
bridge.bindCallContext(ctx, runController.signal);
|
|
187
|
-
bridge.resetCallBudget();
|
|
188
|
-
refreshCatalog();
|
|
189
|
-
bridge.beginSpeculation();
|
|
190
177
|
const call = ++programSeq;
|
|
191
|
-
|
|
178
|
+
runBridge.ledger.beginProgram(call);
|
|
192
179
|
const emitProgress = progressEmitter(onUpdate);
|
|
193
|
-
|
|
180
|
+
runBridge.setCallListener((_record, trace) => emitProgress(trace));
|
|
194
181
|
emitProgress([]);
|
|
195
|
-
|
|
182
|
+
const started = performance.now();
|
|
196
183
|
let outcome;
|
|
197
184
|
try {
|
|
198
|
-
|
|
185
|
+
const runCatalog = refreshCatalog(runBridge);
|
|
186
|
+
runBridge.beginSpeculation();
|
|
187
|
+
outcome = await runGuestProgram({
|
|
188
|
+
code: params?.code,
|
|
189
|
+
nova: makeNovaApi(runBridge, runCatalog, abortRun),
|
|
190
|
+
config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
191
|
+
signal: runController.signal,
|
|
192
|
+
onTimeout: abortRun,
|
|
193
|
+
});
|
|
194
|
+
runBridge.close();
|
|
195
|
+
if (outcome.ok) {
|
|
196
|
+
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished nova.speculate branch; await it before returning");
|
|
197
|
+
await runBridge.commitSpeculation();
|
|
198
|
+
}
|
|
199
|
+
else runBridge.rollbackSpeculation();
|
|
200
|
+
} catch (error) {
|
|
201
|
+
abortRun();
|
|
202
|
+
runBridge.close();
|
|
203
|
+
runBridge.rollbackSpeculation();
|
|
204
|
+
outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
|
|
199
205
|
} finally {
|
|
200
|
-
|
|
206
|
+
runBridge.setCallListener(null);
|
|
201
207
|
emitProgress.flush();
|
|
202
208
|
signal?.removeEventListener("abort", abortRun);
|
|
203
209
|
}
|
|
204
|
-
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
await bridge.commitSpeculation();
|
|
211
|
-
return result(bridge.ledger.dedupe(successText(outcome, call), call), {
|
|
212
|
-
ok: true,
|
|
213
|
-
wallMs: outcome.wallMs,
|
|
214
|
-
returnTruncated: outcome.returnTruncated,
|
|
215
|
-
logTruncated: outcome.logTruncated,
|
|
216
|
-
logs: outcome.logs,
|
|
217
|
-
result: outcome.result,
|
|
218
|
-
trace,
|
|
210
|
+
const trace = runBridge.getTrace();
|
|
211
|
+
const text = outcome.ok ? successText(outcome, call) : errorText(outcome, call);
|
|
212
|
+
return result(runBridge.ledger.dedupe(text, call), {
|
|
213
|
+
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
214
|
+
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
215
|
+
logs: outcome.logs, result: outcome.result, trace,
|
|
219
216
|
});
|
|
220
217
|
},
|
|
221
218
|
});
|
|
222
219
|
|
|
223
|
-
async function runProgram(params, signal, onTimeout) {
|
|
224
|
-
const runStartedAt = performance.now();
|
|
225
|
-
const runConfig = {
|
|
226
|
-
...config,
|
|
227
|
-
timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs,
|
|
228
|
-
};
|
|
229
|
-
try {
|
|
230
|
-
return await runGuestProgram({ code: String(params?.code || ""), nova: makeNovaApi(), config: runConfig, signal, onTimeout });
|
|
231
|
-
} catch (error) {
|
|
232
|
-
return {
|
|
233
|
-
ok: false,
|
|
234
|
-
error: error instanceof Error ? error.message : String(error),
|
|
235
|
-
logs: [],
|
|
236
|
-
wallMs: Math.round(performance.now() - runStartedAt),
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
220
|
pi.on("session_start", (_event, ctx) => {
|
|
242
221
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
243
222
|
// A new session is a new model context: nothing has been seen yet.
|
|
223
|
+
bridge.bindCallContext(ctx);
|
|
244
224
|
bridge.ledger.reset();
|
|
245
225
|
programSeq = 0;
|
|
246
226
|
refreshCatalog();
|
|
@@ -248,14 +228,15 @@ export default function piSupernova(pi) {
|
|
|
248
228
|
});
|
|
249
229
|
|
|
250
230
|
pi.registerCommand("supernova", {
|
|
251
|
-
description: "Show pi-supernova status (
|
|
231
|
+
description: "Show pi-supernova status (callable tools and session statistics)",
|
|
252
232
|
handler: async (_args, ctx) => {
|
|
233
|
+
bridge.bindCallContext(ctx);
|
|
253
234
|
refreshCatalog();
|
|
254
|
-
const
|
|
255
|
-
const natives = Object.keys(bridge.natives).sort();
|
|
235
|
+
const external = bridge.externalNames().filter(bridge.isCallable).sort();
|
|
236
|
+
const natives = Object.keys(bridge.natives).filter(name => bridge.isCallable(name) && !external.includes(name)).sort();
|
|
256
237
|
const lines = [
|
|
257
238
|
`pi-supernova catalog: ${catalog.length} tools`,
|
|
258
|
-
`
|
|
239
|
+
`external tools: ${external.length ? external.join(", ") : "(none)"}`,
|
|
259
240
|
`native adapters: ${natives.join(", ")}`,
|
|
260
241
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
261
242
|
sessionStats(bridge.ledger.stats),
|