@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
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 +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
|
@@ -3,7 +3,7 @@ import { Settings } from "../config/settings";
|
|
|
3
3
|
import { OutputSink } from "../session/streaming-output";
|
|
4
4
|
import type { ToolSession } from "../tools";
|
|
5
5
|
import { resolveOutputMaxColumns, resolveOutputSinkHeadBytes } from "../tools/output-meta";
|
|
6
|
-
import { isEvalTimeoutControlEvent } from "./bridge-timeout";
|
|
6
|
+
import { EVAL_TIMEOUT_PAUSE_OP, EVAL_TIMEOUT_RESUME_OP, isEvalTimeoutControlEvent } from "./bridge-timeout";
|
|
7
7
|
import type { JsStatusEvent } from "./js/shared/types";
|
|
8
8
|
import type { KernelDisplayOutput } from "./py/display";
|
|
9
9
|
import { registerPyToolBridge } from "./py/tool-bridge";
|
|
@@ -158,6 +158,77 @@ export async function waitForPromiseWithCancellation<T>(
|
|
|
158
158
|
return await resultPromise;
|
|
159
159
|
}
|
|
160
160
|
|
|
161
|
+
interface BridgeAbortShield {
|
|
162
|
+
signal: AbortSignal | undefined;
|
|
163
|
+
abortRequested: boolean;
|
|
164
|
+
timedOut: boolean;
|
|
165
|
+
handleStatus?: (event: JsStatusEvent) => void;
|
|
166
|
+
dispose?: () => void;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function createBridgeAbortShield(source: AbortSignal | undefined): BridgeAbortShield {
|
|
170
|
+
const shield: BridgeAbortShield = {
|
|
171
|
+
signal: undefined,
|
|
172
|
+
abortRequested: false,
|
|
173
|
+
timedOut: false,
|
|
174
|
+
};
|
|
175
|
+
if (!source) return shield;
|
|
176
|
+
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
let pauseDepth = 0;
|
|
179
|
+
let abortReason: unknown;
|
|
180
|
+
let removeAbortListener: (() => void) | undefined;
|
|
181
|
+
|
|
182
|
+
const requestAbort = (reason: unknown): void => {
|
|
183
|
+
shield.abortRequested = true;
|
|
184
|
+
shield.timedOut =
|
|
185
|
+
shield.timedOut ||
|
|
186
|
+
(typeof DOMException !== "undefined" && reason instanceof DOMException
|
|
187
|
+
? reason.name === "TimeoutError"
|
|
188
|
+
: reason instanceof Error && reason.name === "TimeoutError");
|
|
189
|
+
abortReason = reason;
|
|
190
|
+
if (pauseDepth > 0 || controller.signal.aborted) return;
|
|
191
|
+
controller.abort(reason);
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
const onAbort = (): void => {
|
|
195
|
+
const reason = source.reason;
|
|
196
|
+
requestAbort(reason);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
shield.signal = controller.signal;
|
|
200
|
+
shield.handleStatus = (event: JsStatusEvent): void => {
|
|
201
|
+
if (event.deferExternalAbort !== true) return;
|
|
202
|
+
if (event.op === EVAL_TIMEOUT_PAUSE_OP) {
|
|
203
|
+
pauseDepth++;
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (event.op !== EVAL_TIMEOUT_RESUME_OP || pauseDepth === 0) return;
|
|
207
|
+
pauseDepth--;
|
|
208
|
+
if (shield.abortRequested && !controller.signal.aborted) controller.abort(abortReason);
|
|
209
|
+
};
|
|
210
|
+
shield.dispose = (): void => {
|
|
211
|
+
removeAbortListener?.();
|
|
212
|
+
removeAbortListener = undefined;
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
if (source.aborted) {
|
|
216
|
+
requestAbort(source.reason);
|
|
217
|
+
} else {
|
|
218
|
+
source.addEventListener("abort", onAbort, { once: true });
|
|
219
|
+
removeAbortListener = () => {
|
|
220
|
+
source.removeEventListener("abort", onAbort);
|
|
221
|
+
};
|
|
222
|
+
if (source.aborted) {
|
|
223
|
+
source.removeEventListener("abort", onAbort);
|
|
224
|
+
removeAbortListener = undefined;
|
|
225
|
+
requestAbort(source.reason);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
return shield;
|
|
230
|
+
}
|
|
231
|
+
|
|
161
232
|
export function createCancelledKernelResult(output: string): KernelExecutionResult {
|
|
162
233
|
const outputBytes = Buffer.byteLength(output, "utf-8");
|
|
163
234
|
const outputLines = output.length > 0 ? 1 : 0;
|
|
@@ -304,9 +375,11 @@ export async function executeWithKernelBase<
|
|
|
304
375
|
const displayOutputs: KernelDisplayOutput[] = [];
|
|
305
376
|
const deadlineMs = (resolveDeadlineMs ?? getExecutionDeadlineMs)(options);
|
|
306
377
|
let executionTimeoutMs: number | undefined;
|
|
378
|
+
const abortShield = createBridgeAbortShield(options?.signal);
|
|
307
379
|
|
|
308
380
|
const collectDisplay = (output: KernelDisplayOutput): void => {
|
|
309
381
|
if (output.type === "status") {
|
|
382
|
+
abortShield.handleStatus?.(output.event);
|
|
310
383
|
options?.onStatus?.(output.event);
|
|
311
384
|
if (!isJulia && isEvalTimeoutControlEvent(output.event)) return;
|
|
312
385
|
}
|
|
@@ -320,8 +393,11 @@ export async function executeWithKernelBase<
|
|
|
320
393
|
options?.toolSession && options?.bridgeSessionId
|
|
321
394
|
? registerPyToolBridge(options.bridgeSessionId, runId, {
|
|
322
395
|
toolSession: options.toolSession,
|
|
323
|
-
signal:
|
|
396
|
+
signal: abortShield.signal,
|
|
324
397
|
emitStatus,
|
|
398
|
+
abortRequested: () => {
|
|
399
|
+
return abortShield.abortRequested;
|
|
400
|
+
},
|
|
325
401
|
})
|
|
326
402
|
: null;
|
|
327
403
|
|
|
@@ -338,14 +414,15 @@ export async function executeWithKernelBase<
|
|
|
338
414
|
cwd: options?.cwd,
|
|
339
415
|
env: buildKernelEnvPatch(options ?? ({} as TOptions)),
|
|
340
416
|
id: runId,
|
|
341
|
-
signal:
|
|
417
|
+
signal: abortShield.signal,
|
|
342
418
|
timeoutMs: executionTimeoutMs,
|
|
343
419
|
onChunk: text => sink.push(text),
|
|
344
420
|
onDisplay: output => collectDisplay(output),
|
|
345
421
|
});
|
|
346
422
|
|
|
347
|
-
if (result.cancelled) {
|
|
348
|
-
const
|
|
423
|
+
if (result.cancelled || abortShield.abortRequested) {
|
|
424
|
+
const timedOut = result.timedOut || abortShield.timedOut;
|
|
425
|
+
const annotation = timedOut
|
|
349
426
|
? formatKernelTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs, result.kernelKilled ?? false)
|
|
350
427
|
: undefined;
|
|
351
428
|
const dumped = await sink.dump(annotation);
|
|
@@ -397,8 +474,8 @@ export async function executeWithKernelBase<
|
|
|
397
474
|
stdinRequested: false,
|
|
398
475
|
};
|
|
399
476
|
} catch (err) {
|
|
400
|
-
if (isCancellationError(err, cancelledErrorClass) ||
|
|
401
|
-
const timedOut = isTimedOutCancellation(err, cancelledErrorClass,
|
|
477
|
+
if (isCancellationError(err, cancelledErrorClass) || abortShield.abortRequested || abortShield.signal?.aborted) {
|
|
478
|
+
const timedOut = abortShield.timedOut || isTimedOutCancellation(err, cancelledErrorClass, abortShield.signal);
|
|
402
479
|
const dumped = await sink.dump(
|
|
403
480
|
timedOut ? formatTimeoutAnnotation(executionTimeoutMs ?? options?.idleTimeoutMs) : undefined,
|
|
404
481
|
);
|
|
@@ -421,5 +498,6 @@ export async function executeWithKernelBase<
|
|
|
421
498
|
throw error;
|
|
422
499
|
} finally {
|
|
423
500
|
unregisterBridge?.();
|
|
501
|
+
abortShield.dispose?.();
|
|
424
502
|
}
|
|
425
503
|
}
|
package/src/eval/jl/kernel.ts
CHANGED
|
@@ -13,7 +13,7 @@ import { $ } from "bun";
|
|
|
13
13
|
import { Settings } from "../../config/settings";
|
|
14
14
|
import { BaseKernel, getRemainingTimeMs, type KernelStartOptions } from "../kernel-base";
|
|
15
15
|
import type { KernelDisplayOutput } from "../py/display";
|
|
16
|
-
import { hostHasInheritableConsole, shouldHideKernelWindow } from "../py/spawn-options";
|
|
16
|
+
import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "../py/spawn-options";
|
|
17
17
|
import { JULIA_PRELUDE } from "./prelude";
|
|
18
18
|
import RUNNER_SCRIPT from "./runner.jl" with { type: "text" };
|
|
19
19
|
import {
|
|
@@ -187,6 +187,7 @@ export class JuliaKernel extends BaseKernel<KernelExecuteOptions> {
|
|
|
187
187
|
[runtime.juliaPath, "--startup-file=no", "--history-file=no", "--color=no", "--project=@.", scriptPath],
|
|
188
188
|
{
|
|
189
189
|
cwd: options.cwd,
|
|
190
|
+
detached: shouldDetachKernel(process.platform),
|
|
190
191
|
env: spawnEnv,
|
|
191
192
|
stdin: "pipe",
|
|
192
193
|
stdout: "pipe",
|
|
@@ -1,6 +1,14 @@
|
|
|
1
|
-
import { logger, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
1
|
+
import { logger, postmortem, Snowflake, workerHostEntry } from "@oh-my-pi/pi-utils";
|
|
2
|
+
import {
|
|
3
|
+
createWorkerHandle,
|
|
4
|
+
createWorkerSubprocess,
|
|
5
|
+
resolveWorkerSpawnCmd,
|
|
6
|
+
workerEnvFromParent,
|
|
7
|
+
} from "../../subprocess/worker-client";
|
|
2
8
|
import type { ToolSession } from "../../tools";
|
|
3
9
|
import { ToolAbortError, ToolError } from "../../tools/tool-errors";
|
|
10
|
+
import { safeSend as safeSendIpc } from "../../utils/ipc";
|
|
11
|
+
import { shouldDetachKernel } from "../py/spawn-options";
|
|
4
12
|
import { callSessionTool, type JsStatusEvent } from "./tool-bridge";
|
|
5
13
|
import { WorkerCore } from "./worker-core";
|
|
6
14
|
// Coding-agent binary/bundle workers route through the CLI entrypoint with a
|
|
@@ -24,7 +32,7 @@ export interface VmRunState {
|
|
|
24
32
|
}
|
|
25
33
|
|
|
26
34
|
interface WorkerHandle {
|
|
27
|
-
mode: "worker" | "inline";
|
|
35
|
+
mode: "process" | "worker" | "inline";
|
|
28
36
|
send(msg: WorkerInbound): void;
|
|
29
37
|
onMessage(handler: (msg: WorkerOutbound) => void): () => void;
|
|
30
38
|
onError(handler: (error: Error) => void): () => void;
|
|
@@ -57,16 +65,18 @@ const resettingSessions = new Map<string, Promise<void>>();
|
|
|
57
65
|
// Worker startup (module-graph import + WorkerCore construction) is infrastructure
|
|
58
66
|
// cost, not user compute. Floor it independently of Bun's 5s default per-test timeout
|
|
59
67
|
// so a slow cold-start under load isn't aborted mid-init — terminating a still-
|
|
60
|
-
// initializing
|
|
68
|
+
// initializing eval runtime triggers the same kind of terminate-race that motivates
|
|
61
69
|
// avoiding `vm.runInContext` (see shared/indirect-eval.ts), here surfacing as a
|
|
62
70
|
// SIGILL/SIGSEGV. Callers that pass a larger per-cell budget still dominate.
|
|
63
71
|
const WORKER_INIT_TIMEOUT_MS = 15_000;
|
|
64
72
|
const WORKER_CLOSE_TIMEOUT_MS = 1_000;
|
|
73
|
+
const JS_EVAL_PROCESS_ARG = "__omp_worker_js_eval_process";
|
|
65
74
|
// Active graceful-close grace period before a worker that ack'd `close` but never
|
|
66
75
|
// emitted its `close` event is force-terminated. Defaults to the production floor;
|
|
67
76
|
// tests override it (and restore it) to exercise the close-timeout -> terminate
|
|
68
77
|
// path without a real wall-clock wait.
|
|
69
78
|
let workerCloseTimeoutMs: number = WORKER_CLOSE_TIMEOUT_MS;
|
|
79
|
+
let useWorkerThreadForTests = false;
|
|
70
80
|
|
|
71
81
|
/**
|
|
72
82
|
* Test-only seam: override the graceful-close grace period (ms). Returns the
|
|
@@ -79,6 +89,13 @@ export function setWorkerCloseTimeoutMsForTests(ms: number): number {
|
|
|
79
89
|
return previous;
|
|
80
90
|
}
|
|
81
91
|
|
|
92
|
+
/** Test-only seam for the legacy Worker lifecycle mocks. */
|
|
93
|
+
export function setJsEvalWorkerThreadForTests(enabled: boolean): boolean {
|
|
94
|
+
const previous = useWorkerThreadForTests;
|
|
95
|
+
useWorkerThreadForTests = enabled;
|
|
96
|
+
return previous;
|
|
97
|
+
}
|
|
98
|
+
|
|
82
99
|
export async function executeInVmContext(options: {
|
|
83
100
|
sessionKey: string;
|
|
84
101
|
sessionId: string;
|
|
@@ -144,9 +161,9 @@ export async function disposeAllVmContexts(): Promise<void> {
|
|
|
144
161
|
}
|
|
145
162
|
|
|
146
163
|
/**
|
|
147
|
-
* Smoke probe: spawn the JS
|
|
148
|
-
* it answers the `init` handshake
|
|
149
|
-
* fallback). Catches
|
|
164
|
+
* Smoke probe: spawn the JS evaluator through the worker-host entry and prove
|
|
165
|
+
* it answers the `init` handshake in a real isolated subprocess (not the inline
|
|
166
|
+
* fallback). Catches silent process-load and init-message regressions
|
|
150
167
|
* that otherwise strand every cell on the init timeout in a distribution build —
|
|
151
168
|
* the failure mode that motivated `installWorkerInbox`. Wired into
|
|
152
169
|
* `omp --smoke-test` so binary / source / tarball installs all exercise it.
|
|
@@ -163,8 +180,8 @@ export async function smokeTestJsEvalWorker(): Promise<void> {
|
|
|
163
180
|
};
|
|
164
181
|
try {
|
|
165
182
|
await initWorker(session, { cwd: process.cwd(), sessionId: "smoke" }, WORKER_INIT_TIMEOUT_MS);
|
|
166
|
-
if (worker.mode !== "
|
|
167
|
-
throw new Error("JS eval worker smoke fell back
|
|
183
|
+
if (worker.mode !== "process") {
|
|
184
|
+
throw new Error("JS eval worker smoke fell back from the isolated subprocess");
|
|
168
185
|
}
|
|
169
186
|
} finally {
|
|
170
187
|
await worker.terminate().catch(() => undefined);
|
|
@@ -237,10 +254,8 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
237
254
|
if (starting) return await starting;
|
|
238
255
|
|
|
239
256
|
const startup = (async (): Promise<JsSession> => {
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// `ready` from its constructor on load. `spawnJsWorker` + `initWorker` run with
|
|
243
|
-
// no intervening await, so `ready` can never race the attach.
|
|
257
|
+
// Attach the message listener before sending init. Both Bun Worker messages
|
|
258
|
+
// and subprocess IPC can arrive immediately after the evaluator loads.
|
|
244
259
|
const worker = spawnJsWorker();
|
|
245
260
|
const session: JsSession = {
|
|
246
261
|
sessionKey,
|
|
@@ -253,27 +268,29 @@ async function acquireSession(sessionKey: string, snapshot: SessionSnapshot, tim
|
|
|
253
268
|
// Init headroom is the fixed infrastructure floor; the caller's per-cell timeout
|
|
254
269
|
// dominates when larger so users can grant more by raising `timeout` on a cell.
|
|
255
270
|
const readyTimeoutMs = Math.max(WORKER_INIT_TIMEOUT_MS, timeoutMs ?? 0);
|
|
256
|
-
|
|
257
|
-
await initWorker(session, snapshot, readyTimeoutMs);
|
|
258
|
-
} catch (error) {
|
|
259
|
-
// Worker-thread crash/load failures surface asynchronously via the worker
|
|
260
|
-
// `error` event — after `spawnJsWorker`'s synchronous try/catch already
|
|
261
|
-
// returned — so the only signal is the rejected handshake. Retry on the
|
|
262
|
-
// inline worker so a broken module graph fails fast instead of stalling
|
|
263
|
-
// every cell on the init timeout and then dying with exitCode 1.
|
|
264
|
-
await worker.terminate().catch(() => undefined);
|
|
265
|
-
if (worker.mode === "inline") throw error;
|
|
266
|
-
logger.warn("JS eval worker init failed; retrying with inline worker (no sync-loop guard)", {
|
|
267
|
-
error: error instanceof Error ? error.message : String(error),
|
|
268
|
-
});
|
|
269
|
-
const inline = spawnInlineWorker();
|
|
270
|
-
session.worker = inline;
|
|
271
|
-
session.state = "alive";
|
|
271
|
+
while (true) {
|
|
272
272
|
try {
|
|
273
273
|
await initWorker(session, snapshot, readyTimeoutMs);
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
274
|
+
break;
|
|
275
|
+
} catch (error) {
|
|
276
|
+
// Runtime crash/load failures surface asynchronously via the runtime's
|
|
277
|
+
// error callback, after the synchronous spawn try/catch has returned.
|
|
278
|
+
// Preserve the full process -> Worker -> inline ladder for those failures.
|
|
279
|
+
const failed = session.worker;
|
|
280
|
+
await failed.terminate().catch(() => undefined);
|
|
281
|
+
if (failed.mode === "inline") throw error;
|
|
282
|
+
if (failed.mode === "process") {
|
|
283
|
+
logger.warn("JS eval subprocess init failed; retrying with a Bun Worker", {
|
|
284
|
+
error: error instanceof Error ? error.message : String(error),
|
|
285
|
+
});
|
|
286
|
+
session.worker = spawnBunWorker();
|
|
287
|
+
} else {
|
|
288
|
+
logger.warn("JS eval worker init failed; retrying with inline worker (no sync-loop guard)", {
|
|
289
|
+
error: error instanceof Error ? error.message : String(error),
|
|
290
|
+
});
|
|
291
|
+
session.worker = spawnInlineWorker();
|
|
292
|
+
}
|
|
293
|
+
session.state = "alive";
|
|
277
294
|
}
|
|
278
295
|
}
|
|
279
296
|
sessions.set(sessionKey, session);
|
|
@@ -480,6 +497,22 @@ async function raceWithTimeout<T>(promise: Promise<T>, timeoutMs: number, reason
|
|
|
480
497
|
}
|
|
481
498
|
|
|
482
499
|
function spawnJsWorker(): WorkerHandle {
|
|
500
|
+
if (!useWorkerThreadForTests) {
|
|
501
|
+
try {
|
|
502
|
+
return spawnJsProcess();
|
|
503
|
+
} catch (err) {
|
|
504
|
+
// Fall through to the Bun Worker rung: a worker thread still interrupts
|
|
505
|
+
// synchronous infinite loops via terminate(), which the inline fallback
|
|
506
|
+
// cannot.
|
|
507
|
+
logger.warn("JS eval subprocess spawn failed; falling back to a Bun Worker", {
|
|
508
|
+
error: err instanceof Error ? err.message : String(err),
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
return spawnBunWorker();
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function spawnBunWorker(): WorkerHandle {
|
|
483
516
|
try {
|
|
484
517
|
const hostEntry = workerHostEntry();
|
|
485
518
|
const worker = hostEntry
|
|
@@ -494,6 +527,47 @@ function spawnJsWorker(): WorkerHandle {
|
|
|
494
527
|
}
|
|
495
528
|
}
|
|
496
529
|
|
|
530
|
+
function spawnJsProcess(): WorkerHandle {
|
|
531
|
+
const spawned = createWorkerSubprocess<WorkerOutbound>({
|
|
532
|
+
spawnCommand: resolveWorkerSpawnCmd(JS_EVAL_PROCESS_ARG),
|
|
533
|
+
env: workerEnvFromParent(),
|
|
534
|
+
exitLabel: "JS eval worker",
|
|
535
|
+
detached: shouldDetachKernel(process.platform),
|
|
536
|
+
reportCleanExit: true,
|
|
537
|
+
unref: false,
|
|
538
|
+
});
|
|
539
|
+
const base = createWorkerHandle<WorkerInbound, WorkerOutbound>(spawned, message =>
|
|
540
|
+
safeSendIpc(spawned.proc, message, "js-eval"),
|
|
541
|
+
);
|
|
542
|
+
return {
|
|
543
|
+
mode: "process",
|
|
544
|
+
send: message => base.send(message),
|
|
545
|
+
onMessage: handler => base.onMessage(handler),
|
|
546
|
+
onError: handler => base.onError(handler),
|
|
547
|
+
async close() {
|
|
548
|
+
const { promise, resolve } = Promise.withResolvers<boolean>();
|
|
549
|
+
let settled = false;
|
|
550
|
+
let timeout: NodeJS.Timeout | undefined;
|
|
551
|
+
let unsubscribe = (): void => {};
|
|
552
|
+
const finish = (value: boolean): void => {
|
|
553
|
+
if (settled) return;
|
|
554
|
+
settled = true;
|
|
555
|
+
if (timeout) clearTimeout(timeout);
|
|
556
|
+
unsubscribe();
|
|
557
|
+
resolve(value);
|
|
558
|
+
};
|
|
559
|
+
unsubscribe = base.onMessage(message => {
|
|
560
|
+
if (message.type !== "closed") return;
|
|
561
|
+
void base.terminate().finally(() => finish(true));
|
|
562
|
+
});
|
|
563
|
+
timeout = setTimeout(() => finish(false), workerCloseTimeoutMs);
|
|
564
|
+
base.send({ type: "close" });
|
|
565
|
+
return await promise;
|
|
566
|
+
},
|
|
567
|
+
terminate: () => base.terminate(),
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
497
571
|
function wrapBunWorker(worker: Worker): WorkerHandle {
|
|
498
572
|
return {
|
|
499
573
|
mode: "worker",
|
|
@@ -582,7 +656,10 @@ function spawnInlineWorker(): WorkerHandle {
|
|
|
582
656
|
},
|
|
583
657
|
close: () => {},
|
|
584
658
|
};
|
|
585
|
-
const core = new WorkerCore(workerTransport
|
|
659
|
+
const core = new WorkerCore(workerTransport, {
|
|
660
|
+
mode: "inline",
|
|
661
|
+
interceptUnhandledRejections: postmortem.interceptUnhandledRejections,
|
|
662
|
+
});
|
|
586
663
|
return {
|
|
587
664
|
mode: "inline",
|
|
588
665
|
send: msg =>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { WorkerCore } from "./worker-core";
|
|
2
|
+
import type { WorkerInbound, WorkerOutbound } from "./worker-protocol";
|
|
3
|
+
|
|
4
|
+
/** Start the JavaScript evaluator inside a subprocess IPC transport. */
|
|
5
|
+
export function startJsEvalProcess(transport: {
|
|
6
|
+
send(message: WorkerOutbound): void;
|
|
7
|
+
onMessage(handler: (message: WorkerInbound) => void): () => void;
|
|
8
|
+
}): void {
|
|
9
|
+
new WorkerCore(
|
|
10
|
+
{
|
|
11
|
+
send: message => transport.send(message),
|
|
12
|
+
onMessage: handler => transport.onMessage(handler),
|
|
13
|
+
// The parent owns process lifetime and kills the subprocess after the
|
|
14
|
+
// WorkerCore `closed` acknowledgement has crossed IPC.
|
|
15
|
+
close: () => {},
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
mode: "isolated",
|
|
19
|
+
// The subprocess starts with its real cwd at the worker-host entry dir
|
|
20
|
+
// (a `resolveWorkerSpawnCmd` requirement); mirror the session cwd so
|
|
21
|
+
// cell code using relative paths or spawning children resolves against
|
|
22
|
+
// the project instead of the install dir. Worker threads cannot pass
|
|
23
|
+
// this — `process.chdir` is unavailable there.
|
|
24
|
+
chdir: cwd => process.chdir(cwd),
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
}
|
|
@@ -6,7 +6,7 @@ import * as path from "node:path";
|
|
|
6
6
|
import { Writable } from "node:stream";
|
|
7
7
|
import * as util from "node:util";
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import * as logger from "@oh-my-pi/pi-utils/logger";
|
|
10
10
|
|
|
11
11
|
import { createHelpers, type HelperBundle } from "./helpers";
|
|
12
12
|
import { awaitMaybePromise, indirectEval } from "./indirect-eval";
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import { isMainThread } from "node:worker_threads";
|
|
2
|
-
import { postmortem } from "@oh-my-pi/pi-utils";
|
|
3
1
|
import { ToolError } from "../../tools/tool-errors";
|
|
4
2
|
import { JsRuntime, type RuntimeHooks } from "./shared/runtime";
|
|
5
3
|
import type {
|
|
@@ -27,6 +25,23 @@ interface ActiveRun {
|
|
|
27
25
|
|
|
28
26
|
type RunResult = Extract<WorkerOutbound, { type: "result" }>;
|
|
29
27
|
|
|
28
|
+
export type WorkerCoreOptions =
|
|
29
|
+
| {
|
|
30
|
+
mode: "isolated";
|
|
31
|
+
/**
|
|
32
|
+
* Mirror the session cwd onto the real process cwd so cell code using
|
|
33
|
+
* `process.cwd()`, relative paths, or child processes without an explicit
|
|
34
|
+
* `cwd` resolves against the project. Only the dedicated subprocess may
|
|
35
|
+
* pass this: `process.chdir` is unavailable in Worker threads and would
|
|
36
|
+
* mutate the host's own cwd on the inline fallback.
|
|
37
|
+
*/
|
|
38
|
+
chdir?: (cwd: string) => void;
|
|
39
|
+
}
|
|
40
|
+
| {
|
|
41
|
+
mode: "inline";
|
|
42
|
+
interceptUnhandledRejections(handler: (reason: unknown) => boolean): () => void;
|
|
43
|
+
};
|
|
44
|
+
|
|
30
45
|
/** Finished-cell filenames retained for attributing rejections that surface after the run settled. */
|
|
31
46
|
const RECENT_CELL_FILES_MAX = 256;
|
|
32
47
|
|
|
@@ -82,9 +97,11 @@ export class WorkerCore {
|
|
|
82
97
|
#recentCellFiles = new Set<string>();
|
|
83
98
|
#unsubscribe: () => void;
|
|
84
99
|
#uninstallRejectionGuard: () => void;
|
|
100
|
+
#options: WorkerCoreOptions;
|
|
85
101
|
|
|
86
|
-
constructor(transport: Transport) {
|
|
102
|
+
constructor(transport: Transport, options: WorkerCoreOptions) {
|
|
87
103
|
this.#transport = transport;
|
|
104
|
+
this.#options = options;
|
|
88
105
|
this.#unsubscribe = transport.onMessage(msg => this.#handle(msg));
|
|
89
106
|
this.#uninstallRejectionGuard = this.#installRejectionGuard();
|
|
90
107
|
}
|
|
@@ -98,8 +115,8 @@ export class WorkerCore {
|
|
|
98
115
|
* without a usable stack, while anything else keeps its default fatality.
|
|
99
116
|
*/
|
|
100
117
|
#installRejectionGuard(): () => void {
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
118
|
+
if (this.#options.mode === "inline") {
|
|
119
|
+
return this.#options.interceptUnhandledRejections(reason => this.#consumeRejection(reason));
|
|
103
120
|
}
|
|
104
121
|
const onRejection = (reason: unknown): void => {
|
|
105
122
|
if (this.#consumeRejection(reason)) return;
|
|
@@ -161,7 +178,7 @@ export class WorkerCore {
|
|
|
161
178
|
return true;
|
|
162
179
|
}
|
|
163
180
|
}
|
|
164
|
-
if (
|
|
181
|
+
if (this.#options.mode === "isolated" && this.#runs.size > 0) {
|
|
165
182
|
// Dedicated eval worker: during a live run, a rejection without a cell
|
|
166
183
|
// frame (e.g. `Promise.reject("msg")` or a library-created reason) is
|
|
167
184
|
// still cell activity — nothing else runs user code in this realm.
|
|
@@ -206,7 +223,8 @@ export class WorkerCore {
|
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
225
|
|
|
209
|
-
#ensureRuntime(snapshot: SessionSnapshot): JsRuntime {
|
|
226
|
+
#ensureRuntime(snapshot: SessionSnapshot, currentRunId?: string): JsRuntime {
|
|
227
|
+
this.#syncProcessCwd(snapshot.cwd, currentRunId);
|
|
210
228
|
if (this.#runtime) {
|
|
211
229
|
this.#runtime.setCwd(snapshot.cwd);
|
|
212
230
|
return this.#runtime;
|
|
@@ -219,6 +237,41 @@ export class WorkerCore {
|
|
|
219
237
|
return this.#runtime;
|
|
220
238
|
}
|
|
221
239
|
|
|
240
|
+
#syncProcessCwd(cwd: string, currentRunId?: string): void {
|
|
241
|
+
if (this.#options.mode !== "isolated" || !this.#options.chdir) return;
|
|
242
|
+
try {
|
|
243
|
+
if (process.cwd() === cwd) return;
|
|
244
|
+
} catch {
|
|
245
|
+
// The current cwd was deleted; the chdir below is the recovery.
|
|
246
|
+
}
|
|
247
|
+
// Process cwd is realm-wide state. Moving it while another cell is mid-run
|
|
248
|
+
// would silently redirect that cell's `process.cwd()`, relative fs access,
|
|
249
|
+
// and child spawns, so keep it in place; this run still resolves against
|
|
250
|
+
// its own virtual cwd, and the next cell to start alone lands the move.
|
|
251
|
+
for (const runId of this.#runs.keys()) {
|
|
252
|
+
if (runId === currentRunId) continue;
|
|
253
|
+
this.#transport.send({
|
|
254
|
+
type: "log",
|
|
255
|
+
level: "warn",
|
|
256
|
+
msg: "JS eval subprocess kept its process cwd: other cells are mid-run",
|
|
257
|
+
meta: { cwd },
|
|
258
|
+
});
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
this.#options.chdir(cwd);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
// `process.chdir` throws when the session cwd no longer exists; keep
|
|
265
|
+
// the cell on the runtime's virtual cwd instead of failing the run.
|
|
266
|
+
this.#transport.send({
|
|
267
|
+
type: "log",
|
|
268
|
+
level: "warn",
|
|
269
|
+
msg: "JS eval subprocess could not enter the session cwd",
|
|
270
|
+
meta: { cwd, error: errorPayload(error) },
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
222
275
|
async #runOne(runId: string, code: string, filename: string, snapshot: SessionSnapshot): Promise<void> {
|
|
223
276
|
const active: ActiveRun = { runId, filename, pendingTools: new Map(), floatingRejections: [] };
|
|
224
277
|
this.#runs.set(runId, active);
|
|
@@ -229,7 +282,7 @@ export class WorkerCore {
|
|
|
229
282
|
};
|
|
230
283
|
let result: RunResult;
|
|
231
284
|
try {
|
|
232
|
-
const runtime = this.#ensureRuntime(snapshot);
|
|
285
|
+
const runtime = this.#ensureRuntime(snapshot, runId);
|
|
233
286
|
runtime.setCwd(snapshot.cwd);
|
|
234
287
|
const value = await runtime.run(code, filename, hooks, { runId, cwd: snapshot.cwd });
|
|
235
288
|
runtime.displayValue(value, hooks);
|
|
@@ -263,7 +316,15 @@ export class WorkerCore {
|
|
|
263
316
|
const id = `tc-${active.runId}-${crypto.randomUUID()}`;
|
|
264
317
|
const { promise, resolve, reject } = Promise.withResolvers<unknown>();
|
|
265
318
|
active.pendingTools.set(id, { runId: active.runId, resolve, reject });
|
|
266
|
-
|
|
319
|
+
try {
|
|
320
|
+
this.#transport.send({ type: "tool-call", id, runId: active.runId, name, args });
|
|
321
|
+
} catch (error) {
|
|
322
|
+
// Non-serializable args (DataCloneError from postMessage / IPC send).
|
|
323
|
+
// No reply will ever arrive; fail this call instead of stranding a
|
|
324
|
+
// pending entry until close.
|
|
325
|
+
active.pendingTools.delete(id);
|
|
326
|
+
reject(error);
|
|
327
|
+
}
|
|
267
328
|
return await promise;
|
|
268
329
|
}
|
|
269
330
|
|
|
@@ -1,6 +1,30 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test";
|
|
2
2
|
import { PYTHON_PRELUDE } from "../prelude";
|
|
3
3
|
|
|
4
|
+
const pythonPath = Bun.env.PYTHON ?? "python3";
|
|
5
|
+
|
|
6
|
+
async function runPrelude(
|
|
7
|
+
code: string,
|
|
8
|
+
env: Record<string, string>,
|
|
9
|
+
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
|
|
10
|
+
const prelude = PYTHON_PRELUDE.replace(
|
|
11
|
+
"from __future__ import annotations",
|
|
12
|
+
"from __future__ import annotations\n__omp_display = lambda *args, **kwargs: None",
|
|
13
|
+
);
|
|
14
|
+
const script = `${prelude}\n${code}`;
|
|
15
|
+
const proc = Bun.spawn([pythonPath, "-c", script], {
|
|
16
|
+
stdout: "pipe",
|
|
17
|
+
stderr: "pipe",
|
|
18
|
+
env: { ...process.env, ...env },
|
|
19
|
+
});
|
|
20
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
21
|
+
new Response(proc.stdout).text(),
|
|
22
|
+
new Response(proc.stderr).text(),
|
|
23
|
+
proc.exited,
|
|
24
|
+
]);
|
|
25
|
+
return { stdout, stderr, exitCode };
|
|
26
|
+
}
|
|
27
|
+
|
|
4
28
|
describe("python prelude", () => {
|
|
5
29
|
it("exposes read(path, offset?, limit?) with positional optional args", () => {
|
|
6
30
|
// The eval docs advertise `read(path, offset?=1, limit?=None)`. A
|
|
@@ -17,6 +41,54 @@ describe("python prelude", () => {
|
|
|
17
41
|
expect(signature).toContain("limit");
|
|
18
42
|
});
|
|
19
43
|
|
|
44
|
+
it("appends line selectors to delegated URI paths", async () => {
|
|
45
|
+
const requests: unknown[] = [];
|
|
46
|
+
const server = Bun.serve({
|
|
47
|
+
hostname: "127.0.0.1",
|
|
48
|
+
port: 0,
|
|
49
|
+
fetch: async request => {
|
|
50
|
+
requests.push(await request.json());
|
|
51
|
+
return Response.json({
|
|
52
|
+
ok: true,
|
|
53
|
+
value: { text: "resource contents", details: { resolvedPath: "/tmp/resource.txt" } },
|
|
54
|
+
});
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const result = await runPrelude(
|
|
60
|
+
[`print(read("artifact://21", 3, 2))`, `print(read("mcp://server/resource", 10, 5))`].join("\n"),
|
|
61
|
+
{
|
|
62
|
+
PI_TOOL_BRIDGE_URL: server.url.toString(),
|
|
63
|
+
PI_TOOL_BRIDGE_TOKEN: "test-token",
|
|
64
|
+
PI_TOOL_BRIDGE_SESSION: "test-session",
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
expect(result).toEqual({
|
|
69
|
+
stdout: "resource contents\nresource contents\n",
|
|
70
|
+
stderr: "",
|
|
71
|
+
exitCode: 0,
|
|
72
|
+
});
|
|
73
|
+
expect(requests).toEqual([
|
|
74
|
+
{
|
|
75
|
+
session: "test-session",
|
|
76
|
+
run: null,
|
|
77
|
+
name: "read",
|
|
78
|
+
args: { path: "artifact://21:3-4" },
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
session: "test-session",
|
|
82
|
+
run: null,
|
|
83
|
+
name: "read",
|
|
84
|
+
args: { path: "mcp://server/resource:10-14" },
|
|
85
|
+
},
|
|
86
|
+
]);
|
|
87
|
+
} finally {
|
|
88
|
+
server.stop(true);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
|
|
20
92
|
it("exposes isolation artifacts on the agent() handle node", () => {
|
|
21
93
|
// agent(..., handle=True) is the only escape hatch for
|
|
22
94
|
// recovering apply=False patch/branch/nested artifacts (the bare
|
package/src/eval/py/kernel.ts
CHANGED
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
resolveExplicitPythonRuntime,
|
|
24
24
|
resolvePythonRuntime,
|
|
25
25
|
} from "./runtime";
|
|
26
|
-
import { hostHasInheritableConsole, shouldHideKernelWindow } from "./spawn-options";
|
|
26
|
+
import { hostHasInheritableConsole, shouldDetachKernel, shouldHideKernelWindow } from "./spawn-options";
|
|
27
27
|
|
|
28
28
|
export type {
|
|
29
29
|
KernelExecuteOptions,
|
|
@@ -193,6 +193,7 @@ export class PythonKernel extends BaseKernel {
|
|
|
193
193
|
|
|
194
194
|
const proc = Bun.spawn([runtime.pythonPath, "-u", scriptPath], {
|
|
195
195
|
cwd: options.cwd,
|
|
196
|
+
detached: shouldDetachKernel(process.platform),
|
|
196
197
|
env: spawnEnv,
|
|
197
198
|
stdin: "pipe",
|
|
198
199
|
stdout: "pipe",
|