@oh-my-pi/pi-coding-agent 16.1.12 → 16.1.14
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 +27 -0
- package/dist/cli.js +5650 -2860
- package/dist/types/config/settings-schema.d.ts +44 -14
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/eval/__tests__/julia-prelude.test.d.ts +1 -0
- package/dist/types/eval/backend-helpers.d.ts +23 -0
- package/dist/types/eval/executor-base.d.ts +118 -0
- package/dist/types/eval/index.d.ts +2 -0
- package/dist/types/eval/jl/executor.d.ts +44 -0
- package/dist/types/eval/jl/index.d.ts +11 -0
- package/dist/types/eval/jl/kernel.d.ts +28 -0
- package/dist/types/eval/jl/prelude.d.ts +1 -0
- package/dist/types/eval/jl/runtime.d.ts +22 -0
- package/dist/types/eval/kernel-base.d.ts +105 -0
- package/dist/types/eval/py/kernel.d.ts +3 -61
- package/dist/types/eval/rb/executor.d.ts +77 -0
- package/dist/types/eval/rb/index.d.ts +11 -0
- package/dist/types/eval/rb/kernel.d.ts +31 -0
- package/dist/types/eval/rb/prelude.d.ts +1 -0
- package/dist/types/eval/rb/runtime.d.ts +23 -0
- package/dist/types/eval/runtime-env.d.ts +24 -0
- package/dist/types/eval/types.d.ts +3 -3
- package/dist/types/extensibility/extensions/runner.d.ts +2 -15
- package/dist/types/extensibility/hooks/loader.d.ts +1 -25
- package/dist/types/extensibility/session-handler-types.d.ts +23 -0
- package/dist/types/jsonrpc/message-framing.d.ts +35 -0
- package/dist/types/mnemopi/embed-client.d.ts +7 -24
- package/dist/types/modes/components/chat-transcript-builder.d.ts +1 -1
- package/dist/types/modes/components/custom-editor.d.ts +11 -0
- package/dist/types/modes/components/selector-helpers.d.ts +53 -0
- package/dist/types/modes/interactive-mode.d.ts +0 -2
- package/dist/types/modes/theme/theme.d.ts +8 -1
- package/dist/types/modes/types.d.ts +0 -2
- package/dist/types/modes/utils/copy-targets.d.ts +1 -1
- package/dist/types/modes/utils/interactive-context-helpers.d.ts +14 -0
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +54 -0
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +2 -2
- package/dist/types/stt/asr-client.d.ts +3 -29
- package/dist/types/subprocess/worker-client.d.ts +149 -0
- package/dist/types/subprocess/worker-runtime.d.ts +107 -0
- package/dist/types/tiny/title-client.d.ts +14 -34
- package/dist/types/tools/eval-backends.d.ts +6 -3
- package/dist/types/tools/eval-render.d.ts +6 -5
- package/dist/types/tools/eval.d.ts +13 -15
- package/dist/types/tools/index.d.ts +3 -3
- package/dist/types/tts/tts-client.d.ts +3 -28
- package/dist/types/tui/code-cell.d.ts +7 -0
- package/dist/types/utils/file-display-mode.d.ts +1 -1
- package/dist/types/web/parallel.d.ts +6 -0
- package/package.json +12 -12
- package/src/config/settings-schema.ts +50 -18
- package/src/config/settings.ts +5 -0
- package/src/dap/client.ts +13 -107
- package/src/eval/__tests__/julia-prelude.test.ts +77 -0
- package/src/eval/backend-helpers.ts +48 -0
- package/src/eval/executor-base.ts +425 -0
- package/src/eval/index.ts +2 -0
- package/src/eval/jl/executor.ts +540 -0
- package/src/eval/jl/index.ts +54 -0
- package/src/eval/jl/kernel.ts +235 -0
- package/src/eval/jl/prelude.jl +930 -0
- package/src/eval/jl/prelude.ts +3 -0
- package/src/eval/jl/runner.jl +634 -0
- package/src/eval/jl/runtime.ts +118 -0
- package/src/eval/js/index.ts +3 -14
- package/src/eval/kernel-base.ts +569 -0
- package/src/eval/py/executor.ts +43 -252
- package/src/eval/py/index.ts +9 -20
- package/src/eval/py/kernel.ts +29 -544
- package/src/eval/rb/executor.ts +504 -0
- package/src/eval/rb/index.ts +54 -0
- package/src/eval/rb/kernel.ts +230 -0
- package/src/eval/rb/prelude.rb +721 -0
- package/src/eval/rb/prelude.ts +3 -0
- package/src/eval/rb/runner.rb +474 -0
- package/src/eval/rb/runtime.ts +132 -0
- package/src/eval/runtime-env.ts +104 -0
- package/src/eval/types.ts +3 -3
- package/src/extensibility/extensions/runner.ts +4 -11
- package/src/extensibility/hooks/loader.ts +3 -21
- package/src/extensibility/session-handler-types.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/jsonrpc/message-framing.ts +142 -0
- package/src/lsp/client.ts +13 -109
- package/src/mnemopi/embed-client.ts +43 -198
- package/src/modes/components/agent-dashboard.ts +17 -40
- package/src/modes/components/chat-transcript-builder.ts +18 -102
- package/src/modes/components/custom-editor.ts +19 -0
- package/src/modes/components/extensions/extension-dashboard.ts +4 -13
- package/src/modes/components/extensions/extension-list.ts +16 -44
- package/src/modes/components/history-search.ts +4 -16
- package/src/modes/components/selector-helpers.ts +129 -0
- package/src/modes/components/settings-selector.ts +7 -5
- package/src/modes/components/tree-selector.ts +13 -18
- package/src/modes/controllers/event-controller.ts +3 -9
- package/src/modes/controllers/input-controller.ts +32 -54
- package/src/modes/interactive-mode.ts +5 -7
- package/src/modes/theme/theme.ts +35 -0
- package/src/modes/types.ts +0 -2
- package/src/modes/utils/copy-targets.ts +3 -2
- package/src/modes/utils/interactive-context-helpers.ts +27 -0
- package/src/modes/utils/transcript-render-helpers.ts +157 -0
- package/src/modes/utils/ui-helpers.ts +21 -126
- package/src/prompts/system/system-prompt.md +10 -10
- package/src/prompts/tools/bash.md +0 -1
- package/src/prompts/tools/eval.md +6 -4
- package/src/prompts/tools/find.md +0 -4
- package/src/prompts/tools/read.md +1 -2
- package/src/prompts/tools/replace.md +1 -1
- package/src/prompts/tools/search.md +0 -1
- package/src/sdk.ts +13 -7
- package/src/session/agent-session.ts +9 -7
- package/src/stt/asr-client.ts +35 -215
- package/src/stt/asr-worker.ts +29 -181
- package/src/subprocess/worker-client.ts +297 -0
- package/src/subprocess/worker-runtime.ts +277 -0
- package/src/task/executor.ts +4 -4
- package/src/tiny/title-client.ts +53 -219
- package/src/tiny/worker.ts +29 -180
- package/src/tools/eval-backends.ts +10 -3
- package/src/tools/eval-render.ts +17 -8
- package/src/tools/eval.ts +187 -22
- package/src/tools/index.ts +51 -28
- package/src/tts/tts-client.ts +38 -206
- package/src/tts/tts-worker.ts +23 -97
- package/src/tui/code-cell.ts +12 -1
- package/src/utils/file-display-mode.ts +2 -3
- package/src/web/parallel.ts +43 -42
- package/src/web/search/providers/parallel.ts +10 -99
package/src/eval/py/kernel.ts
CHANGED
|
@@ -11,10 +11,9 @@ import * as fs from "node:fs";
|
|
|
11
11
|
import * as os from "node:os";
|
|
12
12
|
import * as path from "node:path";
|
|
13
13
|
import { $flag, isBunTestRuntime, logger, Snowflake } from "@oh-my-pi/pi-utils";
|
|
14
|
-
import type { Subprocess } from "bun";
|
|
15
14
|
import { $ } from "bun";
|
|
16
15
|
import { Settings } from "../../config/settings";
|
|
17
|
-
import {
|
|
16
|
+
import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
|
|
18
17
|
import { PYTHON_PRELUDE } from "./prelude";
|
|
19
18
|
import RUNNER_SCRIPT from "./runner.py" with { type: "text" };
|
|
20
19
|
import {
|
|
@@ -26,6 +25,14 @@ import {
|
|
|
26
25
|
} from "./runtime";
|
|
27
26
|
import { hostHasInheritableConsole, shouldHideKernelWindow } from "./spawn-options";
|
|
28
27
|
|
|
28
|
+
export type {
|
|
29
|
+
KernelExecuteOptions,
|
|
30
|
+
KernelExecuteResult,
|
|
31
|
+
KernelRuntimeEnv,
|
|
32
|
+
KernelShutdownOptions,
|
|
33
|
+
KernelShutdownResult,
|
|
34
|
+
} from "../kernel-base";
|
|
35
|
+
|
|
29
36
|
export type { KernelDisplayOutput, PythonStatusEvent } from "./display";
|
|
30
37
|
export { renderKernelDisplay } from "./display";
|
|
31
38
|
|
|
@@ -58,62 +65,6 @@ const STARTUP_TIMEOUT_MS = 10_000;
|
|
|
58
65
|
// kernel's state, so we only kill as a last-resort recovery path.
|
|
59
66
|
const INTERRUPT_ESCALATION_MS = 5_000;
|
|
60
67
|
|
|
61
|
-
export type KernelRuntimeEnv = Record<string, string | null>;
|
|
62
|
-
|
|
63
|
-
export interface KernelExecuteOptions {
|
|
64
|
-
id?: string;
|
|
65
|
-
/** Runtime working directory applied immediately before this request executes. */
|
|
66
|
-
cwd?: string;
|
|
67
|
-
/** Managed runtime environment variables applied immediately before this request executes. */
|
|
68
|
-
env?: KernelRuntimeEnv;
|
|
69
|
-
signal?: AbortSignal;
|
|
70
|
-
onChunk?: (text: string) => Promise<void> | void;
|
|
71
|
-
onDisplay?: (output: KernelDisplayOutput) => Promise<void> | void;
|
|
72
|
-
timeoutMs?: number;
|
|
73
|
-
silent?: boolean;
|
|
74
|
-
storeHistory?: boolean;
|
|
75
|
-
allowStdin?: boolean;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export interface KernelExecuteResult {
|
|
79
|
-
status: "ok" | "error";
|
|
80
|
-
executionCount?: number;
|
|
81
|
-
error?: { name: string; value: string; traceback: string[] };
|
|
82
|
-
cancelled: boolean;
|
|
83
|
-
timedOut: boolean;
|
|
84
|
-
stdinRequested: boolean;
|
|
85
|
-
/**
|
|
86
|
-
* True when the kernel subprocess was killed as part of settling this
|
|
87
|
-
* execution (e.g. SIGINT was ignored and we escalated to shutdown, or the
|
|
88
|
-
* kernel died unexpectedly). When false, the kernel remains reusable.
|
|
89
|
-
*/
|
|
90
|
-
kernelKilled?: boolean;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
export interface KernelShutdownResult {
|
|
94
|
-
confirmed: boolean;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
interface KernelLifecycleOptions {
|
|
98
|
-
signal?: AbortSignal;
|
|
99
|
-
deadlineMs?: number;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
interface KernelStartOptions extends KernelLifecycleOptions {
|
|
103
|
-
cwd: string;
|
|
104
|
-
env?: Record<string, string | undefined>;
|
|
105
|
-
/**
|
|
106
|
-
* Explicit interpreter path (`python.interpreter` from the session's
|
|
107
|
-
* settings). When set, runtime discovery is skipped entirely.
|
|
108
|
-
*/
|
|
109
|
-
interpreter?: string;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
interface KernelShutdownOptions {
|
|
113
|
-
signal?: AbortSignal;
|
|
114
|
-
timeoutMs?: number;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
68
|
export interface PythonKernelAvailability {
|
|
118
69
|
ok: boolean;
|
|
119
70
|
pythonPath?: string;
|
|
@@ -122,24 +73,6 @@ export interface PythonKernelAvailability {
|
|
|
122
73
|
runtime?: PythonRuntime;
|
|
123
74
|
}
|
|
124
75
|
|
|
125
|
-
function getRemainingTimeMs(deadlineMs?: number): number | undefined {
|
|
126
|
-
if (deadlineMs === undefined) return undefined;
|
|
127
|
-
return Math.max(0, deadlineMs - Date.now());
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function createAbortError(name: "AbortError" | "TimeoutError", message: string): Error {
|
|
131
|
-
const err = new Error(message);
|
|
132
|
-
err.name = name;
|
|
133
|
-
return err;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
function throwIfAborted(signal: AbortSignal | undefined, fallbackReason: string): void {
|
|
137
|
-
if (!signal?.aborted) return;
|
|
138
|
-
const reason = signal.reason;
|
|
139
|
-
if (reason instanceof Error) throw reason;
|
|
140
|
-
throw createAbortError("AbortError", typeof reason === "string" ? reason : fallbackReason);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
76
|
// Cache successful probes per resolved cwd + explicit interpreter: every cell
|
|
144
77
|
// otherwise pays one (or two — backend.isAvailable + ensureKernelAvailable)
|
|
145
78
|
// interpreter spawns even when the kernel is already hot. Failures are not
|
|
@@ -207,48 +140,24 @@ async function probePythonKernelAvailability(cwd: string, interpreter?: string):
|
|
|
207
140
|
}
|
|
208
141
|
}
|
|
209
142
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
interface Frame {
|
|
213
|
-
type: FrameType;
|
|
214
|
-
id?: string;
|
|
215
|
-
data?: string;
|
|
216
|
-
bundle?: Record<string, unknown>;
|
|
217
|
-
ename?: string;
|
|
218
|
-
evalue?: string;
|
|
219
|
-
traceback?: string[];
|
|
220
|
-
status?: "ok" | "error";
|
|
221
|
-
executionCount?: number;
|
|
222
|
-
cancelled?: boolean;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
interface PendingExecution {
|
|
226
|
-
resolve: (result: KernelExecuteResult) => void;
|
|
227
|
-
options?: KernelExecuteOptions;
|
|
228
|
-
status: "ok" | "error";
|
|
229
|
-
executionCount?: number;
|
|
230
|
-
error?: { name: string; value: string; traceback: string[] };
|
|
231
|
-
cancelled: boolean;
|
|
232
|
-
timedOut: boolean;
|
|
233
|
-
stdinRequested: boolean;
|
|
234
|
-
kernelKilled: boolean;
|
|
235
|
-
settled: boolean;
|
|
236
|
-
escalationTimer?: NodeJS.Timeout;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
export class PythonKernel {
|
|
240
|
-
readonly id: string;
|
|
241
|
-
#proc: Subprocess | null = null;
|
|
242
|
-
#stdin: Bun.FileSink | null = null;
|
|
243
|
-
#alive = true;
|
|
244
|
-
#disposed = false;
|
|
245
|
-
#shutdownConfirmed = false;
|
|
246
|
-
#exitedPromise: Promise<number> | null = null;
|
|
247
|
-
#pending = new Map<string, PendingExecution>();
|
|
248
|
-
#readBuffer = "";
|
|
249
|
-
|
|
143
|
+
export class PythonKernel extends BaseKernel {
|
|
250
144
|
private constructor(id: string) {
|
|
251
|
-
|
|
145
|
+
super(id, {
|
|
146
|
+
languageName: "Python",
|
|
147
|
+
traceIpc: TRACE_IPC,
|
|
148
|
+
exitPayload: JSON.stringify({ type: "exit" }),
|
|
149
|
+
interruptEscalationMs: INTERRUPT_ESCALATION_MS,
|
|
150
|
+
shutdownGraceMs: SHUTDOWN_GRACE_MS,
|
|
151
|
+
buildPayload: (code, msgId, opts) =>
|
|
152
|
+
JSON.stringify({
|
|
153
|
+
id: msgId,
|
|
154
|
+
code,
|
|
155
|
+
cwd: opts?.cwd,
|
|
156
|
+
env: opts?.env,
|
|
157
|
+
silent: opts?.silent ?? false,
|
|
158
|
+
storeHistory: opts?.storeHistory ?? !(opts?.silent ?? false),
|
|
159
|
+
}),
|
|
160
|
+
});
|
|
252
161
|
}
|
|
253
162
|
|
|
254
163
|
static async start(options: KernelStartOptions): Promise<PythonKernel> {
|
|
@@ -262,10 +171,6 @@ export class PythonKernel {
|
|
|
262
171
|
throw new Error(availability.reason ?? "Python kernel unavailable");
|
|
263
172
|
}
|
|
264
173
|
|
|
265
|
-
// Reuse the interpreter the availability probe selected so the spawned
|
|
266
|
-
// kernel matches what we verified actually runs. The fallback computes a
|
|
267
|
-
// runtime only for the skip-check fast path (test runtime /
|
|
268
|
-
// PI_PYTHON_SKIP_CHECK), where no candidate was probed.
|
|
269
174
|
let runtime = availability.runtime;
|
|
270
175
|
if (!runtime) {
|
|
271
176
|
const { env: shellEnv } = (await Settings.init()).getShellConfig();
|
|
@@ -280,7 +185,6 @@ export class PythonKernel {
|
|
|
280
185
|
for (const [key, value] of Object.entries(options.env ?? {})) {
|
|
281
186
|
if (typeof value === "string") spawnEnv[key] = value;
|
|
282
187
|
}
|
|
283
|
-
// Unbuffered IO is critical for streaming.
|
|
284
188
|
spawnEnv.PYTHONUNBUFFERED = "1";
|
|
285
189
|
spawnEnv.PYTHONIOENCODING = "utf-8";
|
|
286
190
|
|
|
@@ -293,447 +197,28 @@ export class PythonKernel {
|
|
|
293
197
|
stdin: "pipe",
|
|
294
198
|
stdout: "pipe",
|
|
295
199
|
stderr: "pipe",
|
|
296
|
-
// Detached from any inherited console only when the host itself
|
|
297
|
-
// has no console — kernel32!GetConsoleWindow() is authoritative
|
|
298
|
-
// (works even when every stdio stream is redirected), with a
|
|
299
|
-
// TTY-OR fallback when the FFI probe is unavailable. See #1960
|
|
300
|
-
// for the numpy/pandas LoadLibraryExW hang + SIGINT-recovery
|
|
301
|
-
// failure that motivates the predicate.
|
|
302
200
|
windowsHide: shouldHideKernelWindow({
|
|
303
201
|
platform: process.platform,
|
|
304
202
|
hostHasInheritableConsole: hostHasInheritableConsole(),
|
|
305
203
|
}),
|
|
306
204
|
});
|
|
307
|
-
kernel.#proc = proc;
|
|
308
|
-
kernel.#stdin = proc.stdin;
|
|
309
|
-
kernel.#exitedPromise = proc.exited;
|
|
310
|
-
void kernel.#exitedPromise.then(code => {
|
|
311
|
-
kernel.#alive = false;
|
|
312
|
-
kernel.#abortPendingExecutions(`Python kernel exited with code ${code}`, { kernelKilled: true });
|
|
313
|
-
});
|
|
314
205
|
|
|
315
|
-
kernel
|
|
316
|
-
kernel.#startStderrDrain(proc.stderr as ReadableStream<Uint8Array>);
|
|
206
|
+
kernel.setProcess(proc);
|
|
317
207
|
|
|
318
208
|
const startup = { signal: options.signal, deadlineMs: options.deadlineMs };
|
|
319
209
|
const startupBudget = Math.min(getRemainingTimeMs(startup.deadlineMs) ?? STARTUP_TIMEOUT_MS, STARTUP_TIMEOUT_MS);
|
|
320
210
|
|
|
321
211
|
try {
|
|
322
212
|
const initScript = buildInitScript(options.cwd, options.env);
|
|
323
|
-
await kernel
|
|
324
|
-
await kernel
|
|
213
|
+
await kernel.executeWithBudget(initScript, startup.signal, startupBudget, "Python kernel init");
|
|
214
|
+
await kernel.executeWithBudget(PYTHON_PRELUDE, startup.signal, startupBudget, "Python kernel prelude");
|
|
325
215
|
return kernel;
|
|
326
216
|
} catch (err) {
|
|
327
217
|
await kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }).catch(() => {});
|
|
328
218
|
throw err;
|
|
329
219
|
}
|
|
330
220
|
}
|
|
331
|
-
|
|
332
|
-
isAlive(): boolean {
|
|
333
|
-
return this.#alive && !this.#disposed;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
async execute(code: string, options?: KernelExecuteOptions): Promise<KernelExecuteResult> {
|
|
337
|
-
if (!this.isAlive()) {
|
|
338
|
-
throw new Error("Python kernel is not running");
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
const msgId = options?.id ?? Snowflake.next();
|
|
342
|
-
const { promise, resolve } = Promise.withResolvers<KernelExecuteResult>();
|
|
343
|
-
const pending: PendingExecution = {
|
|
344
|
-
resolve,
|
|
345
|
-
options,
|
|
346
|
-
status: "ok",
|
|
347
|
-
cancelled: false,
|
|
348
|
-
timedOut: false,
|
|
349
|
-
stdinRequested: false,
|
|
350
|
-
settled: false,
|
|
351
|
-
kernelKilled: false,
|
|
352
|
-
};
|
|
353
|
-
this.#pending.set(msgId, pending);
|
|
354
|
-
|
|
355
|
-
const finalize = () => {
|
|
356
|
-
if (pending.settled) return;
|
|
357
|
-
pending.settled = true;
|
|
358
|
-
this.#pending.delete(msgId);
|
|
359
|
-
cleanup();
|
|
360
|
-
resolve({
|
|
361
|
-
status: pending.status,
|
|
362
|
-
executionCount: pending.executionCount,
|
|
363
|
-
error: pending.error,
|
|
364
|
-
cancelled: pending.cancelled,
|
|
365
|
-
timedOut: pending.timedOut,
|
|
366
|
-
stdinRequested: pending.stdinRequested,
|
|
367
|
-
kernelKilled: pending.kernelKilled,
|
|
368
|
-
});
|
|
369
|
-
};
|
|
370
|
-
|
|
371
|
-
const requestCancel = () => {
|
|
372
|
-
if (pending.settled || pending.escalationTimer) return;
|
|
373
|
-
void this.interrupt();
|
|
374
|
-
const escalation = setTimeout(() => {
|
|
375
|
-
if (pending.settled) return;
|
|
376
|
-
logger.warn("Python runner did not respond to SIGINT; terminating subprocess", {
|
|
377
|
-
kernelId: this.id,
|
|
378
|
-
});
|
|
379
|
-
// SIGINT was ignored; mark the cell as kernel-killed so callers can
|
|
380
|
-
// surface the harsher recovery message. `shutdown()` aborts pending
|
|
381
|
-
// executions immediately and escalates to SIGTERM/SIGKILL, so the
|
|
382
|
-
// host queue unblocks even if the runner is stuck in a
|
|
383
|
-
// non-interruptible state.
|
|
384
|
-
pending.kernelKilled = true;
|
|
385
|
-
void this.shutdown();
|
|
386
|
-
}, INTERRUPT_ESCALATION_MS);
|
|
387
|
-
escalation.unref?.();
|
|
388
|
-
pending.escalationTimer = escalation;
|
|
389
|
-
};
|
|
390
|
-
|
|
391
|
-
const onAbort = () => {
|
|
392
|
-
pending.cancelled = true;
|
|
393
|
-
pending.timedOut = pending.timedOut || isTimeoutReason(options?.signal?.reason);
|
|
394
|
-
requestCancel();
|
|
395
|
-
};
|
|
396
|
-
const timeoutId =
|
|
397
|
-
typeof options?.timeoutMs === "number" && options.timeoutMs > 0
|
|
398
|
-
? setTimeout(() => {
|
|
399
|
-
pending.timedOut = true;
|
|
400
|
-
pending.cancelled = true;
|
|
401
|
-
requestCancel();
|
|
402
|
-
}, options.timeoutMs)
|
|
403
|
-
: undefined;
|
|
404
|
-
|
|
405
|
-
const cleanup = () => {
|
|
406
|
-
if (timeoutId) clearTimeout(timeoutId);
|
|
407
|
-
if (pending.escalationTimer) clearTimeout(pending.escalationTimer);
|
|
408
|
-
pending.escalationTimer = undefined;
|
|
409
|
-
options?.signal?.removeEventListener("abort", onAbort);
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
if (options?.signal) {
|
|
413
|
-
if (options.signal.aborted) {
|
|
414
|
-
onAbort();
|
|
415
|
-
} else {
|
|
416
|
-
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Stash finalize on the pending entry so the reader can call it on `done`.
|
|
421
|
-
(pending as PendingExecution & { finalize: () => void }).finalize = finalize;
|
|
422
|
-
|
|
423
|
-
const payload = JSON.stringify({
|
|
424
|
-
id: msgId,
|
|
425
|
-
code,
|
|
426
|
-
cwd: options?.cwd,
|
|
427
|
-
env: options?.env,
|
|
428
|
-
silent: options?.silent ?? false,
|
|
429
|
-
storeHistory: options?.storeHistory ?? !(options?.silent ?? false),
|
|
430
|
-
});
|
|
431
|
-
|
|
432
|
-
try {
|
|
433
|
-
await this.#writeLine(payload);
|
|
434
|
-
} catch (err) {
|
|
435
|
-
pending.cancelled = true;
|
|
436
|
-
pending.error = {
|
|
437
|
-
name: "TransportError",
|
|
438
|
-
value: err instanceof Error ? err.message : String(err),
|
|
439
|
-
traceback: [],
|
|
440
|
-
};
|
|
441
|
-
finalize();
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
return promise;
|
|
445
|
-
}
|
|
446
|
-
|
|
447
|
-
async interrupt(): Promise<void> {
|
|
448
|
-
if (!this.#proc || this.#disposed) return;
|
|
449
|
-
try {
|
|
450
|
-
this.#proc.kill("SIGINT");
|
|
451
|
-
} catch (err) {
|
|
452
|
-
logger.warn("Failed to interrupt python runner", { error: err instanceof Error ? err.message : String(err) });
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
async shutdown(options?: KernelShutdownOptions): Promise<KernelShutdownResult> {
|
|
457
|
-
if (this.#shutdownConfirmed) return { confirmed: true };
|
|
458
|
-
|
|
459
|
-
this.#alive = false;
|
|
460
|
-
this.#abortPendingExecutions("Python kernel shutdown", { kernelKilled: true });
|
|
461
|
-
|
|
462
|
-
const timeoutMs = options?.timeoutMs ?? SHUTDOWN_GRACE_MS;
|
|
463
|
-
const proc = this.#proc;
|
|
464
|
-
if (!proc) {
|
|
465
|
-
this.#shutdownConfirmed = true;
|
|
466
|
-
this.#disposed = true;
|
|
467
|
-
return { confirmed: true };
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
try {
|
|
471
|
-
await this.#writeLine(JSON.stringify({ type: "exit" })).catch(() => {});
|
|
472
|
-
} catch {
|
|
473
|
-
/* writer may already be closed */
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
this.#stdin?.end();
|
|
478
|
-
} catch {
|
|
479
|
-
/* ignore */
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
const exited = this.#waitForExitWithTimeout(timeoutMs);
|
|
483
|
-
let result = await exited;
|
|
484
|
-
if (!result) {
|
|
485
|
-
try {
|
|
486
|
-
proc.kill("SIGTERM");
|
|
487
|
-
} catch {
|
|
488
|
-
/* ignore */
|
|
489
|
-
}
|
|
490
|
-
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
491
|
-
}
|
|
492
|
-
if (!result) {
|
|
493
|
-
try {
|
|
494
|
-
proc.kill("SIGKILL");
|
|
495
|
-
} catch {
|
|
496
|
-
/* ignore */
|
|
497
|
-
}
|
|
498
|
-
result = await this.#waitForExitWithTimeout(timeoutMs);
|
|
499
|
-
}
|
|
500
|
-
|
|
501
|
-
const confirmed = !!result;
|
|
502
|
-
this.#shutdownConfirmed = confirmed;
|
|
503
|
-
this.#disposed = true;
|
|
504
|
-
return { confirmed };
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
#abortPendingExecutions(reason: string, options?: { kernelKilled?: boolean }): void {
|
|
508
|
-
if (this.#pending.size === 0) return;
|
|
509
|
-
const pending = Array.from(this.#pending.values());
|
|
510
|
-
this.#pending.clear();
|
|
511
|
-
const kernelKilledDefault = options?.kernelKilled ?? false;
|
|
512
|
-
for (const entry of pending) {
|
|
513
|
-
if (entry.settled) continue;
|
|
514
|
-
entry.settled = true;
|
|
515
|
-
void entry.options?.onChunk?.(`[kernel] ${reason}\n`);
|
|
516
|
-
entry.resolve({
|
|
517
|
-
status: "error",
|
|
518
|
-
cancelled: true,
|
|
519
|
-
timedOut: entry.timedOut,
|
|
520
|
-
stdinRequested: entry.stdinRequested,
|
|
521
|
-
executionCount: entry.executionCount,
|
|
522
|
-
error: entry.error,
|
|
523
|
-
kernelKilled: entry.kernelKilled || kernelKilledDefault,
|
|
524
|
-
});
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
async #writeLine(line: string): Promise<void> {
|
|
529
|
-
if (!this.#stdin) {
|
|
530
|
-
throw new Error("Python kernel stdin is not open");
|
|
531
|
-
}
|
|
532
|
-
if (TRACE_IPC) {
|
|
533
|
-
logger.debug("PythonKernel send", { preview: line.slice(0, 120) });
|
|
534
|
-
}
|
|
535
|
-
this.#stdin.write(`${line}\n`);
|
|
536
|
-
this.#stdin.flush();
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
#startReader(stream: ReadableStream<Uint8Array>): void {
|
|
540
|
-
const reader = stream.getReader();
|
|
541
|
-
const decoder = new TextDecoder();
|
|
542
|
-
const loop = async () => {
|
|
543
|
-
try {
|
|
544
|
-
while (true) {
|
|
545
|
-
const { done, value } = await reader.read();
|
|
546
|
-
if (done) break;
|
|
547
|
-
this.#readBuffer += decoder.decode(value, { stream: true });
|
|
548
|
-
await this.#flushFrames();
|
|
549
|
-
}
|
|
550
|
-
this.#readBuffer += decoder.decode();
|
|
551
|
-
await this.#flushFrames();
|
|
552
|
-
} catch (err) {
|
|
553
|
-
logger.warn("Python kernel reader failed", { error: err instanceof Error ? err.message : String(err) });
|
|
554
|
-
} finally {
|
|
555
|
-
try {
|
|
556
|
-
reader.releaseLock();
|
|
557
|
-
} catch {
|
|
558
|
-
/* ignore */
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
};
|
|
562
|
-
void loop();
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
#startStderrDrain(stream: ReadableStream<Uint8Array>): void {
|
|
566
|
-
// Wrapper writes its own crashes to stderr; surface them via logger so the
|
|
567
|
-
// host operator can debug runtime issues without polluting tool output.
|
|
568
|
-
const reader = stream.getReader();
|
|
569
|
-
const decoder = new TextDecoder();
|
|
570
|
-
const loop = async () => {
|
|
571
|
-
try {
|
|
572
|
-
while (true) {
|
|
573
|
-
const { done, value } = await reader.read();
|
|
574
|
-
if (done) break;
|
|
575
|
-
const text = decoder.decode(value);
|
|
576
|
-
if (text.trim()) {
|
|
577
|
-
logger.warn("Python runner stderr", { text });
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
} catch {
|
|
581
|
-
/* ignore */
|
|
582
|
-
} finally {
|
|
583
|
-
try {
|
|
584
|
-
reader.releaseLock();
|
|
585
|
-
} catch {
|
|
586
|
-
/* ignore */
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
};
|
|
590
|
-
void loop();
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
async #flushFrames(): Promise<void> {
|
|
594
|
-
while (true) {
|
|
595
|
-
const nl = this.#readBuffer.indexOf("\n");
|
|
596
|
-
if (nl < 0) return;
|
|
597
|
-
const line = this.#readBuffer.slice(0, nl);
|
|
598
|
-
this.#readBuffer = this.#readBuffer.slice(nl + 1);
|
|
599
|
-
if (!line.trim()) continue;
|
|
600
|
-
let frame: Frame;
|
|
601
|
-
try {
|
|
602
|
-
frame = JSON.parse(line) as Frame;
|
|
603
|
-
} catch (err) {
|
|
604
|
-
logger.warn("Python runner emitted invalid JSON", {
|
|
605
|
-
line: line.slice(0, 200),
|
|
606
|
-
error: err instanceof Error ? err.message : String(err),
|
|
607
|
-
});
|
|
608
|
-
continue;
|
|
609
|
-
}
|
|
610
|
-
if (TRACE_IPC) {
|
|
611
|
-
logger.debug("PythonKernel recv", { type: frame.type, id: frame.id });
|
|
612
|
-
}
|
|
613
|
-
await this.#handleFrame(frame);
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
async #handleFrame(frame: Frame): Promise<void> {
|
|
618
|
-
const rid = frame.id;
|
|
619
|
-
if (!rid) return;
|
|
620
|
-
const pending = this.#pending.get(rid) as (PendingExecution & { finalize?: () => void }) | undefined;
|
|
621
|
-
if (!pending) return;
|
|
622
|
-
|
|
623
|
-
switch (frame.type) {
|
|
624
|
-
case "started":
|
|
625
|
-
return;
|
|
626
|
-
case "stdout":
|
|
627
|
-
case "stderr": {
|
|
628
|
-
const text = frame.data ?? "";
|
|
629
|
-
if (text && pending.options?.onChunk) {
|
|
630
|
-
await pending.options.onChunk(text);
|
|
631
|
-
}
|
|
632
|
-
return;
|
|
633
|
-
}
|
|
634
|
-
case "display":
|
|
635
|
-
case "result": {
|
|
636
|
-
const bundle = frame.bundle ?? {};
|
|
637
|
-
const { text, outputs } = await renderKernelDisplay(bundle);
|
|
638
|
-
if (text && pending.options?.onChunk) {
|
|
639
|
-
await pending.options.onChunk(text);
|
|
640
|
-
}
|
|
641
|
-
if (outputs.length > 0 && pending.options?.onDisplay) {
|
|
642
|
-
for (const output of outputs) {
|
|
643
|
-
await pending.options.onDisplay(output);
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
return;
|
|
647
|
-
}
|
|
648
|
-
case "error": {
|
|
649
|
-
const traceback = Array.isArray(frame.traceback) ? frame.traceback.map(String) : [];
|
|
650
|
-
pending.status = "error";
|
|
651
|
-
pending.error = {
|
|
652
|
-
name: String(frame.ename ?? "Error"),
|
|
653
|
-
value: String(frame.evalue ?? ""),
|
|
654
|
-
traceback,
|
|
655
|
-
};
|
|
656
|
-
const message =
|
|
657
|
-
traceback.length > 0 ? `${traceback.join("\n")}\n` : `${pending.error.name}: ${pending.error.value}\n`;
|
|
658
|
-
if (pending.options?.onChunk) {
|
|
659
|
-
await pending.options.onChunk(message);
|
|
660
|
-
}
|
|
661
|
-
return;
|
|
662
|
-
}
|
|
663
|
-
case "done": {
|
|
664
|
-
if (typeof frame.executionCount === "number") {
|
|
665
|
-
pending.executionCount = frame.executionCount;
|
|
666
|
-
}
|
|
667
|
-
if (frame.status === "error" && pending.status === "ok") {
|
|
668
|
-
pending.status = "error";
|
|
669
|
-
}
|
|
670
|
-
if (frame.cancelled) {
|
|
671
|
-
pending.cancelled = true;
|
|
672
|
-
}
|
|
673
|
-
pending.finalize?.();
|
|
674
|
-
return;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
async #executeWithBudget(
|
|
680
|
-
code: string,
|
|
681
|
-
signal: AbortSignal | undefined,
|
|
682
|
-
timeoutMs: number,
|
|
683
|
-
label: string,
|
|
684
|
-
): Promise<void> {
|
|
685
|
-
const controller = new AbortController();
|
|
686
|
-
const cleanups: Array<() => void> = [];
|
|
687
|
-
if (signal) {
|
|
688
|
-
if (signal.aborted) {
|
|
689
|
-
controller.abort(signal.reason);
|
|
690
|
-
} else {
|
|
691
|
-
const onAbort = () => controller.abort(signal.reason);
|
|
692
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
693
|
-
cleanups.push(() => signal.removeEventListener("abort", onAbort));
|
|
694
|
-
}
|
|
695
|
-
}
|
|
696
|
-
const timer =
|
|
697
|
-
timeoutMs > 0
|
|
698
|
-
? setTimeout(() => controller.abort(createAbortError("TimeoutError", `${label} timed out`)), timeoutMs)
|
|
699
|
-
: undefined;
|
|
700
|
-
if (timer) cleanups.push(() => clearTimeout(timer));
|
|
701
|
-
try {
|
|
702
|
-
throwIfAborted(controller.signal, label);
|
|
703
|
-
const result = await this.execute(code, {
|
|
704
|
-
signal: controller.signal,
|
|
705
|
-
silent: true,
|
|
706
|
-
storeHistory: false,
|
|
707
|
-
});
|
|
708
|
-
if (result.cancelled) {
|
|
709
|
-
throw createAbortError(result.timedOut ? "TimeoutError" : "AbortError", `${label} cancelled`);
|
|
710
|
-
}
|
|
711
|
-
if (result.status === "error") {
|
|
712
|
-
const reason = result.error?.value ?? "Python kernel init failed";
|
|
713
|
-
throw new Error(`${label} failed: ${reason}`);
|
|
714
|
-
}
|
|
715
|
-
} finally {
|
|
716
|
-
for (const cleanup of cleanups) cleanup();
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
#waitForExitWithTimeout(timeoutMs: number): Promise<number | null> {
|
|
721
|
-
if (!this.#exitedPromise) return Promise.resolve(0);
|
|
722
|
-
const exitedPromise = this.#exitedPromise;
|
|
723
|
-
const timeout = new Promise<null>(resolve => {
|
|
724
|
-
const timer = setTimeout(() => resolve(null), Math.max(0, timeoutMs));
|
|
725
|
-
timer.unref?.();
|
|
726
|
-
});
|
|
727
|
-
return Promise.race([exitedPromise.then(code => code as number | null), timeout]);
|
|
728
|
-
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
function isTimeoutReason(reason: unknown): boolean {
|
|
732
|
-
if (reason instanceof DOMException) return reason.name === "TimeoutError";
|
|
733
|
-
if (reason instanceof Error) return reason.name === "TimeoutError";
|
|
734
|
-
return false;
|
|
735
221
|
}
|
|
736
|
-
|
|
737
222
|
function buildInitScript(cwd: string, env?: Record<string, string | undefined>): string {
|
|
738
223
|
const envEntries = Object.entries(env ?? {}).filter(([, value]) => value !== undefined);
|
|
739
224
|
const envPayload = Object.fromEntries(envEntries);
|