@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
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
3
|
+
import type { ToolSession } from "../../tools";
|
|
4
|
+
import { attachSessionOwner, createCancelledKernelResult, executeWithKernelBase } from "../executor-base";
|
|
5
|
+
import { ensurePyToolBridge, type PyToolBridgeInfo } from "../py/tool-bridge";
|
|
6
|
+
import type { EvalDisplayOutput, EvalStatusEvent } from "../types";
|
|
7
|
+
import {
|
|
8
|
+
checkJuliaKernelAvailability,
|
|
9
|
+
JuliaKernel,
|
|
10
|
+
type KernelExecuteOptions,
|
|
11
|
+
type KernelExecuteResult,
|
|
12
|
+
} from "./kernel";
|
|
13
|
+
import { resolveExplicitJuliaRuntime } from "./runtime";
|
|
14
|
+
|
|
15
|
+
const SHUTDOWN_GRACE_MS = 1_000;
|
|
16
|
+
|
|
17
|
+
export interface JuliaExecutorOptions {
|
|
18
|
+
cwd?: string;
|
|
19
|
+
sessionId?: string;
|
|
20
|
+
sessionFile?: string;
|
|
21
|
+
artifactsDir?: string;
|
|
22
|
+
localRoots?: Record<string, string>;
|
|
23
|
+
interpreter?: string;
|
|
24
|
+
onChunk?: (text: string) => void | Promise<void>;
|
|
25
|
+
onStatus?: (event: EvalStatusEvent) => void;
|
|
26
|
+
signal?: AbortSignal;
|
|
27
|
+
timeoutMs?: number;
|
|
28
|
+
deadlineMs?: number;
|
|
29
|
+
idleTimeoutMs?: number;
|
|
30
|
+
kernelOwnerId?: string;
|
|
31
|
+
reset?: boolean;
|
|
32
|
+
toolSession?: ToolSession;
|
|
33
|
+
bridge?: PyToolBridgeInfo;
|
|
34
|
+
bridgeSessionId?: string;
|
|
35
|
+
artifactId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface JuliaKernelExecutor {
|
|
39
|
+
execute: (code: string, options?: KernelExecuteOptions) => Promise<KernelExecuteResult>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface JuliaResult {
|
|
43
|
+
output: string;
|
|
44
|
+
exitCode: number | undefined;
|
|
45
|
+
cancelled: boolean;
|
|
46
|
+
truncated: boolean;
|
|
47
|
+
artifactId: string | undefined;
|
|
48
|
+
totalLines: number;
|
|
49
|
+
totalBytes: number;
|
|
50
|
+
outputLines: number;
|
|
51
|
+
outputBytes: number;
|
|
52
|
+
displayOutputs: EvalDisplayOutput[];
|
|
53
|
+
stdinRequested: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface JuliaSessionOwners {
|
|
57
|
+
ownerIds: Set<string>;
|
|
58
|
+
hasFallbackOwner: boolean;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
interface JuliaSession extends JuliaSessionOwners {
|
|
62
|
+
sessionKey: string;
|
|
63
|
+
sessionId: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
kernel: JuliaKernel;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
interface StartingJuliaSession extends JuliaSessionOwners {
|
|
69
|
+
promise: Promise<JuliaSession>;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class JuliaExecutionCancelledError extends Error {
|
|
73
|
+
constructor(readonly timedOut: boolean) {
|
|
74
|
+
super(timedOut ? "Julia execution timed out" : "Julia execution cancelled");
|
|
75
|
+
this.name = "JuliaExecutionCancelledError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const sessions = new Map<string, JuliaSession>();
|
|
80
|
+
const startingSessions = new Map<string, StartingJuliaSession>();
|
|
81
|
+
const resettingSessions = new Map<string, Promise<void>>();
|
|
82
|
+
|
|
83
|
+
function normalizeSessionCwd(cwd: string): string {
|
|
84
|
+
return path.resolve(cwd);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function normalizeExplicitInterpreter(cwd: string, interpreter: string | undefined): string {
|
|
88
|
+
if (interpreter === undefined) return "";
|
|
89
|
+
const resolved = resolveExplicitJuliaRuntime(interpreter, cwd, {}).juliaPath;
|
|
90
|
+
try {
|
|
91
|
+
return path.resolve(resolved);
|
|
92
|
+
} catch {
|
|
93
|
+
return resolved;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function buildSessionKey(sessionId: string, cwd: string, interpreter: string | undefined): string {
|
|
98
|
+
const normalizedCwd = normalizeSessionCwd(cwd);
|
|
99
|
+
const normalizedInterpreter = normalizeExplicitInterpreter(normalizedCwd, interpreter);
|
|
100
|
+
return `${sessionId}::${normalizedCwd}::${normalizedInterpreter}`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isCancellationError(error: unknown): boolean {
|
|
104
|
+
if (error instanceof JuliaExecutionCancelledError) return true;
|
|
105
|
+
if (error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError")) return true;
|
|
106
|
+
if (
|
|
107
|
+
error &&
|
|
108
|
+
typeof error === "object" &&
|
|
109
|
+
"name" in error &&
|
|
110
|
+
(error.name === "AbortError" || error.name === "TimeoutError")
|
|
111
|
+
)
|
|
112
|
+
return true;
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isTimedOutCancellation(error: unknown, signal?: AbortSignal): boolean {
|
|
117
|
+
if (error instanceof JuliaExecutionCancelledError) return error.timedOut;
|
|
118
|
+
if (error instanceof Error && error.name === "TimeoutError") return true;
|
|
119
|
+
if (error && typeof error === "object" && "name" in error && error.name === "TimeoutError") return true;
|
|
120
|
+
if (signal?.reason instanceof Error && signal.reason.name === "TimeoutError") return true;
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function getExecutionDeadlineMs(options?: Pick<JuliaExecutorOptions, "deadlineMs" | "timeoutMs">): number | undefined {
|
|
125
|
+
if (options?.deadlineMs !== undefined) return options.deadlineMs;
|
|
126
|
+
if (options?.timeoutMs !== undefined && options.timeoutMs > 0) return Date.now() + options.timeoutMs;
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
131
|
+
if (deadlineMs === undefined) return undefined;
|
|
132
|
+
return Math.max(0, deadlineMs - Date.now());
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function requireRemainingTimeoutMs(deadlineMs?: number): number | undefined {
|
|
136
|
+
if (deadlineMs === undefined) return undefined;
|
|
137
|
+
const remaining = getRemainingTimeoutMs(deadlineMs);
|
|
138
|
+
if (remaining !== undefined && remaining <= 0) {
|
|
139
|
+
throw new JuliaExecutionCancelledError(true);
|
|
140
|
+
}
|
|
141
|
+
return remaining;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function waitForPromiseWithCancellation<T>(
|
|
145
|
+
promise: Promise<T>,
|
|
146
|
+
options: Pick<JuliaExecutorOptions, "signal" | "deadlineMs">,
|
|
147
|
+
): Promise<T> {
|
|
148
|
+
if (options.signal?.aborted) {
|
|
149
|
+
throw new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
150
|
+
}
|
|
151
|
+
const cleanups: Array<() => void> = [];
|
|
152
|
+
const { promise: cancelPromise, reject } = Promise.withResolvers<never>();
|
|
153
|
+
|
|
154
|
+
if (options.signal) {
|
|
155
|
+
const onAbort = () => {
|
|
156
|
+
reject(new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal?.reason, options.signal)));
|
|
157
|
+
};
|
|
158
|
+
options.signal.addEventListener("abort", onAbort, { once: true });
|
|
159
|
+
cleanups.push(() => options.signal?.removeEventListener("abort", onAbort));
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const deadlineMs = options.deadlineMs;
|
|
163
|
+
if (typeof deadlineMs === "number" && deadlineMs > Date.now()) {
|
|
164
|
+
const timeout = setTimeout(() => {
|
|
165
|
+
reject(new JuliaExecutionCancelledError(true));
|
|
166
|
+
}, deadlineMs - Date.now());
|
|
167
|
+
timeout.unref?.();
|
|
168
|
+
cleanups.push(() => clearTimeout(timeout));
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
return await Promise.race([promise, cancelPromise]);
|
|
173
|
+
} finally {
|
|
174
|
+
for (const cleanup of cleanups) cleanup();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function formatTimeoutAnnotation(timeoutMs?: number): string | undefined {
|
|
179
|
+
if (timeoutMs === undefined) return undefined;
|
|
180
|
+
const rounded = (timeoutMs / 1000).toFixed(0);
|
|
181
|
+
return `[cell timed out after ${rounded}s]`;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function formatKernelTimeoutAnnotation(timeoutMs: number | undefined, kernelKilled: boolean): string {
|
|
185
|
+
const explanation = kernelKilled ? "; active subprocess terminated to recover" : "; kernel is still running";
|
|
186
|
+
if (timeoutMs === undefined) return `[execution timed out${explanation}]`;
|
|
187
|
+
const rounded = (timeoutMs / 1000).toFixed(0);
|
|
188
|
+
return `[execution timed out after ${rounded}s${explanation}]`;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function createCancelledJuliaResult(_timedOut: boolean, timeoutMs?: number): JuliaResult {
|
|
192
|
+
const output = formatTimeoutAnnotation(timeoutMs) ?? "[execution cancelled]\n";
|
|
193
|
+
return createCancelledKernelResult(output);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function buildKernelEnvPatch(options: {
|
|
197
|
+
sessionFile?: string;
|
|
198
|
+
artifactsDir?: string;
|
|
199
|
+
bridge?: PyToolBridgeInfo;
|
|
200
|
+
bridgeSessionId?: string;
|
|
201
|
+
localRoots?: Record<string, string>;
|
|
202
|
+
}): Record<string, string | undefined> {
|
|
203
|
+
const patch: Record<string, string | undefined> = {};
|
|
204
|
+
if (options.sessionFile) patch.PI_SESSION_FILE = options.sessionFile;
|
|
205
|
+
if (options.artifactsDir) patch.PI_ARTIFACTS_DIR = options.artifactsDir;
|
|
206
|
+
if (options.bridge) {
|
|
207
|
+
patch.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
208
|
+
patch.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
|
|
209
|
+
patch.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId ?? "";
|
|
210
|
+
}
|
|
211
|
+
if (options.localRoots) {
|
|
212
|
+
patch.PI_EVAL_LOCAL_ROOTS = JSON.stringify(options.localRoots);
|
|
213
|
+
}
|
|
214
|
+
return patch;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function buildKernelEnv(options: {
|
|
218
|
+
sessionFile?: string;
|
|
219
|
+
artifactsDir?: string;
|
|
220
|
+
bridge?: PyToolBridgeInfo;
|
|
221
|
+
bridgeSessionId?: string;
|
|
222
|
+
localRoots?: Record<string, string>;
|
|
223
|
+
}): Record<string, string> | undefined {
|
|
224
|
+
const patch = buildKernelEnvPatch(options);
|
|
225
|
+
const keys = Object.keys(patch);
|
|
226
|
+
if (keys.length === 0) return undefined;
|
|
227
|
+
const realEnv: Record<string, string> = {};
|
|
228
|
+
for (const key in patch) {
|
|
229
|
+
const val = patch[key];
|
|
230
|
+
if (typeof val === "string") realEnv[key] = val;
|
|
231
|
+
}
|
|
232
|
+
return realEnv;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
async function startKernel(cwd: string, options: JuliaExecutorOptions): Promise<JuliaKernel> {
|
|
236
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
237
|
+
const env: Record<string, string | undefined> = {};
|
|
238
|
+
const patch = buildKernelEnv(options);
|
|
239
|
+
if (patch) {
|
|
240
|
+
for (const key in patch) {
|
|
241
|
+
const value = patch[key];
|
|
242
|
+
if (typeof value === "string") env[key] = value;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return await JuliaKernel.start({
|
|
246
|
+
cwd,
|
|
247
|
+
interpreter: options.interpreter,
|
|
248
|
+
env,
|
|
249
|
+
signal: options.signal,
|
|
250
|
+
deadlineMs: options.deadlineMs,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async function acquireSession(
|
|
255
|
+
sessionKey: string,
|
|
256
|
+
sessionId: string,
|
|
257
|
+
cwd: string,
|
|
258
|
+
options: JuliaExecutorOptions,
|
|
259
|
+
): Promise<JuliaSession> {
|
|
260
|
+
const existing = sessions.get(sessionKey);
|
|
261
|
+
if (existing) {
|
|
262
|
+
attachSessionOwner(existing, sessionId, options.kernelOwnerId);
|
|
263
|
+
return existing;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const inFlight = startingSessions.get(sessionKey);
|
|
267
|
+
if (inFlight) {
|
|
268
|
+
attachSessionOwner(inFlight, sessionId, options.kernelOwnerId);
|
|
269
|
+
return await waitForPromiseWithCancellation(inFlight.promise, options);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let startingSession!: StartingJuliaSession;
|
|
273
|
+
const startPromise = (async () => {
|
|
274
|
+
const kernel = await startKernel(cwd, options);
|
|
275
|
+
const session: JuliaSession = {
|
|
276
|
+
sessionKey,
|
|
277
|
+
sessionId,
|
|
278
|
+
cwd,
|
|
279
|
+
kernel,
|
|
280
|
+
ownerIds: new Set(startingSession.ownerIds),
|
|
281
|
+
hasFallbackOwner: startingSession.hasFallbackOwner,
|
|
282
|
+
};
|
|
283
|
+
if (startingSessions.get(sessionKey) === startingSession) {
|
|
284
|
+
sessions.set(sessionKey, session);
|
|
285
|
+
}
|
|
286
|
+
return session;
|
|
287
|
+
})();
|
|
288
|
+
|
|
289
|
+
startingSession = {
|
|
290
|
+
ownerIds: new Set(),
|
|
291
|
+
hasFallbackOwner: false,
|
|
292
|
+
promise: startPromise,
|
|
293
|
+
};
|
|
294
|
+
attachSessionOwner(startingSession, sessionId, options.kernelOwnerId);
|
|
295
|
+
startingSessions.set(sessionKey, startingSession);
|
|
296
|
+
try {
|
|
297
|
+
return await waitForPromiseWithCancellation(startPromise, options);
|
|
298
|
+
} finally {
|
|
299
|
+
if (startingSessions.get(sessionKey) === startingSession) startingSessions.delete(sessionKey);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function replaceSessionKernel(session: JuliaSession, cwd: string, options: JuliaExecutorOptions): Promise<void> {
|
|
304
|
+
logger.warn("Julia subprocess died or is unresponsive; spawning fresh process", {
|
|
305
|
+
sessionKey: session.sessionKey,
|
|
306
|
+
});
|
|
307
|
+
const oldKernel = session.kernel;
|
|
308
|
+
const remaining = getRemainingTimeoutMs(options.deadlineMs);
|
|
309
|
+
await oldKernel
|
|
310
|
+
.shutdown(remaining !== undefined ? { timeoutMs: Math.max(0, remaining) } : undefined)
|
|
311
|
+
.catch(() => undefined);
|
|
312
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
313
|
+
throw new JuliaExecutionCancelledError(false);
|
|
314
|
+
}
|
|
315
|
+
requireRemainingTimeoutMs(options.deadlineMs);
|
|
316
|
+
const nextKernel = await startKernel(cwd, options);
|
|
317
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
318
|
+
await nextKernel.shutdown().catch(() => undefined);
|
|
319
|
+
throw new JuliaExecutionCancelledError(false);
|
|
320
|
+
}
|
|
321
|
+
session.kernel = nextKernel;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function resetSession(sessionKey: string): Promise<void> {
|
|
325
|
+
const session = sessions.get(sessionKey) ?? (await startingSessions.get(sessionKey)?.promise.catch(() => undefined));
|
|
326
|
+
if (!session) return;
|
|
327
|
+
sessions.delete(sessionKey);
|
|
328
|
+
await session.kernel.shutdown({ timeoutMs: SHUTDOWN_GRACE_MS }).catch(() => undefined);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export async function disposeAllJuliaKernelSessions(): Promise<void> {
|
|
332
|
+
const pending = [...startingSessions.values()].map(starting => starting.promise);
|
|
333
|
+
startingSessions.clear();
|
|
334
|
+
resettingSessions.clear();
|
|
335
|
+
const started = await Promise.allSettled(pending);
|
|
336
|
+
const all = [...sessions.entries()];
|
|
337
|
+
for (const result of started) {
|
|
338
|
+
if (result.status !== "fulfilled") continue;
|
|
339
|
+
if (!all.some(([, session]) => session === result.value)) {
|
|
340
|
+
all.push([result.value.sessionKey, result.value]);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
for (const [id, session] of all) {
|
|
344
|
+
if (sessions.get(id) === session) sessions.delete(id);
|
|
345
|
+
}
|
|
346
|
+
const results = await Promise.allSettled(all.map(([, session]) => session.kernel.shutdown()));
|
|
347
|
+
for (let i = 0; i < all.length; i += 1) {
|
|
348
|
+
const [id, session] = all[i];
|
|
349
|
+
const result = results[i];
|
|
350
|
+
if (result.status === "fulfilled" && result.value?.confirmed !== false) continue;
|
|
351
|
+
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
352
|
+
logger.warn("Julia kernel shutdown not confirmed", {
|
|
353
|
+
sessionId: session.sessionId,
|
|
354
|
+
sessionKey: id,
|
|
355
|
+
cwd: session.cwd,
|
|
356
|
+
reason,
|
|
357
|
+
});
|
|
358
|
+
if (!sessions.has(id)) sessions.set(id, session);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export async function disposeJuliaKernelSessionsByOwner(ownerId: string): Promise<void> {
|
|
363
|
+
const toShutdown: JuliaSession[] = [];
|
|
364
|
+
const startingToShutdown: StartingJuliaSession[] = [];
|
|
365
|
+
for (const session of [...sessions.values()]) {
|
|
366
|
+
if (!session.ownerIds.has(ownerId)) continue;
|
|
367
|
+
if (session.ownerIds.size === 1) {
|
|
368
|
+
toShutdown.push(session);
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
session.ownerIds.delete(ownerId);
|
|
372
|
+
}
|
|
373
|
+
for (const [sessionKey, starting] of [...startingSessions.entries()]) {
|
|
374
|
+
if (sessions.has(sessionKey) || !starting.ownerIds.has(ownerId)) continue;
|
|
375
|
+
if (starting.ownerIds.size === 1) {
|
|
376
|
+
startingSessions.delete(sessionKey);
|
|
377
|
+
startingToShutdown.push(starting);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
starting.ownerIds.delete(ownerId);
|
|
381
|
+
}
|
|
382
|
+
for (const session of toShutdown) {
|
|
383
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
384
|
+
}
|
|
385
|
+
const started = await Promise.allSettled(startingToShutdown.map(starting => starting.promise));
|
|
386
|
+
for (const result of started) {
|
|
387
|
+
if (result.status !== "fulfilled") continue;
|
|
388
|
+
const session = result.value;
|
|
389
|
+
if (sessions.get(session.sessionKey) === session) sessions.delete(session.sessionKey);
|
|
390
|
+
toShutdown.push(session);
|
|
391
|
+
}
|
|
392
|
+
const results = await Promise.allSettled(toShutdown.map(session => session.kernel.shutdown()));
|
|
393
|
+
for (let i = 0; i < toShutdown.length; i += 1) {
|
|
394
|
+
const session = toShutdown[i];
|
|
395
|
+
const result = results[i];
|
|
396
|
+
if (result.status === "fulfilled" && result.value?.confirmed !== false) {
|
|
397
|
+
session.ownerIds.delete(ownerId);
|
|
398
|
+
continue;
|
|
399
|
+
}
|
|
400
|
+
const reason = result.status === "rejected" ? result.reason : "not confirmed";
|
|
401
|
+
logger.warn("Julia kernel shutdown not confirmed", {
|
|
402
|
+
sessionId: session.sessionId,
|
|
403
|
+
sessionKey: session.sessionKey,
|
|
404
|
+
cwd: session.cwd,
|
|
405
|
+
reason,
|
|
406
|
+
});
|
|
407
|
+
if (!sessions.has(session.sessionKey)) sessions.set(session.sessionKey, session);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
async function executeWithKernel(
|
|
412
|
+
kernel: JuliaKernel,
|
|
413
|
+
code: string,
|
|
414
|
+
options: JuliaExecutorOptions | undefined,
|
|
415
|
+
): Promise<JuliaResult> {
|
|
416
|
+
return executeWithKernelBase<JuliaExecutorOptions, Record<string, string | undefined>>({
|
|
417
|
+
kernel,
|
|
418
|
+
code,
|
|
419
|
+
options,
|
|
420
|
+
runIdPrefix: "jl",
|
|
421
|
+
errorLogLabel: "Julia",
|
|
422
|
+
isJulia: true,
|
|
423
|
+
cancelledErrorClass: JuliaExecutionCancelledError,
|
|
424
|
+
buildKernelEnvPatch,
|
|
425
|
+
formatKernelTimeoutAnnotation,
|
|
426
|
+
formatTimeoutAnnotation,
|
|
427
|
+
resolveDeadlineMs: opts => opts?.deadlineMs,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
async function ensureKernelAvailable(cwd: string, options: JuliaExecutorOptions): Promise<void> {
|
|
432
|
+
const availability = await waitForPromiseWithCancellation(
|
|
433
|
+
checkJuliaKernelAvailability(cwd, options.interpreter),
|
|
434
|
+
options,
|
|
435
|
+
);
|
|
436
|
+
if (!availability.ok) {
|
|
437
|
+
throw new Error(availability.reason ?? "Julia kernel unavailable");
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function ensureToolBridge(options: JuliaExecutorOptions): Promise<void> {
|
|
442
|
+
if (!options.toolSession || options.bridge) return;
|
|
443
|
+
try {
|
|
444
|
+
options.bridge = await ensurePyToolBridge();
|
|
445
|
+
} catch (err) {
|
|
446
|
+
logger.warn("Failed to start Julia tool bridge", {
|
|
447
|
+
error: err instanceof Error ? err.message : String(err),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
async function executeOnSession(code: string, cwd: string, options: JuliaExecutorOptions): Promise<JuliaResult> {
|
|
453
|
+
const sessionId = options.sessionId ?? `session:${cwd}`;
|
|
454
|
+
const sessionKey = buildSessionKey(sessionId, cwd, options.interpreter);
|
|
455
|
+
if (options.bridge && !options.bridgeSessionId) {
|
|
456
|
+
options.bridgeSessionId = sessionId;
|
|
457
|
+
}
|
|
458
|
+
if (options.reset) {
|
|
459
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
460
|
+
if (inFlight) await inFlight.catch(() => undefined);
|
|
461
|
+
else {
|
|
462
|
+
const resetPromise = resetSession(sessionKey);
|
|
463
|
+
resettingSessions.set(
|
|
464
|
+
sessionKey,
|
|
465
|
+
resetPromise.then(() => undefined),
|
|
466
|
+
);
|
|
467
|
+
try {
|
|
468
|
+
await resetPromise;
|
|
469
|
+
} finally {
|
|
470
|
+
resettingSessions.delete(sessionKey);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
const inFlight = resettingSessions.get(sessionKey);
|
|
475
|
+
if (inFlight) await inFlight.catch(() => undefined);
|
|
476
|
+
}
|
|
477
|
+
const session = await acquireSession(sessionKey, sessionId, cwd, options);
|
|
478
|
+
if (options.signal?.aborted) {
|
|
479
|
+
throw new JuliaExecutionCancelledError(isTimedOutCancellation(options.signal.reason, options.signal));
|
|
480
|
+
}
|
|
481
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
482
|
+
throw new JuliaExecutionCancelledError(false);
|
|
483
|
+
}
|
|
484
|
+
if (!session.kernel.isAlive()) {
|
|
485
|
+
await replaceSessionKernel(session, cwd, options);
|
|
486
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
487
|
+
throw new JuliaExecutionCancelledError(false);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
const runOptions = { ...options, cwd };
|
|
491
|
+
try {
|
|
492
|
+
return await executeWithKernel(session.kernel, code, runOptions);
|
|
493
|
+
} catch (err) {
|
|
494
|
+
if (isCancellationError(err) || options.signal?.aborted) throw err;
|
|
495
|
+
if (session.kernel.isAlive()) throw err;
|
|
496
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
497
|
+
throw new JuliaExecutionCancelledError(false);
|
|
498
|
+
}
|
|
499
|
+
await replaceSessionKernel(session, cwd, options);
|
|
500
|
+
if (sessions.get(session.sessionKey) !== session) {
|
|
501
|
+
throw new JuliaExecutionCancelledError(false);
|
|
502
|
+
}
|
|
503
|
+
return await executeWithKernel(session.kernel, code, runOptions);
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export async function executeJuliaWithKernel(
|
|
508
|
+
kernel: JuliaKernel,
|
|
509
|
+
code: string,
|
|
510
|
+
options?: JuliaExecutorOptions,
|
|
511
|
+
): Promise<JuliaResult> {
|
|
512
|
+
return await executeWithKernel(kernel, code, options);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
export async function executeJulia(code: string, options?: JuliaExecutorOptions): Promise<JuliaResult> {
|
|
516
|
+
const cwd = normalizeSessionCwd(options?.cwd ?? getProjectDir());
|
|
517
|
+
const deadlineMs = getExecutionDeadlineMs(options);
|
|
518
|
+
const executionOptions: JuliaExecutorOptions = {
|
|
519
|
+
...(options ?? {}),
|
|
520
|
+
cwd,
|
|
521
|
+
deadlineMs,
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
try {
|
|
525
|
+
requireRemainingTimeoutMs(deadlineMs);
|
|
526
|
+
if (executionOptions.signal?.aborted) {
|
|
527
|
+
throw new JuliaExecutionCancelledError(
|
|
528
|
+
isTimedOutCancellation(executionOptions.signal.reason, executionOptions.signal),
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
await ensureKernelAvailable(cwd, executionOptions);
|
|
532
|
+
await ensureToolBridge(executionOptions);
|
|
533
|
+
return await executeOnSession(code, cwd, executionOptions);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
if (isCancellationError(err) || executionOptions.signal?.aborted) {
|
|
536
|
+
return createCancelledJuliaResult(isTimedOutCancellation(err, executionOptions.signal));
|
|
537
|
+
}
|
|
538
|
+
throw err;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { ToolSession } from "../../tools";
|
|
2
|
+
import {
|
|
3
|
+
type ExecutorBackend,
|
|
4
|
+
type ExecutorBackendExecOptions,
|
|
5
|
+
type ExecutorBackendResult,
|
|
6
|
+
resolveEvalUrlRoots,
|
|
7
|
+
} from "../backend";
|
|
8
|
+
import {
|
|
9
|
+
namespaceSessionId as sharedNamespace,
|
|
10
|
+
readInterpreterSetting as sharedReadInterpreterSetting,
|
|
11
|
+
toExecutorBackendResult,
|
|
12
|
+
} from "../backend-helpers";
|
|
13
|
+
import { executeJulia } from "./executor";
|
|
14
|
+
import { checkJuliaKernelAvailability } from "./kernel";
|
|
15
|
+
|
|
16
|
+
const JULIA_SESSION_PREFIX = "julia:";
|
|
17
|
+
|
|
18
|
+
export function namespaceSessionId(sessionId: string): string {
|
|
19
|
+
return sharedNamespace(sessionId, JULIA_SESSION_PREFIX);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function readInterpreterSetting(session: ToolSession): string | undefined {
|
|
23
|
+
return sharedReadInterpreterSetting(session, "julia.interpreter");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export default {
|
|
27
|
+
id: "julia",
|
|
28
|
+
label: "Julia",
|
|
29
|
+
highlightLang: "julia",
|
|
30
|
+
|
|
31
|
+
async isAvailable(session: ToolSession): Promise<boolean> {
|
|
32
|
+
const availability = await checkJuliaKernelAvailability(session.cwd, readInterpreterSetting(session));
|
|
33
|
+
return availability.ok;
|
|
34
|
+
},
|
|
35
|
+
|
|
36
|
+
async execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult> {
|
|
37
|
+
const result = await executeJulia(code, {
|
|
38
|
+
cwd: opts.cwd,
|
|
39
|
+
idleTimeoutMs: opts.idleTimeoutMs,
|
|
40
|
+
signal: opts.signal,
|
|
41
|
+
sessionId: namespaceSessionId(opts.sessionId),
|
|
42
|
+
interpreter: readInterpreterSetting(opts.session),
|
|
43
|
+
sessionFile: opts.sessionFile,
|
|
44
|
+
artifactsDir: opts.session.getArtifactsDir?.() ?? undefined,
|
|
45
|
+
localRoots: resolveEvalUrlRoots(opts.session),
|
|
46
|
+
kernelOwnerId: opts.kernelOwnerId,
|
|
47
|
+
reset: opts.reset,
|
|
48
|
+
onChunk: opts.onChunk,
|
|
49
|
+
onStatus: opts.onStatus,
|
|
50
|
+
toolSession: opts.session,
|
|
51
|
+
});
|
|
52
|
+
return toExecutorBackendResult(result);
|
|
53
|
+
},
|
|
54
|
+
} satisfies ExecutorBackend;
|