@oh-my-pi/pi-coding-agent 14.5.11 → 14.5.13

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 (89) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/package.json +18 -10
  3. package/src/cli/jupyter-cli.ts +1 -1
  4. package/src/config/model-equivalence.ts +49 -16
  5. package/src/config/model-registry.ts +100 -25
  6. package/src/config/model-resolver.ts +29 -15
  7. package/src/config/settings-schema.ts +20 -6
  8. package/src/config/settings.ts +9 -8
  9. package/src/config.ts +9 -0
  10. package/src/eval/backend.ts +43 -0
  11. package/src/eval/eval.lark +43 -0
  12. package/src/eval/index.ts +5 -0
  13. package/src/eval/js/context-manager.ts +717 -0
  14. package/src/eval/js/executor.ts +131 -0
  15. package/src/eval/js/index.ts +46 -0
  16. package/src/eval/js/prelude.ts +2 -0
  17. package/src/eval/js/prelude.txt +84 -0
  18. package/src/eval/js/tool-bridge.ts +124 -0
  19. package/src/eval/parse.ts +337 -0
  20. package/src/{ipy → eval/py}/executor.ts +2 -180
  21. package/src/{ipy → eval/py}/gateway-coordinator.ts +4 -3
  22. package/src/eval/py/index.ts +58 -0
  23. package/src/{ipy → eval/py}/kernel.ts +5 -41
  24. package/src/{ipy → eval/py}/prelude.py +39 -227
  25. package/src/eval/types.ts +48 -0
  26. package/src/export/html/template.generated.ts +1 -1
  27. package/src/export/html/template.js +23 -17
  28. package/src/extensibility/extensions/types.ts +2 -3
  29. package/src/internal-urls/docs-index.generated.ts +5 -5
  30. package/src/lsp/client.ts +9 -0
  31. package/src/lsp/index.ts +395 -0
  32. package/src/lsp/types.ts +15 -4
  33. package/src/main.ts +25 -14
  34. package/src/mcp/oauth-flow.ts +1 -1
  35. package/src/memories/index.ts +1 -1
  36. package/src/modes/acp/acp-event-mapper.ts +1 -1
  37. package/src/modes/components/{python-execution.ts → eval-execution.ts} +11 -4
  38. package/src/modes/components/login-dialog.ts +1 -1
  39. package/src/modes/components/oauth-selector.ts +2 -1
  40. package/src/modes/components/tool-execution.ts +3 -4
  41. package/src/modes/controllers/command-controller.ts +28 -8
  42. package/src/modes/controllers/input-controller.ts +4 -4
  43. package/src/modes/controllers/selector-controller.ts +2 -1
  44. package/src/modes/interactive-mode.ts +4 -5
  45. package/src/modes/types.ts +3 -3
  46. package/src/modes/utils/ui-helpers.ts +2 -2
  47. package/src/prompts/system/system-prompt.md +3 -3
  48. package/src/prompts/tools/atom.md +3 -2
  49. package/src/prompts/tools/browser.md +61 -16
  50. package/src/prompts/tools/eval.md +92 -0
  51. package/src/prompts/tools/lsp.md +7 -3
  52. package/src/sdk.ts +45 -31
  53. package/src/session/agent-session.ts +44 -54
  54. package/src/session/messages.ts +1 -1
  55. package/src/slash-commands/builtin-registry.ts +1 -1
  56. package/src/system-prompt.ts +34 -66
  57. package/src/task/executor.ts +5 -9
  58. package/src/tools/browser/attach.ts +175 -0
  59. package/src/tools/browser/launch.ts +576 -0
  60. package/src/tools/browser/readable.ts +90 -0
  61. package/src/tools/browser/registry.ts +198 -0
  62. package/src/tools/browser/render.ts +212 -0
  63. package/src/tools/browser/tab-protocol.ts +101 -0
  64. package/src/tools/browser/tab-supervisor.ts +429 -0
  65. package/src/tools/browser/tab-worker-entry.ts +21 -0
  66. package/src/tools/browser/tab-worker.ts +1006 -0
  67. package/src/tools/browser.ts +231 -1567
  68. package/src/tools/checkpoint.ts +2 -2
  69. package/src/tools/{python.ts → eval.ts} +324 -315
  70. package/src/tools/exit-plan-mode.ts +1 -1
  71. package/src/tools/index.ts +62 -100
  72. package/src/tools/plan-mode-guard.ts +27 -1
  73. package/src/tools/read.ts +0 -6
  74. package/src/tools/recipe/runners/pkg.ts +34 -32
  75. package/src/tools/renderers.ts +4 -2
  76. package/src/tools/resolve.ts +7 -2
  77. package/src/tools/todo-write.ts +0 -1
  78. package/src/tools/tool-timeouts.ts +2 -2
  79. package/src/utils/markit.ts +15 -7
  80. package/src/utils/tools-manager.ts +5 -5
  81. package/src/web/search/index.ts +5 -5
  82. package/src/web/search/provider.ts +121 -39
  83. package/src/web/search/providers/gemini.ts +2 -2
  84. package/src/web/search/render.ts +2 -2
  85. package/src/ipy/modules.ts +0 -144
  86. package/src/prompts/tools/python.md +0 -57
  87. /package/src/{ipy → eval/py}/cancellation.ts +0 -0
  88. /package/src/{ipy → eval/py}/prelude.ts +0 -0
  89. /package/src/{ipy → eval/py}/runtime.ts +0 -0
package/src/sdk.ts CHANGED
@@ -36,6 +36,7 @@ import { CursorExecHandlers } from "./cursor";
36
36
  import "./discovery";
37
37
  import { resolveConfigValue } from "./config/resolve-config-value";
38
38
  import { initializeWithSettings } from "./discovery";
39
+ import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./eval/py/executor";
39
40
  import { TtsrManager } from "./export/ttsr";
40
41
  import {
41
42
  type CustomCommandsLoadResult,
@@ -73,7 +74,6 @@ import {
73
74
  RuleProtocolHandler,
74
75
  SkillProtocolHandler,
75
76
  } from "./internal-urls";
76
- import { disposeAllKernelSessions, disposeKernelSessionsByOwner } from "./ipy/executor";
77
77
  import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "./lsp/startup-events";
78
78
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
79
79
  import {
@@ -99,6 +99,8 @@ import { SessionManager } from "./session/session-manager";
99
99
  import { closeAllConnections } from "./ssh/connection-manager";
100
100
  import { unmountAll } from "./ssh/sshfs-mount";
101
101
  import {
102
+ type AgentsMdSearch,
103
+ buildAgentsMdSearch,
102
104
  buildSystemPrompt as buildSystemPromptInternal,
103
105
  buildSystemPromptToolMetadata,
104
106
  loadProjectContextFiles as loadContextFilesInternal,
@@ -111,13 +113,13 @@ import {
111
113
  createTools,
112
114
  discoverStartupLspServers,
113
115
  EditTool,
116
+ EvalTool,
114
117
  FindTool,
115
118
  getSearchTools,
116
119
  HIDDEN_TOOLS,
117
120
  isSearchProviderPreference,
118
121
  type LspStartupServerInfo,
119
122
  loadSshTool,
120
- PythonTool,
121
123
  ReadTool,
122
124
  ResolveTool,
123
125
  renderSearchToolBm25Description,
@@ -204,9 +206,6 @@ export interface CreateAgentSessionOptions {
204
206
  enableLsp?: boolean;
205
207
  /** Skip Python kernel availability check and prelude warmup */
206
208
  skipPythonPreflight?: boolean;
207
- /** Force Python prelude warmup even when test env would normally skip it */
208
- forcePythonWarmup?: boolean;
209
-
210
209
  /** Tool names explicitly requested (enables disabled-by-default tools) */
211
210
  toolNames?: string[];
212
211
 
@@ -275,10 +274,10 @@ export {
275
274
  BUILTIN_TOOLS,
276
275
  createTools,
277
276
  EditTool,
277
+ EvalTool,
278
278
  FindTool,
279
279
  HIDDEN_TOOLS,
280
280
  loadSshTool,
281
- PythonTool,
282
281
  ReadTool,
283
282
  ResolveTool,
284
283
  SearchTool,
@@ -667,17 +666,40 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
667
666
  const modelRegistry = options.modelRegistry ?? new ModelRegistry(authStorage);
668
667
 
669
668
  const settings = options.settings ?? (await logger.time("settings", Settings.init, { cwd, agentDir }));
670
- logger.time("initializeWithSettings");
671
- initializeWithSettings(settings);
669
+ logger.time("initializeWithSettings", initializeWithSettings, settings);
672
670
  if (!options.modelRegistry) {
673
671
  modelRegistry.refreshInBackground();
674
672
  }
673
+ // Kick off AGENTS.md filesystem search in parallel — it is the slowest piece of buildSystemPrompt
674
+ // (~200ms on large repos) and only needs `cwd`, so it can overlap with everything that follows.
675
+ const agentsMdSearchPromise: Promise<AgentsMdSearch> = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, cwd);
676
+ agentsMdSearchPromise.catch(() => {});
677
+
678
+ // Independent discoveries that depend only on cwd/agentDir — kicked off in parallel and awaited
679
+ // at their respective consumer sites. Their work can overlap with model resolution, secret loading,
680
+ // session-context build, tool creation, MCP discovery, and extension discovery.
681
+ const contextFilesPromise = options.contextFiles
682
+ ? Promise.resolve(options.contextFiles)
683
+ : logger.time("discoverContextFiles", discoverContextFiles, cwd, agentDir);
684
+ contextFilesPromise.catch(() => {});
685
+ const promptTemplatesPromise = options.promptTemplates
686
+ ? Promise.resolve(options.promptTemplates)
687
+ : logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir);
688
+ promptTemplatesPromise.catch(() => {});
689
+ const slashCommandsPromise = options.slashCommands
690
+ ? Promise.resolve(options.slashCommands)
691
+ : logger.time("discoverSlashCommands", discoverSlashCommands, cwd);
692
+ slashCommandsPromise.catch(() => {});
675
693
  const skillsSettings = settings.getGroup("skills");
676
694
  const disabledExtensionIds = settings.get("disabledExtensions") ?? [];
677
695
  const discoveredSkillsPromise =
678
696
  options.skills === undefined
679
- ? discoverSkills(cwd, agentDir, { ...skillsSettings, disabledExtensions: disabledExtensionIds })
697
+ ? logger.time("discoverSkills", discoverSkills, cwd, agentDir, {
698
+ ...skillsSettings,
699
+ disabledExtensions: disabledExtensionIds,
700
+ })
680
701
  : undefined;
702
+ discoveredSkillsPromise?.catch(() => {});
681
703
 
682
704
  // Initialize provider preferences from settings
683
705
  const webSearchProvider = settings.get("providers.webSearch");
@@ -814,10 +836,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
814
836
  skills = options.skills;
815
837
  skillWarnings = [];
816
838
  } else {
817
- const discovered = await logger.time(
818
- "discoverSkills",
819
- () => discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }),
820
- );
839
+ const discovered = await (discoveredSkillsPromise ?? Promise.resolve({ skills: [], warnings: [] }));
821
840
  skills = discovered.skills;
822
841
  skillWarnings = discovered.warnings;
823
842
  }
@@ -851,10 +870,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
851
870
  return { ttsrManager, rulebookRules, alwaysApplyRules };
852
871
  });
853
872
 
854
- const contextFiles = await logger.time(
855
- "discoverContextFiles",
856
- async () => options.contextFiles ?? (await discoverContextFiles(cwd, agentDir)),
857
- );
873
+ const contextFiles = await contextFilesPromise;
858
874
 
859
875
  let agent: Agent;
860
876
  let session!: AgentSession;
@@ -917,7 +933,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
917
933
  const resolvedAgentId = options.agentId ?? options.parentTaskPrefix ?? MAIN_AGENT_ID;
918
934
  const resolvedAgentDisplayName =
919
935
  options.agentDisplayName ?? ((options.taskDepth ?? 0) > 0 || options.parentTaskPrefix ? "sub" : "main");
920
- const pythonKernelOwnerId = `agent-session:${Snowflake.next()}`;
936
+ const evalKernelOwnerId = `agent-session:${Snowflake.next()}`;
921
937
 
922
938
  try {
923
939
  const getActiveModelString = (): string | undefined => {
@@ -937,7 +953,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
937
953
  return !requestedToolNames || requestedToolNames.includes("edit");
938
954
  },
939
955
  skipPythonPreflight: options.skipPythonPreflight,
940
- forcePythonWarmup: options.forcePythonWarmup,
941
956
  contextFiles,
942
957
  skills,
943
958
  eventBus,
@@ -945,12 +960,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
945
960
  requireYieldTool: options.requireYieldTool,
946
961
  taskDepth: options.taskDepth ?? 0,
947
962
  getSessionFile: () => sessionManager.getSessionFile() ?? null,
948
- getPythonKernelOwnerId: () => pythonKernelOwnerId,
949
- assertPythonExecutionAllowed: () => session?.assertPythonExecutionAllowed(),
950
- trackPythonExecution: (execution, abortController) =>
951
- session ? session.trackPythonExecution(execution, abortController) : execution,
963
+ getEvalKernelOwnerId: () => evalKernelOwnerId,
964
+ assertEvalExecutionAllowed: () => session?.assertEvalExecutionAllowed(),
965
+ trackEvalExecution: (execution, abortController) =>
966
+ session ? session.trackEvalExecution(execution, abortController) : execution,
952
967
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
953
968
  getAgentId: () => resolvedAgentId,
969
+ getToolByName: name => session?.getToolByName(name),
954
970
  agentRegistry,
955
971
  getSessionSpawns: () => options.spawns ?? "*",
956
972
  getModelString: () => (hasExplicitModel && model ? formatModelString(model) : undefined),
@@ -1353,6 +1369,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1353
1369
  mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1354
1370
  eagerTasks,
1355
1371
  secretsEnabled,
1372
+ agentsMdSearch: agentsMdSearchPromise,
1356
1373
  });
1357
1374
 
1358
1375
  if (options.systemPrompt === undefined) {
@@ -1376,6 +1393,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1376
1393
  mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1377
1394
  eagerTasks,
1378
1395
  secretsEnabled,
1396
+ agentsMdSearch: agentsMdSearchPromise,
1379
1397
  });
1380
1398
  }
1381
1399
  return options.systemPrompt(defaultPrompt);
@@ -1446,13 +1464,10 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1446
1464
 
1447
1465
  const systemPrompt = await logger.time("buildSystemPrompt", rebuildSystemPrompt, initialToolNames, toolRegistry);
1448
1466
 
1449
- const promptTemplates =
1450
- options.promptTemplates ??
1451
- (await logger.time("discoverPromptTemplates", discoverPromptTemplates, cwd, agentDir));
1467
+ const promptTemplates = await promptTemplatesPromise;
1452
1468
  toolSession.promptTemplates = promptTemplates;
1453
1469
 
1454
- const slashCommands =
1455
- options.slashCommands ?? (await logger.time("discoverSlashCommands", discoverSlashCommands, cwd));
1470
+ const slashCommands = await slashCommandsPromise;
1456
1471
 
1457
1472
  // Create convertToLlm wrapper that filters images if blockImages is enabled (defense-in-depth)
1458
1473
  const convertToLlmWithBlockImages = (messages: AgentMessage[]): Message[] => {
@@ -1596,7 +1611,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1596
1611
  thinkingLevel,
1597
1612
  sessionManager,
1598
1613
  settings,
1599
- pythonKernelOwnerId,
1614
+ evalKernelOwnerId,
1600
1615
  scopedModels: options.scopedModels,
1601
1616
  promptTemplates,
1602
1617
  slashCommands,
@@ -1765,7 +1780,6 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1765
1780
  });
1766
1781
  }
1767
1782
 
1768
- logger.time("createAgentSession:return");
1769
1783
  return {
1770
1784
  session,
1771
1785
  extensionsResult,
@@ -1780,7 +1794,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1780
1794
  if (hasSession) {
1781
1795
  await session.dispose();
1782
1796
  } else {
1783
- await disposeKernelSessionsByOwner(pythonKernelOwnerId);
1797
+ await disposeKernelSessionsByOwner(evalKernelOwnerId);
1784
1798
  }
1785
1799
  } catch (cleanupError) {
1786
1800
  logger.warn("Failed to clean up createAgentSession resources after startup error", {
@@ -52,16 +52,8 @@ import {
52
52
  parseRateLimitReason,
53
53
  streamSimple,
54
54
  } from "@oh-my-pi/pi-ai";
55
- import { killTree, MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
56
- import {
57
- abortableSleep,
58
- getAgentDbPath,
59
- isEnoent,
60
- logger,
61
- prompt,
62
- Snowflake,
63
- setNativeKillTree,
64
- } from "@oh-my-pi/pi-utils";
55
+ import { MacOSPowerAssertion } from "@oh-my-pi/pi-natives";
56
+ import { abortableSleep, getAgentDbPath, isEnoent, logger, prompt, Snowflake } from "@oh-my-pi/pi-utils";
65
57
  import type { AsyncJob, AsyncJobManager } from "../async";
66
58
  import type { Rule } from "../capability/rule";
67
59
  import { MODEL_ROLE_IDS, type ModelRegistry } from "../config/model-registry";
@@ -76,6 +68,11 @@ import {
76
68
  import { expandPromptTemplate, type PromptTemplate } from "../config/prompt-templates";
77
69
  import type { Settings, SkillsSettings } from "../config/settings";
78
70
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../edit";
71
+ import {
72
+ disposeKernelSessionsByOwner,
73
+ executePython as executePythonCommand,
74
+ type PythonResult,
75
+ } from "../eval/py/executor";
79
76
  import { type BashResult, executeBash as executeBashCommand } from "../exec/bash-executor";
80
77
  import { exportSessionToHtml } from "../export/html";
81
78
  import type { TtsrManager, TtsrMatchContext } from "../export/ttsr";
@@ -106,11 +103,6 @@ import type { HookCommandContext } from "../extensibility/hooks/types";
106
103
  import type { Skill, SkillWarning } from "../extensibility/skills";
107
104
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
108
105
  import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
109
- import {
110
- disposeKernelSessionsByOwner,
111
- executePython as executePythonCommand,
112
- type PythonResult,
113
- } from "../ipy/executor";
114
106
  import {
115
107
  buildDiscoverableMCPSearchIndex,
116
108
  collectDiscoverableMCPTools,
@@ -267,7 +259,7 @@ export interface AgentSessionConfig {
267
259
  /** Secret obfuscator for deobfuscating streaming edit content */
268
260
  obfuscator?: SecretObfuscator;
269
261
  /** Logical owner for retained Python kernels created by this session. */
270
- pythonKernelOwnerId?: string;
262
+ evalKernelOwnerId?: string;
271
263
  /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
272
264
  agentId?: string;
273
265
  /** Shared agent registry (for forwarding IRC observations to the main session UI). */
@@ -482,11 +474,11 @@ export class AgentSession {
482
474
  #pendingBashMessages: BashExecutionMessage[] = [];
483
475
 
484
476
  // Python execution state
485
- #pythonAbortControllers = new Set<AbortController>();
486
- #pythonKernelOwnerId: string;
477
+ #evalAbortControllers = new Set<AbortController>();
478
+ #evalKernelOwnerId: string;
487
479
  #pendingPythonMessages: PythonExecutionMessage[] = [];
488
- #activePythonExecutions = new Set<Promise<unknown>>();
489
- #pythonExecutionDisposing = false;
480
+ #activeEvalExecutions = new Set<Promise<unknown>>();
481
+ #evalExecutionDisposing = false;
490
482
 
491
483
  // Background-channel IRC exchanges queued while the recipient was streaming.
492
484
  // Drained into history (via emitExternalEvent) once the recipient becomes idle.
@@ -580,14 +572,12 @@ export class AgentSession {
580
572
  }
581
573
 
582
574
  constructor(config: AgentSessionConfig) {
583
- setNativeKillTree(killTree);
584
-
585
575
  this.agent = config.agent;
586
576
  this.sessionManager = config.sessionManager;
587
577
  this.settings = config.settings;
588
578
  this.#startPowerAssertion();
589
579
  this.#asyncJobManager = config.asyncJobManager;
590
- this.#pythonKernelOwnerId = config.pythonKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
580
+ this.#evalKernelOwnerId = config.evalKernelOwnerId ?? `agent-session:${Snowflake.next()}`;
591
581
  this.#scopedModels = config.scopedModels ?? [];
592
582
  this.#thinkingLevel = config.thinkingLevel;
593
583
  this.#promptTemplates = config.promptTemplates ?? [];
@@ -1948,7 +1938,7 @@ export class AgentSession {
1948
1938
  * Call this when completely done with the session.
1949
1939
  */
1950
1940
  async dispose(): Promise<void> {
1951
- this.#pythonExecutionDisposing = true;
1941
+ this.#evalExecutionDisposing = true;
1952
1942
  try {
1953
1943
  if (this.#extensionRunner?.hasHandlers("session_shutdown")) {
1954
1944
  await this.#extensionRunner.emit({ type: "session_shutdown" });
@@ -1963,13 +1953,13 @@ export class AgentSession {
1963
1953
  if (drained === false && deliveryState) {
1964
1954
  logger.warn("Async job completion deliveries still pending during dispose", { ...deliveryState });
1965
1955
  }
1966
- const pythonExecutionsSettled = await this.#preparePythonExecutionsForDispose();
1956
+ const pythonExecutionsSettled = await this.#prepareEvalExecutionsForDispose();
1967
1957
  if (!pythonExecutionsSettled) {
1968
1958
  logger.warn(
1969
1959
  "Detaching retained Python kernel ownership during dispose while Python execution is still active",
1970
1960
  );
1971
1961
  }
1972
- await disposeKernelSessionsByOwner(this.#pythonKernelOwnerId);
1962
+ await disposeKernelSessionsByOwner(this.#evalKernelOwnerId);
1973
1963
  this.#stopPowerAssertion();
1974
1964
  await this.sessionManager.close();
1975
1965
  this.#closeAllProviderSessions("dispose");
@@ -3433,7 +3423,7 @@ export class AgentSession {
3433
3423
  this.abortCompaction();
3434
3424
  this.abortHandoff();
3435
3425
  this.abortBash();
3436
- this.abortPython();
3426
+ this.abortEval();
3437
3427
  const postPromptDrain = this.#cancelPostPromptTasks();
3438
3428
  this.agent.abort();
3439
3429
  await postPromptDrain;
@@ -5905,7 +5895,7 @@ export class AgentSession {
5905
5895
 
5906
5896
  /**
5907
5897
  * Execute Python code in the shared kernel.
5908
- * Uses the same kernel session as the agent's Python tool, allowing collaborative editing.
5898
+ * Uses the same kernel session as eval's Python backend, allowing collaborative editing.
5909
5899
  * @param code The Python code to execute
5910
5900
  * @param onChunk Optional streaming callback for output
5911
5901
  * @param options.excludeFromContext If true, execution won't be sent to LLM ($$ prefix)
@@ -5917,7 +5907,7 @@ export class AgentSession {
5917
5907
  ): Promise<PythonResult> {
5918
5908
  const excludeFromContext = options?.excludeFromContext === true;
5919
5909
  const cwd = this.sessionManager.getCwd();
5920
- this.assertPythonExecutionAllowed();
5910
+ this.assertEvalExecutionAllowed();
5921
5911
 
5922
5912
  const abortController = new AbortController();
5923
5913
  const execution = (async (): Promise<PythonResult> => {
@@ -5928,20 +5918,20 @@ export class AgentSession {
5928
5918
  excludeFromContext,
5929
5919
  cwd,
5930
5920
  });
5931
- this.assertPythonExecutionAllowed();
5921
+ this.assertEvalExecutionAllowed();
5932
5922
  if (hookResult?.result) {
5933
5923
  this.recordPythonResult(code, hookResult.result, options);
5934
5924
  return hookResult.result;
5935
5925
  }
5936
5926
  }
5937
5927
 
5938
- // Use the same session ID as the Python tool for kernel sharing
5928
+ // Use the same session ID as eval's Python backend for kernel sharing
5939
5929
  const sessionFile = this.sessionManager.getSessionFile();
5940
5930
  const sessionId = sessionFile ? `session:${sessionFile}:cwd:${cwd}` : `cwd:${cwd}`;
5941
5931
  const result = await executePythonCommand(code, {
5942
5932
  cwd,
5943
5933
  sessionId,
5944
- kernelOwnerId: this.#pythonKernelOwnerId,
5934
+ kernelOwnerId: this.#evalKernelOwnerId,
5945
5935
  kernelMode: this.settings.get("python.kernelMode"),
5946
5936
  useSharedGateway: this.settings.get("python.sharedGateway"),
5947
5937
  onChunk,
@@ -5950,11 +5940,11 @@ export class AgentSession {
5950
5940
  this.recordPythonResult(code, result, options);
5951
5941
  return result;
5952
5942
  })();
5953
- return await this.trackPythonExecution(execution, abortController);
5943
+ return await this.trackEvalExecution(execution, abortController);
5954
5944
  }
5955
5945
 
5956
- assertPythonExecutionAllowed(): void {
5957
- if (this.#pythonExecutionDisposing) {
5946
+ assertEvalExecutionAllowed(): void {
5947
+ if (this.#evalExecutionDisposing) {
5958
5948
  throw new Error("Python execution is unavailable while session disposal is in progress");
5959
5949
  }
5960
5950
  }
@@ -5962,17 +5952,17 @@ export class AgentSession {
5962
5952
  /**
5963
5953
  * Track Python work started outside AgentSession.executePython so dispose can await and abort it too.
5964
5954
  */
5965
- trackPythonExecution<T>(execution: Promise<T>, abortController: AbortController): Promise<T> {
5966
- this.#pythonAbortControllers.add(abortController);
5967
- this.#activePythonExecutions.add(execution);
5955
+ trackEvalExecution<T>(execution: Promise<T>, abortController: AbortController): Promise<T> {
5956
+ this.#evalAbortControllers.add(abortController);
5957
+ this.#activeEvalExecutions.add(execution);
5968
5958
  void execution.then(
5969
5959
  () => {
5970
- this.#pythonAbortControllers.delete(abortController);
5971
- this.#activePythonExecutions.delete(execution);
5960
+ this.#evalAbortControllers.delete(abortController);
5961
+ this.#activeEvalExecutions.delete(execution);
5972
5962
  },
5973
5963
  () => {
5974
- this.#pythonAbortControllers.delete(abortController);
5975
- this.#activePythonExecutions.delete(execution);
5964
+ this.#evalAbortControllers.delete(abortController);
5965
+ this.#activeEvalExecutions.delete(execution);
5976
5966
  },
5977
5967
  );
5978
5968
  return execution;
@@ -6007,35 +5997,35 @@ export class AgentSession {
6007
5997
  /**
6008
5998
  * Cancel running Python execution.
6009
5999
  */
6010
- abortPython(): void {
6011
- for (const abortController of this.#pythonAbortControllers) {
6000
+ abortEval(): void {
6001
+ for (const abortController of this.#evalAbortControllers) {
6012
6002
  abortController.abort();
6013
6003
  }
6014
6004
  }
6015
6005
 
6016
- async #waitForPythonExecutionsToSettle(timeoutMs: number): Promise<boolean> {
6006
+ async #waitForEvalExecutionsToSettle(timeoutMs: number): Promise<boolean> {
6017
6007
  const deadline = Date.now() + timeoutMs;
6018
- while (this.#activePythonExecutions.size > 0) {
6008
+ while (this.#activeEvalExecutions.size > 0) {
6019
6009
  const remainingMs = deadline - Date.now();
6020
6010
  if (remainingMs <= 0) {
6021
6011
  return false;
6022
6012
  }
6023
6013
  const settled = await Promise.race([
6024
- Promise.allSettled(Array.from(this.#activePythonExecutions)).then(() => true),
6014
+ Promise.allSettled(Array.from(this.#activeEvalExecutions)).then(() => true),
6025
6015
  Bun.sleep(remainingMs).then(() => false),
6026
6016
  ]);
6027
- if (!settled && this.#activePythonExecutions.size > 0) {
6017
+ if (!settled && this.#activeEvalExecutions.size > 0) {
6028
6018
  return false;
6029
6019
  }
6030
6020
  }
6031
6021
  return true;
6032
6022
  }
6033
6023
 
6034
- async #preparePythonExecutionsForDispose(): Promise<boolean> {
6035
- if (!(await this.#waitForPythonExecutionsToSettle(3_000))) {
6024
+ async #prepareEvalExecutionsForDispose(): Promise<boolean> {
6025
+ if (!(await this.#waitForEvalExecutionsToSettle(3_000))) {
6036
6026
  logger.warn("Aborting active Python execution during dispose before retained kernel cleanup");
6037
- this.abortPython();
6038
- if (!(await this.#waitForPythonExecutionsToSettle(1_000))) {
6027
+ this.abortEval();
6028
+ if (!(await this.#waitForEvalExecutionsToSettle(1_000))) {
6039
6029
  logger.warn(
6040
6030
  "Python execution is still active after dispose aborted all active runs; retained kernel ownership will still be detached",
6041
6031
  );
@@ -6046,8 +6036,8 @@ export class AgentSession {
6046
6036
  }
6047
6037
 
6048
6038
  /** Whether a Python execution is currently running */
6049
- get isPythonRunning(): boolean {
6050
- return this.#pythonAbortControllers.size > 0;
6039
+ get isEvalRunning(): boolean {
6040
+ return this.#evalAbortControllers.size > 0;
6051
6041
  }
6052
6042
 
6053
6043
  /** Whether there are pending Python messages waiting to be flushed */
@@ -59,7 +59,7 @@ export interface BashExecutionMessage {
59
59
 
60
60
  /**
61
61
  * Message type for user-initiated Python executions via the $ command.
62
- * Shares the same kernel session as the agent's Python tool.
62
+ * Shares the same kernel session as eval's Python backend.
63
63
  */
64
64
  export interface PythonExecutionMessage {
65
65
  role: "pythonExecution";
@@ -1,7 +1,7 @@
1
1
  import * as os from "node:os";
2
2
  import * as path from "node:path";
3
3
 
4
- import { getOAuthProviders } from "@oh-my-pi/pi-ai";
4
+ import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
5
5
  import { getConfigDirName } from "@oh-my-pi/pi-utils";
6
6
  import { invalidate as invalidateFsCache } from "../capability/fs";
7
7
  import type { SettingPath, SettingValue } from "../config/settings";
@@ -2,10 +2,9 @@
2
2
  * System prompt construction and project context loading
3
3
  */
4
4
 
5
- import * as fs from "node:fs";
6
5
  import * as os from "node:os";
7
- import * as path from "node:path";
8
6
  import type { AgentTool } from "@oh-my-pi/pi-agent-core";
7
+ import { FileType, glob } from "@oh-my-pi/pi-natives";
9
8
  import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger, prompt } from "@oh-my-pi/pi-utils";
10
9
  import { $ } from "bun";
11
10
  import { contextFileCapability } from "./capability/context-file";
@@ -89,81 +88,44 @@ const AGENTS_MD_LIMIT = 200;
89
88
  const SYSTEM_PROMPT_PREP_TIMEOUT_MS = 5000;
90
89
  const AGENTS_MD_EXCLUDED_DIRS = new Set(["node_modules", ".git"]);
91
90
 
92
- interface AgentsMdSearch {
91
+ export interface AgentsMdSearch {
93
92
  scopePath: string;
94
93
  limit: number;
95
94
  pattern: string;
96
95
  files: string[];
97
96
  }
98
97
 
99
- function normalizePath(value: string): string {
100
- return value.replace(/\\/g, "/");
101
- }
102
-
103
- function shouldSkipAgentsDir(name: string): boolean {
104
- if (AGENTS_MD_EXCLUDED_DIRS.has(name)) return true;
105
- return name.startsWith(".");
106
- }
107
-
108
- async function collectAgentsMdFiles(
109
- root: string,
110
- dir: string,
111
- depth: number,
112
- limit: number,
113
- discovered: Set<string>,
114
- ): Promise<void> {
115
- if (depth > AGENTS_MD_MAX_DEPTH || discovered.size >= limit) {
116
- return;
117
- }
118
-
119
- let entries: fs.Dirent[];
120
- try {
121
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
122
- } catch {
123
- return;
124
- }
125
-
126
- if (depth >= AGENTS_MD_MIN_DEPTH) {
127
- const hasAgentsMd = entries.some(entry => entry.isFile() && entry.name === "AGENTS.md");
128
- if (hasAgentsMd) {
129
- const relPath = normalizePath(path.relative(root, path.join(dir, "AGENTS.md")));
130
- if (relPath.length > 0) {
131
- discovered.add(relPath);
132
- }
133
- if (discovered.size >= limit) {
134
- return;
135
- }
136
- }
137
- }
138
-
139
- if (depth === AGENTS_MD_MAX_DEPTH) {
140
- return;
141
- }
142
-
143
- const childDirs = entries
144
- .filter(entry => entry.isDirectory() && !shouldSkipAgentsDir(entry.name))
145
- .map(entry => entry.name)
146
- .sort();
147
-
148
- await Promise.all(
149
- childDirs.map(async child => {
150
- if (discovered.size >= limit) return;
151
- await collectAgentsMdFiles(root, path.join(dir, child), depth + 1, limit, discovered);
152
- }),
153
- );
154
- }
155
-
156
98
  async function listAgentsMdFiles(root: string, limit: number): Promise<string[]> {
157
99
  try {
158
- const discovered = new Set<string>();
159
- await collectAgentsMdFiles(root, root, 0, limit, discovered);
160
- return Array.from(discovered).sort().slice(0, limit);
100
+ const result = await glob({
101
+ pattern: "**/AGENTS.md",
102
+ path: root,
103
+ fileType: FileType.File,
104
+ recursive: true,
105
+ hidden: false,
106
+ gitignore: true,
107
+ maxResults: limit * 4,
108
+ cache: true,
109
+ });
110
+ const files: string[] = [];
111
+ for (const m of result.matches) {
112
+ const rel = m.path.replace(/\\/g, "/");
113
+ if (!rel?.endsWith("AGENTS.md")) continue;
114
+ const segments = rel.split("/");
115
+ const depth = segments.length - 1;
116
+ if (depth < AGENTS_MD_MIN_DEPTH || depth > AGENTS_MD_MAX_DEPTH) continue;
117
+ const dirSegments = segments.slice(0, -1);
118
+ if (dirSegments.some(seg => AGENTS_MD_EXCLUDED_DIRS.has(seg) || seg.startsWith("."))) continue;
119
+ files.push(rel);
120
+ if (files.length >= limit) break;
121
+ }
122
+ return Array.from(new Set(files)).sort().slice(0, limit);
161
123
  } catch {
162
124
  return [];
163
125
  }
164
126
  }
165
127
 
166
- async function buildAgentsMdSearch(cwd: string): Promise<AgentsMdSearch> {
128
+ export async function buildAgentsMdSearch(cwd: string): Promise<AgentsMdSearch> {
167
129
  const files = await listAgentsMdFiles(cwd, AGENTS_MD_LIMIT);
168
130
  return {
169
131
  scopePath: ".",
@@ -445,6 +407,8 @@ export interface BuildSystemPromptOptions {
445
407
  alwaysApplyRules?: AlwaysApplyRule[];
446
408
  /** Whether secret obfuscation is active. When true, explains the redaction format in the prompt. */
447
409
  secretsEnabled?: boolean;
410
+ /** Pre-loaded AGENTS.md search (skips discovery if provided). May be a Promise to allow early kick-off. */
411
+ agentsMdSearch?: AgentsMdSearch | Promise<AgentsMdSearch>;
448
412
  }
449
413
 
450
414
  /** Build the system prompt with tools, guidelines, and context */
@@ -470,6 +434,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
470
434
  mcpDiscoveryServerSummaries = [],
471
435
  eagerTasks = false,
472
436
  secretsEnabled = false,
437
+ agentsMdSearch: providedAgentsMdSearch,
473
438
  } = options;
474
439
  const resolvedCwd = cwd ?? getProjectDir();
475
440
 
@@ -480,7 +445,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
480
445
  const contextFilesPromise = providedContextFiles
481
446
  ? Promise.resolve(providedContextFiles)
482
447
  : logger.time("loadProjectContextFiles", loadProjectContextFiles, { cwd: resolvedCwd });
483
- const agentsMdSearchPromise = logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
448
+ const agentsMdSearchPromise =
449
+ providedAgentsMdSearch !== undefined
450
+ ? Promise.resolve(providedAgentsMdSearch)
451
+ : logger.time("buildAgentsMdSearch", buildAgentsMdSearch, resolvedCwd);
484
452
  const skillsPromise: Promise<Skill[]> =
485
453
  providedSkills !== undefined
486
454
  ? Promise.resolve(providedSkills)
@@ -572,7 +540,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
572
540
  toolNames = Array.from(tools.keys());
573
541
  } else {
574
542
  // Use defaults
575
- toolNames = ["read", "bash", "python", "edit", "write"]; // TODO: Why?
543
+ toolNames = ["read", "bash", "eval", "edit", "write"]; // TODO: Why?
576
544
  }
577
545
  }
578
546
 
@@ -532,16 +532,12 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
532
532
  if (atMaxDepth && toolNames?.includes("task")) {
533
533
  toolNames = toolNames.filter(name => name !== "task");
534
534
  }
535
- const pythonToolMode = settings.get("python.toolMode") ?? "both";
536
535
  if (toolNames?.includes("exec")) {
536
+ const allowEvalPy = settings.get("eval.py") ?? true;
537
+ const allowEvalJs = settings.get("eval.js") ?? true;
537
538
  const expanded = toolNames.filter(name => name !== "exec");
538
- if (pythonToolMode === "bash-only") {
539
- expanded.push("bash");
540
- } else if (pythonToolMode === "ipy-only") {
541
- expanded.push("python");
542
- } else {
543
- expanded.push("python", "bash");
544
- }
539
+ if (allowEvalPy || allowEvalJs) expanded.push("eval");
540
+ expanded.push("bash");
545
541
  toolNames = Array.from(new Set(expanded));
546
542
  }
547
543
 
@@ -557,7 +553,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
557
553
 
558
554
  const lspEnabled = enableLsp ?? true;
559
555
  const ircEnabled = subagentSettings.get("irc.enabled") === true;
560
- const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("python");
556
+ const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
561
557
 
562
558
  const outputChunks: string[] = [];
563
559
  const finalOutputChunks: string[] = [];