@code-yeongyu/senpi-codemode 2026.10.6 → 2026.10.8
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 +37 -0
- package/README.md +20 -2
- package/package.json +5 -4
- package/src/bridge/kernel-tools-protocol.ts +8 -0
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +100 -0
- package/src/bridges/handle-bridge.ts +72 -0
- package/src/bridges/reserved-dispatch.ts +20 -2
- package/src/bridges/schema-bridge.ts +4 -0
- package/src/completion/handle.ts +69 -0
- package/src/completion/tool-bridge.ts +31 -12
- package/src/environments/editable-check.ts +116 -0
- package/src/environments/install-lock.ts +223 -0
- package/src/environments/py-environment.ts +98 -0
- package/src/environments/py-installer.ts +187 -0
- package/src/environments/revision-store.ts +95 -0
- package/src/extension/bridge-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/kernel-registration.ts +5 -0
- package/src/extension/removed-tool-hints.ts +15 -0
- package/src/extension/runtime-factory.ts +3 -0
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +32 -5
- package/src/extension/start-recovery.ts +111 -0
- package/src/handles/completion-handles.ts +202 -0
- package/src/handles/handle-args.ts +103 -0
- package/src/handles/handle-registry.ts +164 -0
- package/src/handles/wait.ts +214 -0
- package/src/handles/watch-queue.ts +102 -0
- package/src/index.ts +99 -94
- package/src/kernels/jl/handles.jl +124 -0
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +6 -1
- package/src/kernels/js/handles.js +93 -0
- package/src/kernels/js/kernel-tools-define.d.ts +6 -0
- package/src/kernels/js/kernel-tools-define.js +3 -1
- package/src/kernels/js/kernel-tools-errors.js +2 -0
- package/src/kernels/js/kernel-tools-errors.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
- package/src/kernels/js/kernel-tools-registry.js +18 -9
- package/src/kernels/js/kernel-tools-schema.js +6 -2
- package/src/kernels/js/worker-runtime.js +10 -1
- package/src/kernels/py/host-cell.ts +42 -0
- package/src/kernels/py/kernel-contract.ts +13 -2
- package/src/kernels/py/kernel-tools-host.ts +97 -0
- package/src/kernels/py/kernel.ts +88 -12
- package/src/kernels/py/prelude.py +797 -11
- package/src/kernels/py/process.ts +15 -4
- package/src/kernels/py/startup.ts +58 -20
- package/src/kernels/py/transport.ts +36 -7
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +7 -7
- package/src/kernels/sandbox/vendor/pi-codemode/LICENSE +21 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +42 -0
- package/src/kernels/sandbox/vendor/pi-codemode/identifier.ts +14 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +514 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +431 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/protocol.ts +61 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +231 -0
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +185 -0
- package/src/kernels/sandbox/vendor/pi-codemode/wasm.ts +37 -0
- package/src/kernels/shared/kernel-tools-pump.ts +6 -0
- package/src/kernels/shared/process-cpu.ts +69 -0
- package/src/kernels/shared/subprocess-queue.ts +1 -0
- package/src/tool/cell-handler.ts +21 -18
- package/src/tool/cell-reserved-dispatch.ts +51 -0
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/run-eval-cell.ts +2 -0
- package/src/tool/types.ts +15 -0
|
@@ -0,0 +1,431 @@
|
|
|
1
|
+
// Vendored from https://github.com/earendil-works/pi (packages/codemode/src) at v1.0.1, commit a7229ddc21810d6245105978033b7df645ecc2f7.
|
|
2
|
+
// MIT license; see LICENSE beside this file. Local changes are listed in VENDORED.md.
|
|
3
|
+
/**
|
|
4
|
+
* JavaScript evaluated inside the QuickJS VM before the script runs.
|
|
5
|
+
*
|
|
6
|
+
* The VM is its own wasm instance, so nothing here guards a realm boundary.
|
|
7
|
+
* The prelude keeps the host bridge in a closure so the script cannot call it
|
|
8
|
+
* directly, and builds `tools`, `ALL_TOOLS`, the output helpers (`text`, `image`,
|
|
9
|
+
* `exit`, `console`), and globals on top of it. Tool arguments and results cross
|
|
10
|
+
* as JSON strings and are parsed on this side.
|
|
11
|
+
*
|
|
12
|
+
* Output helpers: `text(value)` appends a text item (non-strings are
|
|
13
|
+
* JSON-stringified), `image(urlOrItem)` appends an image from a base64 `data:` URL, an
|
|
14
|
+
* `{ image_url }` object, or an MCP `ImageContent` block, and `exit()` ends the script
|
|
15
|
+
* successfully. `console.*` appends text items like `text()`.
|
|
16
|
+
*
|
|
17
|
+
* `store(key, value)` and `load(key)` are synchronous: they work on a snapshot of
|
|
18
|
+
* JSON text passed in as `storeJson`, and the keys the script wrote are reported
|
|
19
|
+
* with a successful "done".
|
|
20
|
+
*
|
|
21
|
+
* Evaluates to a function `(bridge, toolsJson, globalsJson, storeJson) => { settle, run, stalled }`.
|
|
22
|
+
* `toolsJson` lists `{ name, jsName, description }`: `tools[jsName]` and `tools[name]` call the
|
|
23
|
+
* tool, and `ALL_TOOLS` lists `{ name: jsName, description }`.
|
|
24
|
+
* `stalled()` reports a script that has not finished while no host call is pending: with no timers
|
|
25
|
+
* or I/O in the VM, nothing can ever resume it.
|
|
26
|
+
* `globalsJson` lists `{ name, spread }`; `a.b` names are grouped into a frozen `a` object.
|
|
27
|
+
* `bridge(kind, a, b, c)` with kind "call" or "global" (id, name, argsJson),
|
|
28
|
+
* "output" ("text", text) or ("image", data, mimeType), or "done" (ok, valueJsonOrErrorJson, writesJson).
|
|
29
|
+
*/
|
|
30
|
+
export const MAX_STORE_VALUE_CHARS = 256 * 1024;
|
|
31
|
+
export const MAX_STORE_TOTAL_CHARS = 1024 * 1024;
|
|
32
|
+
/**
|
|
33
|
+
* Output one script may produce with `text()`, `image()`, and `console.*`: characters of text and
|
|
34
|
+
* base64 image data, and items. The host keeps all output until the script ends, so without a
|
|
35
|
+
* limit a script that prints in a loop grows the host's memory until it crashes. The item limit
|
|
36
|
+
* covers loops that print empty strings.
|
|
37
|
+
*/
|
|
38
|
+
export const MAX_OUTPUT_CHARS = 16 * 1024 * 1024;
|
|
39
|
+
export const MAX_OUTPUT_ITEMS = 100_000;
|
|
40
|
+
|
|
41
|
+
const IMAGE_HELPER_EXPECTS =
|
|
42
|
+
"image expects a non-empty image URL string, an object with image_url, or a raw MCP image block";
|
|
43
|
+
|
|
44
|
+
export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson, storeJson, optionsJson) { // senpi-change
|
|
45
|
+
// senpi-change begin: output streaming and builtin policy
|
|
46
|
+
const options = optionsJson === undefined ? {} : JSON.parse(optionsJson);
|
|
47
|
+
const streamOutput = options.streamOutput === true;
|
|
48
|
+
const rejectStore = options.store === "reject";
|
|
49
|
+
// senpi-change end
|
|
50
|
+
"use strict";
|
|
51
|
+
const stringify = JSON.stringify;
|
|
52
|
+
const parse = JSON.parse;
|
|
53
|
+
const promiseThen = Promise.prototype.then;
|
|
54
|
+
const ErrorCtor = Error;
|
|
55
|
+
const TypeErrorCtor = TypeError;
|
|
56
|
+
const RangeErrorCtor = RangeError;
|
|
57
|
+
const pending = new Map();
|
|
58
|
+
let nextId = 1;
|
|
59
|
+
let finished = false;
|
|
60
|
+
// Thrown by exit() to unwind the script after it already reported success.
|
|
61
|
+
const EXIT = Object.freeze({});
|
|
62
|
+
|
|
63
|
+
function done(ok, payload, writes) {
|
|
64
|
+
if (finished) return;
|
|
65
|
+
finished = true;
|
|
66
|
+
bridge("done", ok, payload, writes);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function serialize(value) {
|
|
70
|
+
return value === undefined ? undefined : stringify(value);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// QuickJS stacks list frames only. Prefix "Name: message" like V8 so the
|
|
74
|
+
// text reads the same as a Node error, and drop this prelude's frames.
|
|
75
|
+
function errorText(error) {
|
|
76
|
+
const head = error.message ? error.name + ": " + error.message : String(error.name);
|
|
77
|
+
const frames =
|
|
78
|
+
typeof error.stack === "string"
|
|
79
|
+
? error.stack.split("\\n").filter((line) => line.trim() && !line.includes("codemode-prelude.js"))
|
|
80
|
+
: [];
|
|
81
|
+
return [head, ...frames].join("\\n");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function format(value) {
|
|
85
|
+
if (typeof value === "string") return value;
|
|
86
|
+
if (value instanceof ErrorCtor) return errorText(value);
|
|
87
|
+
try {
|
|
88
|
+
const json = stringify(value);
|
|
89
|
+
return json === undefined ? String(value) : json;
|
|
90
|
+
} catch {
|
|
91
|
+
return String(value);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function describeError(error) {
|
|
96
|
+
if (error instanceof ErrorCtor) {
|
|
97
|
+
return stringify({ name: error.name, message: error.message, stack: errorText(error) });
|
|
98
|
+
}
|
|
99
|
+
return stringify({ message: format(error) });
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function caller(kind, name, spread) {
|
|
103
|
+
return (...args) =>
|
|
104
|
+
new Promise((resolve, reject) => {
|
|
105
|
+
let json;
|
|
106
|
+
try {
|
|
107
|
+
json = serialize(spread ? args : args[0]);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
reject(error);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const id = nextId++;
|
|
113
|
+
pending.set(id, { resolve, reject });
|
|
114
|
+
bridge(kind, id, name, json);
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const tools = Object.create(null);
|
|
119
|
+
const allTools = [];
|
|
120
|
+
for (const { name, jsName, description } of parse(toolsJson)) {
|
|
121
|
+
const fn = caller("call", name);
|
|
122
|
+
// The first tool wins when two names normalize to the same identifier.
|
|
123
|
+
if (!(jsName in tools)) {
|
|
124
|
+
tools[jsName] = fn;
|
|
125
|
+
allTools.push(Object.freeze({ name: jsName, description }));
|
|
126
|
+
}
|
|
127
|
+
if (!(name in tools)) tools[name] = fn;
|
|
128
|
+
}
|
|
129
|
+
Object.freeze(tools);
|
|
130
|
+
Object.freeze(allTools);
|
|
131
|
+
|
|
132
|
+
// Reading a member that does not exist throws an error that names the close matches, instead of
|
|
133
|
+
// a later "not a function". \`in\` checks still work.
|
|
134
|
+
const comparable = (name) => name.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
135
|
+
function guard(target, label, names, hint) {
|
|
136
|
+
return new Proxy(target, {
|
|
137
|
+
get(object, property, receiver) {
|
|
138
|
+
if (typeof property !== "string" || property in object || property in Object.prototype || property === "then" || property === "toJSON") {
|
|
139
|
+
return Reflect.get(object, property, receiver);
|
|
140
|
+
}
|
|
141
|
+
const wanted = comparable(property);
|
|
142
|
+
const exact = names.filter((name) => comparable(name) === wanted);
|
|
143
|
+
const close = exact.length > 0 ? exact : names.filter((name) => wanted && (comparable(name).includes(wanted) || wanted.includes(comparable(name))));
|
|
144
|
+
let message = label + "." + property + " does not exist.";
|
|
145
|
+
if (close.length > 0) message += " Did you mean " + close.slice(0, 5).map((name) => label + "." + name).join(", ") + "?";
|
|
146
|
+
else if (names.length <= 20) message += " Available: " + names.join(", ") + ".";
|
|
147
|
+
if (hint) message += " " + hint;
|
|
148
|
+
message += ' Check for a member with "' + property + '" in ' + label + ".";
|
|
149
|
+
throw new TypeErrorCtor(message);
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
const toolsProxy = guard(
|
|
154
|
+
tools,
|
|
155
|
+
"tools",
|
|
156
|
+
allTools.map((tool) => tool.name),
|
|
157
|
+
"ALL_TOOLS lists every tool; searchTools(query) finds tools by topic.",
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
const namespaces = new Map();
|
|
161
|
+
for (const { name, spread } of parse(globalsJson)) {
|
|
162
|
+
const fn = caller("global", name, spread);
|
|
163
|
+
const dot = name.indexOf(".");
|
|
164
|
+
if (dot === -1) {
|
|
165
|
+
Object.defineProperty(globalThis, name, { value: fn, enumerable: true });
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const namespace = name.slice(0, dot);
|
|
169
|
+
if (!namespaces.has(namespace)) namespaces.set(namespace, Object.create(null));
|
|
170
|
+
namespaces.get(namespace)[name.slice(dot + 1)] = fn;
|
|
171
|
+
}
|
|
172
|
+
for (const [namespace, members] of namespaces) {
|
|
173
|
+
Object.freeze(members);
|
|
174
|
+
const value = guard(members, namespace, Object.keys(members));
|
|
175
|
+
Object.defineProperty(globalThis, namespace, { value, enumerable: true });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// key -> JSON text. Sizes count key and JSON characters.
|
|
179
|
+
const stored = new Map(Object.entries(parse(storeJson)));
|
|
180
|
+
const writes = new Map();
|
|
181
|
+
let storedChars = 0;
|
|
182
|
+
for (const [key, json] of stored) storedChars += key.length + json.length;
|
|
183
|
+
|
|
184
|
+
const STORE_HINT =
|
|
185
|
+
"store() is for small state such as IDs or summaries. Show images with image(), keep large data in variables, or write it to a file with a tool.";
|
|
186
|
+
|
|
187
|
+
function checkKey(name, key) {
|
|
188
|
+
if (typeof key !== "string") throw new TypeError(name + "() key must be a string");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function store(key, value) {
|
|
192
|
+
checkKey("store", key);
|
|
193
|
+
// senpi-change begin: builtin policy
|
|
194
|
+
if (rejectStore) {
|
|
195
|
+
const error = new Error("store() is not available here: this sandbox keeps no state between runs");
|
|
196
|
+
error.name = "CodemodeStoreDisabledError";
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
// senpi-change end
|
|
200
|
+
const previous = stored.has(key) ? key.length + stored.get(key).length : 0;
|
|
201
|
+
if (value === undefined) {
|
|
202
|
+
stored.delete(key);
|
|
203
|
+
storedChars -= previous;
|
|
204
|
+
writes.set(key, undefined);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
let json;
|
|
208
|
+
try {
|
|
209
|
+
json = stringify(value);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
throw new TypeError("store(" + stringify(key) + ") value is not JSON-serializable: " + format(error));
|
|
212
|
+
}
|
|
213
|
+
if (json === undefined) {
|
|
214
|
+
throw new TypeError("store(" + stringify(key) + ") value is not JSON-serializable");
|
|
215
|
+
}
|
|
216
|
+
if (json.length > ${MAX_STORE_VALUE_CHARS}) {
|
|
217
|
+
throw new RangeError(
|
|
218
|
+
"store(" + stringify(key) + ") value has " + json.length + " characters of JSON, more than the limit of ${MAX_STORE_VALUE_CHARS}. " +
|
|
219
|
+
STORE_HINT,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
const next = storedChars - previous + key.length + json.length;
|
|
223
|
+
if (next > ${MAX_STORE_TOTAL_CHARS}) {
|
|
224
|
+
throw new RangeError(
|
|
225
|
+
"store is full: stored values would exceed ${MAX_STORE_TOTAL_CHARS} characters of JSON. Delete keys with store(key, undefined). " +
|
|
226
|
+
STORE_HINT,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
stored.set(key, json);
|
|
230
|
+
storedChars = next;
|
|
231
|
+
writes.set(key, json);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function load(key) {
|
|
235
|
+
checkKey("load", key);
|
|
236
|
+
// senpi-change begin: builtin policy
|
|
237
|
+
if (rejectStore) return undefined;
|
|
238
|
+
// senpi-change end
|
|
239
|
+
const json = stored.get(key);
|
|
240
|
+
return json === undefined ? undefined : parse(json);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function serializeWrites() {
|
|
244
|
+
const entries = [];
|
|
245
|
+
for (const [key, json] of writes) entries.push(json === undefined ? [key] : [key, json]);
|
|
246
|
+
return stringify(entries);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Object.defineProperty(globalThis, "store", { value: store, enumerable: true });
|
|
250
|
+
Object.defineProperty(globalThis, "load", { value: load, enumerable: true });
|
|
251
|
+
|
|
252
|
+
let outputChars = 0;
|
|
253
|
+
let outputItems = 0;
|
|
254
|
+
|
|
255
|
+
// Past the output limits the script fails: done() reports the error, so catching it does not
|
|
256
|
+
// resume output, and the host ends the script.
|
|
257
|
+
function output(kind, data, mimeType) {
|
|
258
|
+
if (finished) return;
|
|
259
|
+
outputChars += data.length;
|
|
260
|
+
outputItems++;
|
|
261
|
+
// senpi-change begin: output streaming (the credit window bounds memory; nothing is kept to cap)
|
|
262
|
+
if (!streamOutput && (outputChars > ${MAX_OUTPUT_CHARS} || outputItems > ${MAX_OUTPUT_ITEMS})) { // senpi-change
|
|
263
|
+
// senpi-change end
|
|
264
|
+
const error = new RangeErrorCtor(
|
|
265
|
+
"script output exceeded the limit of ${MAX_OUTPUT_CHARS} characters or ${MAX_OUTPUT_ITEMS} text(), image(), and console calls. " +
|
|
266
|
+
"Print a summary instead, or write large data to a file with a tool.",
|
|
267
|
+
);
|
|
268
|
+
done(false, describeError(error));
|
|
269
|
+
throw error;
|
|
270
|
+
}
|
|
271
|
+
bridge("output", kind, data, mimeType);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Primitives become their string form, everything else JSON.
|
|
275
|
+
function outputText(value) {
|
|
276
|
+
if (value === undefined || value === null || typeof value !== "object" && typeof value !== "function") {
|
|
277
|
+
return String(value);
|
|
278
|
+
}
|
|
279
|
+
const json = stringify(value);
|
|
280
|
+
return json === undefined ? String(value) : json;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function text(value) {
|
|
284
|
+
let rendered;
|
|
285
|
+
try {
|
|
286
|
+
rendered = outputText(value);
|
|
287
|
+
} catch (error) {
|
|
288
|
+
throw new TypeErrorCtor(error instanceof ErrorCtor ? error.message : String(error));
|
|
289
|
+
}
|
|
290
|
+
output("text", rendered);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function imageUrl(value) {
|
|
294
|
+
if (typeof value === "string") return value;
|
|
295
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
|
296
|
+
throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
297
|
+
}
|
|
298
|
+
if (value.image_url !== undefined) {
|
|
299
|
+
if (typeof value.image_url !== "string") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
300
|
+
return value.image_url;
|
|
301
|
+
}
|
|
302
|
+
if (typeof value.type !== "string") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
303
|
+
if (value.type !== "image") {
|
|
304
|
+
throw new TypeErrorCtor('image only accepts MCP image blocks, got "' + value.type + '"');
|
|
305
|
+
}
|
|
306
|
+
if (typeof value.data !== "string" || value.data === "") throw new TypeErrorCtor("image expected MCP image data");
|
|
307
|
+
if (value.data.toLowerCase().startsWith("data:")) return value.data;
|
|
308
|
+
return "data:;base64," + value.data;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Base64 of the signatures of the formats providers accept inline (PNG, JPEG except
|
|
312
|
+
// JPEG-LS, GIF, "RIFF....WEBP"). Signatures start at byte 0, so their encodings are prefixes.
|
|
313
|
+
const IMAGE_SIGNATURES = [
|
|
314
|
+
["image/png", /^iVBORw0KGg/],
|
|
315
|
+
["image/jpeg", /^[/]9j[/](?!9)/],
|
|
316
|
+
["image/gif", /^R0lGOD[dl]h/],
|
|
317
|
+
["image/webp", /^UklG.{8}RUJQ/],
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
function image(value) {
|
|
321
|
+
const url = imageUrl(value);
|
|
322
|
+
if (url === "") throw new TypeErrorCtor(${JSON.stringify(IMAGE_HELPER_EXPECTS)});
|
|
323
|
+
const colon = url.indexOf(":");
|
|
324
|
+
const scheme = colon === -1 ? "" : url.slice(0, colon).toLowerCase();
|
|
325
|
+
if (scheme === "http" || scheme === "https") {
|
|
326
|
+
throw new TypeErrorCtor("remote image URLs are not supported in tool outputs. Pass a base64 data URI instead");
|
|
327
|
+
}
|
|
328
|
+
const comma = url.indexOf(",");
|
|
329
|
+
const header = comma === -1 ? [] : url.slice(colon + 1, comma).split(";");
|
|
330
|
+
if (scheme !== "data" || comma === -1 || header.slice(1).every((part) => part.toLowerCase() !== "base64")) {
|
|
331
|
+
throw new TypeErrorCtor("invalid image output. Pass a base64 data URI instead");
|
|
332
|
+
}
|
|
333
|
+
// Providers reject the whole request on a bad image, and a persisted image block would be
|
|
334
|
+
// resent on every later turn. Line breaks from wrapped base64 are dropped. The declared type
|
|
335
|
+
// is ignored in favor of the detected one, as providers also reject mismatches.
|
|
336
|
+
const data = url.slice(comma + 1).replace(/\\s+/g, "");
|
|
337
|
+
if (data.length % 4 !== 0 || !/^[A-Za-z0-9+/]+={0,2}$/.test(data)) {
|
|
338
|
+
throw new TypeErrorCtor("invalid image output. The image data is not valid base64 (truncated or corrupted?)");
|
|
339
|
+
}
|
|
340
|
+
const head = data.slice(0, 16);
|
|
341
|
+
const signature = IMAGE_SIGNATURES.find(([, pattern]) => pattern.test(head));
|
|
342
|
+
if (!signature) {
|
|
343
|
+
throw new TypeErrorCtor("invalid image output. The image data is not a PNG, JPEG, GIF, or WebP image");
|
|
344
|
+
}
|
|
345
|
+
output("image", data, signature[0]);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function exit() {
|
|
349
|
+
let writesJson;
|
|
350
|
+
try {
|
|
351
|
+
writesJson = serializeWrites();
|
|
352
|
+
} catch (error) {
|
|
353
|
+
done(false, describeError(error));
|
|
354
|
+
throw EXIT;
|
|
355
|
+
}
|
|
356
|
+
done(true, undefined, writesJson);
|
|
357
|
+
throw EXIT;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const console = {};
|
|
361
|
+
for (const level of ["log", "info", "warn", "error", "debug"]) {
|
|
362
|
+
console[level] = (...args) => {
|
|
363
|
+
output("text", args.map(format).join(" "));
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
Object.freeze(console);
|
|
367
|
+
|
|
368
|
+
Object.defineProperty(globalThis, "tools", { value: toolsProxy, enumerable: true });
|
|
369
|
+
Object.defineProperty(globalThis, "ALL_TOOLS", { value: allTools, enumerable: true });
|
|
370
|
+
Object.defineProperty(globalThis, "console", { value: console, enumerable: true });
|
|
371
|
+
Object.defineProperty(globalThis, "text", { value: text, enumerable: true });
|
|
372
|
+
Object.defineProperty(globalThis, "image", { value: image, enumerable: true });
|
|
373
|
+
Object.defineProperty(globalThis, "exit", { value: exit, enumerable: true });
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
settle(id, ok, payload) {
|
|
377
|
+
const entry = pending.get(id);
|
|
378
|
+
if (!entry) return;
|
|
379
|
+
pending.delete(id);
|
|
380
|
+
if (!ok) {
|
|
381
|
+
entry.reject(new ErrorCtor(payload));
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
let value;
|
|
385
|
+
try {
|
|
386
|
+
value = payload === undefined ? undefined : parse(payload);
|
|
387
|
+
} catch (error) {
|
|
388
|
+
entry.reject(error);
|
|
389
|
+
return;
|
|
390
|
+
}
|
|
391
|
+
entry.resolve(value);
|
|
392
|
+
},
|
|
393
|
+
run(fn) {
|
|
394
|
+
let promise;
|
|
395
|
+
try {
|
|
396
|
+
promise = fn(toolsProxy, console);
|
|
397
|
+
} catch (error) {
|
|
398
|
+
done(false, describeError(error));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
promiseThen.call(
|
|
402
|
+
promise,
|
|
403
|
+
(value) => {
|
|
404
|
+
let json;
|
|
405
|
+
try {
|
|
406
|
+
json = serialize(value);
|
|
407
|
+
} catch (error) {
|
|
408
|
+
done(false, describeError(error));
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
done(true, json, serializeWrites());
|
|
412
|
+
},
|
|
413
|
+
(error) => {
|
|
414
|
+
done(false, describeError(error));
|
|
415
|
+
},
|
|
416
|
+
);
|
|
417
|
+
},
|
|
418
|
+
stalled() {
|
|
419
|
+
if (finished || pending.size > 0) return false;
|
|
420
|
+
done(
|
|
421
|
+
false,
|
|
422
|
+
stringify({
|
|
423
|
+
name: "Error",
|
|
424
|
+
message:
|
|
425
|
+
"The script is waiting on a promise that can never settle: no tool call is pending, and timers do not exist here.",
|
|
426
|
+
}),
|
|
427
|
+
);
|
|
428
|
+
return true;
|
|
429
|
+
},
|
|
430
|
+
};
|
|
431
|
+
})`;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Vendored from https://github.com/earendil-works/pi (packages/codemode/src) at v1.0.1, commit a7229ddc21810d6245105978033b7df645ecc2f7.
|
|
2
|
+
// MIT license; see LICENSE beside this file. Local changes are listed in VENDORED.md.
|
|
3
|
+
import type { CodemodeOutputFrame, CodemodeOutputItem } from "../types.ts"; // senpi-change
|
|
4
|
+
import type { CodemodeWasmModule } from "../wasm.ts";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Messages between the host (main thread) and the worker. Tool arguments,
|
|
8
|
+
* results, and values cross as JSON strings: the worker passes them into and
|
|
9
|
+
* out of the QuickJS VM as strings and never builds structured values itself.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export interface WorkerData {
|
|
13
|
+
code: string;
|
|
14
|
+
/** `jsName` is the identifier the script uses; `description` is listed in `ALL_TOOLS`. */
|
|
15
|
+
tools: { name: string; jsName: string; description: string }[];
|
|
16
|
+
globals: { name: string; spread: boolean }[];
|
|
17
|
+
/** Compiled `quickjs-wasi` module. Structured clone shares the compiled code with the worker. */
|
|
18
|
+
wasm: CodemodeWasmModule;
|
|
19
|
+
memoryLimitBytes: number | undefined;
|
|
20
|
+
/** Snapshot for `load()`: key to JSON text. */
|
|
21
|
+
store: Record<string, string>;
|
|
22
|
+
/**
|
|
23
|
+
* One Int32 the host sets to non-zero before terminating the worker. The VM's interrupt handler
|
|
24
|
+
* polls it, because Bun's `worker.terminate()` cannot stop a thread that is spinning in wasm.
|
|
25
|
+
*/
|
|
26
|
+
interrupt: SharedArrayBuffer;
|
|
27
|
+
// senpi-change begin: output streaming and builtin policy
|
|
28
|
+
/** Present in `output: "stream"` mode: one Int32 of free credit bytes the host refills as frames are consumed. */
|
|
29
|
+
stream?: { credits: SharedArrayBuffer; frameBytes: number };
|
|
30
|
+
storePolicy?: "collect" | "reject";
|
|
31
|
+
// senpi-change end
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** JSON-encoded `{ name?, message, stack? }` of an error thrown by the script. */
|
|
35
|
+
export type ScriptErrorJson = string;
|
|
36
|
+
|
|
37
|
+
export type WorkerToHostMessage =
|
|
38
|
+
| { type: "call"; id: number; target: "tool" | "global"; name: string; args: string | undefined }
|
|
39
|
+
| { type: "output"; item: CodemodeOutputItem }
|
|
40
|
+
// senpi-change begin: output streaming
|
|
41
|
+
| { type: "output-frame"; frame: CodemodeOutputFrame; bytes: number }
|
|
42
|
+
// senpi-change end
|
|
43
|
+
/** `writes` is a JSON array of `[key, json]` for `store()` and `[key]` for deletions. */
|
|
44
|
+
| { type: "done"; ok: true; value: string | undefined; writes: string }
|
|
45
|
+
| { type: "done"; ok: false; error: ScriptErrorJson }
|
|
46
|
+
/** The VM failed outside the script's control, for example a wasm trap. */
|
|
47
|
+
| { type: "crash"; message: string };
|
|
48
|
+
|
|
49
|
+
export type HostToWorkerMessage =
|
|
50
|
+
/** `payload` is the JSON result when `ok`, otherwise the error message. */
|
|
51
|
+
{ type: "result"; id: number; ok: boolean; payload: string | undefined };
|
|
52
|
+
|
|
53
|
+
export function isWorkerToHostMessage(value: unknown): value is WorkerToHostMessage {
|
|
54
|
+
if (typeof value !== "object" || value === null) return false;
|
|
55
|
+
const type = (value as { type?: unknown }).type;
|
|
56
|
+
return type === "call" || type === "output" || type === "output-frame" || type === "done" || type === "crash"; // senpi-change
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isHostToWorkerMessage(value: unknown): value is HostToWorkerMessage {
|
|
60
|
+
return typeof value === "object" && value !== null && (value as { type?: unknown }).type === "result";
|
|
61
|
+
}
|