@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
|
@@ -6,7 +6,8 @@ import { orderedArgs, resolveToolMetadata, validateInvokeArgs } from "./kernel-t
|
|
|
6
6
|
|
|
7
7
|
export { createToolNamespace };
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
// "defined" and "undefine" are the tool namespace's own members, so a kernel tool by those names could never be reached.
|
|
10
|
+
const DEFAULT_RESERVED = Object.freeze(["__agent__", "__output__", "__schema__", "defined", "undefine"]);
|
|
10
11
|
|
|
11
12
|
function currentNames(source) {
|
|
12
13
|
return typeof source === "function" ? source() : (source ?? []);
|
|
@@ -47,21 +48,22 @@ export function createKernelToolRegistry(options = {}) {
|
|
|
47
48
|
assertJs();
|
|
48
49
|
const parsed = parseToolFunction(fn);
|
|
49
50
|
const resolved = resolveToolMetadata(metadata, parsed.params);
|
|
50
|
-
|
|
51
|
+
const name = resolved.name ?? parsed.name;
|
|
52
|
+
if (sanitizeNamePart(name) !== name || name.length > MCP_TOOL_NAME_MAX_LENGTH) {
|
|
51
53
|
throw kernelToolError("invalid_tool_definition", "Kernel tool name must match MCP name grammar");
|
|
52
54
|
}
|
|
53
|
-
const normalizedName =
|
|
54
|
-
const key = kernelToolKey(
|
|
55
|
-
if (reservedKeys.has(key)) throw kernelToolError("reserved_tool_name", `Kernel tool name is reserved: ${
|
|
55
|
+
const normalizedName = name;
|
|
56
|
+
const key = kernelToolKey(name);
|
|
57
|
+
if (reservedKeys.has(key)) throw kernelToolError("reserved_tool_name", `Kernel tool name is reserved: ${name}`);
|
|
56
58
|
if (hasNameKey(hostSource, key) || hasNameKey(foreignSource, key)) {
|
|
57
|
-
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${
|
|
59
|
+
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${name}`);
|
|
58
60
|
}
|
|
59
61
|
const existing = entries.get(key);
|
|
60
|
-
if (existing && existing.originalName !==
|
|
61
|
-
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${
|
|
62
|
+
if (existing && existing.originalName !== name) {
|
|
63
|
+
throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${name}`);
|
|
62
64
|
}
|
|
63
65
|
const entry = {
|
|
64
|
-
originalName:
|
|
66
|
+
originalName: name,
|
|
65
67
|
normalizedName,
|
|
66
68
|
fn,
|
|
67
69
|
params: parsed.params,
|
|
@@ -114,6 +116,13 @@ export function createKernelToolRegistry(options = {}) {
|
|
|
114
116
|
throw kernelToolError("kernel_tool_failed", error instanceof Error ? error.message : String(error));
|
|
115
117
|
}
|
|
116
118
|
},
|
|
119
|
+
defined() {
|
|
120
|
+
return [...entries.values()].map((entry) => entry.normalizedName).sort();
|
|
121
|
+
},
|
|
122
|
+
undefine(name) {
|
|
123
|
+
if (typeof name !== "string") return false;
|
|
124
|
+
return entries.delete(kernelToolKey(name));
|
|
125
|
+
},
|
|
117
126
|
bumpGeneration() {
|
|
118
127
|
generation += 1;
|
|
119
128
|
entries.clear();
|
|
@@ -18,7 +18,11 @@ export function resolveToolMetadata(metadata, params) {
|
|
|
18
18
|
if (typeof description !== "string") {
|
|
19
19
|
throw kernelToolError("invalid_tool_definition", "tool() description must be a string");
|
|
20
20
|
}
|
|
21
|
-
if (metadata.
|
|
21
|
+
if (metadata.name !== undefined && typeof metadata.name !== "string") {
|
|
22
|
+
throw kernelToolError("invalid_tool_definition", "tool() name must be a string");
|
|
23
|
+
}
|
|
24
|
+
const named = metadata.name === undefined ? {} : { name: metadata.name };
|
|
25
|
+
if (metadata.schema === undefined) return { ...named, description, input_schema: defaultInputSchema(params) };
|
|
22
26
|
const schema = metadata.schema;
|
|
23
27
|
if (!isPlainJsonObject(schema) || schema.type !== "object" || !isPlainJsonObject(schema.properties)) {
|
|
24
28
|
throw kernelToolError("invalid_tool_definition", "tool() metadata schema must be a JSON object schema");
|
|
@@ -27,7 +31,7 @@ export function resolveToolMetadata(metadata, params) {
|
|
|
27
31
|
if (propertyKeys.length !== params.length || params.some((name) => !Object.hasOwn(schema.properties, name))) {
|
|
28
32
|
throw kernelToolError("invalid_tool_definition", "tool() metadata schema properties must match parameters");
|
|
29
33
|
}
|
|
30
|
-
return { description, input_schema: schema };
|
|
34
|
+
return { ...named, description, input_schema: schema };
|
|
31
35
|
}
|
|
32
36
|
|
|
33
37
|
export function validateInvokeArgs(schema, args) {
|
|
@@ -8,6 +8,7 @@ import { awaitMaybePromise, indirectEval, wrapUserCode } from "./worker-indirect
|
|
|
8
8
|
import { installShellCapture } from "./worker-shell-capture.js";
|
|
9
9
|
import { bindKernelBun } from "./worker-webview.js";
|
|
10
10
|
import { createWorkpool } from "./workpool.js";
|
|
11
|
+
import { createHandleHelpers } from "./handles.js";
|
|
11
12
|
import { inKernelToolInvoke } from "./kernel-tools-context.js";
|
|
12
13
|
import { kernelToolError } from "./kernel-tools-errors.js";
|
|
13
14
|
import { createKernelToolRegistry, createToolNamespace } from "./kernel-tools-registry.js";
|
|
@@ -137,10 +138,18 @@ export class JsWorkerRuntime {
|
|
|
137
138
|
globalThis.workpool = (agent, name, options) => createWorkpool((toolName, args) => this.#callTool(toolName, args), agent, name, options);
|
|
138
139
|
globalThis.parallel = async thunks => await this.#parallel(thunks);
|
|
139
140
|
globalThis.pipeline = async (items, ...stages) => await this.#pipeline(items, stages);
|
|
140
|
-
|
|
141
|
+
const handles = createHandleHelpers(async (toolName, args) => await this.#callTool(toolName, args));
|
|
142
|
+
globalThis.completion = async (prompt, opts) => {
|
|
143
|
+
const value = await this.#callTool("completion", { prompt, opts });
|
|
144
|
+
// The host answers a {handle: true} request with a saved reference; the cell gets the control view.
|
|
145
|
+
return isPlainObject(opts) && opts.handle === true ? handles.handle(value) : value;
|
|
146
|
+
};
|
|
147
|
+
globalThis.wait = async (list, options) => await handles.wait(list, options);
|
|
148
|
+
globalThis.handle = value => handles.handle(value);
|
|
141
149
|
globalThis.tool = createToolNamespace(
|
|
142
150
|
(fn, metadata) => this.#tools.define(fn, metadata),
|
|
143
151
|
async (name, args) => await this.#callTool(name, args),
|
|
152
|
+
{ defined: () => this.#tools.defined(), undefine: (name) => this.#tools.undefine(name) },
|
|
144
153
|
);
|
|
145
154
|
globalThis.tools = globalThis.tool;
|
|
146
155
|
const originalLog = console.log.bind(console);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import type { HostCellExecutor } from "../../tool/types.ts";
|
|
3
|
+
|
|
4
|
+
type HostCellOutcome = Awaited<ReturnType<HostCellExecutor>>;
|
|
5
|
+
|
|
6
|
+
import type { PendingRun, ResultMessage } from "./kernel-contract.ts";
|
|
7
|
+
import { failedPythonResult } from "./transport.ts";
|
|
8
|
+
|
|
9
|
+
export function runHostCell(
|
|
10
|
+
pending: PendingRun,
|
|
11
|
+
host: HostCellExecutor,
|
|
12
|
+
io: { readonly emit: (message: KernelToHostMessage) => void; readonly settle: (result: ResultMessage) => void },
|
|
13
|
+
): void {
|
|
14
|
+
const abort = new AbortController();
|
|
15
|
+
pending.hostAbort = abort;
|
|
16
|
+
const cellId = pending.input.cellId;
|
|
17
|
+
let finished = false;
|
|
18
|
+
// Output from an executor that was aborted, or after its outcome, belongs to no cell.
|
|
19
|
+
const emit = (message: KernelToHostMessage): void => {
|
|
20
|
+
if (!finished && !abort.signal.aborted) io.emit(message);
|
|
21
|
+
};
|
|
22
|
+
// Calling the executor inside a promise keeps a synchronous throw in this cell instead of failing the queue.
|
|
23
|
+
const done = new Promise<HostCellOutcome>((resolve) => resolve(host({ signal: abort.signal, emit }))).then(
|
|
24
|
+
(outcome): ResultMessage =>
|
|
25
|
+
outcome.ok
|
|
26
|
+
? {
|
|
27
|
+
type: "result",
|
|
28
|
+
cellId,
|
|
29
|
+
ok: true,
|
|
30
|
+
durationMs: 0,
|
|
31
|
+
...(outcome.valueRepr === undefined ? {} : { valueRepr: outcome.valueRepr }),
|
|
32
|
+
}
|
|
33
|
+
: { type: "result", cellId, ok: false, error: outcome.error, durationMs: 0 },
|
|
34
|
+
(error: unknown): ResultMessage =>
|
|
35
|
+
failedPythonResult(cellId, error instanceof Error ? error.message : String(error)),
|
|
36
|
+
);
|
|
37
|
+
pending.hostDone = done;
|
|
38
|
+
void done.then((result) => {
|
|
39
|
+
finished = true;
|
|
40
|
+
if (!abort.signal.aborted) io.settle(result);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
@@ -3,7 +3,8 @@ import type { BridgeConnectionConfig, KernelToHostMessage } from "../../bridge/p
|
|
|
3
3
|
import type { EvalKernelRunInput } from "../../tool/types.ts";
|
|
4
4
|
import type { SessionEnvironment } from "../session-env.ts";
|
|
5
5
|
import type { KernelLifecycle } from "../shared/kernel-death.ts";
|
|
6
|
-
import type {
|
|
6
|
+
import type { PeerKernelToolsDescribe } from "./kernel-tools-host.ts";
|
|
7
|
+
import type { KernelSpawnProcess, KillProcessGroup } from "./process.ts";
|
|
7
8
|
import type { PythonStartupStage } from "./startup.ts";
|
|
8
9
|
import type { PythonTransportResult } from "./transport.ts";
|
|
9
10
|
|
|
@@ -15,14 +16,22 @@ export interface PythonKernelStartOptions extends KernelLifecycle {
|
|
|
15
16
|
readonly env?: NodeJS.ProcessEnv;
|
|
16
17
|
/** Per-session PI_* values merged into the interpreter environment at spawn. */
|
|
17
18
|
readonly sessionEnv?: SessionEnvironment;
|
|
18
|
-
/**
|
|
19
|
+
/** How long a starting interpreter may show no stage change, output or CPU use before startup fails. */
|
|
19
20
|
readonly startupTimeoutMs?: number;
|
|
21
|
+
/** Total startup backstop for an interpreter that stays busy but never becomes ready. */
|
|
22
|
+
readonly startupCeilingMs?: number;
|
|
23
|
+
/** Reads the interpreter's CPU time; the default reads the process group (Unix) or the process (Windows). */
|
|
24
|
+
readonly readCpuTime?: (pid: number | undefined) => bigint | undefined;
|
|
25
|
+
/** Signals the interpreter's process group when it is retired; the default is `process.kill(-pid, signal)`. */
|
|
26
|
+
readonly killProcessGroup?: KillProcessGroup;
|
|
20
27
|
/** Observes bootstrap control events without mixing them into cell output callbacks. */
|
|
21
28
|
readonly onStartupProgress?: (stage: PythonStartupStage) => void;
|
|
22
29
|
readonly onMessage?: (message: KernelToHostMessage) => void;
|
|
23
30
|
readonly spawnProcess?: KernelSpawnProcess;
|
|
24
31
|
/** Post-cell collection, notice, and ceiling thresholds sent on `init`; absent leaves memory unmanaged. */
|
|
25
32
|
readonly memory?: KernelMemoryThresholds;
|
|
33
|
+
/** The JavaScript kernel's describe, so a name defined in both languages is a describe-time collision. */
|
|
34
|
+
readonly peerKernelToolsDescribe?: PeerKernelToolsDescribe;
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
export type PythonKernelRunOptions = EvalKernelRunInput;
|
|
@@ -39,4 +48,6 @@ export interface PendingRun {
|
|
|
39
48
|
interruptReason?: string;
|
|
40
49
|
/** Set while an interrupt outcome is pending; resolved once the kernel knows whether state survived. */
|
|
41
50
|
resolveStateRetained?: (retained: boolean) => void;
|
|
51
|
+
hostAbort?: AbortController;
|
|
52
|
+
hostDone?: Promise<ResultMessage>;
|
|
42
53
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
|
+
import { kernelToolError } from "../js/kernel-tools-errors.ts";
|
|
3
|
+
import type {
|
|
4
|
+
KernelToolsDescribeResult,
|
|
5
|
+
KernelToolsInvokeOptions,
|
|
6
|
+
KernelToolsInvokeRequest,
|
|
7
|
+
} from "../js/kernel-tools-types.ts";
|
|
8
|
+
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
9
|
+
|
|
10
|
+
export type KernelToolReplyEvent = {
|
|
11
|
+
readonly requestId: string;
|
|
12
|
+
readonly ok: boolean;
|
|
13
|
+
readonly code?: string;
|
|
14
|
+
readonly message?: string;
|
|
15
|
+
readonly output?: string;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function replyEvent(message: Extract<KernelToHostMessage, { type: "kernel-tool-invoke-reply" }>): KernelToolReplyEvent {
|
|
19
|
+
return {
|
|
20
|
+
requestId: message.requestId,
|
|
21
|
+
ok: message.ok,
|
|
22
|
+
...(message.ok ? {} : { message: message.error.message }),
|
|
23
|
+
...(!message.ok && message.error.code !== undefined ? { code: message.error.code } : {}),
|
|
24
|
+
...(message.output === undefined ? {} : { output: message.output }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type PeerKernelToolsDescribe = (names: readonly string[]) => Promise<KernelToolsDescribeResult> | undefined;
|
|
29
|
+
|
|
30
|
+
/** The host half of Python kernel tools: frames over the live transport, names, and generation fencing. */
|
|
31
|
+
export class PythonKernelTools {
|
|
32
|
+
readonly #pump: KernelToolHostPump;
|
|
33
|
+
readonly #peerDescribe: PeerKernelToolsDescribe | undefined;
|
|
34
|
+
#names: readonly string[] = [];
|
|
35
|
+
|
|
36
|
+
constructor(options: {
|
|
37
|
+
readonly post: (message: HostToKernelMessage) => void;
|
|
38
|
+
readonly isOpen: () => boolean;
|
|
39
|
+
readonly peerDescribe?: PeerKernelToolsDescribe;
|
|
40
|
+
}) {
|
|
41
|
+
this.#pump = new KernelToolHostPump(options.post, options.isOpen);
|
|
42
|
+
this.#peerDescribe = options.peerDescribe;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get events(): EventTarget {
|
|
46
|
+
return this.#pump.events;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
listNames(): readonly string[] {
|
|
50
|
+
return this.#names;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
consume(message: KernelToHostMessage): boolean {
|
|
54
|
+
if (message.type === "kernel-tools-defined") {
|
|
55
|
+
this.#names = [...message.names];
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
if (message.type === "kernel-tool-invoke-reply") {
|
|
59
|
+
// Published even when the waiter is gone (a cancelled call), so its explicit outcome stays observable.
|
|
60
|
+
this.#pump.events.dispatchEvent(
|
|
61
|
+
new CustomEvent<KernelToolReplyEvent>("kernelToolReply", { detail: replyEvent(message) }),
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
return this.#pump.consume(message);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The interpreter is gone (restart, reset, close, death): its definitions and in-flight calls are too. */
|
|
68
|
+
retire(reason: "kernel_tool_stale" | "tools_unavailable", message: string): void {
|
|
69
|
+
this.#names = [];
|
|
70
|
+
this.#pump.rejectAll(kernelToolError(reason, message));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async describe(names: readonly string[]): Promise<KernelToolsDescribeResult> {
|
|
74
|
+
const own = await this.#pump.describe(names);
|
|
75
|
+
const peer = await this.#peerDescribe?.(names)?.catch(() => undefined);
|
|
76
|
+
if (!peer) return own;
|
|
77
|
+
const definedInJs = new Set(peer.results.filter((entry) => entry.ok).map((entry) => entry.name));
|
|
78
|
+
return {
|
|
79
|
+
results: own.results.map((entry) =>
|
|
80
|
+
entry.ok && definedInJs.has(entry.name)
|
|
81
|
+
? {
|
|
82
|
+
name: entry.name,
|
|
83
|
+
ok: false,
|
|
84
|
+
error: {
|
|
85
|
+
code: "tool_name_collision",
|
|
86
|
+
message: `Kernel tool ${entry.name} is defined in both js and py`,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
: entry,
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async invoke(request: KernelToolsInvokeRequest, options?: AbortSignal | KernelToolsInvokeOptions): Promise<unknown> {
|
|
95
|
+
return await this.#pump.invoke(request, options);
|
|
96
|
+
}
|
|
97
|
+
}
|
package/src/kernels/py/kernel.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// allow: SIZE_OK — one persistent Python lifecycle state machine owns queue, generation, and retirement.
|
|
2
2
|
import type { KernelInterruptHandle, PendingCell } from "../../tool/types.ts";
|
|
3
|
-
import type {
|
|
4
|
-
|
|
3
|
+
import type {
|
|
4
|
+
KernelToolsDescribeResult,
|
|
5
|
+
KernelToolsInvokeOptions,
|
|
6
|
+
KernelToolsInvokeRequest,
|
|
7
|
+
} from "../js/kernel-tools-types.ts";
|
|
5
8
|
import { describeExit } from "../shared/kernel-death.ts";
|
|
6
9
|
import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
|
|
7
10
|
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
11
|
+
import { runHostCell } from "./host-cell.ts";
|
|
8
12
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
9
|
-
import {
|
|
13
|
+
import { PythonKernelTools } from "./kernel-tools-host.ts";
|
|
14
|
+
import { pythonStartupCeilingMs, pythonStartupHangGuardMs } from "./startup.ts";
|
|
10
15
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
11
16
|
|
|
12
17
|
export type { PythonKernelRunOptions, PythonKernelStartOptions } from "./kernel-contract.ts";
|
|
@@ -14,6 +19,15 @@ export type { KernelChild, KernelSpawnOptions, KernelSpawnProcess } from "./proc
|
|
|
14
19
|
|
|
15
20
|
const interruptEscalationMs = 5_000;
|
|
16
21
|
|
|
22
|
+
// Every interpreter this process starts gets its own generation, so a kernel tool descriptor taken from one
|
|
23
|
+
// never resolves in another, whether it was replaced by reset, by a lazy restart after a crash, or by a new
|
|
24
|
+
// kernel instance an owner created.
|
|
25
|
+
let lastInterpreterGeneration = 0;
|
|
26
|
+
function nextInterpreterGeneration(): number {
|
|
27
|
+
lastInterpreterGeneration += 1;
|
|
28
|
+
return lastInterpreterGeneration;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
export class PythonKernel {
|
|
18
32
|
readonly #options: PythonKernelStartOptions;
|
|
19
33
|
#transport: PythonKernelTransport | null = null;
|
|
@@ -28,11 +42,17 @@ export class PythonKernel {
|
|
|
28
42
|
readonly #memory: KernelMemoryHost | null;
|
|
29
43
|
#generation = 0;
|
|
30
44
|
#closed = false;
|
|
45
|
+
readonly #tools: PythonKernelTools;
|
|
31
46
|
#dead = false;
|
|
32
47
|
|
|
33
48
|
private constructor(options: PythonKernelStartOptions) {
|
|
34
49
|
this.#options = options;
|
|
35
50
|
this.#memory = options.memory === undefined ? null : new KernelMemoryHost("py", options.memory);
|
|
51
|
+
this.#tools = new PythonKernelTools({
|
|
52
|
+
post: (message) => this.#transport?.post(message),
|
|
53
|
+
isOpen: () => this.#transport !== null && !this.#closed,
|
|
54
|
+
...(options.peerKernelToolsDescribe === undefined ? {} : { peerDescribe: options.peerKernelToolsDescribe }),
|
|
55
|
+
});
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
static async start(options: PythonKernelStartOptions): Promise<PythonKernel> {
|
|
@@ -42,7 +62,11 @@ export class PythonKernel {
|
|
|
42
62
|
}
|
|
43
63
|
|
|
44
64
|
listKernelToolNames(): readonly string[] {
|
|
45
|
-
return
|
|
65
|
+
return this.#tools.listNames();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get kernelToolEvents(): EventTarget {
|
|
69
|
+
return this.#tools.events;
|
|
46
70
|
}
|
|
47
71
|
|
|
48
72
|
isAlive(): boolean {
|
|
@@ -57,12 +81,15 @@ export class PythonKernel {
|
|
|
57
81
|
});
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
describeKernelTools(
|
|
61
|
-
return
|
|
84
|
+
describeKernelTools(names: readonly string[]): Promise<KernelToolsDescribeResult> {
|
|
85
|
+
return this.#tools.describe(names);
|
|
62
86
|
}
|
|
63
87
|
|
|
64
|
-
invokeKernelTool(
|
|
65
|
-
|
|
88
|
+
invokeKernelTool(
|
|
89
|
+
request: KernelToolsInvokeRequest,
|
|
90
|
+
options?: AbortSignal | KernelToolsInvokeOptions,
|
|
91
|
+
): Promise<unknown> {
|
|
92
|
+
return this.#tools.invoke(request, options);
|
|
66
93
|
}
|
|
67
94
|
|
|
68
95
|
run(input: PythonKernelRunOptions): Promise<ResultMessage> {
|
|
@@ -104,6 +131,11 @@ export class PythonKernel {
|
|
|
104
131
|
}
|
|
105
132
|
}
|
|
106
133
|
const active = this.#active;
|
|
134
|
+
if (active?.hostAbort && active.interruptReason === undefined) {
|
|
135
|
+
active.interruptReason = reason;
|
|
136
|
+
void this.#stopHostEntry(active, failedPythonResult(active.input.cellId, "Eval interrupted"));
|
|
137
|
+
return { stateRetained: Promise.resolve(true) };
|
|
138
|
+
}
|
|
107
139
|
const transport = this.#transport;
|
|
108
140
|
if (!active || !transport || active.interruptReason !== undefined)
|
|
109
141
|
return { stateRetained: Promise.resolve(true) };
|
|
@@ -125,6 +157,7 @@ export class PythonKernel {
|
|
|
125
157
|
if (this.#failure) throw this.#failure;
|
|
126
158
|
if (this.#closed) throw new Error("Python kernel is closed");
|
|
127
159
|
const generation = ++this.#generation;
|
|
160
|
+
this.#tools.retire("kernel_tool_stale", "Python kernel was reset; its kernel tools are gone");
|
|
128
161
|
const prior = this.#starting;
|
|
129
162
|
const operation = (async () => {
|
|
130
163
|
await prior?.catch(() => undefined);
|
|
@@ -150,6 +183,7 @@ export class PythonKernel {
|
|
|
150
183
|
if (this.#closePromise) return await this.#closePromise;
|
|
151
184
|
this.#closed = true;
|
|
152
185
|
this.#generation += 1;
|
|
186
|
+
this.#tools.retire("tools_unavailable", "Python kernel is closed");
|
|
153
187
|
this.#settleAllPending("Python kernel closed");
|
|
154
188
|
const starting = this.#starting;
|
|
155
189
|
this.#closePromise = (async () => {
|
|
@@ -201,6 +235,14 @@ export class PythonKernel {
|
|
|
201
235
|
const timeoutMs = pending.input.timeoutMs;
|
|
202
236
|
if (timeoutMs !== undefined)
|
|
203
237
|
pending.timeoutTimer = setTimeout(() => this.#timeoutRun(pending, timeoutMs), timeoutMs);
|
|
238
|
+
const host = pending.input.host;
|
|
239
|
+
if (host !== undefined) {
|
|
240
|
+
runHostCell(pending, host, {
|
|
241
|
+
emit: (message) => (pending.input.onMessage ?? this.#options.onMessage)?.(message),
|
|
242
|
+
settle: (result) => this.#settleRun(pending, result),
|
|
243
|
+
});
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
204
246
|
try {
|
|
205
247
|
this.#transport?.run({
|
|
206
248
|
...pending.input,
|
|
@@ -219,11 +261,40 @@ export class PythonKernel {
|
|
|
219
261
|
|
|
220
262
|
#timeoutRun(pending: PendingRun, timeoutMs: number): void {
|
|
221
263
|
if (this.#active !== pending) return;
|
|
264
|
+
const timedOut = failedPythonResult(pending.input.cellId, `Python kernel timed out after ${timeoutMs}ms`);
|
|
265
|
+
if (pending.hostAbort) {
|
|
266
|
+
void this.#stopHostEntry(pending, timedOut);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
222
269
|
if (this.#transport) void this.#beginRetirement(this.#transport).catch(() => undefined);
|
|
223
|
-
this.#settleRun(
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
270
|
+
this.#settleRun(pending, timedOut);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Aborts a running host entry and settles it only once its executor has stopped (or after the same bound the
|
|
275
|
+
* interpreter gets before escalation), so the next queue entry never overlaps the aborted host work. An
|
|
276
|
+
* executor that finished successfully despite the abort reports that outcome: its work did commit.
|
|
277
|
+
*/
|
|
278
|
+
async #stopHostEntry(pending: PendingRun, stopped: ResultMessage): Promise<void> {
|
|
279
|
+
pending.hostAbort?.abort();
|
|
280
|
+
const done = pending.hostDone;
|
|
281
|
+
let bound: ReturnType<typeof setTimeout> | undefined;
|
|
282
|
+
const outcome =
|
|
283
|
+
done === undefined
|
|
284
|
+
? undefined
|
|
285
|
+
: await Promise.race([
|
|
286
|
+
done,
|
|
287
|
+
new Promise<undefined>((resolve) => {
|
|
288
|
+
bound = setTimeout(() => resolve(undefined), interruptEscalationMs);
|
|
289
|
+
}),
|
|
290
|
+
]);
|
|
291
|
+
if (bound !== undefined) clearTimeout(bound);
|
|
292
|
+
if (outcome?.ok === true) {
|
|
293
|
+
pending.interruptReason = undefined;
|
|
294
|
+
this.#settleRun(pending, outcome);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
this.#settleRun(pending, stopped);
|
|
227
298
|
}
|
|
228
299
|
|
|
229
300
|
async #ensureStarted(): Promise<void> {
|
|
@@ -240,6 +311,7 @@ export class PythonKernel {
|
|
|
240
311
|
...this.#options,
|
|
241
312
|
onMessage: (message) => {
|
|
242
313
|
if (message.type === "result") return;
|
|
314
|
+
if (generation === this.#generation && this.#tools.consume(message)) return;
|
|
243
315
|
const callback =
|
|
244
316
|
message.type === "ready" || message.type === "init-failed" || message.type === "closed"
|
|
245
317
|
? this.#options.onMessage
|
|
@@ -247,6 +319,8 @@ export class PythonKernel {
|
|
|
247
319
|
callback?.(message);
|
|
248
320
|
},
|
|
249
321
|
startupTimeoutMs: this.#options.startupTimeoutMs ?? pythonStartupHangGuardMs,
|
|
322
|
+
startupCeilingMs: this.#options.startupCeilingMs ?? pythonStartupCeilingMs,
|
|
323
|
+
kernelGeneration: nextInterpreterGeneration(),
|
|
250
324
|
isOwned: () => !this.#closed && generation === this.#generation,
|
|
251
325
|
onRetirementFailure: (transport, error) => {
|
|
252
326
|
if (!this.#transport) this.#transport = transport;
|
|
@@ -281,6 +355,7 @@ export class PythonKernel {
|
|
|
281
355
|
#onExit(transport: PythonKernelTransport, error: Error, reason: string): void {
|
|
282
356
|
if (this.#transport !== transport) return;
|
|
283
357
|
this.#transport = null;
|
|
358
|
+
this.#tools.retire("kernel_tool_stale", "Python kernel died; its kernel tools are gone");
|
|
284
359
|
// With an owner listening, a death is final for this instance: the cells that never started stay
|
|
285
360
|
// queued for `drainPending` and the owner replaces it. Without one it respawns lazily, as before.
|
|
286
361
|
const onDeath = this.#options.onDeath;
|
|
@@ -354,6 +429,7 @@ export class PythonKernel {
|
|
|
354
429
|
}
|
|
355
430
|
|
|
356
431
|
#removePending(pending: PendingRun): void {
|
|
432
|
+
pending.hostAbort?.abort();
|
|
357
433
|
if (pending.timeoutTimer) clearTimeout(pending.timeoutTimer);
|
|
358
434
|
if (pending.escalationTimer) clearTimeout(pending.escalationTimer);
|
|
359
435
|
pending.timeoutTimer = null;
|