@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +7 -7
  3. package/src/cli/setup-cli.ts +14 -161
  4. package/src/cli/stats-cli.ts +56 -2
  5. package/src/cli.ts +0 -1
  6. package/src/config/settings-schema.ts +0 -10
  7. package/src/eval/eval.lark +30 -10
  8. package/src/eval/js/context-manager.ts +334 -564
  9. package/src/eval/js/shared/helpers.ts +237 -0
  10. package/src/eval/js/shared/indirect-eval.ts +30 -0
  11. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  12. package/src/eval/js/shared/runtime.ts +168 -0
  13. package/src/eval/js/shared/types.ts +18 -0
  14. package/src/eval/js/tool-bridge.ts +2 -4
  15. package/src/eval/js/worker-core.ts +146 -0
  16. package/src/eval/js/worker-entry.ts +24 -0
  17. package/src/eval/js/worker-protocol.ts +41 -0
  18. package/src/eval/parse.ts +218 -49
  19. package/src/eval/py/display.ts +71 -0
  20. package/src/eval/py/executor.ts +74 -89
  21. package/src/eval/py/index.ts +1 -2
  22. package/src/eval/py/kernel.ts +472 -900
  23. package/src/eval/py/prelude.py +95 -7
  24. package/src/eval/py/runner.py +879 -0
  25. package/src/eval/py/runtime.ts +3 -16
  26. package/src/eval/py/tool-bridge.ts +137 -0
  27. package/src/export/html/template.generated.ts +1 -1
  28. package/src/export/html/template.js +93 -5
  29. package/src/internal-urls/docs-index.generated.ts +3 -3
  30. package/src/modes/controllers/command-controller.ts +0 -23
  31. package/src/prompts/tools/eval.md +14 -27
  32. package/src/session/agent-session.ts +0 -1
  33. package/src/session/history-storage.ts +77 -19
  34. package/src/tools/browser/tab-protocol.ts +4 -0
  35. package/src/tools/browser/tab-supervisor.ts +86 -5
  36. package/src/tools/browser/tab-worker.ts +104 -58
  37. package/src/tools/eval.ts +1 -1
  38. package/src/web/search/index.ts +6 -4
  39. package/src/cli/jupyter-cli.ts +0 -106
  40. package/src/commands/jupyter.ts +0 -32
  41. package/src/eval/py/cancellation.ts +0 -28
  42. package/src/eval/py/gateway-coordinator.ts +0 -424
  43. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +0 -0
  44. /package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -0
@@ -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 { shutdownSharedGateway } from "./gateway-coordinator";
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: { sessionFile?: string; artifactsDir?: string }): Record<string, string> | undefined {
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
- let kernel: PythonKernel;
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
- if (!session.kernelInvalidatedByRecovery) {
596
- const deadKernel = session.dead || !session.kernel.isAlive();
597
- const shutdownTimeoutMs = requireRemainingTimeoutMs(options.deadlineMs);
598
- const shutdownResult = await session.kernel.shutdown({ signal: options.signal, timeoutMs: shutdownTimeoutMs });
599
- if (!shutdownResult.confirmed && !deadKernel) {
600
- throw new Error("Failed to confirm crashed kernel shutdown before restart");
601
- }
602
- if (!shutdownResult.confirmed) {
603
- logger.warn("Proceeding with retained kernel restart after unconfirmed dead-kernel shutdown", {
604
- sessionId: session.id,
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) {
@@ -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 {