@code-yeongyu/senpi-codemode 2026.10.5 → 2026.10.7
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 +41 -0
- package/README.md +20 -2
- package/package.json +4 -4
- package/src/bridge/kernel-tools-protocol.ts +1 -1
- package/src/bridge/reserved.ts +10 -0
- package/src/bridges/eval-virtual-schemas.ts +79 -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/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-dispatch.ts +41 -0
- package/src/extension/bridge-tool-call.ts +41 -0
- package/src/extension/host-facts.ts +18 -0
- package/src/extension/idle-parking-kernel.ts +272 -0
- package/src/extension/kernel-registration.ts +25 -0
- package/src/extension/kernel-tools-probe.ts +24 -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 +58 -58
- 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/kernel.ts +3 -1
- package/src/kernels/jl/prelude.jl +9 -0
- package/src/kernels/jl/runner.jl +160 -1
- package/src/kernels/js/context-manager.ts +1 -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-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/kernel-tools-types.ts +2 -1
- package/src/kernels/js/worker-global-sizes.js +84 -14
- 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 +2 -0
- package/src/kernels/py/kernel.ts +48 -4
- package/src/kernels/py/prelude.py +145 -4
- package/src/kernels/rb/handles.rb +97 -0
- package/src/kernels/rb/kernel.ts +3 -1
- package/src/kernels/rb/paths.rb +37 -0
- package/src/kernels/rb/prelude.rb +15 -38
- package/src/kernels/rb/runner.rb +141 -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 +3 -0
- package/src/kernels/shared/subprocess-kernel.ts +33 -1
- package/src/kernels/shared/subprocess-startup.ts +90 -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 +7 -6
- package/src/tool/types.ts +15 -0
|
@@ -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,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
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Migration guidance for the tools eval replaced; the host returns it when a model calls them. */
|
|
2
|
+
export interface RemovedToolHintRegistrar {
|
|
3
|
+
registerRemovedToolHint(name: string, hint: string): void;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export const WAIT_REMOVED_TOOL_HINT =
|
|
7
|
+
"wait is not a tool; inside a cell use the wait(handles) helper (tool_schema('eval:wait')), or let detached cells notify.";
|
|
8
|
+
|
|
9
|
+
export function registerRemovedToolHints(pi: RemovedToolHintRegistrar): void {
|
|
10
|
+
pi.registerRemovedToolHint(
|
|
11
|
+
"exec",
|
|
12
|
+
'exec was removed; use eval({ language: "js", code }) instead. Long eval cells detach on their own and notify when complete.',
|
|
13
|
+
);
|
|
14
|
+
pi.registerRemovedToolHint("wait", WAIT_REMOVED_TOOL_HINT);
|
|
15
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
type ResolvedCodemodeSettings,
|
|
9
9
|
resolveEnabledLanguages,
|
|
10
10
|
} from "../config/settings.ts";
|
|
11
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
11
12
|
import {
|
|
12
13
|
createInterpreterDetector,
|
|
13
14
|
getInterpreterAvailability,
|
|
@@ -54,6 +55,7 @@ export async function createRuntime(
|
|
|
54
55
|
event: unknown,
|
|
55
56
|
complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>,
|
|
56
57
|
options: RuntimeFactoryOptions,
|
|
58
|
+
handles?: HandleRegistry,
|
|
57
59
|
): Promise<SessionRuntime> {
|
|
58
60
|
const loaded = await loadCodemodeSettings({ cwd: ctx.cwd });
|
|
59
61
|
const settings: ResolvedCodemodeSettings = {
|
|
@@ -81,6 +83,7 @@ export async function createRuntime(
|
|
|
81
83
|
executeTool,
|
|
82
84
|
listTools: () => pi.getAllTools(),
|
|
83
85
|
complete,
|
|
86
|
+
...(handles === undefined ? {} : { handles }),
|
|
84
87
|
});
|
|
85
88
|
return {
|
|
86
89
|
sessionId,
|
|
@@ -2,6 +2,7 @@ import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
|
2
2
|
import type { EvalSchemaToolInfo } from "../bridges/schema-bridge.ts";
|
|
3
3
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
4
4
|
import type { CodemodeSettings } from "../config/settings.ts";
|
|
5
|
+
import type { HandleRegistry } from "../handles/handle-registry.ts";
|
|
5
6
|
import type { InterpreterAvailability } from "../interpreters/detect.ts";
|
|
6
7
|
import type { SessionEnvironment } from "../kernels/session-env.ts";
|
|
7
8
|
import type { EvalKernelManager, ExecuteTool } from "../tool/types.ts";
|
|
@@ -39,4 +40,6 @@ export interface CreateCodemodeSessionManagerOptions {
|
|
|
39
40
|
readonly executeTool: ExecuteTool;
|
|
40
41
|
readonly listTools?: () => readonly EvalSchemaToolInfo[];
|
|
41
42
|
readonly complete: (request: CompletionRequest, ctx: ExtensionContext) => Promise<CompletionResult>;
|
|
43
|
+
/** The session generation's handle registry; subprocess kernels reach `wait()`/`handle()` through the bridge. */
|
|
44
|
+
readonly handles?: HandleRegistry;
|
|
42
45
|
}
|
|
@@ -2,22 +2,22 @@ import { join } from "node:path";
|
|
|
2
2
|
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
3
|
import { type BridgeServerHandle, startBridgeServer } from "../bridge/http-server.ts";
|
|
4
4
|
import type { KernelToHostMessage } from "../bridge/protocol.ts";
|
|
5
|
-
import { isReservedToolName, runReservedTool } from "../bridges/reserved-dispatch.ts";
|
|
6
5
|
import type { CompletionRequest, CompletionResult } from "../completion/handler.ts";
|
|
7
6
|
import { resolveKernelMemoryThresholds } from "../config/memory-settings.ts";
|
|
8
|
-
import { defaultCodemodeSettings } from "../config/settings.ts";
|
|
9
7
|
import { collectOrphanedChildren } from "../host-sdk.ts";
|
|
10
8
|
import { JavaScriptKernel } from "../kernels/js/context-manager.ts";
|
|
11
9
|
import type { KernelLifecycle } from "../kernels/shared/kernel-death.ts";
|
|
12
|
-
import { marshalToolResult } from "../tool/image.ts";
|
|
13
10
|
import type { EvalKernel, EvalLanguage } from "../tool/types.ts";
|
|
11
|
+
import { type BridgeDispatchContext, dispatchBridgeCompletion } from "./bridge-dispatch.ts";
|
|
12
|
+
import { type BridgeToolCallRequest, routeBridgeToolCall } from "./bridge-tool-call.ts";
|
|
13
|
+
import { parkWhenIdle } from "./idle-parking-kernel.ts";
|
|
14
14
|
import {
|
|
15
15
|
javaScriptKernelMemory,
|
|
16
|
-
|
|
16
|
+
SessionKernelRegistrations,
|
|
17
17
|
type StartedKernel,
|
|
18
18
|
startSubprocessKernel,
|
|
19
19
|
} from "./kernel-registration.ts";
|
|
20
|
-
import {
|
|
20
|
+
import type { RegisteredKernelSource } from "./kernel-registry.ts";
|
|
21
21
|
import { ReplaceableKernel } from "./kernel-replacement.ts";
|
|
22
22
|
import { assertSessionCwdAvailable } from "./session-cwd.ts";
|
|
23
23
|
import type {
|
|
@@ -61,7 +61,7 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
61
61
|
readonly #options: CreateCodemodeSessionManagerOptions;
|
|
62
62
|
#bridge: BridgeServerHandle | undefined;
|
|
63
63
|
#kernels = new Map<EvalLanguage, EvalKernel>();
|
|
64
|
-
readonly #registrations
|
|
64
|
+
readonly #registrations: SessionKernelRegistrations;
|
|
65
65
|
#kernelCreations = new Map<EvalLanguage, Promise<EvalKernel>>();
|
|
66
66
|
#onMessageRefs = new Map<EvalLanguage, (message: KernelToHostMessage) => void>();
|
|
67
67
|
#context: ExtensionContext | undefined;
|
|
@@ -70,40 +70,27 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
70
70
|
|
|
71
71
|
constructor(options: CreateCodemodeSessionManagerOptions) {
|
|
72
72
|
this.#options = options;
|
|
73
|
+
this.#registrations = new SessionKernelRegistrations(options.ownerSessionId ?? options.sessionId);
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
async start(): Promise<void> {
|
|
77
|
+
const manager = this;
|
|
78
|
+
const dispatch: BridgeDispatchContext = {
|
|
79
|
+
get context() {
|
|
80
|
+
return manager.#context;
|
|
81
|
+
},
|
|
82
|
+
contextFor: (signal) => this.#contextFor(signal),
|
|
83
|
+
requireContext: () => this.#requireContext(),
|
|
84
|
+
};
|
|
76
85
|
this.#bridge = await startBridgeServer({
|
|
77
86
|
onCall: async (request) => await this.#call(request),
|
|
78
87
|
onEmit: async () => undefined,
|
|
79
|
-
onCompletion: async (request) =>
|
|
80
|
-
this.#options.complete({ prompt: request.prompt, opts: request.opts }, this.#contextFor(request.signal)),
|
|
88
|
+
onCompletion: async (request) => await dispatchBridgeCompletion(this.#options, request, dispatch),
|
|
81
89
|
});
|
|
82
90
|
}
|
|
83
91
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
// through runReservedTool (forwarding them made agent() fail with "Unknown tool __agent__"),
|
|
87
|
-
// and ordinary tool results are marshalled to { text, images, details, hasError } — the raw
|
|
88
|
-
// { content } shape left python cells unable to reach tool.read image blocks.
|
|
89
|
-
async #call(request: { toolName: string; args: unknown; callId: string; signal: AbortSignal }): Promise<unknown> {
|
|
90
|
-
if (!isReservedToolName(request.toolName)) {
|
|
91
|
-
return marshalToolResult(
|
|
92
|
-
await this.#options.executeTool(request.toolName, request.args, { signal: request.signal }),
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
const taskTools = this.#options.settings.taskTools ?? defaultCodemodeSettings.taskTools;
|
|
96
|
-
return await runReservedTool(request.toolName, {
|
|
97
|
-
callId: request.callId,
|
|
98
|
-
args: request.args,
|
|
99
|
-
executeTool: this.#options.executeTool,
|
|
100
|
-
taskToolName: taskTools.task,
|
|
101
|
-
taskOutputToolName: taskTools.output,
|
|
102
|
-
listTools: this.#options.listTools,
|
|
103
|
-
signal: request.signal,
|
|
104
|
-
emitStatus: () => {},
|
|
105
|
-
marshalToolResult,
|
|
106
|
-
});
|
|
92
|
+
async #call(request: BridgeToolCallRequest): Promise<unknown> {
|
|
93
|
+
return await routeBridgeToolCall(this.#options, request, this.#context?.evalHandleHost);
|
|
107
94
|
}
|
|
108
95
|
|
|
109
96
|
async getKernel(language: EvalLanguage, onMessage: (message: KernelToHostMessage) => void): Promise<EvalKernel> {
|
|
@@ -170,7 +157,6 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
170
157
|
const kernels = [...this.#kernels.values()];
|
|
171
158
|
const bridge = this.#bridge;
|
|
172
159
|
this.#kernels.clear();
|
|
173
|
-
for (const id of this.#registrations.values()) kernelRegistry.unregister(id);
|
|
174
160
|
this.#registrations.clear();
|
|
175
161
|
this.#onMessageRefs.clear();
|
|
176
162
|
this.#bridge = undefined;
|
|
@@ -194,36 +180,29 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
194
180
|
generation: number,
|
|
195
181
|
): Promise<EvalKernel> {
|
|
196
182
|
await assertSessionCwdAvailable(this.#options.cwd);
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
183
|
+
const started = await this.#startLanguageKernel(language, onMessage);
|
|
184
|
+
const idleParkMinutes = this.#options.settings.memory?.idleParkMinutes ?? 0;
|
|
185
|
+
const resolvedKernel = parkWhenIdle(
|
|
186
|
+
language,
|
|
187
|
+
idleParkMinutes,
|
|
188
|
+
started.kernel,
|
|
189
|
+
async () => {
|
|
190
|
+
const restarted = await this.#startLanguageKernel(language, onMessage);
|
|
191
|
+
if (generation !== this.#generation) {
|
|
192
|
+
await restarted.kernel.close();
|
|
193
|
+
throw new CodemodeSessionDisposedError();
|
|
194
|
+
}
|
|
195
|
+
this.#registrations.register(language, restarted.memory);
|
|
196
|
+
return restarted.kernel;
|
|
197
|
+
},
|
|
198
|
+
() => this.#registrations.unregister(language),
|
|
199
|
+
);
|
|
214
200
|
if (generation !== this.#generation) {
|
|
215
201
|
await resolvedKernel.close();
|
|
216
202
|
throw new CodemodeSessionDisposedError();
|
|
217
203
|
}
|
|
218
204
|
this.#kernels.set(language, resolvedKernel);
|
|
219
|
-
this.#registrations.
|
|
220
|
-
language,
|
|
221
|
-
registerKernel(
|
|
222
|
-
this.#options.ownerSessionId ?? this.#options.sessionId,
|
|
223
|
-
language,
|
|
224
|
-
memory as RegisteredKernelSource,
|
|
225
|
-
),
|
|
226
|
-
);
|
|
205
|
+
this.#registrations.register(language, started.memory);
|
|
227
206
|
// The directory can vanish while the interpreter starts; every caller sharing this creation
|
|
228
207
|
// must see that, not only the next one. The kernel stays stored and dispose still closes it.
|
|
229
208
|
await assertSessionCwdAvailable(this.#options.cwd);
|
|
@@ -232,6 +211,22 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
232
211
|
return resolvedKernel;
|
|
233
212
|
}
|
|
234
213
|
|
|
214
|
+
// py/rb/jl instances can die; the session holds one replaceable kernel per language so every
|
|
215
|
+
// cell that kept a reference to it survives the death (JS heals its own worker).
|
|
216
|
+
async #startLanguageKernel(
|
|
217
|
+
language: EvalLanguage,
|
|
218
|
+
onMessage: (message: KernelToHostMessage) => void,
|
|
219
|
+
): Promise<{ readonly kernel: EvalKernel; readonly memory: RegisteredKernelSource | undefined }> {
|
|
220
|
+
if (language === "js") return await this.#createKernel(language, onMessage);
|
|
221
|
+
let memory: RegisteredKernelSource | undefined;
|
|
222
|
+
const kernel = await ReplaceableKernel.create(language, async (lifecycle) => {
|
|
223
|
+
const created = await this.#createKernel(language, onMessage, lifecycle);
|
|
224
|
+
memory ??= created.memory;
|
|
225
|
+
return created.kernel;
|
|
226
|
+
});
|
|
227
|
+
return { kernel, memory };
|
|
228
|
+
}
|
|
229
|
+
|
|
235
230
|
async #createKernel(
|
|
236
231
|
language: EvalLanguage,
|
|
237
232
|
onMessage: (message: KernelToHostMessage) => void,
|
|
@@ -293,9 +288,14 @@ class DefaultCodemodeSessionManager implements CodemodeSessionManager {
|
|
|
293
288
|
return names;
|
|
294
289
|
}
|
|
295
290
|
|
|
296
|
-
#
|
|
291
|
+
#requireContext(): ExtensionContext {
|
|
297
292
|
const ctx = this.#context;
|
|
298
293
|
if (!ctx) throw new CodemodeContextUnavailableError();
|
|
294
|
+
return ctx;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#contextFor(signal: AbortSignal): ExtensionContext {
|
|
298
|
+
const ctx = this.#requireContext();
|
|
299
299
|
return { ...ctx, signal: ctx.signal ? AbortSignal.any([ctx.signal, signal]) : signal };
|
|
300
300
|
}
|
|
301
301
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import type { ExtensionContext } from "@code-yeongyu/senpi";
|
|
3
|
+
import type { createEvalTool } from "../tool/eval-tool.ts";
|
|
4
|
+
import { CodemodeSessionDisposedError } from "./session-manager.ts";
|
|
5
|
+
import { CodemodeSessionNotStartedError } from "./session-manager-proxy.ts";
|
|
6
|
+
|
|
7
|
+
export class CodemodeRuntimeRecreationError extends Error {
|
|
8
|
+
readonly name = "CodemodeRuntimeRecreationError";
|
|
9
|
+
|
|
10
|
+
constructor(reason: string) {
|
|
11
|
+
super(`codemode runtime could not be re-created: ${reason}. Start a new session or reload to bring eval back.`);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type StartSession = (event: unknown, ctx: ExtensionContext) => Promise<void>;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A session_start whose runtime could not be created leaves the session running with no usable manager. The
|
|
19
|
+
* next eval re-creates the runtime once for that failed start (concurrent evals share the attempt) and runs; a
|
|
20
|
+
* failed re-creation is reported to every later eval until the next session_start. One stderr line names the
|
|
21
|
+
* failed start with a hashed session id so a recurrence stays visible. A session that ended is never recovered.
|
|
22
|
+
*/
|
|
23
|
+
export class StartRecovery {
|
|
24
|
+
readonly #startSession: StartSession;
|
|
25
|
+
readonly #report: (line: string) => void;
|
|
26
|
+
#failedStartEvent: { readonly event: unknown } | undefined;
|
|
27
|
+
#recreation: Promise<void> | undefined;
|
|
28
|
+
#recreationFailure: CodemodeRuntimeRecreationError | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
startSession: StartSession,
|
|
32
|
+
report: (line: string) => void = (line) => globalThis.process.stderr.write(`${line}\n`),
|
|
33
|
+
) {
|
|
34
|
+
this.#startSession = startSession;
|
|
35
|
+
this.#report = report;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async runStart(event: unknown, ctx: ExtensionContext): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
await this.#startSession(event, ctx);
|
|
41
|
+
this.#reset();
|
|
42
|
+
} catch (error) {
|
|
43
|
+
this.#reset();
|
|
44
|
+
this.#failedStartEvent = { event };
|
|
45
|
+
throw error;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
sessionEnded(): void {
|
|
50
|
+
this.#reset();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
wrapExecute<Args extends unknown[], Result>(
|
|
54
|
+
execute: (...args: Args) => Promise<Result>,
|
|
55
|
+
contextOf: (args: Args) => ExtensionContext,
|
|
56
|
+
): (...args: Args) => Promise<Result> {
|
|
57
|
+
return async (...args) => {
|
|
58
|
+
try {
|
|
59
|
+
return await execute(...args);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
if (!(error instanceof CodemodeSessionDisposedError || error instanceof CodemodeSessionNotStartedError)) {
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
if (this.#recreationFailure !== undefined) throw this.#recreationFailure;
|
|
65
|
+
const recreation = this.#recreation ?? this.#beginRecreation(contextOf(args));
|
|
66
|
+
if (recreation === undefined) throw error;
|
|
67
|
+
await recreation;
|
|
68
|
+
return await execute(...args);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
#beginRecreation(ctx: ExtensionContext): Promise<void> | undefined {
|
|
74
|
+
const failed = this.#failedStartEvent;
|
|
75
|
+
if (failed === undefined) return undefined;
|
|
76
|
+
this.#report(
|
|
77
|
+
`[senpi-codemode] eval re-created a runtime left disposed by a failed session_start (session ${sessionHash(ctx)})`,
|
|
78
|
+
);
|
|
79
|
+
const recreation = this.#startSession(failed.event, ctx).then(
|
|
80
|
+
() => {
|
|
81
|
+
this.#recreation = undefined;
|
|
82
|
+
this.#reset();
|
|
83
|
+
},
|
|
84
|
+
(error: unknown) => {
|
|
85
|
+
this.#recreation = undefined;
|
|
86
|
+
this.#recreationFailure = new CodemodeRuntimeRecreationError(
|
|
87
|
+
error instanceof Error ? error.message : String(error),
|
|
88
|
+
);
|
|
89
|
+
throw this.#recreationFailure;
|
|
90
|
+
},
|
|
91
|
+
);
|
|
92
|
+
this.#recreation = recreation;
|
|
93
|
+
return recreation;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
#reset(): void {
|
|
97
|
+
this.#failedStartEvent = undefined;
|
|
98
|
+
this.#recreationFailure = undefined;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function withStartRecovery<Tool extends ReturnType<typeof createEvalTool>>(
|
|
103
|
+
recovery: StartRecovery,
|
|
104
|
+
tool: Tool,
|
|
105
|
+
): Tool {
|
|
106
|
+
return { ...tool, execute: recovery.wrapExecute(tool.execute, (args) => args[4]) };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sessionHash(ctx: ExtensionContext): string {
|
|
110
|
+
return createHash("sha256").update(ctx.sessionManager.getSessionId()).digest("hex").slice(0, 12);
|
|
111
|
+
}
|