@code-yeongyu/senpi-codemode 2026.10.7 → 2026.10.9
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 +38 -0
- package/README.md +67 -5
- package/package.json +5 -4
- package/src/bridge/http-server.ts +9 -1
- package/src/bridge/kernel-tools-protocol.ts +8 -0
- package/src/bridge/protocol.ts +4 -0
- package/src/bridges/eval-virtual-schemas.ts +22 -0
- 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 +247 -0
- package/src/environments/python-environments.ts +85 -0
- package/src/environments/revision-store.ts +110 -0
- package/src/extension/bridge-tool-call.ts +2 -0
- package/src/extension/cell-kernel-tools.ts +28 -0
- package/src/extension/kernel-registration.ts +5 -0
- package/src/extension/kernel-replacement.ts +20 -0
- package/src/extension/runtime-factory.ts +12 -0
- package/src/extension/session-manager-proxy.ts +6 -0
- package/src/extension/session-manager.ts +24 -2
- package/src/index.ts +1 -0
- package/src/kernels/jl/prelude.jl +6 -1
- package/src/kernels/js/context-manager.ts +15 -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/local-module-loader.ts +9 -4
- package/src/kernels/js/worker-runtime.js +3 -1
- package/src/kernels/js/workpool.js +5 -2
- package/src/kernels/py/kernel-contract.ts +11 -2
- package/src/kernels/py/kernel-tools-host.ts +97 -0
- package/src/kernels/py/kernel.ts +48 -10
- package/src/kernels/py/prelude.py +771 -18
- package/src/kernels/py/process.ts +15 -4
- package/src/kernels/py/startup.ts +58 -20
- package/src/kernels/py/transport.ts +49 -8
- package/src/kernels/rb/workpool.rb +7 -1
- 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/cell-source-at-start.ts +18 -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 +1 -1
- package/src/tool/eval-tool-options.ts +3 -0
- package/src/tool/load-cell.ts +73 -0
- package/src/tool/magic-cell-host.ts +65 -0
- package/src/tool/magic-cells.ts +65 -0
- package/src/tool/run-eval-cell.ts +30 -0
- package/src/tool/types.ts +27 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
2
2
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
3
3
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
|
+
import type { KernelToolsCapability } from "../kernels/js/kernel-tools-types.ts";
|
|
4
5
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
5
6
|
import {
|
|
6
7
|
CodemodeSessionDisposedError,
|
|
@@ -106,6 +107,11 @@ export class SessionManagerProxy implements CodemodeSessionManager, EvalExecutio
|
|
|
106
107
|
this.#current?.releaseKernelListener?.(language, onMessage);
|
|
107
108
|
}
|
|
108
109
|
|
|
110
|
+
bindCellKernelTools(token: string, capability: KernelToolsCapability): () => void {
|
|
111
|
+
// Bound to the generation that runs the cell: a later generation never serves that cell's host calls.
|
|
112
|
+
return this.#current?.bindCellKernelTools?.(token, capability) ?? (() => undefined);
|
|
113
|
+
}
|
|
114
|
+
|
|
109
115
|
async complete(request: CompletionRequest, ctx: ExtensionContext): Promise<CompletionResult> {
|
|
110
116
|
this.assertEvalExecutionAllowed();
|
|
111
117
|
const current = this.#current;
|
|
@@ -6,10 +6,12 @@ import type { CompletionRequest, CompletionResult } from "../completion/handler.
|
|
|
6
6
|
import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
|
|
7
7
|
import { collectOrphanedChildren } from "../host-sdk.ts";
|
|
8
8
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
9
|
+
import type { KernelToolsCapability, KernelToolsDescribeResult } from "../kernels/js/kernel-tools-types.ts";
|
|
9
10
|
import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
|
|
10
11
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
11
12
|
import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
|
|
12
13
|
import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
|
|
14
|
+
import { CellKernelTools } from "./cell-kernel-tools.ts";
|
|
13
15
|
import { parkWhenIdle } from "./idle-parking-kernel.ts";
|
|
14
16
|
import {
|
|
15
17
|
javaScriptKernelMemory,
|
|
@@ -64,6 +66,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
64
66
|
readonly #registrations: SessionKernelRegistrations;
|
|
65
67
|
#kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
|
|
66
68
|
#onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
|
|
69
|
+
readonly #cellKernelTools = new CellKernelTools();
|
|
67
70
|
#context: ExtensionContext | undefined;
|
|
68
71
|
#generation = 0;
|
|
69
72
|
#disposePromise: Promise<void> | undefined;
|
|
@@ -90,7 +93,13 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
90
93
|
}
|
|
91
94
|
|
|
92
95
|
async #call(request: BridgeToolCallRequest): Promise<unknown> {
|
|
93
|
-
return await
|
|
96
|
+
return await this.#cellKernelTools.run(request.cellToken, () =>
|
|
97
|
+
routeBridgeToolCall(this.#options, request, this.#context?.evalHandleHost),
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
bindCellKernelTools(token: string, capability: KernelToolsCapability): () => void {
|
|
102
|
+
return this.#cellKernelTools.bind(token, capability);
|
|
94
103
|
}
|
|
95
104
|
|
|
96
105
|
async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
|
|
@@ -276,7 +285,20 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
276
285
|
...lifecycle,
|
|
277
286
|
};
|
|
278
287
|
const memory = resolveKernelMemoryThresholds(this.#options.settings.memory);
|
|
279
|
-
return await startSubprocessKernel({
|
|
288
|
+
return await startSubprocessKernel({
|
|
289
|
+
language,
|
|
290
|
+
interpreterPath: detected.path,
|
|
291
|
+
memory,
|
|
292
|
+
shared,
|
|
293
|
+
peerKernelToolsDescribe: (names) => this.#javaScriptKernelToolsDescribe(names),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/** Only an already running JavaScript kernel can define a colliding name; none is started for this. */
|
|
298
|
+
#javaScriptKernelToolsDescribe(names: readonly string[]): Promise<KernelToolsDescribeResult> | undefined {
|
|
299
|
+
const js = this.#kernels.get("js");
|
|
300
|
+
if (!js || !("describeKernelTools" in js) || typeof js.describeKernelTools !== "function") return undefined;
|
|
301
|
+
return (js.describeKernelTools as (names: readonly string[]) => Promise<KernelToolsDescribeResult>)(names);
|
|
280
302
|
}
|
|
281
303
|
|
|
282
304
|
#foreignKernelToolNames(): string[] {
|
package/src/index.ts
CHANGED
|
@@ -157,6 +157,7 @@ export default function senpiCodemode(pi: CodemodeExtensionAPI, options: SenpiCo
|
|
|
157
157
|
complete,
|
|
158
158
|
settings: runtime.settings,
|
|
159
159
|
artifactsDir: runtime.artifactsDir,
|
|
160
|
+
...(runtime.pythonEnvironments === undefined ? {} : { pythonEnvironments: runtime.pythonEnvironments }),
|
|
160
161
|
cellManager,
|
|
161
162
|
handles,
|
|
162
163
|
executionTracker: manager,
|
|
@@ -210,9 +210,14 @@ function Base.getproperty(pool::SenpiWorkpool, name::Symbol)
|
|
|
210
210
|
getfield(pool, name)
|
|
211
211
|
end
|
|
212
212
|
|
|
213
|
-
function workpool(agent::AbstractDict, name::AbstractString; mode=nothing)
|
|
213
|
+
function workpool(agent::AbstractDict, name::AbstractString; mode=nothing, tools=nothing)
|
|
214
214
|
args = Dict{String, Any}("op" => "create", "agent" => agent, "name" => name)
|
|
215
215
|
mode !== nothing && (args["mode"] = mode)
|
|
216
|
+
if tools !== nothing
|
|
217
|
+
(tools isa AbstractVector && all(name -> name isa AbstractString, tools)) ||
|
|
218
|
+
throw(SenpiBridgeError("workpool(tools=...) takes a vector of tool names; got $(typeof(tools))", "invalid_tools"))
|
|
219
|
+
args["tools"] = collect(String, tools)
|
|
220
|
+
end
|
|
216
221
|
result = senpi_workpool_call(args)
|
|
217
222
|
details = get(result, "details", nothing)
|
|
218
223
|
if details isa AbstractDict
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { HostToKernelMessage, KernelToHostMessage } from "../../bridge/protocol.ts";
|
|
2
2
|
import { CHILD_LIFECYCLE_OP, INTERRUPT_ACK_OP } from "../../bridge/reserved.ts";
|
|
3
3
|
import type { KernelInterruptHandle } from "../../tool/types.ts";
|
|
4
|
+
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
4
5
|
import { KernelToolHostPump } from "../shared/kernel-tools-pump.ts";
|
|
5
6
|
import { ActiveCellControl } from "./active-cell-control.ts";
|
|
6
7
|
import { DEFAULT_INTERRUPT_BOUNDS, type WorkerRetirement } from "./interrupt-bounds.ts";
|
|
@@ -195,6 +196,19 @@ export class JavaScriptKernel {
|
|
|
195
196
|
if (this.#lifecycle !== "open" || this.#runs.active || !this.#slot.present) return;
|
|
196
197
|
const next = this.#runs.startNext(performance.now());
|
|
197
198
|
if (!next) return;
|
|
199
|
+
const input = inputAtStart(next.input);
|
|
200
|
+
if ("refused" in input) {
|
|
201
|
+
this.#runs.releaseActive(next);
|
|
202
|
+
this.#runs.settle(next, {
|
|
203
|
+
type: "result",
|
|
204
|
+
cellId: next.input.cellId,
|
|
205
|
+
ok: false,
|
|
206
|
+
error: { message: input.refused },
|
|
207
|
+
durationMs: 0,
|
|
208
|
+
});
|
|
209
|
+
this.#startNext();
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
198
212
|
this.#activeCell.arm(next);
|
|
199
213
|
this.#slot.postMessage({
|
|
200
214
|
type: "kernel-tools-names",
|
|
@@ -204,7 +218,7 @@ export class JavaScriptKernel {
|
|
|
204
218
|
this.#slot.postMessage({
|
|
205
219
|
type: "run",
|
|
206
220
|
cellId: next.input.cellId,
|
|
207
|
-
code: this.#moduleLoader.prepareCell(
|
|
221
|
+
code: this.#moduleLoader.prepareCell(input.code, input.kernelPreludes, input.sourceFile),
|
|
208
222
|
timeoutMs: next.input.timeoutMs,
|
|
209
223
|
});
|
|
210
224
|
}
|
|
@@ -8,6 +8,8 @@ export const KERNEL_TOOL_ERROR_CODES = [
|
|
|
8
8
|
"kernel_tool_failed",
|
|
9
9
|
"kernel_tool_recursion",
|
|
10
10
|
"kernel_tool_host_denied",
|
|
11
|
+
"kernel_tool_cancelled",
|
|
12
|
+
"kernel_tool_loop_mismatch",
|
|
11
13
|
] as const;
|
|
12
14
|
|
|
13
15
|
export type KernelToolErrorCode = (typeof KERNEL_TOOL_ERROR_CODES)[number];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { join, resolve, sep } from "node:path";
|
|
1
|
+
import { dirname, join, resolve, sep } from "node:path";
|
|
2
2
|
import { pathToFileURL } from "node:url";
|
|
3
3
|
import type { KernelPreludeContribution } from "@code-yeongyu/senpi";
|
|
4
4
|
import type { BridgeConnectionConfig } from "../../bridge/protocol.ts";
|
|
@@ -121,8 +121,13 @@ export class LocalModuleLoader {
|
|
|
121
121
|
this.#prelude = loaderPrelude(runtimeContext(options));
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
prepareCell(code: string, contributions: readonly KernelPreludeContribution[] = []): string {
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
prepareCell(code: string, contributions: readonly KernelPreludeContribution[] = [], sourceFile?: string): string {
|
|
125
|
+
// A %load cell resolves its relative imports from its own file's directory for that cell only.
|
|
126
|
+
const base =
|
|
127
|
+
sourceFile === undefined
|
|
128
|
+
? ""
|
|
129
|
+
: `\nglobalThis.__senpi_module_context__ = { ...globalThis.__senpi_module_context__, cwdUrl: ${JSON.stringify(directoryUrl(dirname(sourceFile)))} };`;
|
|
130
|
+
const prelude = `${this.#prelude}${base}\n${contributionPrelude(this.#contributions.plan(contributions))}`;
|
|
131
|
+
return `${PREPARED_CELL_PREFIX}${JSON.stringify({ prelude, code: rewriteImports(code), ...(sourceFile === undefined ? {} : { sourceFile }) })}`;
|
|
127
132
|
}
|
|
128
133
|
}
|
|
@@ -65,13 +65,15 @@ export class JsWorkerRuntime {
|
|
|
65
65
|
try {
|
|
66
66
|
let prelude = "";
|
|
67
67
|
let cellCode = code;
|
|
68
|
+
let sourceName = cellId;
|
|
68
69
|
if (code.startsWith(PREPARED_CELL_PREFIX)) {
|
|
69
70
|
const prepared = JSON.parse(code.slice(PREPARED_CELL_PREFIX.length));
|
|
70
71
|
if (!isPlainObject(prepared) || typeof prepared.prelude !== "string" || typeof prepared.code !== "string") throw new Error("Invalid prepared JavaScript cell payload");
|
|
71
72
|
({ prelude, code: cellCode } = prepared);
|
|
73
|
+
if (typeof prepared.sourceFile === "string") sourceName = prepared.sourceFile;
|
|
72
74
|
}
|
|
73
75
|
if (prelude) indirectEval(prelude, PRELUDE_SOURCE_URL);
|
|
74
|
-
const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)),
|
|
76
|
+
const value = await awaitMaybePromise(indirectEval(bindKernelBun(wrapUserCode(cellCode)), sourceName));
|
|
75
77
|
await this.#drainPendingDisplays();
|
|
76
78
|
return value;
|
|
77
79
|
} finally {
|
|
@@ -4,8 +4,11 @@ import { kernelToolError } from "./kernel-tools-errors.js";
|
|
|
4
4
|
// Only host-tool sugar: no admission, worker, or queue state belongs to the kernel.
|
|
5
5
|
export async function createWorkpool(callTool, agent, name, options = {}) {
|
|
6
6
|
if (inKernelToolInvoke()) throw kernelToolError("kernel_tool_recursion", "kernel tools may not invoke workpool()");
|
|
7
|
-
if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "mode")) {
|
|
8
|
-
throw new TypeError("workpool() options only accept mode");
|
|
7
|
+
if (options === null || typeof options !== "object" || Array.isArray(options) || Object.keys(options).some(key => key !== "mode" && key !== "tools")) {
|
|
8
|
+
throw new TypeError("workpool() options only accept mode and tools");
|
|
9
|
+
}
|
|
10
|
+
if (options.tools !== undefined && (!Array.isArray(options.tools) || !options.tools.every(name => typeof name === "string"))) {
|
|
11
|
+
throw workpoolError("invalid_tools", `workpool() tools takes an array of tool names; got ${Array.isArray(options.tools) ? "an array with a non-string" : typeof options.tools}`);
|
|
9
12
|
}
|
|
10
13
|
const call = async args => {
|
|
11
14
|
try {
|
|
@@ -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;
|
|
@@ -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,13 +1,18 @@
|
|
|
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";
|
|
8
|
+
import { inputAtStart } from "../shared/cell-source-at-start.ts";
|
|
5
9
|
import { describeExit } from "../shared/kernel-death.ts";
|
|
6
10
|
import { KernelMemoryHost } from "../shared/kernel-memory-host.ts";
|
|
7
11
|
import { KernelPreludeTracker } from "../shared/kernel-prelude-plan.ts";
|
|
8
12
|
import { runHostCell } from "./host-cell.ts";
|
|
9
13
|
import type { PendingRun, PythonKernelRunOptions, PythonKernelStartOptions, ResultMessage } from "./kernel-contract.ts";
|
|
10
|
-
import {
|
|
14
|
+
import { PythonKernelTools } from "./kernel-tools-host.ts";
|
|
15
|
+
import { pythonStartupCeilingMs, pythonStartupHangGuardMs } from "./startup.ts";
|
|
11
16
|
import { failedPythonResult, PythonKernelTransport } from "./transport.ts";
|
|
12
17
|
|
|
13
18
|
export type { PythonKernelRunOptions, PythonKernelStartOptions } from "./kernel-contract.ts";
|
|
@@ -15,6 +20,15 @@ export type { KernelChild, KernelSpawnOptions, KernelSpawnProcess } from "./proc
|
|
|
15
20
|
|
|
16
21
|
const interruptEscalationMs = 5_000;
|
|
17
22
|
|
|
23
|
+
// Every interpreter this process starts gets its own generation, so a kernel tool descriptor taken from one
|
|
24
|
+
// never resolves in another, whether it was replaced by reset, by a lazy restart after a crash, or by a new
|
|
25
|
+
// kernel instance an owner created.
|
|
26
|
+
let lastInterpreterGeneration = 0;
|
|
27
|
+
function nextInterpreterGeneration(): number {
|
|
28
|
+
lastInterpreterGeneration += 1;
|
|
29
|
+
return lastInterpreterGeneration;
|
|
30
|
+
}
|
|
31
|
+
|
|
18
32
|
export class PythonKernel {
|
|
19
33
|
readonly #options: PythonKernelStartOptions;
|
|
20
34
|
#transport: PythonKernelTransport | null = null;
|
|
@@ -29,11 +43,17 @@ export class PythonKernel {
|
|
|
29
43
|
readonly #memory: KernelMemoryHost | null;
|
|
30
44
|
#generation = 0;
|
|
31
45
|
#closed = false;
|
|
46
|
+
readonly #tools: PythonKernelTools;
|
|
32
47
|
#dead = false;
|
|
33
48
|
|
|
34
49
|
private constructor(options: PythonKernelStartOptions) {
|
|
35
50
|
this.#options = options;
|
|
36
51
|
this.#memory = options.memory === undefined ? null : new KernelMemoryHost("py", options.memory);
|
|
52
|
+
this.#tools = new PythonKernelTools({
|
|
53
|
+
post: (message) => this.#transport?.post(message),
|
|
54
|
+
isOpen: () => this.#transport !== null && !this.#closed,
|
|
55
|
+
...(options.peerKernelToolsDescribe === undefined ? {} : { peerDescribe: options.peerKernelToolsDescribe }),
|
|
56
|
+
});
|
|
37
57
|
}
|
|
38
58
|
|
|
39
59
|
static async start(options: PythonKernelStartOptions): Promise<PythonKernel> {
|
|
@@ -43,7 +63,11 @@ export class PythonKernel {
|
|
|
43
63
|
}
|
|
44
64
|
|
|
45
65
|
listKernelToolNames(): readonly string[] {
|
|
46
|
-
return
|
|
66
|
+
return this.#tools.listNames();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
get kernelToolEvents(): EventTarget {
|
|
70
|
+
return this.#tools.events;
|
|
47
71
|
}
|
|
48
72
|
|
|
49
73
|
isAlive(): boolean {
|
|
@@ -58,12 +82,15 @@ export class PythonKernel {
|
|
|
58
82
|
});
|
|
59
83
|
}
|
|
60
84
|
|
|
61
|
-
describeKernelTools(
|
|
62
|
-
return
|
|
85
|
+
describeKernelTools(names: readonly string[]): Promise<KernelToolsDescribeResult> {
|
|
86
|
+
return this.#tools.describe(names);
|
|
63
87
|
}
|
|
64
88
|
|
|
65
|
-
invokeKernelTool(
|
|
66
|
-
|
|
89
|
+
invokeKernelTool(
|
|
90
|
+
request: KernelToolsInvokeRequest,
|
|
91
|
+
options?: AbortSignal | KernelToolsInvokeOptions,
|
|
92
|
+
): Promise<unknown> {
|
|
93
|
+
return this.#tools.invoke(request, options);
|
|
67
94
|
}
|
|
68
95
|
|
|
69
96
|
run(input: PythonKernelRunOptions): Promise<ResultMessage> {
|
|
@@ -131,6 +158,7 @@ export class PythonKernel {
|
|
|
131
158
|
if (this.#failure) throw this.#failure;
|
|
132
159
|
if (this.#closed) throw new Error("Python kernel is closed");
|
|
133
160
|
const generation = ++this.#generation;
|
|
161
|
+
this.#tools.retire("kernel_tool_stale", "Python kernel was reset; its kernel tools are gone");
|
|
134
162
|
const prior = this.#starting;
|
|
135
163
|
const operation = (async () => {
|
|
136
164
|
await prior?.catch(() => undefined);
|
|
@@ -156,6 +184,7 @@ export class PythonKernel {
|
|
|
156
184
|
if (this.#closePromise) return await this.#closePromise;
|
|
157
185
|
this.#closed = true;
|
|
158
186
|
this.#generation += 1;
|
|
187
|
+
this.#tools.retire("tools_unavailable", "Python kernel is closed");
|
|
159
188
|
this.#settleAllPending("Python kernel closed");
|
|
160
189
|
const starting = this.#starting;
|
|
161
190
|
this.#closePromise = (async () => {
|
|
@@ -215,10 +244,15 @@ export class PythonKernel {
|
|
|
215
244
|
});
|
|
216
245
|
return;
|
|
217
246
|
}
|
|
247
|
+
const input = inputAtStart(pending.input);
|
|
248
|
+
if ("refused" in input) {
|
|
249
|
+
this.#settleRun(pending, failedPythonResult(pending.input.cellId, input.refused));
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
218
252
|
try {
|
|
219
253
|
this.#transport?.run({
|
|
220
|
-
...
|
|
221
|
-
preludePlan: this.#preludes.plan(
|
|
254
|
+
...input,
|
|
255
|
+
preludePlan: this.#preludes.plan(input.kernelPreludes ?? []),
|
|
222
256
|
});
|
|
223
257
|
} catch (error) {
|
|
224
258
|
const failure = error instanceof Error ? error : new Error(String(error));
|
|
@@ -283,6 +317,7 @@ export class PythonKernel {
|
|
|
283
317
|
...this.#options,
|
|
284
318
|
onMessage: (message) => {
|
|
285
319
|
if (message.type === "result") return;
|
|
320
|
+
if (generation === this.#generation && this.#tools.consume(message)) return;
|
|
286
321
|
const callback =
|
|
287
322
|
message.type === "ready" || message.type === "init-failed" || message.type === "closed"
|
|
288
323
|
? this.#options.onMessage
|
|
@@ -290,6 +325,8 @@ export class PythonKernel {
|
|
|
290
325
|
callback?.(message);
|
|
291
326
|
},
|
|
292
327
|
startupTimeoutMs: this.#options.startupTimeoutMs ?? pythonStartupHangGuardMs,
|
|
328
|
+
startupCeilingMs: this.#options.startupCeilingMs ?? pythonStartupCeilingMs,
|
|
329
|
+
kernelGeneration: nextInterpreterGeneration(),
|
|
293
330
|
isOwned: () => !this.#closed && generation === this.#generation,
|
|
294
331
|
onRetirementFailure: (transport, error) => {
|
|
295
332
|
if (!this.#transport) this.#transport = transport;
|
|
@@ -324,6 +361,7 @@ export class PythonKernel {
|
|
|
324
361
|
#onExit(transport: PythonKernelTransport, error: Error, reason: string): void {
|
|
325
362
|
if (this.#transport !== transport) return;
|
|
326
363
|
this.#transport = null;
|
|
364
|
+
this.#tools.retire("kernel_tool_stale", "Python kernel died; its kernel tools are gone");
|
|
327
365
|
// With an owner listening, a death is final for this instance: the cells that never started stay
|
|
328
366
|
// queued for `drainPending` and the owner replaces it. Without one it respawns lazily, as before.
|
|
329
367
|
const onDeath = this.#options.onDeath;
|