pi-supernova 0.3.1 → 0.4.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 +220 -3
- package/docs/CHANGELOG.md +40 -0
- package/docs/TOKEN_COSTS.md +171 -0
- package/index.js +116 -37
- package/package.json +3 -1
- package/src/bridge/catalog.js +37 -2
- package/src/bridge/host-bridge.js +340 -13
- 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 +45 -1
- package/src/context/repo-index.js +63 -1
- package/src/context/search.js +45 -0
- package/src/context/snap.js +80 -4
- package/src/context/surface.js +25 -0
- package/src/fs/check.js +50 -0
- package/src/fs/diff.js +16 -0
- package/src/fs/json-read.js +87 -0
- package/src/fs/patch.js +26 -0
- package/src/fs/vfs.js +101 -6
- package/src/fs/workspace.js +36 -0
- package/src/output/bottleneck.js +46 -0
- package/src/output/format.js +117 -17
- package/src/runtime/guest-worker.js +151 -18
- 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 +25 -0
- package/src/runtime/runtime.js +90 -6
- package/src/shared/decode.js +29 -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,4 +1,6 @@
|
|
|
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";
|
|
@@ -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,35 +84,42 @@ 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
|
+
// Corrective hint, emitted only when a turn actually split. Independent work
|
|
103
|
+
// belongs in one program: a split cannot use the single prewarmed worker and pays
|
|
104
|
+
// one extra spawn per sibling. Costs nothing until it fires, so it needs no room in
|
|
105
|
+
// the tool definition.
|
|
106
|
+
function splitTurnHint(outcome) {
|
|
107
|
+
return outcome.overlappedTurn ? ` (${outcome.overlappedTurn} supernova calls ran at once; independent work belongs in one program)` : "";
|
|
74
108
|
}
|
|
75
109
|
|
|
76
110
|
function errorText(outcome, call) {
|
|
77
|
-
return `error #${call} ${outcome.wallMs}ms: ${outcome
|
|
111
|
+
return `error #${call} ${outcome.wallMs}ms${outcome.returnTruncated ? " [output truncated]" : ""}${mutationText(outcome)}${splitTurnHint(outcome)}
|
|
112
|
+
error: ${outcome.error}${logsBlock(outcome)}`;
|
|
78
113
|
}
|
|
79
114
|
|
|
80
115
|
function successText(outcome, call) {
|
|
81
116
|
const truncated = outcome.returnTruncated ? " [return truncated]" : "";
|
|
82
117
|
const hint = outcome.undefinedReturn ? " (no return statement; add `return` to get a value)" : "";
|
|
83
|
-
|
|
118
|
+
|
|
119
|
+
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}`;
|
|
84
120
|
}
|
|
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
|
|
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
|
-
Object arguments also work: read({path, offset?, limit?, about?, outline?, evidence?, resolve?}), edit({path, edits:[{oldText,newText}]}), edit({path,patch}), write({path,content}), bash({command,timeoutMs?}).
|
|
101
|
-
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.`;
|
|
121
|
+
|
|
122
|
+
const TOOL_DESCRIPTION = REFERENCE;
|
|
102
123
|
|
|
103
124
|
export default function piSupernova(pi) {
|
|
104
125
|
registerCodeMode(pi);
|
|
@@ -106,12 +127,20 @@ export default function piSupernova(pi) {
|
|
|
106
127
|
|
|
107
128
|
// Shared entry used by both host adapters and direct engine integration.
|
|
108
129
|
export function registerCodeMode(pi) {
|
|
109
|
-
//
|
|
110
|
-
|
|
130
|
+
// Citation elision is experimental and disabled by default. A context event is
|
|
131
|
+
// not the final provider payload: hidden details or later transforms can invalidate
|
|
132
|
+
// a citation. A positive seenWindow explicitly opts in despite those limitations.
|
|
133
|
+
const config = loadConfig();
|
|
111
134
|
let cwd = process.cwd();
|
|
112
135
|
let programSeq = 0;
|
|
113
136
|
let stopped = false;
|
|
114
137
|
let warmTimer;
|
|
138
|
+
// Program runs currently executing. Only one pristine worker is ever prewarmed,
|
|
139
|
+
// so concurrent invocations cannot share it and each sibling pays a fresh spawn.
|
|
140
|
+
// Counting them lets a result say so without adding standing guidance to the
|
|
141
|
+
// tool definition, which is resent on every request.
|
|
142
|
+
let inFlight = 0;
|
|
143
|
+
|
|
115
144
|
function cancelWarmTimer() {
|
|
116
145
|
if (warmTimer !== undefined) clearImmediate(warmTimer);
|
|
117
146
|
warmTimer = undefined;
|
|
@@ -147,28 +176,34 @@ export function registerCodeMode(pi) {
|
|
|
147
176
|
name: "supernova",
|
|
148
177
|
label: "Supernova",
|
|
149
178
|
description: TOOL_DESCRIPTION,
|
|
150
|
-
promptSnippet: "
|
|
151
|
-
promptGuidelines: [
|
|
152
|
-
"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.",
|
|
153
|
-
],
|
|
179
|
+
promptSnippet: "JavaScript with read, write, edit, and bash",
|
|
154
180
|
parameters: Type.Object({
|
|
155
|
-
code: Type.String({
|
|
181
|
+
code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
|
|
182
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
183
|
+
data: Type.Optional(Type.Unknown({ description: "Literal JSON input available as data in the program; put Markdown, scripts or argv here instead of nesting JavaScript quoting. JSON-encoded size is limited to the code character budget." })),
|
|
156
184
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1000, description: "Hard timeout in ms." })),
|
|
157
|
-
|
|
185
|
+
programs: Type.Optional(Type.Array(Type.Object({
|
|
186
|
+
code: Type.Optional(Type.String({ maxLength: config.maxCodeChars ?? 48000 })),
|
|
187
|
+
file: Type.Optional(Type.String({ minLength: 1 })),
|
|
188
|
+
data: Type.Optional(Type.Unknown()),
|
|
189
|
+
}, {additionalProperties:false}), {minItems:1,maxItems:32,description:"Instead of top-level code/file/data. JSON-encoded array shares the code character cap."})),
|
|
190
|
+
}),
|
|
158
191
|
// One self-owned result frame is shared by Pi and OMP; renderCall stays empty
|
|
159
192
|
// so separate call/result slots cannot duplicate the lifecycle card.
|
|
160
193
|
renderShell: "self",
|
|
161
194
|
mergeCallAndResult: true,
|
|
162
195
|
renderCall: renderSupernovaCall,
|
|
163
196
|
renderResult: renderSupernovaResult,
|
|
164
|
-
async execute(_id, params, signal, onUpdate, ctx) {
|
|
197
|
+
execute: async function execute(_id, params, signal, onUpdate, ctx, budget) {
|
|
198
|
+
if (params?.programs !== undefined) return runProgramBatch(_id,params,signal,onUpdate,ctx,config,execute);
|
|
165
199
|
cancelWarmTimer();
|
|
166
200
|
const runCwd = ctx?.cwd || cwd;
|
|
167
201
|
const runController = new AbortController();
|
|
168
202
|
const abortRun = () => runController.abort(signal?.reason);
|
|
203
|
+
|
|
169
204
|
if (signal?.aborted) abortRun();
|
|
170
205
|
else signal?.addEventListener("abort", abortRun, { once: true });
|
|
171
|
-
const runBridge = bridge.fork({ getCwd: () => runCwd });
|
|
206
|
+
const runBridge = bridge.fork({ getCwd: () => runCwd, budget });
|
|
172
207
|
runBridge.bindCallContext(ctx, runController.signal);
|
|
173
208
|
runBridge.resetCallBudget();
|
|
174
209
|
|
|
@@ -179,61 +214,103 @@ export function registerCodeMode(pi) {
|
|
|
179
214
|
emitProgress([]);
|
|
180
215
|
const started = performance.now();
|
|
181
216
|
let outcome;
|
|
217
|
+
const overlappedTurn = inFlight > 0 ? inFlight + 1 : 0;
|
|
218
|
+
inFlight += 1;
|
|
219
|
+
|
|
182
220
|
try {
|
|
183
221
|
refreshCatalog(runBridge);
|
|
184
222
|
runBridge.beginSpeculation();
|
|
185
223
|
outcome = await runGuestProgram({
|
|
186
224
|
code: params?.code,
|
|
225
|
+
file: params?.file,
|
|
226
|
+
cwd: runCwd,
|
|
227
|
+
data: params?.data,
|
|
187
228
|
nova: makeNovaApi(runBridge, abortRun),
|
|
188
|
-
config: { ...config, timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
229
|
+
config: { ...config, maxLogLines: Math.max(0,config.maxLogLines-(budget?.logLines ?? 0)), timeoutMs: Number.isInteger(params?.timeoutMs) ? params.timeoutMs : config.timeoutMs },
|
|
189
230
|
signal: runController.signal,
|
|
190
231
|
onTimeout: abortRun,
|
|
191
232
|
});
|
|
192
233
|
runBridge.close();
|
|
234
|
+
|
|
193
235
|
if (outcome.ok) {
|
|
194
236
|
if (runBridge.getOverlayDepth() !== 1) throw new Error("program ended with an unfinished edit checkpoint; await it before returning");
|
|
195
237
|
await runBridge.commitSpeculation();
|
|
196
238
|
}
|
|
197
|
-
else runBridge.rollbackSpeculation();
|
|
239
|
+
else while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
198
240
|
} catch (error) {
|
|
199
241
|
abortRun();
|
|
200
242
|
runBridge.close();
|
|
201
|
-
|
|
243
|
+
|
|
244
|
+
while (runBridge.getOverlayDepth()) runBridge.rollbackSpeculation();
|
|
202
245
|
outcome = { ok: false, error: error instanceof Error ? error.message : String(error), logs: outcome?.logs ?? [], wallMs: Math.round(performance.now() - started) };
|
|
203
246
|
} finally {
|
|
247
|
+
inFlight -= 1;
|
|
204
248
|
runBridge.setCallListener(null);
|
|
205
249
|
emitProgress.flush();
|
|
206
250
|
signal?.removeEventListener("abort", abortRun);
|
|
207
251
|
// Prepare one pristine worker during the model's next decision. Never
|
|
208
252
|
// recycle a worker that has executed arbitrary guest JavaScript.
|
|
209
253
|
cancelWarmTimer();
|
|
254
|
+
|
|
210
255
|
if (!stopped && !runController.signal.aborted) {
|
|
211
256
|
// Deliver the result before paying for another Worker constructor.
|
|
212
257
|
warmTimer = setImmediate(() => {
|
|
213
258
|
warmTimer = undefined;
|
|
259
|
+
|
|
214
260
|
if (!stopped && !runController.signal.aborted) warmGuestWorker(config).catch(() => {});
|
|
215
261
|
});
|
|
216
262
|
warmTimer.unref?.();
|
|
217
263
|
}
|
|
218
264
|
}
|
|
265
|
+
|
|
266
|
+
if (budget) budget.logLines += outcome.logs?.length ?? 0;
|
|
267
|
+
// inFlight has dropped by now, so a non-zero value means a sibling is still
|
|
268
|
+
// running: report the overlap from either side so the hint does not depend on
|
|
269
|
+
// which invocation happened to start first.
|
|
270
|
+
outcome.overlappedTurn = overlappedTurn || (inFlight > 0 ? inFlight + 1 : 0);
|
|
271
|
+
outcome.mutations = runBridge.getMutations();
|
|
219
272
|
const trace = runBridge.getTrace();
|
|
220
|
-
const
|
|
273
|
+
const format = outcome.ok ? successText : errorText;
|
|
274
|
+
let text = format(outcome, call);
|
|
275
|
+
|
|
276
|
+
if (text.length > config.maxReturnChars) {
|
|
277
|
+
outcome.returnTruncated = true;
|
|
278
|
+
text = format(outcome, call);
|
|
279
|
+
}
|
|
280
|
+
|
|
221
281
|
const bounded = truncateChars(text, config.maxReturnChars, "output").text;
|
|
222
282
|
const visible = runBridge.ledger.dedupe(bounded, call);
|
|
223
|
-
|
|
283
|
+
|
|
224
284
|
const response = result(visible, {
|
|
225
285
|
ok: outcome.ok, error: outcome.error, wallMs: outcome.wallMs,
|
|
226
286
|
returnTruncated: outcome.returnTruncated, logTruncated: outcome.logTruncated,
|
|
227
|
-
logs: outcome.logs, result: outcome.result, trace,
|
|
287
|
+
logs: outcome.logs, result: outcome.result, trace, mutations: outcome.mutations,
|
|
228
288
|
});
|
|
289
|
+
|
|
229
290
|
if (outcome.images?.length) response.content.push(...outcome.images);
|
|
291
|
+
|
|
292
|
+
if (!outcome.ok) {
|
|
293
|
+
const error = new Error(visible);
|
|
294
|
+
Object.defineProperty(error,"supernovaResult",{value:response});
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
|
|
230
298
|
return response;
|
|
231
299
|
},
|
|
232
300
|
});
|
|
233
301
|
|
|
234
|
-
|
|
302
|
+
// This is a pre-conversion observation, not a final-payload retention proof.
|
|
303
|
+
// With the shipping seenWindow:0 default, observe is a no-op.
|
|
304
|
+
pi.on("context", event => {
|
|
305
|
+
try { bridge.ledger.observe(event?.messages); } catch {}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
pi.on("session_shutdown", () => { stopped = true; cancelWarmTimer();
|
|
309
|
+
|
|
310
|
+
return stopWarmGuestWorker(); });
|
|
235
311
|
pi.on("session_start", (_event, ctx) => {
|
|
236
312
|
stopped = false;
|
|
313
|
+
|
|
237
314
|
if (ctx && isString(ctx.cwd) && ctx.cwd) cwd = ctx.cwd;
|
|
238
315
|
// A new session is a new model context: nothing has been seen yet.
|
|
239
316
|
bridge.bindCallContext(ctx);
|
|
@@ -249,11 +326,13 @@ export function registerCodeMode(pi) {
|
|
|
249
326
|
bridge.bindCallContext(ctx);
|
|
250
327
|
refreshCatalog();
|
|
251
328
|
const commands = ["read", "edit", "write", "bash"].filter(bridge.isCallable);
|
|
329
|
+
|
|
252
330
|
const lines = [
|
|
253
331
|
`Supernova CodeMode: ${commands.join(", ")}`,
|
|
254
332
|
`timeoutMs=${config.timeoutMs} maxCallResultChars=${config.maxCallResultChars} maxReturnChars=${config.maxReturnChars} maxBridgeCalls=${config.maxBridgeCalls} maxHeapMb=${config.maxHeapMb}`,
|
|
255
333
|
sessionStats(bridge.ledger.stats),
|
|
256
334
|
];
|
|
335
|
+
|
|
257
336
|
ctx.ui.notify(lines.join("\n"), "info");
|
|
258
337
|
},
|
|
259
338
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-supernova",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.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" },
|
|
@@ -13,11 +13,13 @@ const NATIVE_TOOL_DEFINITIONS = [
|
|
|
13
13
|
about: { type: "string", description: "Question or symbol: expand file bodies, or locate and open source inside a directory" },
|
|
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
|
+
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." },
|
|
16
18
|
} },
|
|
17
19
|
},
|
|
18
20
|
{
|
|
19
21
|
name: "write", description: "Write UTF-8 content to a workspace file.",
|
|
20
|
-
parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" } }, required: ["path", "content"] },
|
|
22
|
+
parameters: { type: "object", properties: { path: { type: "string" }, content: { type: "string" }, allowReadArtifacts: { type: "boolean", description: "Explicit opt-in for intentionally writing literal truncation-marker text" } }, required: ["path", "content"] },
|
|
21
23
|
},
|
|
22
24
|
{
|
|
23
25
|
name: "edit", description: "Apply unique text replacements to a workspace file; returns the post-edit lines, a structural check, and references to changed declarations.",
|
|
@@ -84,28 +86,36 @@ export function mergeNativeToolDefinitions(tools, capturedNames = []) {
|
|
|
84
86
|
merged.push(fallback && !captured.has(tool.name)
|
|
85
87
|
? { ...tool, ...fallback, sourceInfo: { path: "<native:" + tool.name + ">" } }
|
|
86
88
|
: tool);
|
|
89
|
+
|
|
87
90
|
if (tool?.name) seen.add(tool.name);
|
|
88
91
|
}
|
|
92
|
+
|
|
89
93
|
for (const fallback of NATIVE_TOOL_DEFINITIONS) {
|
|
90
94
|
if (!seen.has(fallback.name)) {
|
|
91
95
|
merged.push({ ...fallback, sourceInfo: { path: "<native:" + fallback.name + ">" } });
|
|
92
96
|
}
|
|
93
97
|
}
|
|
98
|
+
|
|
94
99
|
return merged;
|
|
95
100
|
}
|
|
96
101
|
|
|
97
102
|
function sourcePathOf(tool) {
|
|
98
103
|
if (tool.sourceInfo && isString(tool.sourceInfo.path)) return tool.sourceInfo.path;
|
|
104
|
+
|
|
99
105
|
if (isString(tool.extensionPath)) return tool.extensionPath;
|
|
106
|
+
|
|
100
107
|
if (isString(tool.sourcePath)) return tool.sourcePath;
|
|
108
|
+
|
|
101
109
|
return undefined;
|
|
102
110
|
}
|
|
103
111
|
|
|
104
112
|
function normalizeTool(tool) {
|
|
105
113
|
if (!tool || !isObject(tool)) return null;
|
|
106
114
|
const name = isString(tool.name) ? tool.name : "";
|
|
115
|
+
|
|
107
116
|
if (!name) return null;
|
|
108
117
|
const description = isString(tool.description) ? tool.description : "";
|
|
118
|
+
|
|
109
119
|
return {
|
|
110
120
|
name,
|
|
111
121
|
nameLower: name.toLowerCase(),
|
|
@@ -120,12 +130,16 @@ function normalizeTool(tool) {
|
|
|
120
130
|
export function buildCatalog(tools, excludeNames = []) {
|
|
121
131
|
const exclude = new Set(excludeNames);
|
|
122
132
|
const rows = [];
|
|
133
|
+
|
|
123
134
|
for (const tool of tools || []) {
|
|
124
135
|
const row = normalizeTool(tool);
|
|
136
|
+
|
|
125
137
|
if (!row || exclude.has(row.name)) continue;
|
|
126
138
|
rows.push(row);
|
|
127
139
|
}
|
|
140
|
+
|
|
128
141
|
rows.sort((a, b) => a.name.localeCompare(b.name));
|
|
142
|
+
|
|
129
143
|
return rows;
|
|
130
144
|
}
|
|
131
145
|
|
|
@@ -141,74 +155,94 @@ function scoreRow(row, tokens) {
|
|
|
141
155
|
const name = row.nameLower || row.name.toLowerCase();
|
|
142
156
|
const desc = row.descLower || row.description.toLowerCase();
|
|
143
157
|
let score = 0;
|
|
158
|
+
|
|
144
159
|
for (const token of tokens) {
|
|
145
160
|
if (name === token) score += 10;
|
|
146
161
|
else if (name.includes(token)) score += 5;
|
|
147
162
|
else if (desc.includes(token)) score += 2;
|
|
148
163
|
}
|
|
164
|
+
|
|
149
165
|
return score;
|
|
150
166
|
}
|
|
151
167
|
|
|
152
168
|
export function searchCatalog(catalog, query, limit = 12) {
|
|
153
169
|
const tokens = tokenize(query);
|
|
154
170
|
const scored = [];
|
|
171
|
+
|
|
155
172
|
for (const row of catalog) {
|
|
156
173
|
const score = scoreRow(row, tokens);
|
|
174
|
+
|
|
157
175
|
if (score <= 0 && tokens.length > 0) continue;
|
|
158
176
|
scored.push({ name: row.name, description: row.description.slice(0, 160), score });
|
|
159
177
|
}
|
|
178
|
+
|
|
160
179
|
scored.sort((a, b) => b.score - a.score || a.name.localeCompare(b.name));
|
|
180
|
+
|
|
161
181
|
return scored.slice(0, Math.max(1, limit)).map(({ score: _s, ...hit }) => hit);
|
|
162
182
|
}
|
|
163
183
|
|
|
164
184
|
/** Optimal string alignment distance: insert/delete/substitute/adjacent-transpose cost 1. */
|
|
165
185
|
function editDistance(a, b) {
|
|
166
186
|
const rows = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array.from({ length: b.length }, () => 0)]);
|
|
187
|
+
|
|
167
188
|
for (let j = 1; j <= b.length; j++) rows[0][j] = j;
|
|
189
|
+
|
|
168
190
|
for (let i = 1; i <= a.length; i++) {
|
|
169
191
|
for (let j = 1; j <= b.length; j++) {
|
|
170
192
|
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
171
193
|
let best = Math.min(rows[i - 1][j] + 1, rows[i][j - 1] + 1, rows[i - 1][j - 1] + cost);
|
|
194
|
+
|
|
172
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);
|
|
173
196
|
rows[i][j] = best;
|
|
174
197
|
}
|
|
175
198
|
}
|
|
199
|
+
|
|
176
200
|
return rows[a.length][b.length];
|
|
177
201
|
}
|
|
178
202
|
|
|
179
203
|
/** Closest tool names for a mistyped name: substring hits first, then a length-scaled edit distance. */
|
|
180
204
|
function suggestNames(name, candidates, limit = 3) {
|
|
181
205
|
const needle = String(name || "").toLowerCase();
|
|
206
|
+
|
|
182
207
|
if (!needle) return [];
|
|
183
208
|
const maxDistance = Math.max(1, Math.floor(needle.length / 3));
|
|
184
209
|
const scored = [];
|
|
210
|
+
|
|
185
211
|
for (const candidate of candidates) {
|
|
186
212
|
const lower = candidate.toLowerCase();
|
|
213
|
+
|
|
187
214
|
if (lower === needle) continue;
|
|
188
215
|
const distance = lower.includes(needle) || needle.includes(lower) ? 1 : editDistance(needle, lower);
|
|
216
|
+
|
|
189
217
|
if (distance <= maxDistance) scored.push({ candidate, distance });
|
|
190
218
|
}
|
|
219
|
+
|
|
191
220
|
scored.sort(
|
|
192
221
|
(a, b) =>
|
|
193
222
|
a.distance - b.distance ||
|
|
194
223
|
Math.abs(a.candidate.length - needle.length) - Math.abs(b.candidate.length - needle.length) ||
|
|
195
224
|
a.candidate.localeCompare(b.candidate),
|
|
196
225
|
);
|
|
226
|
+
|
|
197
227
|
return scored.slice(0, limit).map((s) => s.candidate);
|
|
198
228
|
}
|
|
199
229
|
|
|
200
230
|
export function unknownToolMessage(name, candidates) {
|
|
201
231
|
const close = suggestNames(name, candidates);
|
|
202
232
|
const hint = close.length ? ` Did you mean ${close.map((c) => JSON.stringify(c)).join(", ")}?` : "";
|
|
233
|
+
|
|
203
234
|
return `unknown tool "${name}".${hint} Check the command name and configured tool exclusions.`;
|
|
204
235
|
}
|
|
205
236
|
|
|
206
237
|
export function describeTool(catalog, name) {
|
|
207
238
|
const row = catalog.find((t) => t.name === name);
|
|
239
|
+
|
|
208
240
|
if (!row) {
|
|
209
241
|
return { ok: false, error: unknownToolMessage(name, catalog.map((t) => t.name)) };
|
|
210
242
|
}
|
|
243
|
+
|
|
211
244
|
if (!isObject(row.parameters)) return { ok: false, name, error: "tool schema unavailable: " + (row.schemaError ?? name) };
|
|
245
|
+
|
|
212
246
|
if (!row._described) {
|
|
213
247
|
row._described = {
|
|
214
248
|
ok: true,
|
|
@@ -218,5 +252,6 @@ export function describeTool(catalog, name) {
|
|
|
218
252
|
sourcePath: row.sourcePath,
|
|
219
253
|
};
|
|
220
254
|
}
|
|
255
|
+
|
|
221
256
|
return row._described;
|
|
222
257
|
}
|