@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.
Files changed (72) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +20 -2
  3. package/package.json +5 -4
  4. package/src/bridge/kernel-tools-protocol.ts +8 -0
  5. package/src/bridge/reserved.ts +10 -0
  6. package/src/bridges/eval-virtual-schemas.ts +100 -0
  7. package/src/bridges/handle-bridge.ts +72 -0
  8. package/src/bridges/reserved-dispatch.ts +20 -2
  9. package/src/bridges/schema-bridge.ts +4 -0
  10. package/src/completion/handle.ts +69 -0
  11. package/src/completion/tool-bridge.ts +31 -12
  12. package/src/environments/editable-check.ts +116 -0
  13. package/src/environments/install-lock.ts +223 -0
  14. package/src/environments/py-environment.ts +98 -0
  15. package/src/environments/py-installer.ts +187 -0
  16. package/src/environments/revision-store.ts +95 -0
  17. package/src/extension/bridge-dispatch.ts +41 -0
  18. package/src/extension/bridge-tool-call.ts +5 -1
  19. package/src/extension/host-facts.ts +18 -0
  20. package/src/extension/kernel-registration.ts +5 -0
  21. package/src/extension/removed-tool-hints.ts +15 -0
  22. package/src/extension/runtime-factory.ts +3 -0
  23. package/src/extension/session-manager-contract.ts +3 -0
  24. package/src/extension/session-manager.ts +32 -5
  25. package/src/extension/start-recovery.ts +111 -0
  26. package/src/handles/completion-handles.ts +202 -0
  27. package/src/handles/handle-args.ts +103 -0
  28. package/src/handles/handle-registry.ts +164 -0
  29. package/src/handles/wait.ts +214 -0
  30. package/src/handles/watch-queue.ts +102 -0
  31. package/src/index.ts +99 -94
  32. package/src/kernels/jl/handles.jl +124 -0
  33. package/src/kernels/jl/prelude.jl +9 -0
  34. package/src/kernels/jl/runner.jl +6 -1
  35. package/src/kernels/js/handles.js +93 -0
  36. package/src/kernels/js/kernel-tools-define.d.ts +6 -0
  37. package/src/kernels/js/kernel-tools-define.js +3 -1
  38. package/src/kernels/js/kernel-tools-errors.js +2 -0
  39. package/src/kernels/js/kernel-tools-errors.ts +2 -0
  40. package/src/kernels/js/kernel-tools-registry.d.ts +2 -0
  41. package/src/kernels/js/kernel-tools-registry.js +18 -9
  42. package/src/kernels/js/kernel-tools-schema.js +6 -2
  43. package/src/kernels/js/worker-runtime.js +10 -1
  44. package/src/kernels/py/host-cell.ts +42 -0
  45. package/src/kernels/py/kernel-contract.ts +13 -2
  46. package/src/kernels/py/kernel-tools-host.ts +97 -0
  47. package/src/kernels/py/kernel.ts +88 -12
  48. package/src/kernels/py/prelude.py +797 -11
  49. package/src/kernels/py/process.ts +15 -4
  50. package/src/kernels/py/startup.ts +58 -20
  51. package/src/kernels/py/transport.ts +36 -7
  52. package/src/kernels/rb/handles.rb +97 -0
  53. package/src/kernels/rb/paths.rb +37 -0
  54. package/src/kernels/rb/prelude.rb +15 -38
  55. package/src/kernels/rb/runner.rb +7 -7
  56. package/src/kernels/sandbox/vendor/pi-codemode/LICENSE +21 -0
  57. package/src/kernels/sandbox/vendor/pi-codemode/VENDORED.md +42 -0
  58. package/src/kernels/sandbox/vendor/pi-codemode/identifier.ts +14 -0
  59. package/src/kernels/sandbox/vendor/pi-codemode/runtime/host.ts +514 -0
  60. package/src/kernels/sandbox/vendor/pi-codemode/runtime/prelude-source.ts +431 -0
  61. package/src/kernels/sandbox/vendor/pi-codemode/runtime/protocol.ts +61 -0
  62. package/src/kernels/sandbox/vendor/pi-codemode/runtime/worker.ts +231 -0
  63. package/src/kernels/sandbox/vendor/pi-codemode/types.ts +185 -0
  64. package/src/kernels/sandbox/vendor/pi-codemode/wasm.ts +37 -0
  65. package/src/kernels/shared/kernel-tools-pump.ts +6 -0
  66. package/src/kernels/shared/process-cpu.ts +69 -0
  67. package/src/kernels/shared/subprocess-queue.ts +1 -0
  68. package/src/tool/cell-handler.ts +21 -18
  69. package/src/tool/cell-reserved-dispatch.ts +51 -0
  70. package/src/tool/eval-tool-options.ts +3 -0
  71. package/src/tool/run-eval-cell.ts +2 -0
  72. 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
- const DEFAULT_RESERVED = Object.freeze(["__agent__", "__output__", "__schema__"]);
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
- if (sanitizeNamePart(parsed.name) !== parsed.name || parsed.name.length > MCP_TOOL_NAME_MAX_LENGTH) {
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 = parsed.name;
54
- const key = kernelToolKey(parsed.name);
55
- if (reservedKeys.has(key)) throw kernelToolError("reserved_tool_name", `Kernel tool name is reserved: ${parsed.name}`);
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: ${parsed.name}`);
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 !== parsed.name) {
61
- throw kernelToolError("tool_name_collision", `Kernel tool name collides: ${parsed.name}`);
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: parsed.name,
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.schema === undefined) return { description, input_schema: defaultInputSchema(params) };
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
- globalThis.completion = async (prompt, opts) => await this.#callTool("completion", { prompt, opts });
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 { KernelSpawnProcess } from "./process.ts";
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
- /** Python bootstrap inactivity guard per advancing stage, not a total readiness deadline. */
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
+ }
@@ -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 { KernelToolsInvokeOptions } from "../js/kernel-tools-types.ts";
4
- import { rejectKernelToolsUnavailable } from "../kernel-tools-unavailable.ts";
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 { pythonStartupHangGuardMs } from "./startup.ts";
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(_names: readonly string[]): Promise<never> {
61
- return rejectKernelToolsUnavailable();
84
+ describeKernelTools(names: readonly string[]): Promise<KernelToolsDescribeResult> {
85
+ return this.#tools.describe(names);
62
86
  }
63
87
 
64
- invokeKernelTool(_request: unknown, _options?: AbortSignal | KernelToolsInvokeOptions): Promise<never> {
65
- return rejectKernelToolsUnavailable();
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
- pending,
225
- failedPythonResult(pending.input.cellId, `Python kernel timed out after ${timeoutMs}ms`),
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;