pi-supernova 0.3.2 → 0.5.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/README.md +148 -29
- package/docs/CHANGELOG.md +52 -0
- package/docs/TOKEN_COSTS.md +173 -0
- package/index.js +155 -41
- package/package.json +3 -1
- package/src/bridge/catalog.js +35 -1
- package/src/bridge/host-bridge.js +411 -50
- package/src/bridge/native-tools.js +35 -1
- package/src/config/config.default.json +1 -1
- package/src/config/config.js +19 -0
- package/src/context/evidence.js +119 -5
- package/src/context/fuzzy.js +37 -0
- package/src/context/ledger.js +128 -6
- package/src/context/outline.js +24 -0
- package/src/context/repo-index.js +102 -4
- package/src/context/search.js +45 -0
- package/src/context/snap.js +118 -6
- package/src/context/spans.js +39 -0
- package/src/context/surface.js +33 -6
- package/src/fs/check.js +50 -0
- package/src/fs/diff.js +16 -0
- package/src/fs/json-read.js +91 -0
- package/src/fs/patch.js +26 -0
- package/src/fs/vfs.js +102 -5
- package/src/fs/workspace.js +62 -3
- package/src/output/bottleneck.js +63 -4
- package/src/output/format.js +136 -17
- package/src/runtime/guest-worker.js +169 -31
- package/src/runtime/parallel.js +33 -0
- package/src/runtime/program-batch.js +112 -0
- package/src/runtime/program-file.js +40 -0
- package/src/runtime/reference.js +18 -0
- package/src/runtime/runtime.js +90 -8
- package/src/shared/decode.js +40 -0
- package/src/ui/omp-frame.js +30 -1
- package/src/ui/render-measure.js +24 -0
- package/src/ui/render.js +96 -2
package/index.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { runProgramBatch } from "./src/runtime/program-batch.js";
|
|
3
|
+
import { REFERENCE } from "./src/runtime/reference.js";
|
|
2
4
|
import { isString, isFunction } from "./src/shared/decode.js";
|
|
3
5
|
import { loadConfig } from "./src/config/config.js";
|
|
4
6
|
import { createHostBridge } from "./src/bridge/host-bridge.js";
|
|
5
|
-
import { truncateChars } from "./src/output/format.js";
|
|
7
|
+
import { truncateChars, formatBoundedStringArray } from "./src/output/format.js";
|
|
6
8
|
import { runGuestProgram, warmGuestWorker, stopWarmGuestWorker } from "./src/runtime/runtime.js";
|
|
7
9
|
import { renderSupernovaCall, renderSupernovaResult } from "./src/ui/render.js";
|
|
8
10
|
|
|
@@ -10,13 +12,17 @@ export { renderSupernovaCall, renderSupernovaResult };
|
|
|
10
12
|
|
|
11
13
|
// Sync only, never top-level await. Dynamic import of host/deps hung OMP plugin load.
|
|
12
14
|
const require = createRequire(import.meta.url);
|
|
15
|
+
|
|
13
16
|
let Type;
|
|
17
|
+
|
|
14
18
|
try {
|
|
15
19
|
Type = require("typebox").Type;
|
|
16
20
|
} catch {
|
|
17
21
|
Type = {
|
|
18
22
|
Object: (props, opts) => ({ type: "object", properties: props || {}, additionalProperties: false, ...opts }),
|
|
19
23
|
String: (opts) => ({ type: "string", ...opts }),
|
|
24
|
+
Unknown: (opts) => ({ ...opts }),
|
|
25
|
+
Array: (items, opts) => ({ type: "array", items, ...opts }),
|
|
20
26
|
Integer: (opts) => ({ type: "integer", ...opts }),
|
|
21
27
|
Optional: (s) => ({ ...s }),
|
|
22
28
|
};
|
|
@@ -38,30 +44,38 @@ export function progressEmitter(onUpdate) {
|
|
|
38
44
|
let pending = null;
|
|
39
45
|
let timer = null;
|
|
40
46
|
let lastSent = -Infinity;
|
|
47
|
+
|
|
41
48
|
const send = () => {
|
|
42
49
|
timer = null;
|
|
50
|
+
|
|
43
51
|
if (pending === null) return;
|
|
44
52
|
// Snapshot only at emission, not on every tool event. Completed records must
|
|
45
53
|
// not mutate a previously emitted frame while Pi is still consuming it.
|
|
46
54
|
const trace = pending.map(record => ({ ...record }));
|
|
47
55
|
pending = null;
|
|
48
56
|
lastSent = performance.now();
|
|
57
|
+
|
|
49
58
|
try {
|
|
50
59
|
onUpdate({ content: [{ type: "text", text: "" }], details: { trace, running: true } });
|
|
51
60
|
} catch {}
|
|
52
61
|
};
|
|
62
|
+
|
|
53
63
|
const emit = (trace) => {
|
|
54
64
|
pending = trace;
|
|
65
|
+
|
|
55
66
|
if (timer !== null) return;
|
|
56
67
|
const wait = PROGRESS_FRAME_MS - (performance.now() - lastSent);
|
|
68
|
+
|
|
57
69
|
if (wait <= 0) send();
|
|
58
70
|
else timer = setTimeout(send, wait);
|
|
59
71
|
};
|
|
72
|
+
|
|
60
73
|
emit.flush = () => {
|
|
61
74
|
if (timer !== null) clearTimeout(timer);
|
|
62
75
|
pending = null;
|
|
63
76
|
timer = null;
|
|
64
77
|
};
|
|
78
|
+
|
|
65
79
|
return emit;
|
|
66
80
|
}
|
|
67
81
|
|
|
@@ -70,36 +84,70 @@ function sessionStats({ programs, returnedChars }) {
|
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
function logsBlock(outcome, tail = "") {
|
|
73
|
-
|
|
87
|
+
if (!outcome.logs?.length && !outcome.logTruncated) return "";
|
|
88
|
+
|
|
89
|
+
return `\n--- logs${outcome.logTruncated ? " [logs truncated]" : ""}\n${outcome.logs?.join("\n") ?? ""}${tail}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function mutationText(outcome) {
|
|
93
|
+
const m = outcome.mutations;
|
|
94
|
+
|
|
95
|
+
if (!m) return "";
|
|
96
|
+
const external = m.external ? "; external calls attempted=" + m.external + ", their side effects cannot be rolled back" : "";
|
|
97
|
+
const uncertain = m.pendingCommits || m.recoveryFailed ? "; filesystem outcome uncertain: inspect disk and any recovery backups before retrying" : "";
|
|
98
|
+
|
|
99
|
+
return "\nmutations: committed=" + m.committed + " rolledBack=" + m.rolledBack + " (file versions)" + external + uncertain;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function mutationReceipts(trace) {
|
|
103
|
+
if (!Array.isArray(trace)) return "";
|
|
104
|
+
|
|
105
|
+
return trace
|
|
106
|
+
.filter(row => row?.ok && (row.name === "write" || row.name === "edit") && isString(row.resultText) && row.resultText)
|
|
107
|
+
.map(row => row.resultText)
|
|
108
|
+
.join("\n");
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Corrective hint, emitted only when a turn actually split. Independent work
|
|
112
|
+
// belongs in one program: a split cannot use the single prewarmed worker and pays
|
|
113
|
+
// one extra spawn per sibling. Costs nothing until it fires, so it needs no room in
|
|
114
|
+
// the tool definition.
|
|
115
|
+
function splitTurnHint(outcome) {
|
|
116
|
+
return outcome.overlappedTurn ? ` (${outcome.overlappedTurn} supernova calls ran at once; independent work belongs in one program)` : "";
|
|
74
117
|
}
|
|
75
118
|
|
|
76
119
|
function errorText(outcome, call) {
|
|
77
|
-
return `error #${call} ${outcome.wallMs}ms: ${outcome
|
|
120
|
+
return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome)}${splitTurnHint(outcome)}
|
|
121
|
+
error: ${outcome.error}${logsBlock(outcome)}`;
|
|
78
122
|
}
|
|
79
123
|
|
|
80
124
|
function successText(outcome, call) {
|
|
81
125
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
82
126
|
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
83
|
-
|
|
127
|
+
|
|
128
|
+
return `ok #${call} ${outcome.wallMs}ms${truncated}${outcome.mutations?.committed || outcome.mutations?.rolledBack || outcome.mutations?.external ? mutationText(outcome) : ""}${splitTurnHint(outcome)}${logsBlock(outcome, "\n--- result")}\n${outcome.resultText}${hint}`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function fitOutput(outcome, call, limit, format) {
|
|
132
|
+
let text = format(outcome, call);
|
|
133
|
+
|
|
134
|
+
if (text.length <= limit) return text;
|
|
135
|
+
outcome.returnTruncated = true;
|
|
136
|
+
const wrapper = format({ ...outcome, resultText: "", logs: [] }, call);
|
|
137
|
+
const room = Math.max(256, limit - wrapper.length);
|
|
138
|
+
|
|
139
|
+
if (Array.isArray(outcome.result) && outcome.result.length && outcome.result.every(isString)) {
|
|
140
|
+
outcome.resultText = formatBoundedStringArray(outcome.result, room);
|
|
141
|
+
} else if (isString(outcome.resultText) && outcome.resultText.length > room) {
|
|
142
|
+
outcome.resultText = truncateChars(outcome.resultText, room, "output").text;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
text = format(outcome, call);
|
|
146
|
+
|
|
147
|
+
return text.length <= limit ? text : truncateChars(text, limit, "output").text;
|
|
84
148
|
}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
Native commands (async):
|
|
88
|
-
read(path|paths, offset?, limit?) → file text or text[]; read(directory) → directory entries
|
|
89
|
-
read("symbol or question") → locate and open source in one call, without an index; selected file text stays raw
|
|
90
|
-
read({query, resolve:true}) → {status,path,line,lines,text,complete,nextOffset?} for a direct resolve→edit handoff
|
|
91
|
-
read(path, {about: question}) → relevant file bodies, or source selection inside a directory
|
|
92
|
-
read({query, evidence:true}) → ranked evidence; read({path, outline:true}) → structural declarations
|
|
93
|
-
write(path, text) → write a file; write({path,content,append:true}) appends a chunk without a bounded read
|
|
94
|
-
edit(path, oldText, newText) → post-edit lines, checks, and references
|
|
95
|
-
edit(async () => {...}) → filesystem checkpoint: commit on success, rollback on throw; no shell commands, nesting, or concurrent outside commands
|
|
96
|
-
bash(command, {cwd?, timeoutMs?}) → bounded output; throws on non-zero exit
|
|
97
|
-
bash({command, args:[...]}) → literal argv without shell expansion of arguments
|
|
98
|
-
|
|
99
|
-
Only found selects and opens a file. Uncertain reads return ambiguous, not_found, or incomplete with no selected path. Use resolve:true for structured status checks; narrow the directory with path+about when uncertain.
|
|
100
|
-
For read-modify-write, use read({path,complete:true}); it rejects partial output. Prefer edit for large files. Array reads reject failures; use Promise.allSettled for per-path outcomes.
|
|
101
|
-
Object arguments also work: read({path, offset?, limit?, about?, outline?, evidence?, resolve?, complete?}), edit({path, edits:[{oldText,newText}]}), edit({path,patch}), write({path,content}), bash({command,timeoutMs?}).
|
|
102
|
-
Independent read starts batch automatically. Mutations preserve submission order. Plain reads remain self-contained; oversized reads provide continuation offsets. Return only what the model needs. console.log is captured.`;
|
|
149
|
+
|
|
150
|
+
const TOOL_DESCRIPTION = REFERENCE;
|
|
103
151
|
|
|
104
152
|
export default function piSupernova(pi) {
|
|
105
153
|
registerCodeMode(pi);
|
|
@@ -107,12 +155,23 @@ export default function piSupernova(pi) {
|
|
|
107
155
|
|
|
108
156
|
// Shared entry used by both host adapters and direct engine integration.
|
|
109
157
|
export function registerCodeMode(pi) {
|
|
110
|
-
//
|
|
111
|
-
|
|
158
|
+
// Citation elision is experimental and disabled by default. A context event is
|
|
159
|
+
// not the final provider payload: hidden details or later transforms can invalidate
|
|
160
|
+
// a citation. A positive seenWindow explicitly opts in despite those limitations.
|
|
161
|
+
const config = loadConfig();
|
|
112
162
|
let cwd = process.cwd();
|
|
113
163
|
let programSeq = 0;
|
|
114
164
|
let stopped = false;
|
|
115
165
|
let warmTimer;
|
|
166
|
+
// Program runs currently executing. Only one pristine worker is ever prewarmed,
|
|
167
|
+
// so concurrent invocations cannot share it and each sibling pays a fresh spawn.
|
|
168
|
+
// Counting them lets a result say so without adding standing guidance to the
|
|
169
|
+
// tool definition, which is resent on every request.
|
|
170
|
+
let inFlight = 0;
|
|
171
|
+
// Peak concurrent execute() bodies in the current wave. Start-order or
|
|
172
|
+
// finish-order alone cannot see a first-started call that finishes last.
|
|
173
|
+
let overlapPeak = 0;
|
|
174
|
+
|
|
116
175
|
function cancelWarmTimer() {
|
|
117
176
|
if (warmTimer !== undefined) clearImmediate(warmTimer);
|
|
118
177
|
warmTimer = undefined;
|
|
@@ -148,28 +207,34 @@ export function registerCodeMode(pi) {
|
|
|
148
207
|
name: "supernova",
|
|
149
208
|
label: "Supernova",
|
|
150
209
|
description: TOOL_DESCRIPTION,
|
|
151
|
-
promptSnippet: "
|
|
152
|
-
promptGuidelines: [
|
|
153
|
-
"Use read, write, edit, and bash inside supernova. Start with read(question), or read(directory, {about: question}) for scoped source selection. A source question already opens the selected file; do not issue a redundant read. Use read({query,resolve:true}) and check status before editing its path. Explicit about/outline/evidence reads remain available when needed. Return a compact value.",
|
|
154
|
-
],
|
|
210
|
+
promptSnippet: "read, write, edit, bash",
|
|
155
211
|
parameters: Type.Object({
|
|
156
|
-
code: Type.String({ maxLength: config.maxCodeChars ?? 48000
|
|
157
|
-
|
|
158
|
-
|
|
212
|
+
code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
|
|
213
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
214
|
+
data: Type.Optional(Type.Unknown()),
|
|
215
|
+
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000 })),
|
|
216
|
+
programs: Type.Optional(Type.Array(Type.Object({
|
|
217
|
+
code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
|
|
218
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
219
|
+
data: Type.Optional(Type.Unknown()),
|
|
220
|
+
}, {additionalProperties:false}), {minItems:1,maxItems:32})),
|
|
221
|
+
}),
|
|
159
222
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
160
223
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
161
224
|
renderShell: "self",
|
|
162
225
|
mergeCallAndResult: true,
|
|
163
226
|
renderCall: renderSupernovaCall,
|
|
164
227
|
renderResult: renderSupernovaResult,
|
|
165
|
-
async execute(_id, params, signal, onUpdate, ctx) {
|
|
228
|
+
execute: async function execute(_id, params, signal, onUpdate, ctx, budget) {
|
|
229
|
+
if (params?.programs !== undefined) return runProgramBatch(_id,params,signal,onUpdate,ctx,config,execute);
|
|
166
230
|
cancelWarmTimer();
|
|
167
231
|
const runCwd = ctx?.cwd || cwd;
|
|
168
232
|
const runController = new AbortController();
|
|
169
233
|
const abortRun = () => runController.abort(signal?.reason);
|
|
234
|
+
|
|
170
235
|
if (signal?.aborted) abortRun();
|
|
171
236
|
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
172
|
-
const runBridge = bridge.fork({ getCwd: () => runCwd });
|
|
237
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
|
|
173
238
|
runBridge.bindCallContext(ctx, runController.signal);
|
|
174
239
|
runBridge.resetCallBudget();
|
|
175
240
|
|
|
@@ -180,61 +245,108 @@ export function registerCodeMode(pi) {
|
|
|
180
245
|
emitProgress([]);
|
|
181
246
|
const started = performance.now();
|
|
182
247
|
let outcome;
|
|
248
|
+
inFlight += 1;
|
|
249
|
+
overlapPeak = Math.max(overlapPeak, inFlight);
|
|
250
|
+
let peakSeen = overlapPeak;
|
|
251
|
+
|
|
183
252
|
try {
|
|
184
253
|
refreshCatalog(runBridge);
|
|
185
254
|
runBridge.beginSpeculation();
|
|
186
255
|
outcome = await runGuestProgram({
|
|
187
256
|
code: params?.code,
|
|
257
|
+
file: params?.file,
|
|
258
|
+
cwd: runCwd,
|
|
259
|
+
data: params?.data,
|
|
188
260
|
nova: makeNovaApi(runBridge, abortRun),
|
|
189
|
-
config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
261
|
+
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
190
262
|
signal: runController.signal,
|
|
191
263
|
onTimeout: abortRun,
|
|
192
264
|
});
|
|
193
265
|
runBridge.close();
|
|
266
|
+
|
|
194
267
|
if (outcome.ok) {
|
|
195
268
|
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
196
269
|
await runBridge.commitSpeculation();
|
|
197
270
|
}
|
|
198
|
-
else runBridge.rollbackSpeculation();
|
|
271
|
+
else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
199
272
|
} catch (error) {
|
|
200
273
|
abortRun();
|
|
201
274
|
runBridge.close();
|
|
202
|
-
|
|
275
|
+
|
|
276
|
+
while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
203
277
|
outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
|
|
204
278
|
} finally {
|
|
279
|
+
peakSeen = Math.max(peakSeen, overlapPeak);
|
|
280
|
+
inFlight -= 1;
|
|
281
|
+
if (inFlight === 0) overlapPeak = 0;
|
|
205
282
|
runBridge.setCallListener(null);
|
|
206
283
|
emitProgress.flush();
|
|
207
284
|
signal?.removeEventListener("abort", abortRun);
|
|
208
285
|
// Prepare one pristine worker during the model's next decision. Never
|
|
209
286
|
// recycle a worker that has executed arbitrary guest JavaScript.
|
|
210
287
|
cancelWarmTimer();
|
|
288
|
+
|
|
211
289
|
if (!stopped && !runController.signal.aborted) {
|
|
212
290
|
// Deliver the result before paying for another Worker constructor.
|
|
213
291
|
warmTimer = setImmediate(() => {
|
|
214
292
|
warmTimer = undefined;
|
|
293
|
+
|
|
215
294
|
if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
|
|
216
295
|
});
|
|
217
296
|
warmTimer.unref?.();
|
|
218
297
|
}
|
|
219
298
|
}
|
|
299
|
+
|
|
300
|
+
if (budget) budget.logLines += outcome.logs?.length ?? 0;
|
|
301
|
+
outcome.overlappedTurn = peakSeen > 1 ? peakSeen : 0;
|
|
302
|
+
outcome.mutations = runBridge.getMutations();
|
|
220
303
|
const trace = runBridge.getTrace();
|
|
221
|
-
|
|
222
|
-
|
|
304
|
+
|
|
305
|
+
if (outcome.ok && outcome.result === undefined) {
|
|
306
|
+
const receipts = mutationReceipts(trace);
|
|
307
|
+
|
|
308
|
+
if (receipts) {
|
|
309
|
+
outcome.resultText = receipts;
|
|
310
|
+
outcome.undefinedReturn = false;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const format = outcome.ok ? successText : errorText;
|
|
314
|
+
const bounded = fitOutput(outcome, call, config.maxReturnChars, format);
|
|
223
315
|
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
224
|
-
|
|
316
|
+
|
|
225
317
|
const response = result(visible, {
|
|
226
318
|
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
227
319
|
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
228
|
-
logs: outcome.logs, result: outcome.result, trace,
|
|
320
|
+
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
|
|
229
321
|
});
|
|
322
|
+
|
|
230
323
|
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
324
|
+
|
|
325
|
+
if (!outcome.ok) {
|
|
326
|
+
const error = new Error(visible);
|
|
327
|
+
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
328
|
+
throw error;
|
|
329
|
+
}
|
|
330
|
+
|
|
231
331
|
return response;
|
|
232
332
|
},
|
|
233
333
|
});
|
|
234
334
|
|
|
235
|
-
|
|
335
|
+
// This is a pre-conversion observation, not a final-payload retention proof.
|
|
336
|
+
// Shipping seenWindow:0 must not subscribe: a no-op listener still runs on every
|
|
337
|
+
// provider context event. Opt-in windows register here.
|
|
338
|
+
if ((config.seenWindow ?? 0) > 0) {
|
|
339
|
+
pi.on("context", event => {
|
|
340
|
+
try { bridge.ledger.observe(event?.messages); } catch {}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
pi.on("session_shutdown", () => { stopped = true; cancelWarmTimer();
|
|
345
|
+
|
|
346
|
+
return stopWarmGuestWorker(); });
|
|
236
347
|
pi.on("session_start", (_event, ctx) => {
|
|
237
348
|
stopped = false;
|
|
349
|
+
|
|
238
350
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
239
351
|
// A new session is a new model context: nothing has been seen yet.
|
|
240
352
|
bridge.bindCallContext(ctx);
|
|
@@ -250,11 +362,13 @@ export function registerCodeMode(pi) {
|
|
|
250
362
|
bridge.bindCallContext(ctx);
|
|
251
363
|
refreshCatalog();
|
|
252
364
|
const commands = ["read", "edit", "write", "bash"].filter(bridge.isCallable);
|
|
365
|
+
|
|
253
366
|
const lines = [
|
|
254
367
|
`Supernova CodeMode: ${commands.join(", ")}`,
|
|
255
368
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
256
369
|
sessionStats(bridge.ledger.stats),
|
|
257
370
|
];
|
|
371
|
+
|
|
258
372
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
259
373
|
},
|
|
260
374
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "One CodeMode invocation for Pi and OMP, with four guest commands, automatic read batching and source context.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AdityaVG13",
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"test": "node --test tests/*/*.test.mjs",
|
|
34
34
|
"test:hosts": "node tests/hosts/verify.mjs",
|
|
35
35
|
"measure": "node tests/efficiency/measure.mjs",
|
|
36
|
+
"test:tokens": "node tests/efficiency/tokens.mjs",
|
|
36
37
|
"prepublishOnly": "npm test && node ../../scripts/preflight.mjs"
|
|
37
38
|
},
|
|
38
39
|
"pi": {
|
|
@@ -58,6 +59,7 @@
|
|
|
58
59
|
}
|
|
59
60
|
},
|
|
60
61
|
"devDependencies": {
|
|
62
|
+
"js-tiktoken": "1.0.21",
|
|
61
63
|
"typebox": "^1.0.0"
|
|
62
64
|
},
|
|
63
65
|
"publishConfig": {
|
package/src/bridge/catalog.js
CHANGED
|
@@ -4,7 +4,7 @@ import { isString, isObject } from "../shared/decode.js";
|
|
|
4
4
|
const NATIVE_TOOL_DEFINITIONS = [
|
|
5
5
|
{
|
|
6
6
|
name: "read",
|
|
7
|
-
description: "Read files or directories. Source questions locate and open source directly; resolve returns structured source/status without guessing.",
|
|
7
|
+
description: "Read files, images or directories. JSON selectors project full documents within output budgets. Source questions locate and open source directly; resolve returns structured source/status without guessing.",
|
|
8
8
|
parameters: { type: "object", properties: {
|
|
9
9
|
path: { anyOf: [{ type: "string" }, { type: "array", items: { type: "string" } }], description: "Workspace-relative file or directory, source question, or array of paths" },
|
|
10
10
|
target: { anyOf: [{ type: "string" }, { type: "array" }], description: "File path/query or array of paths" },
|
|
@@ -14,6 +14,7 @@ const NATIVE_TOOL_DEFINITIONS = [
|
|
|
14
14
|
query: { type: "string", description: "Source question; optional path scopes the search directory" },
|
|
15
15
|
resolve: { type: "boolean", description: "Return structured source/status for a direct resolve-to-edit handoff" },
|
|
16
16
|
complete: { type: "boolean", description: "Fail unless the entire requested file fits without clipping" },
|
|
17
|
+
json: { anyOf: [{ type: "boolean" }, { type: "string" }, { type: "array", items: { type: "string" } }], description: "Parse the complete JSON input (up to 16 MiB), then select .field, .items[0:3], or quoted keys. A selector array returns an array of values; true selects the root. Oversized selections fail, never clip." },
|
|
17
18
|
} },
|
|
18
19
|
},
|
|
19
20
|
{
|
|
@@ -85,28 +86,36 @@ export function mergeNativeToolDefinitions(tools, capturedNames = []) {
|
|
|
85
86
|
merged.push(fallback && !captured.has(tool.name)
|
|
86
87
|
? { ...tool, ...fallback, sourceInfo: { path: "<native:" + tool.name + ">" } }
|
|
87
88
|
: tool);
|
|
89
|
+
|
|
88
90
|
if (tool?.name) seen.add(tool.name);
|
|
89
91
|
}
|
|
92
|
+
|
|
90
93
|
for (const fallback of NATIVE_TOOL_DEFINITIONS) {
|
|
91
94
|
if (!seen.has(fallback.name)) {
|
|
92
95
|
merged.push({ ...fallback, sourceInfo: { path: "<native:" + fallback.name + ">" } });
|
|
93
96
|
}
|
|
94
97
|
}
|
|
98
|
+
|
|
95
99
|
return merged;
|
|
96
100
|
}
|
|
97
101
|
|
|
98
102
|
function sourcePathOf(tool) {
|
|
99
103
|
if (tool.sourceInfo && isString(tool.sourceInfo.path)) return tool.sourceInfo.path;
|
|
104
|
+
|
|
100
105
|
if (isString(tool.extensionPath)) return tool.extensionPath;
|
|
106
|
+
|
|
101
107
|
if (isString(tool.sourcePath)) return tool.sourcePath;
|
|
108
|
+
|
|
102
109
|
return undefined;
|
|
103
110
|
}
|
|
104
111
|
|
|
105
112
|
function normalizeTool(tool) {
|
|
106
113
|
if (!tool || !isObject(tool)) return null;
|
|
107
114
|
const name = isString(tool.name) ? tool.name : "";
|
|
115
|
+
|
|
108
116
|
if (!name) return null;
|
|
109
117
|
const description = isString(tool.description) ? tool.description : "";
|
|
118
|
+
|
|
110
119
|
return {
|
|
111
120
|
name,
|
|
112
121
|
nameLower: name.toLowerCase(),
|
|
@@ -121,12 +130,16 @@ function normalizeTool(tool) {
|
|
|
121
130
|
export function buildCatalog(tools, excludeNames = []) {
|
|
122
131
|
const exclude = new Set(excludeNames);
|
|
123
132
|
const rows = [];
|
|
133
|
+
|
|
124
134
|
for (const tool of tools || []) {
|
|
125
135
|
const row = normalizeTool(tool);
|
|
136
|
+
|
|
126
137
|
if (!row || exclude.has(row.name)) continue;
|
|
127
138
|
rows.push(row);
|
|
128
139
|
}
|
|
140
|
+
|
|
129
141
|
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
|
+
|
|
130
143
|
return rows;
|
|
131
144
|
}
|
|
132
145
|
|
|
@@ -142,74 +155,94 @@ function scoreRow(row, tokens) {
|
|
|
142
155
|
const name = row.nameLower || row.name.toLowerCase();
|
|
143
156
|
const desc = row.descLower || row.description.toLowerCase();
|
|
144
157
|
let score = 0;
|
|
158
|
+
|
|
145
159
|
for (const token of tokens) {
|
|
146
160
|
if (name === token) score += 10;
|
|
147
161
|
else if (name.includes(token)) score += 5;
|
|
148
162
|
else if (desc.includes(token)) score += 2;
|
|
149
163
|
}
|
|
164
|
+
|
|
150
165
|
return score;
|
|
151
166
|
}
|
|
152
167
|
|
|
153
168
|
export function searchCatalog(catalog, query, limit = 12) {
|
|
154
169
|
const tokens = tokenize(query);
|
|
155
170
|
const scored = [];
|
|
171
|
+
|
|
156
172
|
for (const row of catalog) {
|
|
157
173
|
const score = scoreRow(row, tokens);
|
|
174
|
+
|
|
158
175
|
if (score <= 0 && tokens.length > 0) continue;
|
|
159
176
|
scored.push({ name: row.name, description: row.description.slice(0, 160), score });
|
|
160
177
|
}
|
|
178
|
+
|
|
161
179
|
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
180
|
+
|
|
162
181
|
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
163
182
|
}
|
|
164
183
|
|
|
165
184
|
/** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
|
|
166
185
|
function editDistance(a, b) {
|
|
167
186
|
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
|
|
187
|
+
|
|
168
188
|
for (let j = 1; j <= b.length; j++) rows[0][j] = j;
|
|
189
|
+
|
|
169
190
|
for (let i = 1; i <= a.length; i++) {
|
|
170
191
|
for (let j = 1; j <= b.length; j++) {
|
|
171
192
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
172
193
|
let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
194
|
+
|
|
173
195
|
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
174
196
|
rows[i][j] = best;
|
|
175
197
|
}
|
|
176
198
|
}
|
|
199
|
+
|
|
177
200
|
return rows[a.length][b.length];
|
|
178
201
|
}
|
|
179
202
|
|
|
180
203
|
/** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
|
|
181
204
|
function suggestNames(name, candidates, limit = 3) {
|
|
182
205
|
const needle = String(name || "").toLowerCase();
|
|
206
|
+
|
|
183
207
|
if (!needle) return [];
|
|
184
208
|
const maxDistance = Math.max(1, Math.floor(needle.length / 3));
|
|
185
209
|
const scored = [];
|
|
210
|
+
|
|
186
211
|
for (const candidate of candidates) {
|
|
187
212
|
const lower = candidate.toLowerCase();
|
|
213
|
+
|
|
188
214
|
if (lower === needle) continue;
|
|
189
215
|
const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
|
|
216
|
+
|
|
190
217
|
if (distance <= maxDistance) scored.push({ candidate, distance });
|
|
191
218
|
}
|
|
219
|
+
|
|
192
220
|
scored.sort(
|
|
193
221
|
(a, b) =>
|
|
194
222
|
a.distance - b.distance ||
|
|
195
223
|
Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
|
|
196
224
|
a.candidate.localeCompare(b.candidate),
|
|
197
225
|
);
|
|
226
|
+
|
|
198
227
|
return scored.slice(0, limit).map((s) => s.candidate);
|
|
199
228
|
}
|
|
200
229
|
|
|
201
230
|
export function unknownToolMessage(name, candidates) {
|
|
202
231
|
const close = suggestNames(name, candidates);
|
|
203
232
|
const hint = close.length ? ` Did you mean ${close.map((c) => JSON.stringify(c)).join(", ")}?` : "";
|
|
233
|
+
|
|
204
234
|
return `unknown tool "${name}".${hint} Check the command name and configured tool exclusions.`;
|
|
205
235
|
}
|
|
206
236
|
|
|
207
237
|
export function describeTool(catalog, name) {
|
|
208
238
|
const row = catalog.find((t) => t.name === name);
|
|
239
|
+
|
|
209
240
|
if (!row) {
|
|
210
241
|
return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
|
|
211
242
|
}
|
|
243
|
+
|
|
212
244
|
if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
|
|
245
|
+
|
|
213
246
|
if (!row._described) {
|
|
214
247
|
row._described = {
|
|
215
248
|
ok: true,
|
|
@@ -219,5 +252,6 @@ export function describeTool(catalog, name) {
|
|
|
219
252
|
sourcePath: row.sourcePath,
|
|
220
253
|
};
|
|
221
254
|
}
|
|
255
|
+
|
|
222
256
|
return row._described;
|
|
223
257
|
}
|