@code-yeongyu/senpi-codemode 2026.10.9 → 2026.10.10-10
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 +158 -0
- package/README.md +161 -11
- package/package.json +6 -5
- package/src/bridge/protocol.ts +8 -3
- package/src/bridge/reserved.ts +2 -0
- package/src/bridges/eval-environment-schemas.ts +48 -0
- package/src/bridges/eval-virtual-schemas.ts +11 -4
- package/src/bridges/reserved-dispatch.ts +13 -1
- package/src/config/executable-settings.json +5 -0
- package/src/config/project-trust.ts +51 -0
- package/src/config/settings-overrides.ts +2 -0
- package/src/environments/js-environments.ts +208 -0
- package/src/environments/js-installer.ts +268 -0
- package/src/environments/js-revision-files.ts +91 -0
- package/src/environments/no-links.ts +132 -0
- package/src/environments/packages-install.ts +98 -0
- package/src/environments/project-resolves.ts +54 -0
- package/src/environments/py-environment.ts +7 -3
- package/src/environments/py-installer.ts +5 -1
- package/src/environments/python-environments.ts +1 -1
- package/src/environments/revision-store.ts +20 -4
- package/src/extension/bridge-tool-call.ts +5 -1
- package/src/extension/eval-notifier.ts +13 -2
- package/src/extension/kernel-registration.ts +7 -2
- package/src/extension/runtime-factory.ts +51 -13
- package/src/extension/runtime-info.ts +6 -2
- package/src/extension/session-manager-contract.ts +3 -0
- package/src/extension/session-manager.ts +16 -2
- package/src/index.ts +3 -2
- package/src/interpreters/detect.ts +34 -1
- package/src/kernels/AGENTS.md +2 -0
- package/src/kernels/js/cell-dispatch.ts +44 -0
- package/src/kernels/js/cell-ownership.js +268 -0
- package/src/kernels/js/cell-run-context.js +89 -0
- package/src/kernels/js/context-manager.ts +46 -51
- package/src/kernels/js/control-frames.ts +24 -0
- package/src/kernels/js/host-entries.ts +62 -0
- package/src/kernels/js/kernel-contract.ts +16 -1
- package/src/kernels/js/kernel-memory-bridge.ts +1 -1
- package/src/kernels/js/kernel-memory.ts +79 -0
- package/src/kernels/js/kernel-tools-registry.js +1 -0
- package/src/kernels/js/local-module-loader.ts +17 -6
- package/src/kernels/js/process-drain-scan.d.ts +8 -0
- package/src/kernels/js/process-drain-scan.js +28 -0
- package/src/kernels/js/process-entry.js +466 -0
- package/src/kernels/js/process-memory-host.ts +83 -0
- package/src/kernels/js/process-worker.ts +298 -0
- package/src/kernels/js/rejection-reports.js +61 -0
- package/src/kernels/js/run-queue.ts +7 -0
- package/src/kernels/js/worker-core.js +53 -8
- package/src/kernels/js/worker-cwd.js +5 -2
- package/src/kernels/js/worker-host.ts +9 -2
- package/src/kernels/js/worker-indirect-eval.js +37 -13
- package/src/kernels/js/worker-package-resolve.js +153 -0
- package/src/kernels/js/worker-require.d.ts +19 -0
- package/src/kernels/js/worker-require.js +52 -0
- package/src/kernels/js/worker-runtime.js +43 -9
- package/src/kernels/js/worker-shadow-guard.js +2 -6
- package/src/kernels/js/worker-shell-capture.js +5 -0
- package/src/kernels/js/worker-slot.ts +9 -0
- package/src/kernels/js/worker-startup.ts +49 -1
- package/src/kernels/js/worker-webview.js +15 -1
- package/src/kernels/py/host-cell.ts +8 -31
- package/src/kernels/py/prelude.py +33 -2
- package/src/kernels/py/process.ts +4 -0
- package/src/kernels/sandbox/sandbox-cell.ts +141 -0
- package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +1 -0
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +14 -1
- package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +4 -1
- package/src/kernels/sandbox/vendor/pi-codemode/types.ts +2 -0
- package/src/kernels/shared/host-cell.ts +57 -0
- package/src/kernels/shared/subprocess-process.ts +14 -0
- package/src/prompt/eval-prompt.ts +8 -1
- package/src/tool/AGENTS.md +1 -0
- package/src/tool/cell-handler.ts +12 -11
- package/src/tool/cell-reserved-dispatch.ts +1 -0
- package/src/tool/cell-runtime.ts +4 -2
- package/src/tool/detached-cell-contract.ts +3 -0
- package/src/tool/detached-cell-manager.ts +4 -8
- package/src/tool/detached-cell-notification.ts +11 -63
- package/src/tool/detached-cell-snapshot.ts +17 -0
- package/src/tool/detached-eval-result.ts +13 -7
- package/src/tool/detached-notification-queue.ts +1 -2
- package/src/tool/eval-request.ts +23 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/eval-tool.ts +17 -4
- package/src/tool/interrupt-note.ts +4 -5
- package/src/tool/live-headline.ts +60 -0
- package/src/tool/load-cell.ts +18 -3
- package/src/tool/magic-cell-host.ts +60 -2
- package/src/tool/magic-cells.ts +12 -2
- package/src/tool/managed-cell.ts +0 -4
- package/src/tool/render.ts +74 -15
- package/src/tool/run-eval-cell.ts +46 -4
- package/src/tool/runtime-label.ts +2 -1
- package/src/tool/types.ts +23 -1
|
@@ -61,6 +61,7 @@ RESERVED_HANDLE_STATUS_TOOL = "__handle_status__"
|
|
|
61
61
|
RESERVED_HANDLE_OUTPUT_TOOL = "__handle_output__"
|
|
62
62
|
RESERVED_HANDLE_SEND_TOOL = "__handle_send__"
|
|
63
63
|
RESERVED_HANDLE_CANCEL_TOOL = "__handle_cancel__"
|
|
64
|
+
RESERVED_PACKAGES_INSTALL_TOOL = "__packages_install__"
|
|
64
65
|
TIMEOUT_PAUSE_OP = "timeout-pause"
|
|
65
66
|
TIMEOUT_RESUME_OP = "timeout-resume"
|
|
66
67
|
|
|
@@ -704,7 +705,8 @@ class _Entry:
|
|
|
704
705
|
|
|
705
706
|
|
|
706
707
|
class Registry:
|
|
707
|
-
def __init__(self, on_change: Callable[[list[str]], None]) -> None:
|
|
708
|
+
def __init__(self, on_change: Callable[[list[str]], None], *, disabled: bool = False) -> None:
|
|
709
|
+
self._disabled = disabled
|
|
708
710
|
self._lock = threading.Lock()
|
|
709
711
|
self._entries: dict[str, _Entry] = {}
|
|
710
712
|
# Revisions outlive undefine: a redefined name never reuses a revision an old descriptor carries.
|
|
@@ -713,6 +715,8 @@ class Registry:
|
|
|
713
715
|
self.generation = 1
|
|
714
716
|
|
|
715
717
|
def define(self, fn: Any, *, name: Any = None, description: Any = None, schema: Any = None) -> Any:
|
|
718
|
+
if self._disabled:
|
|
719
|
+
raise KernelToolError("tools_unavailable", "kernel tools are turned off for this project (kernelTools.enabled is false)")
|
|
716
720
|
inferred = infer_tool(fn, name=name, description=description, schema=schema)
|
|
717
721
|
key = tool_key(inferred["name"])
|
|
718
722
|
with self._lock:
|
|
@@ -983,7 +987,12 @@ async def _awaited(awaitable: Any) -> Any:
|
|
|
983
987
|
|
|
984
988
|
KERNEL_TOOL_TOKEN = OwnerToken()
|
|
985
989
|
KERNEL_TOOL_STREAMS = (ThreadRoutedStream(stderr=False), ThreadRoutedStream(stderr=True))
|
|
986
|
-
|
|
990
|
+
# kernelTools.enabled is read once, while the prelude loads and before any cell runs, then removed from the
|
|
991
|
+
# environment: rewriting os.environ cannot turn kernel tools back on, and cell subprocesses never see the switch.
|
|
992
|
+
KERNEL_TOOL_REGISTRY = Registry(
|
|
993
|
+
lambda names: emit({"type": "kernel-tools-defined", "names": names}),
|
|
994
|
+
disabled=os.environ.pop("SENPI_CODEMODE_KERNEL_TOOLS", None) == "0",
|
|
995
|
+
)
|
|
987
996
|
KERNEL_TOOL_RUNNER = KernelToolRunner(emit, KERNEL_TOOL_TOKEN, KERNEL_TOOL_REGISTRY, KERNEL_TOOL_STREAMS)
|
|
988
997
|
|
|
989
998
|
|
|
@@ -1154,6 +1163,27 @@ def tool_schema(name: str | None = None) -> Any:
|
|
|
1154
1163
|
)
|
|
1155
1164
|
|
|
1156
1165
|
|
|
1166
|
+
class _Packages:
|
|
1167
|
+
"""packages.install(manager, requirements, *, timeout=600): the %pip installer as a call; returns its receipt."""
|
|
1168
|
+
|
|
1169
|
+
__slots__ = ()
|
|
1170
|
+
|
|
1171
|
+
def install(self, manager: str, requirements: Any, *, timeout: float | None = None) -> Any:
|
|
1172
|
+
args: dict[str, Any] = {"manager": manager, "requirements": requirements}
|
|
1173
|
+
if timeout is not None:
|
|
1174
|
+
args["timeout"] = timeout
|
|
1175
|
+
# A long install must not hit the 60 s socket cap; the host bounds it by the timeout (600 s by default).
|
|
1176
|
+
socket_timeout = (600 if timeout is None else float(timeout)) + _WAIT_SOCKET_GRACE_SECONDS
|
|
1177
|
+
return bridge_post(
|
|
1178
|
+
"/call",
|
|
1179
|
+
{"callId": f"py-{uuid.uuid4()}", "toolName": RESERVED_PACKAGES_INSTALL_TOOL, "args": args},
|
|
1180
|
+
socket_timeout=socket_timeout,
|
|
1181
|
+
)
|
|
1182
|
+
|
|
1183
|
+
|
|
1184
|
+
packages = _Packages()
|
|
1185
|
+
|
|
1186
|
+
|
|
1157
1187
|
def output(
|
|
1158
1188
|
*ids: str,
|
|
1159
1189
|
format: str = "raw",
|
|
@@ -1781,6 +1811,7 @@ USER_NS.update(
|
|
|
1781
1811
|
"workpool": workpool,
|
|
1782
1812
|
"output": output,
|
|
1783
1813
|
"tool_schema": tool_schema,
|
|
1814
|
+
"packages": packages,
|
|
1784
1815
|
"wait": wait,
|
|
1785
1816
|
"handle": handle,
|
|
1786
1817
|
"__senpi_magic": _magic,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { isAbsolute } from "node:path";
|
|
2
4
|
import type { Readable, Writable } from "node:stream";
|
|
3
5
|
|
|
4
6
|
export interface KernelChild {
|
|
@@ -53,6 +55,8 @@ export function defaultSpawn(options: KernelSpawnOptions): KernelChild {
|
|
|
53
55
|
}
|
|
54
56
|
|
|
55
57
|
export function splitCommand(commandLine: string): { readonly command: string; readonly args: readonly string[] } {
|
|
58
|
+
// A configured interpreter path is one executable even when it contains spaces.
|
|
59
|
+
if (isAbsolute(commandLine) && existsSync(commandLine)) return { command: commandLine, args: [] };
|
|
56
60
|
const [command, ...args] = commandLine.split(" ").filter(Boolean);
|
|
57
61
|
if (!command) throw new Error("Python interpreter path is empty");
|
|
58
62
|
return { command, args };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { ResolvedSandbox } from "../../config/feature-settings.ts";
|
|
2
|
+
import { marshalToolResult } from "../../tool/image.ts";
|
|
3
|
+
import type { EvalRuntimeInfo, ExecuteTool, HostCellExecutor } from "../../tool/types.ts";
|
|
4
|
+
import type { CodemodeSandbox } from "./vendor/pi-codemode/runtime/host.ts";
|
|
5
|
+
import type { CodemodeError, CodemodeOutputFrame, CodemodeTool } from "./vendor/pi-codemode/types.ts";
|
|
6
|
+
|
|
7
|
+
export interface SandboxCellOptions {
|
|
8
|
+
readonly sandbox: ResolvedSandbox;
|
|
9
|
+
readonly executeTool: ExecuteTool;
|
|
10
|
+
readonly toolNames: () => readonly string[];
|
|
11
|
+
readonly describeTool?: (name: string) => string | undefined;
|
|
12
|
+
/** Where the QuickJS wasm lives; by default the installed `quickjs-wasi` package's file. */
|
|
13
|
+
readonly wasmPath?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The runtime an isolated cell's result reports: the QuickJS build it runs on, never the persistent kernel's (senpi#2811).
|
|
18
|
+
* Read from the installed quickjs-wasi package the wasm loads from, only when an isolated cell runs; when that package
|
|
19
|
+
* cannot be resolved the cell itself fails with eval_isolate_unavailable, and the version says so.
|
|
20
|
+
*/
|
|
21
|
+
export function sandboxRuntimeInfo(): EvalRuntimeInfo {
|
|
22
|
+
let version = "unavailable";
|
|
23
|
+
try {
|
|
24
|
+
// getBuiltinModule, not a static import: node:module would otherwise load with the extension.
|
|
25
|
+
const manifest: unknown = process.getBuiltinModule("node:module").createRequire(import.meta.url)(
|
|
26
|
+
"quickjs-wasi/package.json",
|
|
27
|
+
);
|
|
28
|
+
if (
|
|
29
|
+
typeof manifest === "object" &&
|
|
30
|
+
manifest !== null &&
|
|
31
|
+
"version" in manifest &&
|
|
32
|
+
typeof manifest.version === "string"
|
|
33
|
+
)
|
|
34
|
+
version = manifest.version;
|
|
35
|
+
} catch {
|
|
36
|
+
// Reported as "unavailable": the same missing package makes the cell fail with eval_isolate_unavailable.
|
|
37
|
+
}
|
|
38
|
+
return { name: "quickjs", version, isolation: "sandbox" };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// On the script's first line so reported line numbers still match the cell; globals, so a cell's own
|
|
42
|
+
// `const print` shadows them instead of failing as a redeclaration.
|
|
43
|
+
const OUTPUT_ALIASES =
|
|
44
|
+
'globalThis.print = (...values) => text(values.map((value) => (typeof value === "string" ? value : JSON.stringify(value))).join(" ")); globalThis.display = (value) => ((typeof value === "string" && value.startsWith("data:image/")) || (value !== null && typeof value === "object" && ("image_url" in value || value.type === "image")) ? image(value) : text(typeof value === "string" ? value : JSON.stringify(value))); globalThis.tool = tools; ';
|
|
45
|
+
|
|
46
|
+
const ERROR_CODES: Partial<Record<CodemodeError["kind"], string>> = {
|
|
47
|
+
timeout: "eval_isolate_timeout",
|
|
48
|
+
aborted: "eval_isolate_aborted",
|
|
49
|
+
sandbox: "eval_isolate_unavailable",
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
function codeFor(error: CodemodeError): string | undefined {
|
|
53
|
+
if (error.kind === "script" && error.reason === "memory") return "eval_isolate_memory_limit";
|
|
54
|
+
if (error.kind === "script" && error.name === "CodemodeStoreDisabledError") return "eval_isolate_no_state";
|
|
55
|
+
if (error.kind === "script" && error.reason === "unresolved") return "eval_isolate_unresolved_promise";
|
|
56
|
+
return ERROR_CODES[error.kind];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The persistent kernel never sees an isolated cell; its tools go through executeTool so permission hooks still apply. */
|
|
60
|
+
export function sandboxCellExecutor(code: string, options: SandboxCellOptions): HostCellExecutor {
|
|
61
|
+
return async ({ signal, emit }) => {
|
|
62
|
+
let sandbox: CodemodeSandbox;
|
|
63
|
+
try {
|
|
64
|
+
// Lazy: the vendored QuickJS runtime loads only when a session runs its first isolated cell. The wasm is loaded
|
|
65
|
+
// here, before any of the cell runs, so a missing runtime is reported as unavailable instead of failing inside
|
|
66
|
+
// the script.
|
|
67
|
+
const runtime = await import("./vendor/pi-codemode/runtime/host.ts");
|
|
68
|
+
const { loadQuickJSWasm } = await import("./vendor/pi-codemode/wasm.ts");
|
|
69
|
+
const wasm = await loadQuickJSWasm(options.wasmPath);
|
|
70
|
+
const items = new Map<number, string[]>();
|
|
71
|
+
const tools: CodemodeTool[] = options
|
|
72
|
+
.toolNames()
|
|
73
|
+
.filter((name) => name !== "eval")
|
|
74
|
+
.map((name) => ({
|
|
75
|
+
name,
|
|
76
|
+
description: options.describeTool?.(name) ?? "",
|
|
77
|
+
execute: async (args: unknown, context: { signal: AbortSignal }) =>
|
|
78
|
+
marshalToolResult(await options.executeTool(name, args, { signal: context.signal })),
|
|
79
|
+
}));
|
|
80
|
+
sandbox = new runtime.CodemodeSandbox({
|
|
81
|
+
wasm,
|
|
82
|
+
tools,
|
|
83
|
+
timeoutMs: options.sandbox.timeoutSeconds * 1_000,
|
|
84
|
+
memoryLimitBytes: options.sandbox.memoryMb * 1024 * 1024,
|
|
85
|
+
output: "stream",
|
|
86
|
+
builtins: { store: "reject" },
|
|
87
|
+
onOutputFrame: (frame: CodemodeOutputFrame) => {
|
|
88
|
+
const parts = items.get(frame.itemId) ?? [];
|
|
89
|
+
parts.push(frame.chunk);
|
|
90
|
+
if (!frame.final) {
|
|
91
|
+
items.set(frame.itemId, parts);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
items.delete(frame.itemId);
|
|
95
|
+
const payload = parts.join("");
|
|
96
|
+
if (frame.type === "image") {
|
|
97
|
+
emit({ type: "display", mimeType: frame.mimeType ?? "image/png", dataBase64: payload });
|
|
98
|
+
} else {
|
|
99
|
+
emit({ type: "text", stream: "stdout", data: payload.endsWith("\n") ? payload : `${payload}\n` });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
} catch (error) {
|
|
104
|
+
// The full error, paths included, goes to the host's own log so a maintainer can diagnose it; the cell gets
|
|
105
|
+
// only what failed and the error's code. This covers every setup step: the runtime import, the tool list,
|
|
106
|
+
// the wasm load and the sandbox itself.
|
|
107
|
+
console.error("[senpi-codemode] isolated cell setup failed:", error);
|
|
108
|
+
const code =
|
|
109
|
+
error instanceof Error && "code" in error && typeof error.code === "string" ? ` (${error.code})` : "";
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: {
|
|
113
|
+
name: "SandboxUnavailableError",
|
|
114
|
+
message: `eval_isolate_unavailable: the QuickJS runtime for isolated cells could not be loaded${code}; nothing in the cell ran`,
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
const result = await sandbox.execute(OUTPUT_ALIASES + code, { signal });
|
|
120
|
+
if (result.ok) {
|
|
121
|
+
return result.value === undefined
|
|
122
|
+
? { ok: true }
|
|
123
|
+
: {
|
|
124
|
+
ok: true,
|
|
125
|
+
valueRepr: typeof result.value === "string" ? result.value : JSON.stringify(result.value),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const errorCode = codeFor(result.error);
|
|
129
|
+
return {
|
|
130
|
+
ok: false,
|
|
131
|
+
error: {
|
|
132
|
+
...(result.error.name === undefined ? {} : { name: result.error.name }),
|
|
133
|
+
message: errorCode === undefined ? result.error.message : `${errorCode}: ${result.error.message}`,
|
|
134
|
+
...(result.error.stack === undefined ? {} : { stack: result.error.stack }),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
} finally {
|
|
138
|
+
await sandbox.close();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}
|
|
@@ -40,3 +40,4 @@ A sync must re-apply both kinds: dropping a marked line (for example `"output-fr
|
|
|
40
40
|
- A normal finish settles only after every frame is consumed. An abort or timeout settles at once and drops frames still queued. The timer and the abort signal stay armed while a finished run's last frames drain, so a late abort, timeout or `close()` abandons them too (the run settles with that error): a consumer that never returns cannot keep a run alive.
|
|
41
41
|
- `frameBytes` and `windowBytes` must be at least 4, so a frame can always hold a whole surrogate pair.
|
|
42
42
|
2. **Builtin store policy** (`builtins: { store: "collect" | "reject" }`; `types.ts`, `runtime/protocol.ts`, `runtime/worker.ts`, `runtime/host.ts`, `runtime/prelude-source.ts`). With `"reject"`, `load()` returns `undefined` and `store()` throws `CodemodeStoreDisabledError`. The names stay defined, so scripts written for upstream fail with a clear message instead of a ReferenceError.
|
|
43
|
+
3. **Runtime-set error reasons** (`types.ts` `CodemodeError.reason`, `runtime/prelude-source.ts`, `runtime/worker.ts`). The prelude captures the engine's own `InternalError` before user code runs and tags a failure `reason: "memory"` only for an instance of it whose message is exactly "out of memory"; its stalled-promise failure is tagged `reason: "unresolved"`; the worker tags an engine out-of-memory from `evalCode` the same way; and `text()` rethrows the engine's own error instead of replacing it with a `TypeError`. The host maps the isolate error codes from `reason` only, never from message text. Upstream has no `reason`, so its behaviour is unchanged.
|
|
@@ -46,6 +46,10 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
46
46
|
const options = optionsJson === undefined ? {} : JSON.parse(optionsJson);
|
|
47
47
|
const streamOutput = options.streamOutput === true;
|
|
48
48
|
const rejectStore = options.store === "reject";
|
|
49
|
+
// The engine's own InternalError, captured before user code runs: a failure is tagged by identity, never by its
|
|
50
|
+
// text, so a cell's own error that merely mentions memory keeps its own code. A script can still construct this
|
|
51
|
+
// constructor itself and so label its own failure as the memory limit; that mislabels only its own cell.
|
|
52
|
+
const EngineInternalError = typeof InternalError === "function" ? InternalError : undefined;
|
|
49
53
|
// senpi-change end
|
|
50
54
|
"use strict";
|
|
51
55
|
const stringify = JSON.stringify;
|
|
@@ -94,7 +98,13 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
94
98
|
|
|
95
99
|
function describeError(error) {
|
|
96
100
|
if (error instanceof ErrorCtor) {
|
|
97
|
-
|
|
101
|
+
// senpi-change begin: the engine's out-of-memory error is reported by identity, not by matching its message
|
|
102
|
+
const reason =
|
|
103
|
+
EngineInternalError !== undefined && error instanceof EngineInternalError && error.message === "out of memory"
|
|
104
|
+
? "memory"
|
|
105
|
+
: undefined;
|
|
106
|
+
return stringify({ name: error.name, message: error.message, stack: errorText(error), reason });
|
|
107
|
+
// senpi-change end
|
|
98
108
|
}
|
|
99
109
|
return stringify({ message: format(error) });
|
|
100
110
|
}
|
|
@@ -285,6 +295,8 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
285
295
|
try {
|
|
286
296
|
rendered = outputText(value);
|
|
287
297
|
} catch (error) {
|
|
298
|
+
// The engine's own out-of-memory error keeps its identity, so it is still reported as the memory limit.
|
|
299
|
+
if (EngineInternalError !== undefined && error instanceof EngineInternalError) throw error; // senpi-change
|
|
288
300
|
throw new TypeErrorCtor(error instanceof ErrorCtor ? error.message : String(error));
|
|
289
301
|
}
|
|
290
302
|
output("text", rendered);
|
|
@@ -423,6 +435,7 @@ export const PRELUDE_SOURCE: string = `(function (bridge, toolsJson, globalsJson
|
|
|
423
435
|
name: "Error",
|
|
424
436
|
message:
|
|
425
437
|
"The script is waiting on a promise that can never settle: no tool call is pending, and timers do not exist here.",
|
|
438
|
+
reason: "unresolved", // senpi-change: tagged by the runtime, so a script's own error text never matches it
|
|
426
439
|
}),
|
|
427
440
|
);
|
|
428
441
|
return true;
|
|
@@ -48,7 +48,10 @@ function discardOutput(memory: { readonly buffer: ArrayBufferLike }) {
|
|
|
48
48
|
function describeException(error: JSException): string {
|
|
49
49
|
const head = error.message ? `${error.name}: ${error.message}` : error.name;
|
|
50
50
|
const stack = error.stack?.trimEnd();
|
|
51
|
-
|
|
51
|
+
// senpi-change begin: evalCode fails before any script runs, so an InternalError here is the engine's own
|
|
52
|
+
const reason = error.name === "InternalError" && error.message === "out of memory" ? "memory" : undefined;
|
|
53
|
+
return JSON.stringify({ name: error.name, message: error.message, stack: stack ? `${head}\n${stack}` : head, reason });
|
|
54
|
+
// senpi-change end
|
|
52
55
|
}
|
|
53
56
|
// senpi-change begin: output streaming
|
|
54
57
|
/**
|
|
@@ -71,6 +71,8 @@ export interface CodemodeError {
|
|
|
71
71
|
name?: string;
|
|
72
72
|
message: string;
|
|
73
73
|
stack?: string;
|
|
74
|
+
/** senpi-change: set by the runtime itself (never from script text) for its own memory-limit and stalled failures. */
|
|
75
|
+
reason?: "memory" | "unresolved";
|
|
74
76
|
}
|
|
75
77
|
|
|
76
78
|
/** Keys the script changed with `store()`. Only successful executions report writes. */
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
|
|
4
|
+
type HostCellOutcome = Awaited<ReturnType<HostCellExecutor>>;
|
|
5
|
+
type HostResult = Extract<KernelToHostMessage, { type: "result" }>;
|
|
6
|
+
|
|
7
|
+
export interface HostCellRun {
|
|
8
|
+
readonly cellId: string;
|
|
9
|
+
readonly abort: AbortController;
|
|
10
|
+
readonly done: Promise<HostResult>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Runs a host entry (a cell the host executes, such as an install) for one queue slot: output after the outcome or after
|
|
15
|
+
* an abort belongs to no cell, a synchronous throw stays in this cell, and `settle` receives the cell's result.
|
|
16
|
+
* `settleAfterAbort` says whether an aborted entry still settles here or whoever aborted it settles the cell.
|
|
17
|
+
*/
|
|
18
|
+
export function runHostCell(
|
|
19
|
+
cellId: string,
|
|
20
|
+
host: HostCellExecutor,
|
|
21
|
+
io: {
|
|
22
|
+
readonly emit: (message: KernelToHostMessage) => void;
|
|
23
|
+
readonly settle: (result: HostResult) => void;
|
|
24
|
+
readonly durationMs: () => number;
|
|
25
|
+
readonly settleAfterAbort: boolean;
|
|
26
|
+
},
|
|
27
|
+
): HostCellRun {
|
|
28
|
+
const abort = new AbortController();
|
|
29
|
+
let finished = false;
|
|
30
|
+
const emit = (message: KernelToHostMessage): void => {
|
|
31
|
+
if (!finished && !abort.signal.aborted) io.emit(message);
|
|
32
|
+
};
|
|
33
|
+
const done = new Promise<HostCellOutcome>((resolve) => resolve(host({ signal: abort.signal, emit }))).then(
|
|
34
|
+
(outcome): HostResult =>
|
|
35
|
+
outcome.ok
|
|
36
|
+
? {
|
|
37
|
+
type: "result",
|
|
38
|
+
cellId,
|
|
39
|
+
ok: true,
|
|
40
|
+
durationMs: io.durationMs(),
|
|
41
|
+
...(outcome.valueRepr === undefined ? {} : { valueRepr: outcome.valueRepr }),
|
|
42
|
+
}
|
|
43
|
+
: { type: "result", cellId, ok: false, error: outcome.error, durationMs: 0 },
|
|
44
|
+
(error: unknown): HostResult => ({
|
|
45
|
+
type: "result",
|
|
46
|
+
cellId,
|
|
47
|
+
ok: false,
|
|
48
|
+
error: { message: error instanceof Error ? error.message : String(error) },
|
|
49
|
+
durationMs: 0,
|
|
50
|
+
}),
|
|
51
|
+
);
|
|
52
|
+
void done.then((result) => {
|
|
53
|
+
finished = true;
|
|
54
|
+
if (io.settleAfterAbort || !abort.signal.aborted) io.settle(result);
|
|
55
|
+
});
|
|
56
|
+
return { cellId, abort, done };
|
|
57
|
+
}
|
|
@@ -151,6 +151,11 @@ export class SubprocessProcess {
|
|
|
151
151
|
}
|
|
152
152
|
|
|
153
153
|
private kill(signal: NodeJS.Signals): void {
|
|
154
|
+
if (globalThis.process.platform === "win32") {
|
|
155
|
+
if (this.child.pid !== undefined) killProcessTree(this.child.pid);
|
|
156
|
+
else this.child.kill(signal);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
154
159
|
if (this.child.pid !== undefined) {
|
|
155
160
|
try {
|
|
156
161
|
globalThis.process.kill(-this.child.pid, signal);
|
|
@@ -163,6 +168,15 @@ export class SubprocessProcess {
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
171
|
+
// Windows has no process-group signal: taskkill /T /F walks the tree rooted at the child.
|
|
172
|
+
function killProcessTree(pid: number): void {
|
|
173
|
+
try {
|
|
174
|
+
nodeSpawn("taskkill", ["/PID", String(pid), "/T", "/F"], { stdio: "ignore" });
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (!(error instanceof Error)) throw error;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
166
180
|
export function spawnSubprocess(spawn: SubprocessSpawn | undefined, request: SubprocessSpawnRequest): SubprocessLike {
|
|
167
181
|
if (spawn) return spawn(request.command, request.args, { cwd: request.cwd, env: request.env });
|
|
168
182
|
const child = nodeSpawn(request.command, [...request.args], {
|
|
@@ -18,6 +18,8 @@ export interface EvalPromptParts {
|
|
|
18
18
|
|
|
19
19
|
export interface EvalPromptOptions {
|
|
20
20
|
readonly spawns: boolean;
|
|
21
|
+
/** `prompt.advertiseHelpers`: true appends one pointer line to the helper documentation entry. */
|
|
22
|
+
readonly advertiseHelpers?: boolean;
|
|
21
23
|
/** Whether the session registry exposes the monitor tool through eval. */
|
|
22
24
|
readonly monitor?: boolean;
|
|
23
25
|
readonly spawnDefaultAgent?: string;
|
|
@@ -74,6 +76,10 @@ export function evalEmphasisStyle(modelId: string | undefined): EvalEmphasisStyl
|
|
|
74
76
|
type ContextValue = string | boolean;
|
|
75
77
|
type Context = Readonly<Record<string, ContextValue>>;
|
|
76
78
|
|
|
79
|
+
/** The single line `prompt.advertiseHelpers: true` adds; off by default, so the default description is unchanged. */
|
|
80
|
+
export const ADVERTISED_HELPERS_LINE =
|
|
81
|
+
"Advanced cell helpers (handle controls, wait(), kernel tools in workpools) are documented on demand: tool_schema('eval:helpers').";
|
|
82
|
+
|
|
77
83
|
export function buildEvalPrompt(
|
|
78
84
|
enabled: EnabledLanguages,
|
|
79
85
|
options: EvalPromptOptions = { spawns: false },
|
|
@@ -104,9 +110,10 @@ export function buildEvalPrompt(
|
|
|
104
110
|
maxDetachedCells: String(options.maxDetachedCells ?? DEFAULT_MAX_DETACHED_CELLS),
|
|
105
111
|
kernelPreludeDocs: (options.kernelPreludes ?? []).map((prelude) => prelude.documentation).join("\n"),
|
|
106
112
|
};
|
|
107
|
-
const
|
|
113
|
+
const rendered = renderTemplate(EVAL_PROMPT_TEMPLATE, context)
|
|
108
114
|
.replace(/\n{3,}/g, "\n\n")
|
|
109
115
|
.trim();
|
|
116
|
+
const description = options.advertiseHelpers === true ? `${rendered}\n\n${ADVERTISED_HELPERS_LINE}` : rendered;
|
|
110
117
|
return {
|
|
111
118
|
description,
|
|
112
119
|
promptSnippet: "Run one incremental code cell in a persistent language kernel.",
|
package/src/tool/AGENTS.md
CHANGED
|
@@ -17,6 +17,7 @@ anchor most suites).
|
|
|
17
17
|
| Code preview layout | `code-preview.ts` -> `display-code.ts` (JS: `display-js*.ts` Bun-only masked `Bun.Transpiler`; Python: `display-python*.ts` user interpreter, background + repaint) |
|
|
18
18
|
| Status events, execution events | `status-events.ts`, `eval-execution-event.ts` |
|
|
19
19
|
| Interrupt, capture | `interrupt-note.ts`, `call-capture.ts` |
|
|
20
|
+
| Magic cells | `magic-cells.ts` (parsing), `magic-cell-host.ts` (`%pip`, `%bun`/`%npm`, `%environment`), `load-cell.ts` (`%load`); installs live in `../environments/` |
|
|
20
21
|
|
|
21
22
|
## CONVENTIONS
|
|
22
23
|
|
package/src/tool/cell-handler.ts
CHANGED
|
@@ -14,6 +14,7 @@ import { appendSchemaHint } from "../bridges/schema-hint.ts";
|
|
|
14
14
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
15
15
|
import { handleCompletionToolCall } from "../completion/tool-bridge.ts";
|
|
16
16
|
import type { ResolvedCodemodeSettings } from "../config/settings.ts";
|
|
17
|
+
import type { PackagesInstallEnvironments } from "../environments/packages-install.ts";
|
|
17
18
|
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
18
19
|
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
19
20
|
import {
|
|
@@ -54,6 +55,8 @@ export interface CellBridgeRuntime {
|
|
|
54
55
|
readonly handles?: HandleRegistry;
|
|
55
56
|
/** Absolute wall-clock deadline of this cell; a completion handle it creates is bounded by it. */
|
|
56
57
|
readonly hardDeadlineMs?: number;
|
|
58
|
+
/** A JS cell's session environment for `packages.install()`; Python cells reach theirs over the bridge. */
|
|
59
|
+
readonly environments?: PackagesInstallEnvironments;
|
|
57
60
|
}
|
|
58
61
|
|
|
59
62
|
export class CellHandler {
|
|
@@ -235,22 +238,20 @@ export class CellHandler {
|
|
|
235
238
|
this.#kernel.deliverToolReply({ type: "tool-reply", callId: message.callId, ok: true, value: reply.value });
|
|
236
239
|
} catch (error) {
|
|
237
240
|
if (!this.#state.active) return;
|
|
238
|
-
const
|
|
239
|
-
error instanceof Error ? error.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
241
|
+
const code =
|
|
242
|
+
error instanceof Error && "code" in error && typeof error.code === "string" ? error.code : undefined;
|
|
243
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
244
|
+
// A blocked call (a permission denial or another hook's veto) is not an argument problem.
|
|
245
|
+
const text =
|
|
246
|
+
code === "blocked"
|
|
247
|
+
? errorMessage
|
|
248
|
+
: appendSchemaHint(errorMessage, message.toolName, this.#toolParameters(message.toolName));
|
|
243
249
|
recordToolCall(this.#state.toolCalls, false, capture, undefined, text);
|
|
244
250
|
this.#kernel.deliverToolReply({
|
|
245
251
|
type: "tool-reply",
|
|
246
252
|
callId: message.callId,
|
|
247
253
|
ok: false,
|
|
248
|
-
error: {
|
|
249
|
-
message: text,
|
|
250
|
-
...(error instanceof Error && "code" in error && typeof error.code === "string"
|
|
251
|
-
? { code: error.code }
|
|
252
|
-
: {}),
|
|
253
|
-
},
|
|
254
|
+
error: { message: text, ...(code === undefined ? {} : { code }) },
|
|
254
255
|
});
|
|
255
256
|
}
|
|
256
257
|
this.#resultBuilder.emitUpdate(false);
|
|
@@ -29,6 +29,7 @@ export function reservedDispatchContext(
|
|
|
29
29
|
marshalToolResult,
|
|
30
30
|
...(runtime.handles === undefined ? {} : { handles: runtime.handles }),
|
|
31
31
|
...(host === undefined ? {} : { evalHandleHost: host }),
|
|
32
|
+
...(runtime.environments === undefined ? {} : { environments: runtime.environments }),
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
|
package/src/tool/cell-runtime.ts
CHANGED
|
@@ -219,8 +219,10 @@ export class CellResultBuilder {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
#liveUpdateText(): string {
|
|
222
|
-
if (this.#state.status === "queued" && this.#state.queuedBehind
|
|
223
|
-
return
|
|
222
|
+
if (this.#state.status === "queued" && this.#state.queuedBehind !== undefined) {
|
|
223
|
+
return this.#state.queuedBehind.length === 0
|
|
224
|
+
? `waiting for the ${this.#state.input.language} kernel to be ready`
|
|
225
|
+
: `queued behind ${this.#state.queuedBehind.join(", ")} in the ${this.#state.input.language} kernel`;
|
|
224
226
|
}
|
|
225
227
|
const summary = this.#state.input.summary === undefined ? "" : ` ${this.#state.input.summary}`;
|
|
226
228
|
const output = this.#liveLines.text();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentToolResult } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { WakeSourceState } from "../extension/wake-source-state.ts";
|
|
3
|
+
import type { EvalImageContent } from "./image-resize.ts";
|
|
3
4
|
import type { EvalLanguage, EvalToolDetails } from "./types.ts";
|
|
4
5
|
|
|
5
6
|
export type EvalDetachedCellState = "queued" | "running" | "detached" | "completed" | "failed" | "cancelled";
|
|
@@ -24,6 +25,8 @@ export interface EvalDetachedCellSnapshot {
|
|
|
24
25
|
export interface EvalDetachedCellNotification {
|
|
25
26
|
readonly cellId: string;
|
|
26
27
|
readonly content: string;
|
|
28
|
+
/** Images the cell displayed, delivered with the notification the way a foreground result delivers them. */
|
|
29
|
+
readonly images?: readonly EvalImageContent[];
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface EvalDetachedCellNotifier {
|
|
@@ -13,7 +13,7 @@ import type {
|
|
|
13
13
|
EvalDetachedCellSnapshot,
|
|
14
14
|
EvalDetachedCellStatusEntry,
|
|
15
15
|
} from "./detached-cell-contract.ts";
|
|
16
|
-
import {
|
|
16
|
+
import { cancelledDetachedResult, detachedErrorResult, snapshotDetachedCell } from "./detached-cell-snapshot.ts";
|
|
17
17
|
import {
|
|
18
18
|
activeDetachedCellReuseError,
|
|
19
19
|
allowsDetachedCellTransition,
|
|
@@ -36,7 +36,6 @@ export type {
|
|
|
36
36
|
} from "./detached-cell-contract.ts";
|
|
37
37
|
|
|
38
38
|
export class EvalDetachedCellManager {
|
|
39
|
-
readonly #artifactsDir: string | undefined;
|
|
40
39
|
readonly #onStatusChange: ((entries: readonly EvalDetachedCellStatusEntry[]) => void) | undefined;
|
|
41
40
|
readonly #onWakeSourceState: ((state: WakeSourceState) => void) | undefined;
|
|
42
41
|
readonly #cells = new Map<string, ManagedCell>();
|
|
@@ -49,7 +48,6 @@ export class EvalDetachedCellManager {
|
|
|
49
48
|
readonly #maxDetachedCells: number;
|
|
50
49
|
|
|
51
50
|
constructor(options: EvalDetachedCellManagerOptions = {}) {
|
|
52
|
-
this.#artifactsDir = options.artifactsDir;
|
|
53
51
|
this.#onStatusChange = options.onStatusChange;
|
|
54
52
|
this.#onWakeSourceState = options.onWakeSourceState;
|
|
55
53
|
this.#notificationQueue = new DetachedNotificationQueue(options.notifier);
|
|
@@ -85,7 +83,6 @@ export class EvalDetachedCellManager {
|
|
|
85
83
|
const cell = createManagedCell({
|
|
86
84
|
cellId,
|
|
87
85
|
input,
|
|
88
|
-
artifactsDir: this.#artifactsDir,
|
|
89
86
|
now: this.#now,
|
|
90
87
|
defaultHardLimitSeconds: this.#hardLimitSeconds,
|
|
91
88
|
defaultRunBudgetSeconds: this.#runBudgetSeconds,
|
|
@@ -239,7 +236,6 @@ export class EvalDetachedCellManager {
|
|
|
239
236
|
await cell.interruptOutcome?.promise;
|
|
240
237
|
return this.#snapshot(cell);
|
|
241
238
|
},
|
|
242
|
-
spillPath: cell.spillPath,
|
|
243
239
|
});
|
|
244
240
|
}
|
|
245
241
|
}
|
|
@@ -260,7 +256,7 @@ export class EvalDetachedCellManager {
|
|
|
260
256
|
return;
|
|
261
257
|
}
|
|
262
258
|
if (foreground) {
|
|
263
|
-
if (this.#settle(cell, "cancelled",
|
|
259
|
+
if (this.#settle(cell, "cancelled", cancelledDetachedResult(cell))) cell.onKill?.(expiry.error);
|
|
264
260
|
return;
|
|
265
261
|
}
|
|
266
262
|
await this.#cancel(cell, expiry.error.message);
|
|
@@ -269,7 +265,7 @@ export class EvalDetachedCellManager {
|
|
|
269
265
|
#cancelQueued(cell: ManagedCell, reason: string, error = new Error(reason)): void {
|
|
270
266
|
const dequeued = cell.kernel?.cancelQueued(cell.cellId, reason) ?? false;
|
|
271
267
|
cell.stateRetained = true;
|
|
272
|
-
this.#settle(cell, "cancelled",
|
|
268
|
+
this.#settle(cell, "cancelled", cancelledDetachedResult(cell));
|
|
273
269
|
// Acquisition/reset has no queue promise yet; its execution has no interrupt target either.
|
|
274
270
|
if (!dequeued) cell.onKill?.(error);
|
|
275
271
|
}
|
|
@@ -278,7 +274,7 @@ export class EvalDetachedCellManager {
|
|
|
278
274
|
const outcome = Promise.withResolvers<void>();
|
|
279
275
|
cell.interruptOutcome = outcome;
|
|
280
276
|
try {
|
|
281
|
-
if (!this.#settle(cell, "cancelled",
|
|
277
|
+
if (!this.#settle(cell, "cancelled", cancelledDetachedResult(cell)) || cell.kernel === undefined) return;
|
|
282
278
|
const handle = await cell.kernel.interrupt(reason, cell.cellId);
|
|
283
279
|
cell.interruptNote = handle.note;
|
|
284
280
|
cell.stateRetained = await handle.stateRetained;
|