@oh-my-pi/pi-coding-agent 14.9.5 → 14.9.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/package.json +7 -7
- package/src/cli/setup-cli.ts +14 -161
- package/src/cli/stats-cli.ts +56 -2
- package/src/cli.ts +0 -1
- package/src/config/settings-schema.ts +0 -10
- package/src/eval/eval.lark +30 -10
- package/src/eval/js/context-manager.ts +334 -564
- package/src/eval/js/shared/helpers.ts +237 -0
- package/src/eval/js/shared/indirect-eval.ts +30 -0
- package/src/eval/js/shared/rewrite-imports.ts +211 -0
- package/src/eval/js/shared/runtime.ts +168 -0
- package/src/eval/js/shared/types.ts +18 -0
- package/src/eval/js/tool-bridge.ts +2 -4
- package/src/eval/js/worker-core.ts +146 -0
- package/src/eval/js/worker-entry.ts +24 -0
- package/src/eval/js/worker-protocol.ts +41 -0
- package/src/eval/parse.ts +218 -49
- package/src/eval/py/display.ts +71 -0
- package/src/eval/py/executor.ts +74 -89
- package/src/eval/py/index.ts +1 -2
- package/src/eval/py/kernel.ts +472 -900
- package/src/eval/py/prelude.py +95 -7
- package/src/eval/py/runner.py +879 -0
- package/src/eval/py/runtime.ts +3 -16
- package/src/eval/py/tool-bridge.ts +137 -0
- package/src/export/html/template.generated.ts +1 -1
- package/src/export/html/template.js +93 -5
- package/src/internal-urls/docs-index.generated.ts +3 -3
- package/src/modes/controllers/command-controller.ts +0 -23
- package/src/prompts/tools/eval.md +14 -27
- package/src/session/agent-session.ts +0 -1
- package/src/session/history-storage.ts +77 -19
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +86 -5
- package/src/tools/browser/tab-worker.ts +104 -58
- package/src/tools/eval.ts +1 -1
- package/src/web/search/index.ts +6 -4
- package/src/cli/jupyter-cli.ts +0 -106
- package/src/commands/jupyter.ts +0 -32
- package/src/eval/py/cancellation.ts +0 -28
- package/src/eval/py/gateway-coordinator.ts +0 -424
- /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
- /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
package/src/eval/py/executor.ts
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import { OutputSink } from "../../session/streaming-output";
|
|
3
|
-
import {
|
|
3
|
+
import type { ToolSession } from "../../tools";
|
|
4
|
+
import type { JsStatusEvent } from "../js/shared/types";
|
|
5
|
+
import type { KernelDisplayOutput } from "./display";
|
|
4
6
|
import {
|
|
5
7
|
checkPythonKernelAvailability,
|
|
6
|
-
type KernelDisplayOutput,
|
|
7
8
|
type KernelExecuteOptions,
|
|
8
9
|
type KernelExecuteResult,
|
|
9
10
|
PythonKernel,
|
|
10
11
|
} from "./kernel";
|
|
12
|
+
import { ensurePyToolBridge, registerPyToolBridge } from "./tool-bridge";
|
|
11
13
|
|
|
12
14
|
const IDLE_TIMEOUT_MS = 5 * 60 * 1000; // 5 minutes
|
|
13
15
|
const MAX_KERNEL_SESSIONS = 4;
|
|
@@ -35,8 +37,6 @@ export interface PythonExecutorOptions {
|
|
|
35
37
|
kernelMode?: PythonKernelMode;
|
|
36
38
|
/** Restart the kernel before executing */
|
|
37
39
|
reset?: boolean;
|
|
38
|
-
/** Use shared gateway across pi instances (default: true) */
|
|
39
|
-
useSharedGateway?: boolean;
|
|
40
40
|
/** Session file path for accessing task outputs */
|
|
41
41
|
sessionFile?: string;
|
|
42
42
|
/**
|
|
@@ -49,6 +49,18 @@ export interface PythonExecutorOptions {
|
|
|
49
49
|
/** Artifact path/id for full output storage */
|
|
50
50
|
artifactPath?: string;
|
|
51
51
|
artifactId?: string;
|
|
52
|
+
/**
|
|
53
|
+
* ToolSession used to resolve host-side `tool.<name>(args)` calls made from
|
|
54
|
+
* the Python prelude's bridge proxy. When omitted, the bridge env vars are
|
|
55
|
+
* not injected and any `tool.foo(...)` raises in Python.
|
|
56
|
+
*/
|
|
57
|
+
toolSession?: ToolSession;
|
|
58
|
+
/** Callback for status events emitted by tool bridge invocations. */
|
|
59
|
+
emitStatus?: (event: JsStatusEvent) => void;
|
|
60
|
+
/** @internal Bridge session id, set by `executePython` before delegating. */
|
|
61
|
+
bridgeSessionId?: string;
|
|
62
|
+
/** @internal Bridge endpoint info, set by `executePython` before delegating. */
|
|
63
|
+
bridge?: { url: string; token: string };
|
|
52
64
|
}
|
|
53
65
|
|
|
54
66
|
export interface PythonKernelExecutor {
|
|
@@ -87,7 +99,6 @@ interface KernelSession {
|
|
|
87
99
|
restartCount: number;
|
|
88
100
|
dead: boolean;
|
|
89
101
|
needsRestart: boolean;
|
|
90
|
-
kernelInvalidatedByRecovery: boolean;
|
|
91
102
|
disposing: boolean;
|
|
92
103
|
disposeCapacityPromise?: Promise<void>;
|
|
93
104
|
resolveDisposeCapacity?: () => void;
|
|
@@ -107,12 +118,15 @@ const disposingKernelSessions = new Set<KernelSession>();
|
|
|
107
118
|
let cleanupTimer: NodeJS.Timeout | null = null;
|
|
108
119
|
|
|
109
120
|
interface KernelSessionExecutionOptions {
|
|
110
|
-
useSharedGateway?: boolean;
|
|
111
121
|
sessionFile?: string;
|
|
112
122
|
artifactsDir?: string;
|
|
113
123
|
signal?: AbortSignal;
|
|
114
124
|
deadlineMs?: number;
|
|
115
125
|
kernelOwnerId?: string;
|
|
126
|
+
/** Bridge session identifier exported into the kernel env as PI_TOOL_BRIDGE_SESSION. */
|
|
127
|
+
bridgeSessionId?: string;
|
|
128
|
+
/** Cached bridge connection info. When present, env vars for tool.<name>() get injected. */
|
|
129
|
+
bridge?: { url: string; token: string };
|
|
116
130
|
}
|
|
117
131
|
|
|
118
132
|
class PythonExecutionCancelledError extends Error {
|
|
@@ -137,10 +151,20 @@ function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineM
|
|
|
137
151
|
* directory (preferred by the prelude when resolving output IDs, so subagents
|
|
138
152
|
* see the parent's flat dir instead of a non-existent sibling).
|
|
139
153
|
*/
|
|
140
|
-
function buildKernelEnv(options: {
|
|
154
|
+
function buildKernelEnv(options: {
|
|
155
|
+
sessionFile?: string;
|
|
156
|
+
artifactsDir?: string;
|
|
157
|
+
bridgeSessionId?: string;
|
|
158
|
+
bridge?: { url: string; token: string };
|
|
159
|
+
}): Record<string, string> | undefined {
|
|
141
160
|
const env: Record<string, string> = {};
|
|
142
161
|
if (options.sessionFile) env.PI_SESSION_FILE = options.sessionFile;
|
|
143
162
|
if (options.artifactsDir) env.PI_ARTIFACTS_DIR = options.artifactsDir;
|
|
163
|
+
if (options.bridge && options.bridgeSessionId) {
|
|
164
|
+
env.PI_TOOL_BRIDGE_URL = options.bridge.url;
|
|
165
|
+
env.PI_TOOL_BRIDGE_TOKEN = options.bridge.token;
|
|
166
|
+
env.PI_TOOL_BRIDGE_SESSION = options.bridgeSessionId;
|
|
167
|
+
}
|
|
144
168
|
return Object.keys(env).length > 0 ? env : undefined;
|
|
145
169
|
}
|
|
146
170
|
|
|
@@ -266,7 +290,6 @@ function buildKernelStartOptions(
|
|
|
266
290
|
return {
|
|
267
291
|
cwd,
|
|
268
292
|
env,
|
|
269
|
-
useSharedGateway: options.useSharedGateway,
|
|
270
293
|
signal: options.signal,
|
|
271
294
|
deadlineMs: options.deadlineMs,
|
|
272
295
|
};
|
|
@@ -350,7 +373,6 @@ function finishDisposingKernelSession(session: KernelSession): void {
|
|
|
350
373
|
session.disposeResultPromise = undefined;
|
|
351
374
|
session.disposeResultTimeoutMs = undefined;
|
|
352
375
|
session.nextDisposalRetryAt = undefined;
|
|
353
|
-
session.kernelInvalidatedByRecovery = false;
|
|
354
376
|
syncCleanupTimer();
|
|
355
377
|
}
|
|
356
378
|
|
|
@@ -474,58 +496,6 @@ async function ensureKernelAvailable(
|
|
|
474
496
|
}
|
|
475
497
|
}
|
|
476
498
|
|
|
477
|
-
function isResourceExhaustionError(error: unknown): boolean {
|
|
478
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
479
|
-
return (
|
|
480
|
-
message.includes("Too many open files") ||
|
|
481
|
-
message.includes("EMFILE") ||
|
|
482
|
-
message.includes("ENFILE") ||
|
|
483
|
-
message.includes("resource temporarily unavailable")
|
|
484
|
-
);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function clearSharedGatewayDisposingKernelSessionTracking(): void {
|
|
488
|
-
for (const session of Array.from(disposingKernelSessions.values())) {
|
|
489
|
-
if (!session.kernel.isSharedGateway) continue;
|
|
490
|
-
if (session.heartbeatTimer) {
|
|
491
|
-
clearInterval(session.heartbeatTimer);
|
|
492
|
-
session.heartbeatTimer = undefined;
|
|
493
|
-
}
|
|
494
|
-
disposingKernelSessions.delete(session);
|
|
495
|
-
session.resolveDisposeCapacity?.();
|
|
496
|
-
session.resolveDisposeCapacity = undefined;
|
|
497
|
-
session.disposeCapacityPromise = undefined;
|
|
498
|
-
session.resolveDisposeAttempt?.();
|
|
499
|
-
session.resolveDisposeAttempt = undefined;
|
|
500
|
-
session.disposeAttemptPromise = undefined;
|
|
501
|
-
session.disposeResultPromise = undefined;
|
|
502
|
-
session.disposeResultTimeoutMs = undefined;
|
|
503
|
-
session.nextDisposalRetryAt = undefined;
|
|
504
|
-
session.kernelInvalidatedByRecovery = false;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
function markLiveKernelSessionsForRecovery(): void {
|
|
509
|
-
for (const session of kernelSessions.values()) {
|
|
510
|
-
if (session.heartbeatTimer) {
|
|
511
|
-
clearInterval(session.heartbeatTimer);
|
|
512
|
-
session.heartbeatTimer = undefined;
|
|
513
|
-
}
|
|
514
|
-
session.needsRestart = true;
|
|
515
|
-
session.kernelInvalidatedByRecovery = session.kernel.isSharedGateway;
|
|
516
|
-
session.restartCount = 0;
|
|
517
|
-
}
|
|
518
|
-
}
|
|
519
|
-
|
|
520
|
-
async function recoverFromResourceExhaustion(): Promise<void> {
|
|
521
|
-
logger.warn("Resource exhaustion detected, recovering by restarting shared gateway");
|
|
522
|
-
stopCleanupTimer();
|
|
523
|
-
markLiveKernelSessionsForRecovery();
|
|
524
|
-
clearSharedGatewayDisposingKernelSessionTracking();
|
|
525
|
-
await shutdownSharedGateway();
|
|
526
|
-
syncCleanupTimer();
|
|
527
|
-
}
|
|
528
|
-
|
|
529
499
|
function ensureKernelHeartbeat(session: KernelSession): void {
|
|
530
500
|
if (session.heartbeatTimer) return;
|
|
531
501
|
session.heartbeatTimer = setInterval(() => {
|
|
@@ -541,22 +511,12 @@ async function createKernelSession(
|
|
|
541
511
|
sessionId: string,
|
|
542
512
|
cwd: string,
|
|
543
513
|
options: KernelSessionExecutionOptions = {},
|
|
544
|
-
isRetry?: boolean,
|
|
545
514
|
): Promise<KernelSession> {
|
|
546
515
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
547
516
|
const env = buildKernelEnv(options);
|
|
548
517
|
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
549
518
|
|
|
550
|
-
|
|
551
|
-
try {
|
|
552
|
-
kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
|
|
553
|
-
} catch (err) {
|
|
554
|
-
if (!isRetry && isResourceExhaustionError(err)) {
|
|
555
|
-
await recoverFromResourceExhaustion();
|
|
556
|
-
return createKernelSession(sessionId, cwd, options, true);
|
|
557
|
-
}
|
|
558
|
-
throw err;
|
|
559
|
-
}
|
|
519
|
+
const kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
|
|
560
520
|
|
|
561
521
|
const hasFallbackOwner = options.kernelOwnerId === undefined;
|
|
562
522
|
const initialOwnerId = options.kernelOwnerId ?? sessionId;
|
|
@@ -567,7 +527,6 @@ async function createKernelSession(
|
|
|
567
527
|
restartCount: 0,
|
|
568
528
|
dead: false,
|
|
569
529
|
needsRestart: false,
|
|
570
|
-
kernelInvalidatedByRecovery: false,
|
|
571
530
|
disposing: false,
|
|
572
531
|
disposeResultPromise: undefined,
|
|
573
532
|
nextDisposalRetryAt: undefined,
|
|
@@ -592,18 +551,16 @@ async function restartKernelSession(
|
|
|
592
551
|
}
|
|
593
552
|
requireRemainingTimeoutMs(options.deadlineMs);
|
|
594
553
|
try {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
});
|
|
606
|
-
}
|
|
554
|
+
const deadKernel = session.dead || !session.kernel.isAlive();
|
|
555
|
+
const shutdownTimeoutMs = requireRemainingTimeoutMs(options.deadlineMs);
|
|
556
|
+
const shutdownResult = await session.kernel.shutdown({ signal: options.signal, timeoutMs: shutdownTimeoutMs });
|
|
557
|
+
if (!shutdownResult.confirmed && !deadKernel) {
|
|
558
|
+
throw new Error("Failed to confirm crashed kernel shutdown before restart");
|
|
559
|
+
}
|
|
560
|
+
if (!shutdownResult.confirmed) {
|
|
561
|
+
logger.warn("Proceeding with retained kernel restart after unconfirmed dead-kernel shutdown", {
|
|
562
|
+
sessionId: session.id,
|
|
563
|
+
});
|
|
607
564
|
}
|
|
608
565
|
const env = buildKernelEnv(options);
|
|
609
566
|
const startOptions = buildKernelStartOptions(cwd, env, options);
|
|
@@ -611,7 +568,6 @@ async function restartKernelSession(
|
|
|
611
568
|
session.kernel = kernel;
|
|
612
569
|
session.dead = false;
|
|
613
570
|
session.needsRestart = false;
|
|
614
|
-
session.kernelInvalidatedByRecovery = false;
|
|
615
571
|
session.lastUsedAt = Date.now();
|
|
616
572
|
ensureKernelHeartbeat(session);
|
|
617
573
|
} catch (err) {
|
|
@@ -625,9 +581,6 @@ type KernelDisposalResult = { status: "confirmed" } | { status: "unconfirmed" }
|
|
|
625
581
|
type KernelDisposalWaitResult = KernelDisposalResult | { status: "timedOut" };
|
|
626
582
|
|
|
627
583
|
function createKernelDisposalResultPromise(session: KernelSession, timeoutMs?: number): Promise<KernelDisposalResult> {
|
|
628
|
-
if (session.kernelInvalidatedByRecovery) {
|
|
629
|
-
return Promise.resolve({ status: "confirmed" as const });
|
|
630
|
-
}
|
|
631
584
|
return Promise.resolve()
|
|
632
585
|
.then(() => session.kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs }))
|
|
633
586
|
.then(
|
|
@@ -871,6 +824,20 @@ async function executeWithKernel(
|
|
|
871
824
|
const deadlineMs = getExecutionDeadlineMs(options);
|
|
872
825
|
let executionTimeoutMs: number | undefined;
|
|
873
826
|
|
|
827
|
+
const emitStatus =
|
|
828
|
+
options?.emitStatus ??
|
|
829
|
+
((event: JsStatusEvent) => {
|
|
830
|
+
displayOutputs.push({ type: "status", event });
|
|
831
|
+
});
|
|
832
|
+
const unregisterBridge =
|
|
833
|
+
options?.toolSession && options?.bridgeSessionId
|
|
834
|
+
? registerPyToolBridge(options.bridgeSessionId, {
|
|
835
|
+
toolSession: options.toolSession,
|
|
836
|
+
signal: options.signal,
|
|
837
|
+
emitStatus,
|
|
838
|
+
})
|
|
839
|
+
: null;
|
|
840
|
+
|
|
874
841
|
try {
|
|
875
842
|
executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
|
|
876
843
|
const result = await kernel.execute(code, {
|
|
@@ -923,6 +890,8 @@ async function executeWithKernel(
|
|
|
923
890
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
924
891
|
logger.error("Python execution failed", { error: error.message });
|
|
925
892
|
throw error;
|
|
893
|
+
} finally {
|
|
894
|
+
unregisterBridge?.();
|
|
926
895
|
}
|
|
927
896
|
}
|
|
928
897
|
|
|
@@ -954,7 +923,20 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
954
923
|
|
|
955
924
|
const kernelMode = executionOptions.kernelMode ?? "session";
|
|
956
925
|
|
|
926
|
+
if (executionOptions.toolSession && !executionOptions.bridge) {
|
|
927
|
+
try {
|
|
928
|
+
executionOptions.bridge = await ensurePyToolBridge();
|
|
929
|
+
} catch (err) {
|
|
930
|
+
logger.warn("Failed to start Python tool bridge", {
|
|
931
|
+
error: err instanceof Error ? err.message : String(err),
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
957
936
|
if (kernelMode === "per-call") {
|
|
937
|
+
if (executionOptions.bridge && !executionOptions.bridgeSessionId) {
|
|
938
|
+
executionOptions.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
|
|
939
|
+
}
|
|
958
940
|
const env = buildKernelEnv(executionOptions);
|
|
959
941
|
requireRemainingTimeoutMs(deadlineMs);
|
|
960
942
|
const startOptions = buildKernelStartOptions(cwd, env, executionOptions);
|
|
@@ -967,6 +949,9 @@ export async function executePython(code: string, options?: PythonExecutorOption
|
|
|
967
949
|
}
|
|
968
950
|
|
|
969
951
|
const sessionId = executionOptions.sessionId ?? `session:${cwd}`;
|
|
952
|
+
if (executionOptions.bridge && !executionOptions.bridgeSessionId) {
|
|
953
|
+
executionOptions.bridgeSessionId = sessionId;
|
|
954
|
+
}
|
|
970
955
|
if (executionOptions.reset) {
|
|
971
956
|
const existing = kernelSessions.get(sessionId);
|
|
972
957
|
if (existing) {
|
package/src/eval/py/index.ts
CHANGED
|
@@ -25,7 +25,6 @@ export default {
|
|
|
25
25
|
},
|
|
26
26
|
|
|
27
27
|
async execute(code: string, opts: ExecutorBackendExecOptions): Promise<ExecutorBackendResult> {
|
|
28
|
-
const useSharedGateway = readSetting<boolean>(opts.session, "python.sharedGateway");
|
|
29
28
|
const kernelMode = readSetting<PythonExecutorOptions["kernelMode"]>(opts.session, "python.kernelMode");
|
|
30
29
|
const executorOptions: PythonExecutorOptions = {
|
|
31
30
|
cwd: opts.cwd,
|
|
@@ -33,7 +32,6 @@ export default {
|
|
|
33
32
|
signal: opts.signal,
|
|
34
33
|
sessionId: namespaceSessionId(opts.sessionId),
|
|
35
34
|
kernelMode,
|
|
36
|
-
useSharedGateway,
|
|
37
35
|
sessionFile: opts.sessionFile,
|
|
38
36
|
artifactsDir: opts.session.getArtifactsDir?.() ?? undefined,
|
|
39
37
|
kernelOwnerId: opts.kernelOwnerId,
|
|
@@ -41,6 +39,7 @@ export default {
|
|
|
41
39
|
artifactPath: opts.artifactPath,
|
|
42
40
|
artifactId: opts.artifactId,
|
|
43
41
|
onChunk: opts.onChunk,
|
|
42
|
+
toolSession: opts.session,
|
|
44
43
|
};
|
|
45
44
|
const result = await executePython(code, executorOptions);
|
|
46
45
|
return {
|