@oh-my-pi/pi-coding-agent 14.9.3 → 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 (108) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/package.json +7 -7
  3. package/src/async/job-manager.ts +66 -9
  4. package/src/capability/rule.ts +20 -0
  5. package/src/cli/setup-cli.ts +14 -161
  6. package/src/cli/stats-cli.ts +56 -2
  7. package/src/cli.ts +0 -1
  8. package/src/config/model-registry.ts +13 -0
  9. package/src/config/model-resolver.ts +8 -2
  10. package/src/config/settings-schema.ts +1 -11
  11. package/src/edit/index.ts +8 -0
  12. package/src/edit/renderer.ts +6 -1
  13. package/src/edit/streaming.ts +53 -2
  14. package/src/eval/eval.lark +30 -10
  15. package/src/eval/js/context-manager.ts +334 -601
  16. package/src/eval/js/shared/helpers.ts +237 -0
  17. package/src/eval/js/shared/indirect-eval.ts +30 -0
  18. package/src/eval/js/{prelude.txt → shared/prelude.txt} +0 -2
  19. package/src/eval/js/shared/rewrite-imports.ts +211 -0
  20. package/src/eval/js/shared/runtime.ts +168 -0
  21. package/src/eval/js/shared/types.ts +18 -0
  22. package/src/eval/js/tool-bridge.ts +2 -4
  23. package/src/eval/js/worker-core.ts +146 -0
  24. package/src/eval/js/worker-entry.ts +24 -0
  25. package/src/eval/js/worker-protocol.ts +41 -0
  26. package/src/eval/parse.ts +218 -49
  27. package/src/eval/py/display.ts +71 -0
  28. package/src/eval/py/executor.ts +97 -96
  29. package/src/eval/py/index.ts +2 -2
  30. package/src/eval/py/kernel.ts +472 -900
  31. package/src/eval/py/prelude.py +106 -87
  32. package/src/eval/py/runner.py +879 -0
  33. package/src/eval/py/runtime.ts +3 -16
  34. package/src/eval/py/tool-bridge.ts +137 -0
  35. package/src/export/html/template.css +12 -0
  36. package/src/export/html/template.generated.ts +1 -1
  37. package/src/export/html/template.js +113 -7
  38. package/src/extensibility/plugins/loader.ts +31 -6
  39. package/src/extensibility/skills.ts +20 -0
  40. package/src/internal-urls/agent-protocol.ts +63 -52
  41. package/src/internal-urls/artifact-protocol.ts +51 -51
  42. package/src/internal-urls/docs-index.generated.ts +35 -3
  43. package/src/internal-urls/index.ts +6 -19
  44. package/src/internal-urls/local-protocol.ts +49 -7
  45. package/src/internal-urls/mcp-protocol.ts +2 -8
  46. package/src/internal-urls/memory-protocol.ts +89 -59
  47. package/src/internal-urls/router.ts +38 -22
  48. package/src/internal-urls/rule-protocol.ts +2 -20
  49. package/src/internal-urls/skill-protocol.ts +4 -27
  50. package/src/main.ts +1 -1
  51. package/src/mcp/manager.ts +17 -0
  52. package/src/modes/components/session-observer-overlay.ts +2 -2
  53. package/src/modes/components/tool-execution.ts +6 -0
  54. package/src/modes/components/tree-selector.ts +4 -0
  55. package/src/modes/controllers/command-controller.ts +0 -23
  56. package/src/modes/controllers/event-controller.ts +23 -2
  57. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  58. package/src/modes/interactive-mode.ts +2 -2
  59. package/src/modes/theme/theme.ts +27 -27
  60. package/src/modes/types.ts +1 -1
  61. package/src/modes/utils/ui-helpers.ts +14 -9
  62. package/src/prompts/commands/orchestrate.md +1 -0
  63. package/src/prompts/system/project-prompt.md +10 -2
  64. package/src/prompts/system/subagent-system-prompt.md +8 -8
  65. package/src/prompts/system/system-prompt.md +13 -7
  66. package/src/prompts/tools/ask.md +0 -1
  67. package/src/prompts/tools/bash.md +0 -10
  68. package/src/prompts/tools/eval.md +15 -30
  69. package/src/prompts/tools/github.md +6 -5
  70. package/src/prompts/tools/hashline.md +1 -0
  71. package/src/prompts/tools/job.md +14 -6
  72. package/src/prompts/tools/task.md +20 -3
  73. package/src/registry/agent-registry.ts +2 -1
  74. package/src/sdk.ts +87 -89
  75. package/src/session/agent-session.ts +58 -21
  76. package/src/session/artifacts.ts +7 -4
  77. package/src/session/history-storage.ts +77 -19
  78. package/src/session/session-manager.ts +30 -1
  79. package/src/ssh/connection-manager.ts +32 -16
  80. package/src/ssh/sshfs-mount.ts +10 -7
  81. package/src/system-prompt.ts +0 -5
  82. package/src/task/executor.ts +14 -2
  83. package/src/task/index.ts +19 -5
  84. package/src/tool-discovery/tool-index.ts +21 -8
  85. package/src/tools/ast-edit.ts +3 -2
  86. package/src/tools/ast-grep.ts +3 -2
  87. package/src/tools/bash.ts +15 -9
  88. package/src/tools/browser/tab-protocol.ts +4 -0
  89. package/src/tools/browser/tab-supervisor.ts +98 -7
  90. package/src/tools/browser/tab-worker.ts +104 -58
  91. package/src/tools/eval.ts +49 -11
  92. package/src/tools/fetch.ts +1 -1
  93. package/src/tools/gh.ts +140 -4
  94. package/src/tools/index.ts +12 -11
  95. package/src/tools/job.ts +48 -12
  96. package/src/tools/read.ts +5 -4
  97. package/src/tools/search.ts +3 -2
  98. package/src/tools/todo-write.ts +1 -1
  99. package/src/web/scrapers/mastodon.ts +1 -1
  100. package/src/web/scrapers/repology.ts +7 -7
  101. package/src/web/search/index.ts +6 -4
  102. package/src/cli/jupyter-cli.ts +0 -106
  103. package/src/commands/jupyter.ts +0 -32
  104. package/src/eval/py/cancellation.ts +0 -28
  105. package/src/eval/py/gateway-coordinator.ts +0 -424
  106. package/src/internal-urls/jobs-protocol.ts +0 -120
  107. package/src/prompts/system/now-prompt.md +0 -7
  108. /package/src/eval/js/{prelude.ts → shared/prelude.ts} +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,13 +37,30 @@ 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
+ /**
43
+ * Effective artifacts directory for the current session. Subagents share
44
+ * the parent's directory, so this can differ from `sessionFile`'s sibling
45
+ * dir. When present, exported to the kernel as `PI_ARTIFACTS_DIR` and
46
+ * preferred over `PI_SESSION_FILE`-derived paths.
47
+ */
48
+ artifactsDir?: string;
42
49
  /** Artifact path/id for full output storage */
43
50
  artifactPath?: string;
44
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 };
45
64
  }
46
65
 
47
66
  export interface PythonKernelExecutor {
@@ -80,7 +99,6 @@ interface KernelSession {
80
99
  restartCount: number;
81
100
  dead: boolean;
82
101
  needsRestart: boolean;
83
- kernelInvalidatedByRecovery: boolean;
84
102
  disposing: boolean;
85
103
  disposeCapacityPromise?: Promise<void>;
86
104
  resolveDisposeCapacity?: () => void;
@@ -100,11 +118,15 @@ const disposingKernelSessions = new Set<KernelSession>();
100
118
  let cleanupTimer: NodeJS.Timeout | null = null;
101
119
 
102
120
  interface KernelSessionExecutionOptions {
103
- useSharedGateway?: boolean;
104
121
  sessionFile?: string;
122
+ artifactsDir?: string;
105
123
  signal?: AbortSignal;
106
124
  deadlineMs?: number;
107
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 };
108
130
  }
109
131
 
110
132
  class PythonExecutionCancelledError extends Error {
@@ -123,6 +145,29 @@ function getExecutionDeadlineMs(options?: Pick<PythonExecutorOptions, "deadlineM
123
145
  return Date.now() + options.timeoutMs;
124
146
  }
125
147
 
148
+ /**
149
+ * Build the env block exposed to the Python kernel. Includes the session file
150
+ * (for things that need the raw session path) and the effective artifacts
151
+ * directory (preferred by the prelude when resolving output IDs, so subagents
152
+ * see the parent's flat dir instead of a non-existent sibling).
153
+ */
154
+ function buildKernelEnv(options: {
155
+ sessionFile?: string;
156
+ artifactsDir?: string;
157
+ bridgeSessionId?: string;
158
+ bridge?: { url: string; token: string };
159
+ }): Record<string, string> | undefined {
160
+ const env: Record<string, string> = {};
161
+ if (options.sessionFile) env.PI_SESSION_FILE = options.sessionFile;
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
+ }
168
+ return Object.keys(env).length > 0 ? env : undefined;
169
+ }
170
+
126
171
  function getRemainingTimeoutMs(deadlineMs?: number): number | undefined {
127
172
  if (deadlineMs === undefined) return undefined;
128
173
  return deadlineMs - Date.now();
@@ -245,7 +290,6 @@ function buildKernelStartOptions(
245
290
  return {
246
291
  cwd,
247
292
  env,
248
- useSharedGateway: options.useSharedGateway,
249
293
  signal: options.signal,
250
294
  deadlineMs: options.deadlineMs,
251
295
  };
@@ -329,7 +373,6 @@ function finishDisposingKernelSession(session: KernelSession): void {
329
373
  session.disposeResultPromise = undefined;
330
374
  session.disposeResultTimeoutMs = undefined;
331
375
  session.nextDisposalRetryAt = undefined;
332
- session.kernelInvalidatedByRecovery = false;
333
376
  syncCleanupTimer();
334
377
  }
335
378
 
@@ -453,58 +496,6 @@ async function ensureKernelAvailable(
453
496
  }
454
497
  }
455
498
 
456
- function isResourceExhaustionError(error: unknown): boolean {
457
- const message = error instanceof Error ? error.message : String(error);
458
- return (
459
- message.includes("Too many open files") ||
460
- message.includes("EMFILE") ||
461
- message.includes("ENFILE") ||
462
- message.includes("resource temporarily unavailable")
463
- );
464
- }
465
-
466
- function clearSharedGatewayDisposingKernelSessionTracking(): void {
467
- for (const session of Array.from(disposingKernelSessions.values())) {
468
- if (!session.kernel.isSharedGateway) continue;
469
- if (session.heartbeatTimer) {
470
- clearInterval(session.heartbeatTimer);
471
- session.heartbeatTimer = undefined;
472
- }
473
- disposingKernelSessions.delete(session);
474
- session.resolveDisposeCapacity?.();
475
- session.resolveDisposeCapacity = undefined;
476
- session.disposeCapacityPromise = undefined;
477
- session.resolveDisposeAttempt?.();
478
- session.resolveDisposeAttempt = undefined;
479
- session.disposeAttemptPromise = undefined;
480
- session.disposeResultPromise = undefined;
481
- session.disposeResultTimeoutMs = undefined;
482
- session.nextDisposalRetryAt = undefined;
483
- session.kernelInvalidatedByRecovery = false;
484
- }
485
- }
486
-
487
- function markLiveKernelSessionsForRecovery(): void {
488
- for (const session of kernelSessions.values()) {
489
- if (session.heartbeatTimer) {
490
- clearInterval(session.heartbeatTimer);
491
- session.heartbeatTimer = undefined;
492
- }
493
- session.needsRestart = true;
494
- session.kernelInvalidatedByRecovery = session.kernel.isSharedGateway;
495
- session.restartCount = 0;
496
- }
497
- }
498
-
499
- async function recoverFromResourceExhaustion(): Promise<void> {
500
- logger.warn("Resource exhaustion detected, recovering by restarting shared gateway");
501
- stopCleanupTimer();
502
- markLiveKernelSessionsForRecovery();
503
- clearSharedGatewayDisposingKernelSessionTracking();
504
- await shutdownSharedGateway();
505
- syncCleanupTimer();
506
- }
507
-
508
499
  function ensureKernelHeartbeat(session: KernelSession): void {
509
500
  if (session.heartbeatTimer) return;
510
501
  session.heartbeatTimer = setInterval(() => {
@@ -520,24 +511,12 @@ async function createKernelSession(
520
511
  sessionId: string,
521
512
  cwd: string,
522
513
  options: KernelSessionExecutionOptions = {},
523
- isRetry?: boolean,
524
514
  ): Promise<KernelSession> {
525
515
  requireRemainingTimeoutMs(options.deadlineMs);
526
- const env: Record<string, string> | undefined = options.sessionFile
527
- ? { PI_SESSION_FILE: options.sessionFile }
528
- : undefined;
516
+ const env = buildKernelEnv(options);
529
517
  const startOptions = buildKernelStartOptions(cwd, env, options);
530
518
 
531
- let kernel: PythonKernel;
532
- try {
533
- kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
534
- } catch (err) {
535
- if (!isRetry && isResourceExhaustionError(err)) {
536
- await recoverFromResourceExhaustion();
537
- return createKernelSession(sessionId, cwd, options, true);
538
- }
539
- throw err;
540
- }
519
+ const kernel = await logger.time("createKernelSession:PythonKernel.start", PythonKernel.start, startOptions);
541
520
 
542
521
  const hasFallbackOwner = options.kernelOwnerId === undefined;
543
522
  const initialOwnerId = options.kernelOwnerId ?? sessionId;
@@ -548,7 +527,6 @@ async function createKernelSession(
548
527
  restartCount: 0,
549
528
  dead: false,
550
529
  needsRestart: false,
551
- kernelInvalidatedByRecovery: false,
552
530
  disposing: false,
553
531
  disposeResultPromise: undefined,
554
532
  nextDisposalRetryAt: undefined,
@@ -573,28 +551,23 @@ async function restartKernelSession(
573
551
  }
574
552
  requireRemainingTimeoutMs(options.deadlineMs);
575
553
  try {
576
- if (!session.kernelInvalidatedByRecovery) {
577
- const deadKernel = session.dead || !session.kernel.isAlive();
578
- const shutdownTimeoutMs = requireRemainingTimeoutMs(options.deadlineMs);
579
- const shutdownResult = await session.kernel.shutdown({ signal: options.signal, timeoutMs: shutdownTimeoutMs });
580
- if (!shutdownResult.confirmed && !deadKernel) {
581
- throw new Error("Failed to confirm crashed kernel shutdown before restart");
582
- }
583
- if (!shutdownResult.confirmed) {
584
- logger.warn("Proceeding with retained kernel restart after unconfirmed dead-kernel shutdown", {
585
- sessionId: session.id,
586
- });
587
- }
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
+ });
588
564
  }
589
- const env: Record<string, string> | undefined = options.sessionFile
590
- ? { PI_SESSION_FILE: options.sessionFile }
591
- : undefined;
565
+ const env = buildKernelEnv(options);
592
566
  const startOptions = buildKernelStartOptions(cwd, env, options);
593
567
  const kernel = await PythonKernel.start(startOptions);
594
568
  session.kernel = kernel;
595
569
  session.dead = false;
596
570
  session.needsRestart = false;
597
- session.kernelInvalidatedByRecovery = false;
598
571
  session.lastUsedAt = Date.now();
599
572
  ensureKernelHeartbeat(session);
600
573
  } catch (err) {
@@ -608,9 +581,6 @@ type KernelDisposalResult = { status: "confirmed" } | { status: "unconfirmed" }
608
581
  type KernelDisposalWaitResult = KernelDisposalResult | { status: "timedOut" };
609
582
 
610
583
  function createKernelDisposalResultPromise(session: KernelSession, timeoutMs?: number): Promise<KernelDisposalResult> {
611
- if (session.kernelInvalidatedByRecovery) {
612
- return Promise.resolve({ status: "confirmed" as const });
613
- }
614
584
  return Promise.resolve()
615
585
  .then(() => session.kernel.shutdown(timeoutMs === undefined ? undefined : { timeoutMs }))
616
586
  .then(
@@ -854,6 +824,20 @@ async function executeWithKernel(
854
824
  const deadlineMs = getExecutionDeadlineMs(options);
855
825
  let executionTimeoutMs: number | undefined;
856
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
+
857
841
  try {
858
842
  executionTimeoutMs = requireRemainingTimeoutMs(deadlineMs);
859
843
  const result = await kernel.execute(code, {
@@ -906,6 +890,8 @@ async function executeWithKernel(
906
890
  const error = err instanceof Error ? err : new Error(String(err));
907
891
  logger.error("Python execution failed", { error: error.message });
908
892
  throw error;
893
+ } finally {
894
+ unregisterBridge?.();
909
895
  }
910
896
  }
911
897
 
@@ -936,10 +922,22 @@ export async function executePython(code: string, options?: PythonExecutorOption
936
922
  await ensureKernelAvailable(cwd);
937
923
 
938
924
  const kernelMode = executionOptions.kernelMode ?? "session";
939
- const sessionFile = executionOptions.sessionFile;
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
+ }
940
935
 
941
936
  if (kernelMode === "per-call") {
942
- const env: Record<string, string> | undefined = sessionFile ? { PI_SESSION_FILE: sessionFile } : undefined;
937
+ if (executionOptions.bridge && !executionOptions.bridgeSessionId) {
938
+ executionOptions.bridgeSessionId = `py-bridge:${crypto.randomUUID()}`;
939
+ }
940
+ const env = buildKernelEnv(executionOptions);
943
941
  requireRemainingTimeoutMs(deadlineMs);
944
942
  const startOptions = buildKernelStartOptions(cwd, env, executionOptions);
945
943
  const kernel = await PythonKernel.start(startOptions);
@@ -951,6 +949,9 @@ export async function executePython(code: string, options?: PythonExecutorOption
951
949
  }
952
950
 
953
951
  const sessionId = executionOptions.sessionId ?? `session:${cwd}`;
952
+ if (executionOptions.bridge && !executionOptions.bridgeSessionId) {
953
+ executionOptions.bridgeSessionId = sessionId;
954
+ }
954
955
  if (executionOptions.reset) {
955
956
  const existing = kernelSessions.get(sessionId);
956
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,13 +32,14 @@ export default {
33
32
  signal: opts.signal,
34
33
  sessionId: namespaceSessionId(opts.sessionId),
35
34
  kernelMode,
36
- useSharedGateway,
37
35
  sessionFile: opts.sessionFile,
36
+ artifactsDir: opts.session.getArtifactsDir?.() ?? undefined,
38
37
  kernelOwnerId: opts.kernelOwnerId,
39
38
  reset: opts.reset,
40
39
  artifactPath: opts.artifactPath,
41
40
  artifactId: opts.artifactId,
42
41
  onChunk: opts.onChunk,
42
+ toolSession: opts.session,
43
43
  };
44
44
  const result = await executePython(code, executorOptions);
45
45
  return {