@code-yeongyu/senpi-codemode 2026.10.4 → 2026.10.6
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 +27 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- package/src/bridge/protocol.ts +11 -0
- package/src/config/feature-settings.ts +110 -0
- package/src/config/memory-settings.ts +8 -0
- package/src/config/settings-constants.ts +7 -0
- package/src/config/settings-overrides.ts +72 -0
- package/src/config/settings.ts +35 -78
- package/src/config/unknown-settings.ts +19 -0
- package/src/extension/bridge-tool-call.ts +37 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-replacement.ts +230 -0
- package/src/extension/kernel-tools-probe.ts +24 -0
- package/src/extension/session-manager.ts +49 -38
- package/src/kernels/jl/kernel.ts +6 -2
- package/src/kernels/jl/runner.jl +154 -0
- package/src/kernels/js/context-manager.ts +1 -1
- package/src/kernels/js/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-core.js +1 -0
- package/src/kernels/js/worker-global-sizes.js +84 -14
- package/src/kernels/py/kernel-contract.ts +2 -1
- package/src/kernels/py/kernel.ts +48 -8
- package/src/kernels/py/prelude.py +6 -1
- package/src/kernels/py/transport.ts +25 -4
- package/src/kernels/rb/kernel.ts +6 -2
- package/src/kernels/rb/runner.rb +141 -0
- package/src/kernels/session-env.ts +3 -0
- package/src/kernels/shared/kernel-death.ts +39 -0
- package/src/kernels/shared/kernel-memory-host.ts +14 -6
- package/src/kernels/{js/kernel-tools-host.ts → shared/kernel-tools-pump.ts} +2 -2
- package/src/kernels/shared/process-group-cpu.ts +139 -0
- package/src/kernels/shared/subprocess-contract.ts +5 -1
- package/src/kernels/shared/subprocess-kernel.ts +66 -3
- package/src/kernels/shared/subprocess-queue.ts +5 -0
- package/src/kernels/shared/subprocess-startup.ts +90 -0
- package/src/tool/cell-runtime.ts +17 -4
- package/src/tool/detached-cell-notification.ts +13 -2
- package/src/tool/run-eval-cell.ts +5 -6
- package/src/tool/types.ts +14 -0
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
2
|
+
import { defaultCodemodeSettings } from "../config/settings.ts";
|
|
3
|
+
import { marshalToolResult } from "../tool/image.ts";
|
|
4
|
+
import type { CreateCodemodeSessionManagerOptions } from "./session-manager-contract.ts";
|
|
5
|
+
|
|
6
|
+
export interface BridgeToolCallRequest {
|
|
7
|
+
readonly toolName: string;
|
|
8
|
+
readonly args: unknown;
|
|
9
|
+
readonly callId: string;
|
|
10
|
+
readonly signal: AbortSignal;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Subprocess kernels (py/rb/jl) reach the host only through this route, so every reply
|
|
14
|
+
// must match the in-process JS path in tool/cell-handler.ts: reserved helper names dispatch
|
|
15
|
+
// through runReservedTool (forwarding them made agent() fail with "Unknown tool __agent__"),
|
|
16
|
+
// and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
|
|
17
|
+
// { content } shape left python cells unable to reach tool.read image blocks.
|
|
18
|
+
export async function routeBridgeToolCall(
|
|
19
|
+
options: Pick<CreateCodemodeSessionManagerOptions, "executeTool" | "listTools" | "settings">,
|
|
20
|
+
request: BridgeToolCallRequest,
|
|
21
|
+
): Promise<unknown> {
|
|
22
|
+
if (!isReservedToolName(request.toolName)) {
|
|
23
|
+
return marshalToolResult(await options.executeTool(request.toolName, request.args, { signal: request.signal }));
|
|
24
|
+
}
|
|
25
|
+
const taskTools = options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
|
|
26
|
+
return await runReservedTool(request.toolName, {
|
|
27
|
+
callId: request.callId,
|
|
28
|
+
args: request.args,
|
|
29
|
+
executeTool: options.executeTool,
|
|
30
|
+
taskToolName: taskTools.task,
|
|
31
|
+
taskOutputToolName: taskTools.output,
|
|
32
|
+
listTools: options.listTools,
|
|
33
|
+
signal: request.signal,
|
|
34
|
+
emitStatus: () => {},
|
|
35
|
+
marshalToolResult,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { HostToKernelMessage } from "../bridge/protocol.ts";
|
|
2
|
+
import { kernelToolError } from "../kernels/js/kernel-tools-errors.ts";
|
|
3
|
+
import { restartNotice, unstartedResult } from "../kernels/shared/kernel-death.ts";
|
|
4
|
+
import type {
|
|
5
|
+
EvalKernel,
|
|
6
|
+
EvalKernelResult,
|
|
7
|
+
EvalKernelRunInput,
|
|
8
|
+
EvalLanguage,
|
|
9
|
+
KernelInterruptHandle,
|
|
10
|
+
} from "../tool/types.ts";
|
|
11
|
+
import { hasKernelTools, type KernelToolsMethods } from "./kernel-tools-probe.ts";
|
|
12
|
+
|
|
13
|
+
export type RestartParkedKernel = () => Promise<EvalKernel>;
|
|
14
|
+
|
|
15
|
+
class IdleParkingKernelClosedError extends Error {
|
|
16
|
+
readonly name = "IdleParkingKernelClosedError";
|
|
17
|
+
|
|
18
|
+
constructor() {
|
|
19
|
+
super("Kernel closed");
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface WaitingCell {
|
|
24
|
+
readonly cancel: (reason: string) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Opt-in (`memory.idleParkMinutes` > 0): once no cell has been running or queued on the session's kernel for
|
|
29
|
+
* the configured time, closes it to give its memory back, and starts a fresh one when the next cell arrives.
|
|
30
|
+
* That cell's result says the kernel was restarted and every global is gone. A cell submitted while the
|
|
31
|
+
* kernel is being parked or restarted waits for the fresh kernel; a restart that fails settles that cell
|
|
32
|
+
* with the reason, and the next cell tries again.
|
|
33
|
+
*/
|
|
34
|
+
export class IdleParkingKernel implements EvalKernel {
|
|
35
|
+
readonly #language: EvalLanguage;
|
|
36
|
+
readonly #idleMs: number;
|
|
37
|
+
readonly #restart: RestartParkedKernel;
|
|
38
|
+
readonly #onParked: () => void;
|
|
39
|
+
#kernel: EvalKernel | null;
|
|
40
|
+
#inFlight = 0;
|
|
41
|
+
#timer: ReturnType<typeof setTimeout> | undefined;
|
|
42
|
+
#parking: Promise<void> | null = null;
|
|
43
|
+
#parkFailure: string | null = null;
|
|
44
|
+
#starting: Promise<EvalKernel> | null = null;
|
|
45
|
+
#announce: string | null = null;
|
|
46
|
+
readonly #waiting = new Map<string, WaitingCell>();
|
|
47
|
+
#closed = false;
|
|
48
|
+
// Present only when the wrapped kernel has kernel tools, so a probe for them sees what it saw before.
|
|
49
|
+
declare readonly describeKernelTools?: KernelToolsMethods["describeKernelTools"];
|
|
50
|
+
declare readonly invokeKernelTool?: KernelToolsMethods["invokeKernelTool"];
|
|
51
|
+
|
|
52
|
+
constructor(
|
|
53
|
+
language: EvalLanguage,
|
|
54
|
+
idleMinutes: number,
|
|
55
|
+
first: EvalKernel,
|
|
56
|
+
restart: RestartParkedKernel,
|
|
57
|
+
onParked: () => void = () => {},
|
|
58
|
+
) {
|
|
59
|
+
this.#language = language;
|
|
60
|
+
this.#idleMs = idleMinutes * 60_000;
|
|
61
|
+
this.#restart = restart;
|
|
62
|
+
this.#onParked = onParked;
|
|
63
|
+
this.#kernel = first;
|
|
64
|
+
if (hasKernelTools(first)) {
|
|
65
|
+
const tools: KernelToolsMethods = {
|
|
66
|
+
describeKernelTools: (names) => this.#withTools((kernel) => kernel.describeKernelTools(names)),
|
|
67
|
+
invokeKernelTool: (request, options) =>
|
|
68
|
+
this.#withTools((kernel) => kernel.invokeKernelTool(request, options)),
|
|
69
|
+
};
|
|
70
|
+
Object.assign(this, tools);
|
|
71
|
+
}
|
|
72
|
+
this.#arm();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
run(input: EvalKernelRunInput): Promise<EvalKernelResult> {
|
|
76
|
+
if (this.#closed) return Promise.reject(new IdleParkingKernelClosedError());
|
|
77
|
+
this.#disarm();
|
|
78
|
+
this.#inFlight++;
|
|
79
|
+
return this.#runOnCurrent(input).finally(() => {
|
|
80
|
+
this.#inFlight--;
|
|
81
|
+
this.#arm();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
cancelQueued(cellId: string, reason: string): boolean {
|
|
86
|
+
if (this.#cancelWaiting(cellId, reason)) return true;
|
|
87
|
+
return this.#kernel?.cancelQueued(cellId, reason) ?? false;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async interrupt(reason = "interrupted", cellId?: string): Promise<KernelInterruptHandle> {
|
|
91
|
+
if (cellId !== undefined && this.#cancelWaiting(cellId, reason)) return { stateRetained: Promise.resolve(true) };
|
|
92
|
+
if (cellId === undefined) for (const id of [...this.#waiting.keys()]) this.#cancelWaiting(id, reason);
|
|
93
|
+
const kernel = this.#kernel;
|
|
94
|
+
if (kernel === null) return { stateRetained: Promise.resolve(true) };
|
|
95
|
+
return await kernel.interrupt(reason, cellId);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
queueSnapshot(): { activeCellId: string | null; queuedCellIds: readonly string[] } {
|
|
99
|
+
const current = this.#kernel?.queueSnapshot() ?? { activeCellId: null, queuedCellIds: [] };
|
|
100
|
+
return { activeCellId: current.activeCellId, queuedCellIds: [...current.queuedCellIds, ...this.#waiting.keys()] };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
|
|
104
|
+
this.#kernel?.deliverToolReply(message);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async reset(): Promise<void> {
|
|
108
|
+
this.#announce = null;
|
|
109
|
+
const kernel = this.#kernel;
|
|
110
|
+
if (kernel !== null) await kernel.reset();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async close(): Promise<void> {
|
|
114
|
+
this.#closed = true;
|
|
115
|
+
this.#disarm();
|
|
116
|
+
for (const id of [...this.#waiting.keys()]) this.#cancelWaiting(id, new IdleParkingKernelClosedError().message);
|
|
117
|
+
if (this.#parking !== null) await this.#parking;
|
|
118
|
+
if (this.#starting !== null) await this.#starting.catch(() => undefined);
|
|
119
|
+
const kernel = this.#kernel;
|
|
120
|
+
this.#kernel = null;
|
|
121
|
+
if (kernel !== null) await kernel.close();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
listKernelToolNames(): readonly string[] {
|
|
125
|
+
return this.#kernel?.listKernelToolNames?.() ?? [];
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** A tool call is work on the kernel: it keeps the kernel from being parked under it. */
|
|
129
|
+
async #withTools<T>(use: (kernel: KernelToolsMethods) => Promise<T>): Promise<T> {
|
|
130
|
+
const kernel = this.#parking === null ? this.#kernel : null;
|
|
131
|
+
if (this.#closed || kernel === null || !hasKernelTools(kernel)) {
|
|
132
|
+
throw kernelToolError(
|
|
133
|
+
"tools_unavailable",
|
|
134
|
+
`the ${this.#language} kernel was parked after idling, so the tools it defined are gone; run a cell to start a fresh kernel`,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
this.#disarm();
|
|
138
|
+
this.#inFlight++;
|
|
139
|
+
try {
|
|
140
|
+
return await use(kernel);
|
|
141
|
+
} finally {
|
|
142
|
+
this.#inFlight--;
|
|
143
|
+
this.#arm();
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
#runOnCurrent(input: EvalKernelRunInput): Promise<EvalKernelResult> {
|
|
148
|
+
const kernel = this.#kernel;
|
|
149
|
+
if (kernel !== null && this.#parking === null) return kernel.run(input).then((result) => this.#announced(result));
|
|
150
|
+
return new Promise((settle, reject) => {
|
|
151
|
+
let cancelled = false;
|
|
152
|
+
this.#waiting.set(input.cellId, {
|
|
153
|
+
cancel: (reason) => {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
settle(unstartedResult(input.cellId, reason));
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
this.#current().then(
|
|
159
|
+
(current) => {
|
|
160
|
+
this.#waiting.delete(input.cellId);
|
|
161
|
+
if (cancelled) return;
|
|
162
|
+
current.run(input).then((result) => settle(this.#announced(result)), reject);
|
|
163
|
+
},
|
|
164
|
+
(error: unknown) => {
|
|
165
|
+
this.#waiting.delete(input.cellId);
|
|
166
|
+
if (!cancelled) settle(unstartedResult(input.cellId, this.#restartFailure(error)));
|
|
167
|
+
},
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async #current(): Promise<EvalKernel> {
|
|
173
|
+
if (this.#parking !== null) await this.#parking;
|
|
174
|
+
const failure = this.#parkFailure;
|
|
175
|
+
if (failure !== null) {
|
|
176
|
+
this.#parkFailure = null;
|
|
177
|
+
throw new Error(failure);
|
|
178
|
+
}
|
|
179
|
+
if (this.#kernel !== null) return this.#kernel;
|
|
180
|
+
if (this.#closed) throw new IdleParkingKernelClosedError();
|
|
181
|
+
this.#starting ??= this.#restart()
|
|
182
|
+
.then((kernel) => {
|
|
183
|
+
this.#kernel = kernel;
|
|
184
|
+
this.#announce = restartNotice(this.#language, this.#parkReason());
|
|
185
|
+
return kernel;
|
|
186
|
+
})
|
|
187
|
+
.finally(() => {
|
|
188
|
+
this.#starting = null;
|
|
189
|
+
});
|
|
190
|
+
return await this.#starting;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
#announced(result: EvalKernelResult): EvalKernelResult {
|
|
194
|
+
const notice = this.#announce;
|
|
195
|
+
if (notice === null) return result;
|
|
196
|
+
this.#announce = null;
|
|
197
|
+
return {
|
|
198
|
+
...result,
|
|
199
|
+
notice: result.notice === undefined ? notice : `${notice}\n${result.notice}`,
|
|
200
|
+
kernelState: "restarted",
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
#arm(): void {
|
|
205
|
+
if (this.#closed || this.#inFlight > 0 || this.#kernel === null || this.#timer !== undefined) return;
|
|
206
|
+
this.#timer = setTimeout(() => {
|
|
207
|
+
this.#timer = undefined;
|
|
208
|
+
this.#park();
|
|
209
|
+
}, this.#idleMs);
|
|
210
|
+
this.#timer.unref?.();
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
#disarm(): void {
|
|
214
|
+
if (this.#timer === undefined) return;
|
|
215
|
+
clearTimeout(this.#timer);
|
|
216
|
+
this.#timer = undefined;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
#park(): void {
|
|
220
|
+
const kernel = this.#kernel;
|
|
221
|
+
if (this.#closed || this.#inFlight > 0 || kernel === null) return;
|
|
222
|
+
const snapshot = kernel.queueSnapshot();
|
|
223
|
+
// Tools a cell defined stay callable by child tasks between cells, so their kernel is never parked.
|
|
224
|
+
const hasTools = (kernel.listKernelToolNames?.() ?? []).length > 0;
|
|
225
|
+
if (snapshot.activeCellId !== null || snapshot.queuedCellIds.length > 0 || hasTools) {
|
|
226
|
+
this.#arm();
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.#kernel = null;
|
|
230
|
+
this.#onParked();
|
|
231
|
+
const parking = kernel
|
|
232
|
+
.close()
|
|
233
|
+
.catch((error: unknown) => {
|
|
234
|
+
this.#parkFailure = `the idle ${this.#language} kernel could not be stopped: ${errorText(error)}; run the cell again to start a fresh kernel`;
|
|
235
|
+
})
|
|
236
|
+
.finally(() => {
|
|
237
|
+
if (this.#parking === parking) this.#parking = null;
|
|
238
|
+
});
|
|
239
|
+
this.#parking = parking;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
#cancelWaiting(cellId: string, reason: string): boolean {
|
|
243
|
+
const waiting = this.#waiting.get(cellId);
|
|
244
|
+
if (waiting === undefined) return false;
|
|
245
|
+
this.#waiting.delete(cellId);
|
|
246
|
+
waiting.cancel(reason);
|
|
247
|
+
return true;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
#parkReason(): string {
|
|
251
|
+
const minutes = this.#idleMs / 60_000;
|
|
252
|
+
return `idling for ${minutes} minute${minutes === 1 ? "" : "s"} (memory.idleParkMinutes)`;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
#restartFailure(error: unknown): string {
|
|
256
|
+
return `the ${this.#language} kernel parked after idling did not restart: ${errorText(error)}; run the cell again to retry`;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
export function parkWhenIdle(
|
|
261
|
+
language: EvalLanguage,
|
|
262
|
+
idleMinutes: number,
|
|
263
|
+
kernel: EvalKernel,
|
|
264
|
+
restart: RestartParkedKernel,
|
|
265
|
+
onParked?: () => void,
|
|
266
|
+
): EvalKernel {
|
|
267
|
+
return idleMinutes > 0 ? new IdleParkingKernel(language, idleMinutes, kernel, restart, onParked) : kernel;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function errorText(error: unknown): string {
|
|
271
|
+
return error instanceof Error ? error.message : String(error);
|
|
272
|
+
}
|
|
@@ -35,6 +35,31 @@ export function registerKernel(sessionId: string, language: EvalLanguage, memory
|
|
|
35
35
|
return id;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** One registry entry per language for a session; replacing a language's kernel replaces its entry. */
|
|
39
|
+
export class SessionKernelRegistrations {
|
|
40
|
+
readonly #owner: string;
|
|
41
|
+
readonly #ids = new Map<EvalLanguage, string>();
|
|
42
|
+
|
|
43
|
+
constructor(owner: string) {
|
|
44
|
+
this.#owner = owner;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
register(language: EvalLanguage, memory: RegisteredKernelSource | undefined): void {
|
|
48
|
+
this.unregister(language);
|
|
49
|
+
if (memory !== undefined) this.#ids.set(language, registerKernel(this.#owner, language, memory));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
unregister(language: EvalLanguage): void {
|
|
53
|
+
const id = this.#ids.get(language);
|
|
54
|
+
if (id !== undefined) kernelRegistry.unregister(id);
|
|
55
|
+
this.#ids.delete(language);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
clear(): void {
|
|
59
|
+
for (const language of [...this.#ids.keys()]) this.unregister(language);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
38
63
|
export function javaScriptKernelMemory(kernel: JavaScriptKernel): RegisteredKernelSource {
|
|
39
64
|
return {
|
|
40
65
|
measure: "heap",
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { HostToKernelMessage } from "../bridge/protocol.ts";
|
|
2
|
+
import {
|
|
3
|
+
type KernelLifecycle,
|
|
4
|
+
KernelUnavailableError,
|
|
5
|
+
restartNotice,
|
|
6
|
+
unstartedResult,
|
|
7
|
+
} from "../kernels/shared/kernel-death.ts";
|
|
8
|
+
import type {
|
|
9
|
+
EvalKernel,
|
|
10
|
+
EvalKernelResult,
|
|
11
|
+
EvalKernelRunInput,
|
|
12
|
+
EvalLanguage,
|
|
13
|
+
KernelInterruptHandle,
|
|
14
|
+
PendingCell,
|
|
15
|
+
} from "../tool/types.ts";
|
|
16
|
+
|
|
17
|
+
export type StartKernel = (lifecycle: Required<KernelLifecycle>) => Promise<EvalKernel>;
|
|
18
|
+
|
|
19
|
+
class ReplaceableKernelClosedError extends Error {
|
|
20
|
+
readonly name = "ReplaceableKernelClosedError";
|
|
21
|
+
|
|
22
|
+
constructor() {
|
|
23
|
+
super("Kernel closed");
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The one kernel a session holds for a language whose instances can die (py, rb, jl). Every caller keeps
|
|
29
|
+
* this object for the life of a cell - its handler, its execution, its detached record - so a death never
|
|
30
|
+
* strands them: on the instance's own death event this evicts it, takes back the cells that never started,
|
|
31
|
+
* starts ONE replacement and runs them there in their original order, with their own callbacks. The first
|
|
32
|
+
* result of the replacement says the kernel was restarted. A replacement that dies before it finished a
|
|
33
|
+
* cell is not replaced again for the same cells: they fail with `eval_kernel_unavailable`, and the next
|
|
34
|
+
* cell submitted starts a fresh one. The cell that was running when the interpreter died is never re-run.
|
|
35
|
+
*/
|
|
36
|
+
export class ReplaceableKernel implements EvalKernel {
|
|
37
|
+
readonly #language: EvalLanguage;
|
|
38
|
+
readonly #start: StartKernel;
|
|
39
|
+
#kernel: EvalKernel;
|
|
40
|
+
#generation = 0;
|
|
41
|
+
#nextGeneration = 0;
|
|
42
|
+
#held: PendingCell[] = [];
|
|
43
|
+
#replacing: Promise<void> | null = null;
|
|
44
|
+
/** The death the current instance replaced, until that instance settled a cell of its own. */
|
|
45
|
+
#recovering: string | null = null;
|
|
46
|
+
/** A death whose cells were failed instead of replaced: the next cell's fresh instance announces it. */
|
|
47
|
+
#unannounced: string | null = null;
|
|
48
|
+
/** Carried by the first result the current replacement produces. */
|
|
49
|
+
#announce: string | null = null;
|
|
50
|
+
#closed = false;
|
|
51
|
+
#closeFailure: unknown;
|
|
52
|
+
|
|
53
|
+
private constructor(language: EvalLanguage, start: StartKernel, first: EvalKernel) {
|
|
54
|
+
this.#language = language;
|
|
55
|
+
this.#start = start;
|
|
56
|
+
this.#kernel = first;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Starts the first instance; one that cannot hand back its queue is returned as it is. */
|
|
60
|
+
static async create(language: EvalLanguage, start: StartKernel): Promise<EvalKernel> {
|
|
61
|
+
let owner: ReplaceableKernel | undefined;
|
|
62
|
+
const first = await start({
|
|
63
|
+
onDeath: (reason) => {
|
|
64
|
+
if (owner !== undefined) owner.#died(0, reason);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
if (first.drainPending === undefined) return first;
|
|
68
|
+
owner = new ReplaceableKernel(language, start, first);
|
|
69
|
+
return owner;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
run(input: EvalKernelRunInput): Promise<EvalKernelResult> {
|
|
73
|
+
if (this.#closed) return Promise.reject(new ReplaceableKernelClosedError());
|
|
74
|
+
if (this.#replacing === null && this.#kernel.isAlive?.() !== false) {
|
|
75
|
+
return this.#submit(this.#generation, this.#kernel, input);
|
|
76
|
+
}
|
|
77
|
+
return new Promise((settle) => {
|
|
78
|
+
this.#held.push({ input, settle });
|
|
79
|
+
this.#ensureReplacement();
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
cancelQueued(cellId: string, reason: string): boolean {
|
|
84
|
+
return this.#cancelHeld(cellId, reason) || this.#kernel.cancelQueued(cellId, reason);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async interrupt(reason = "interrupted", cellId?: string): Promise<KernelInterruptHandle> {
|
|
88
|
+
if (cellId !== undefined && this.#cancelHeld(cellId, reason)) return { stateRetained: Promise.resolve(true) };
|
|
89
|
+
if (cellId === undefined) this.#failHeld(reason === "Eval interrupted" ? reason : `Eval interrupted: ${reason}`);
|
|
90
|
+
return await this.#kernel.interrupt(reason, cellId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
queueSnapshot(): { activeCellId: string | null; queuedCellIds: readonly string[] } {
|
|
94
|
+
const current = this.#kernel.queueSnapshot();
|
|
95
|
+
return {
|
|
96
|
+
activeCellId: current.activeCellId,
|
|
97
|
+
queuedCellIds: [...current.queuedCellIds, ...this.#held.map((cell) => cell.input.cellId)],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
deliverToolReply(message: Extract<HostToKernelMessage, { type: "tool-reply" }>): void {
|
|
102
|
+
this.#kernel.deliverToolReply(message);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async reset(): Promise<void> {
|
|
106
|
+
if (this.#replacing !== null) await this.#replacing;
|
|
107
|
+
if (this.#kernel.isAlive?.() === false) {
|
|
108
|
+
// A reset discards every global anyway, so the fresh instance announces nothing.
|
|
109
|
+
this.#unannounced = null;
|
|
110
|
+
const replacing = this.#replace(null);
|
|
111
|
+
this.#replacing = replacing;
|
|
112
|
+
await replacing;
|
|
113
|
+
if (this.#replacing === replacing) this.#replacing = null;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
await this.#kernel.reset();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async close(): Promise<void> {
|
|
120
|
+
this.#closed = true;
|
|
121
|
+
const held = this.#held.splice(0);
|
|
122
|
+
for (const cell of held)
|
|
123
|
+
cell.settle(unstartedResult(cell.input.cellId, new ReplaceableKernelClosedError().message));
|
|
124
|
+
if (this.#replacing !== null) await this.#replacing;
|
|
125
|
+
if (this.#closeFailure !== undefined) throw this.#closeFailure;
|
|
126
|
+
await this.#kernel.close();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
listKernelToolNames(): readonly string[] {
|
|
130
|
+
return this.#kernel.listKernelToolNames?.() ?? [];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
#submit(generation: number, kernel: EvalKernel, input: EvalKernelRunInput): Promise<EvalKernelResult> {
|
|
134
|
+
return kernel.run(input).then((result) => {
|
|
135
|
+
if (generation !== this.#generation) return result;
|
|
136
|
+
// The result the kernel settled for the cell its death interrupted proves nothing about the
|
|
137
|
+
// replacement; a result from a still-live current instance proves it works.
|
|
138
|
+
// A drained cell settles through this same promise already tagged; only the interrupted one is `lost`.
|
|
139
|
+
if (kernel.isAlive?.() === false)
|
|
140
|
+
return result.kernelState === undefined ? { ...result, kernelState: "lost" } : result;
|
|
141
|
+
this.#recovering = null;
|
|
142
|
+
const notice = this.#announce;
|
|
143
|
+
if (notice === null) return result;
|
|
144
|
+
this.#announce = null;
|
|
145
|
+
return { ...result, notice, kernelState: "restarted" };
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
#died(generation: number, reason: string): void {
|
|
150
|
+
if (generation !== this.#generation || this.#closed) return;
|
|
151
|
+
this.#held = [...(this.#kernel.drainPending?.() ?? []), ...this.#held];
|
|
152
|
+
if (this.#recovering !== null) {
|
|
153
|
+
this.#recovering = null;
|
|
154
|
+
this.#unannounced = reason;
|
|
155
|
+
const error = new KernelUnavailableError(this.#language, reason);
|
|
156
|
+
for (const cell of this.#held.splice(0)) cell.settle(unstartedResult(cell.input.cellId, error.message));
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
this.#recovering = reason;
|
|
160
|
+
this.#ensureReplacement();
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#ensureReplacement(): void {
|
|
164
|
+
if (this.#replacing !== null) return;
|
|
165
|
+
const reason = this.#recovering ?? this.#unannounced;
|
|
166
|
+
this.#recovering = reason;
|
|
167
|
+
this.#unannounced = null;
|
|
168
|
+
const replacing = this.#replace(reason).finally(() => {
|
|
169
|
+
if (this.#replacing === replacing) this.#replacing = null;
|
|
170
|
+
});
|
|
171
|
+
this.#replacing = replacing;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Never rejects: every failure settles the held cells with what went wrong. */
|
|
175
|
+
async #replace(reason: string | null): Promise<void> {
|
|
176
|
+
const dead = this.#kernel;
|
|
177
|
+
try {
|
|
178
|
+
await dead.close();
|
|
179
|
+
} catch (error) {
|
|
180
|
+
// The old interpreter may still be running: report that rather than start a second one.
|
|
181
|
+
this.#failUnavailable(reason, `its interpreter could not be retired: ${errorText(error)}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (this.#closed) return;
|
|
185
|
+
const generation = ++this.#nextGeneration;
|
|
186
|
+
let next: EvalKernel;
|
|
187
|
+
try {
|
|
188
|
+
next = await this.#start({ onDeath: (died) => this.#died(generation, died) });
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.#failUnavailable(reason, `its replacement did not start: ${errorText(error)}`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
if (this.#closed) {
|
|
194
|
+
await next.close().catch((error: unknown) => {
|
|
195
|
+
this.#closeFailure = error;
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.#kernel = next;
|
|
200
|
+
this.#generation = generation;
|
|
201
|
+
this.#announce = reason === null ? null : restartNotice(this.#language, reason);
|
|
202
|
+
for (const cell of this.#held.splice(0)) {
|
|
203
|
+
void this.#submit(generation, next, cell.input).then(cell.settle, (error: unknown) =>
|
|
204
|
+
cell.settle(unstartedResult(cell.input.cellId, errorText(error))),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
#failUnavailable(reason: string | null, detail: string): void {
|
|
210
|
+
this.#recovering = null;
|
|
211
|
+
this.#unannounced = reason;
|
|
212
|
+
const error = new KernelUnavailableError(this.#language, reason === null ? detail : `${reason}; ${detail}`);
|
|
213
|
+
for (const cell of this.#held.splice(0)) cell.settle(unstartedResult(cell.input.cellId, error.message));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
#cancelHeld(cellId: string, reason: string): boolean {
|
|
217
|
+
const index = this.#held.findIndex((cell) => cell.input.cellId === cellId);
|
|
218
|
+
const [cell] = index < 0 ? [] : this.#held.splice(index, 1);
|
|
219
|
+
cell?.settle(unstartedResult(cellId, reason));
|
|
220
|
+
return cell !== undefined;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
#failHeld(message: string): void {
|
|
224
|
+
for (const cell of this.#held.splice(0)) cell.settle(unstartedResult(cell.input.cellId, message));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function errorText(error: unknown): string {
|
|
229
|
+
return error instanceof Error ? error.message : String(error);
|
|
230
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
KernelToolsDescribeResult,
|
|
3
|
+
KernelToolsInvokeOptions,
|
|
4
|
+
KernelToolsInvokeRequest,
|
|
5
|
+
} from "../kernels/js/kernel-tools-types.ts";
|
|
6
|
+
import type { EvalKernel } from "../tool/types.ts";
|
|
7
|
+
|
|
8
|
+
/** The kernel-tools surface a JavaScript kernel exposes; `run-eval-cell` finds it by probing for these methods. */
|
|
9
|
+
export interface KernelToolsMethods {
|
|
10
|
+
describeKernelTools(names: readonly string[]): Promise<KernelToolsDescribeResult>;
|
|
11
|
+
invokeKernelTool(
|
|
12
|
+
request: KernelToolsInvokeRequest,
|
|
13
|
+
options?: AbortSignal | KernelToolsInvokeOptions,
|
|
14
|
+
): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function hasKernelTools(kernel: EvalKernel): kernel is EvalKernel & KernelToolsMethods {
|
|
18
|
+
return (
|
|
19
|
+
"describeKernelTools" in kernel &&
|
|
20
|
+
typeof kernel.describeKernelTools === "function" &&
|
|
21
|
+
"invokeKernelTool" in kernel &&
|
|
22
|
+
typeof kernel.invokeKernelTool === "function"
|
|
23
|
+
);
|
|
24
|
+
}
|