@oh-my-pi/pi-coding-agent 14.5.3 → 14.5.6

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 (68) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/examples/extensions/plan-mode.ts +1 -1
  3. package/examples/sdk/README.md +1 -1
  4. package/package.json +7 -7
  5. package/src/config/prompt-templates.ts +103 -8
  6. package/src/config/settings-schema.ts +14 -13
  7. package/src/config/settings.ts +1 -1
  8. package/src/cursor.ts +4 -4
  9. package/src/edit/index.ts +111 -109
  10. package/src/edit/line-hash.ts +33 -3
  11. package/src/edit/modes/apply-patch.ts +6 -4
  12. package/src/edit/modes/atom.lark +27 -0
  13. package/src/edit/modes/atom.ts +1039 -841
  14. package/src/edit/modes/hashline.ts +9 -10
  15. package/src/edit/modes/patch.ts +23 -19
  16. package/src/edit/modes/replace.ts +19 -15
  17. package/src/edit/renderer.ts +65 -8
  18. package/src/edit/streaming.ts +47 -77
  19. package/src/extensibility/extensions/types.ts +11 -11
  20. package/src/extensibility/hooks/types.ts +6 -6
  21. package/src/lsp/edits.ts +8 -5
  22. package/src/lsp/index.ts +4 -4
  23. package/src/lsp/utils.ts +7 -7
  24. package/src/mcp/discoverable-tool-metadata.ts +1 -1
  25. package/src/mcp/manager.ts +3 -3
  26. package/src/mcp/tool-bridge.ts +4 -4
  27. package/src/memories/index.ts +1 -1
  28. package/src/modes/acp/acp-event-mapper.ts +1 -1
  29. package/src/modes/components/session-observer-overlay.ts +1 -1
  30. package/src/modes/components/settings-defs.ts +3 -3
  31. package/src/modes/components/tree-selector.ts +2 -2
  32. package/src/modes/utils/ui-helpers.ts +31 -7
  33. package/src/prompts/agents/explore.md +1 -1
  34. package/src/prompts/agents/librarian.md +2 -2
  35. package/src/prompts/agents/plan.md +2 -2
  36. package/src/prompts/agents/reviewer.md +1 -1
  37. package/src/prompts/agents/task.md +2 -2
  38. package/src/prompts/system/plan-mode-active.md +1 -1
  39. package/src/prompts/system/system-prompt.md +116 -60
  40. package/src/prompts/tools/apply-patch.md +0 -2
  41. package/src/prompts/tools/atom.md +81 -63
  42. package/src/prompts/tools/bash.md +7 -4
  43. package/src/prompts/tools/checkpoint.md +1 -1
  44. package/src/prompts/tools/find.md +6 -1
  45. package/src/prompts/tools/hashline.md +10 -11
  46. package/src/prompts/tools/patch.md +13 -13
  47. package/src/prompts/tools/read.md +4 -4
  48. package/src/prompts/tools/replace.md +3 -3
  49. package/src/prompts/tools/{grep.md → search.md} +4 -4
  50. package/src/sdk.ts +19 -9
  51. package/src/session/agent-session.ts +65 -0
  52. package/src/system-prompt.ts +15 -5
  53. package/src/task/executor.ts +5 -0
  54. package/src/task/index.ts +10 -1
  55. package/src/tools/ast-edit.ts +4 -6
  56. package/src/tools/ast-grep.ts +4 -6
  57. package/src/tools/bash.ts +1 -1
  58. package/src/tools/file-recorder.ts +6 -6
  59. package/src/tools/find.ts +11 -13
  60. package/src/tools/index.ts +7 -7
  61. package/src/tools/path-utils.ts +31 -4
  62. package/src/tools/read.ts +12 -6
  63. package/src/tools/renderers.ts +2 -2
  64. package/src/tools/{grep.ts → search.ts} +32 -40
  65. package/src/tools/write.ts +8 -4
  66. package/src/web/search/index.ts +1 -1
  67. package/src/edit/block.ts +0 -308
  68. package/src/edit/indent.ts +0 -150
@@ -50,9 +50,9 @@ Extracts content from web pages, GitHub issues/PRs, Stack Overflow, Wikipedia, R
50
50
  </instruction>
51
51
 
52
52
  <critical>
53
- - You **MUST** use `read` for all file, directory, archive, and URL reads; never cat/head/ls/tar/unzip/curl, etc.
54
- You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if this method fails to deliver reasonable content.
55
- - You **MUST** always include the `path` parameter.
56
- - For specific line ranges, use `sel`.
53
+ - You **MUST** use `read` for every file, directory, archive, and URL read. `cat`, `head`, `tail`, `less`, `more`, `ls`, `tar`, `unzip`, `curl`, and `wget` are **FORBIDDEN** for inspection — any such Bash call is a bug, regardless of how short or convenient it looks.
54
+ - You **MUST** prefer `read` over a browser/puppeteer tool for fetching URL content; only use a browser if `read` fails to deliver reasonable content.
55
+ - You **MUST** always include the `path` parameter — never call `read` with an empty argument object `{}`.
56
+ - For specific line ranges, use `sel` (e.g. `sel="50-200"`, `sel="50+150"`) — do **NOT** reach for `sed -n`, `awk NR`, or `head`/`tail` pipelines.
57
57
  - You **MAY** use `sel` with URL reads; the tool paginates cached fetched output.
58
58
  </critical>
@@ -1,9 +1,9 @@
1
1
  Performs string replacements in files with fuzzy whitespace matching.
2
2
 
3
3
  <instruction>
4
- - You **MUST** use the smallest edit that uniquely identifies the change
5
- - If `old_text` not unique, you **MUST** expand to include more context or use `all: true` to replace all occurrences
6
- - Fuzzy matching handles minor whitespace/indentation differences automatically
4
+ - Params **MUST** be `{ path, edits }`; `path` is required at the top level and applies to every replacement
5
+ - You **MUST** use the smallest `old_text` that uniquely identifies the change
6
+ - If `old_text` is not unique, you **MUST** expand it with more context or use `all: true` to replace all occurrences
7
7
  - You **SHOULD** prefer editing existing files over creating new ones
8
8
  </instruction>
9
9
 
@@ -17,8 +17,8 @@ Searches files using powerful regex matching.
17
17
  </output>
18
18
 
19
19
  <critical>
20
- - You **MUST** use the built-in Grep tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
21
- - Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The Grep tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
22
- - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the search through the Grep tool instead.
23
- - If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining Grep calls yourself.
20
+ - You **MUST** use the built-in `search` tool for any content search. Do **NOT** shell out to `grep`, `rg`, `ripgrep`, `ag`, `ack`, `git grep`, `awk`, `sed`-for-search, or any other CLI search via Bash — even for a single match, even "just to check quickly", even piped through other commands.
21
+ - Bash `grep`/`rg` loses `.gitignore` semantics, bypasses result limits, and wastes tokens. The `search` tool is faster, structured, and already wired into the workspace — there is no scenario where Bash search is preferable.
22
+ - If you catch yourself typing `grep`, `rg`, or `| grep` in a Bash command, stop and re-issue the lookup through the `search` tool instead.
23
+ - If the search is open-ended, requiring multiple rounds, you **MUST** use the Task tool with the explore subagent instead of chaining `search` calls yourself.
24
24
  </critical>
package/src/sdk.ts CHANGED
@@ -66,6 +66,7 @@ import {
66
66
  InternalUrlRouter,
67
67
  JobsProtocolHandler,
68
68
  LocalProtocolHandler,
69
+ type LocalProtocolOptions,
69
70
  McpProtocolHandler,
70
71
  MemoryProtocolHandler,
71
72
  PiProtocolHandler,
@@ -111,7 +112,6 @@ import {
111
112
  discoverStartupLspServers,
112
113
  EditTool,
113
114
  FindTool,
114
- GrepTool,
115
115
  getSearchTools,
116
116
  HIDDEN_TOOLS,
117
117
  isSearchProviderPreference,
@@ -121,10 +121,12 @@ import {
121
121
  ReadTool,
122
122
  ResolveTool,
123
123
  renderSearchToolBm25Description,
124
+ SearchTool,
124
125
  setPreferredImageProvider,
125
126
  setPreferredSearchProvider,
126
127
  type Tool,
127
128
  type ToolSession,
129
+ WebSearchTool,
128
130
  WriteTool,
129
131
  warmupLspServers,
130
132
  } from "./tools";
@@ -226,6 +228,9 @@ export interface CreateAgentSessionOptions {
226
228
  /** Session manager. Default: session stored under the configured agentDir sessions root */
227
229
  sessionManager?: SessionManager;
228
230
 
231
+ /** Override local:// protocol options for subagent local:// sharing. Default: uses the session's own artifacts dir and session ID. */
232
+ localProtocolOptions?: LocalProtocolOptions;
233
+
229
234
  /** Settings instance. Default: Settings.init({ cwd, agentDir }) */
230
235
  settings?: Settings;
231
236
 
@@ -271,13 +276,14 @@ export {
271
276
  createTools,
272
277
  EditTool,
273
278
  FindTool,
274
- GrepTool,
275
279
  HIDDEN_TOOLS,
276
280
  loadSshTool,
277
281
  PythonTool,
278
282
  ReadTool,
279
283
  ResolveTool,
284
+ SearchTool,
280
285
  type ToolSession,
286
+ WebSearchTool,
281
287
  WriteTool,
282
288
  };
283
289
 
@@ -996,10 +1002,12 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
996
1002
  }),
997
1003
  );
998
1004
  internalRouter.register(
999
- new LocalProtocolHandler({
1000
- getArtifactsDir,
1001
- getSessionId: () => sessionManager.getSessionId(),
1002
- }),
1005
+ new LocalProtocolHandler(
1006
+ options.localProtocolOptions ?? {
1007
+ getArtifactsDir,
1008
+ getSessionId: () => sessionManager.getSessionId(),
1009
+ },
1010
+ ),
1003
1011
  );
1004
1012
  internalRouter.register(
1005
1013
  new SkillProtocolHandler({
@@ -1386,7 +1394,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1386
1394
  ? requestedActiveToolNames
1387
1395
  : requestedActiveToolNames.filter(name => !defaultInactiveToolNames.has(name));
1388
1396
  const explicitlyRequestedMCPToolNames = options.toolNames
1389
- ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1397
+ ? requestedActiveToolNames.filter(name => name.startsWith("mcp__"))
1390
1398
  : [];
1391
1399
  const discoveryDefaultServers = new Set(
1392
1400
  (settings.get("mcp.discoveryDefaultServers") ?? []).map(serverName => serverName.trim()).filter(Boolean),
@@ -1412,7 +1420,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1412
1420
  : [...new Set([...restoredSelectedMCPToolNames, ...defaultSelectedMCPToolNames])];
1413
1421
  initialToolNames = [
1414
1422
  ...new Set([
1415
- ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp_")),
1423
+ ...initialRequestedActiveToolNames.filter(name => !name.startsWith("mcp__")),
1416
1424
  ...initialSelectedMCPToolNames,
1417
1425
  ]),
1418
1426
  ];
@@ -1424,7 +1432,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1424
1432
  ...registeredTools.filter(t => !t.definition.defaultInactive).map(t => t.definition.name),
1425
1433
  ];
1426
1434
  for (const name of alwaysInclude) {
1427
- if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
1435
+ if (mcpDiscoveryEnabled && name.startsWith("mcp__")) {
1428
1436
  continue;
1429
1437
  }
1430
1438
  if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
@@ -1601,6 +1609,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1601
1609
  ttsrManager,
1602
1610
  obfuscator,
1603
1611
  asyncJobManager,
1612
+ agentId: resolvedAgentId,
1613
+ agentRegistry,
1604
1614
  });
1605
1615
  hasSession = true;
1606
1616
 
@@ -130,6 +130,7 @@ import planModeToolDecisionReminderPrompt from "../prompts/system/plan-mode-tool
130
130
  type: "text",
131
131
  };
132
132
  import ttsrInterruptTemplate from "../prompts/system/ttsr-interrupt.md" with { type: "text" };
133
+ import { type AgentRegistry, MAIN_AGENT_ID } from "../registry/agent-registry";
133
134
  import { deobfuscateSessionContext, type SecretObfuscator } from "../secrets/obfuscator";
134
135
  import { resolveThinkingLevelForModel, toReasoningEffort } from "../thinking";
135
136
  import { assertEditableFile } from "../tools/auto-generated-guard";
@@ -263,6 +264,10 @@ export interface AgentSessionConfig {
263
264
  obfuscator?: SecretObfuscator;
264
265
  /** Logical owner for retained Python kernels created by this session. */
265
266
  pythonKernelOwnerId?: string;
267
+ /** Agent identity (registry id like "0-Main" or "3-Alice") used for IRC routing. */
268
+ agentId?: string;
269
+ /** Shared agent registry (for forwarding IRC observations to the main session UI). */
270
+ agentRegistry?: AgentRegistry;
266
271
  }
267
272
 
268
273
  /** Options for AgentSession.prompt() */
@@ -478,6 +483,9 @@ export class AgentSession {
478
483
  // Drained into history (via emitExternalEvent) once the recipient becomes idle.
479
484
  #pendingBackgroundExchanges: CustomMessage[][] = [];
480
485
  #scheduledBackgroundExchangeFlush = false;
486
+ // Agent identity + registry for IRC relay forwarding to the main session UI.
487
+ #agentId: string | undefined;
488
+ #agentRegistry: AgentRegistry | undefined;
481
489
  // Extension system
482
490
  #extensionRunner: ExtensionRunner | undefined = undefined;
483
491
  #turnIndex = 0;
@@ -611,6 +619,8 @@ export class AgentSession {
611
619
  );
612
620
  this.#ttsrManager = config.ttsrManager;
613
621
  this.#obfuscator = config.obfuscator;
622
+ this.#agentId = config.agentId;
623
+ this.#agentRegistry = config.agentRegistry;
614
624
  this.agent.setAssistantMessageEventInterceptor((message, assistantMessageEvent) => {
615
625
  const event: AgentEvent = {
616
626
  type: "message_update",
@@ -5999,6 +6009,13 @@ export class AgentSession {
5999
6009
  timestamp: incomingTimestamp,
6000
6010
  };
6001
6011
  void this.#emitSessionEvent({ type: "irc_message", message: incomingRecord });
6012
+ this.#forwardIrcRelayToMain({
6013
+ from: args.from,
6014
+ to: this.#agentId ?? "?",
6015
+ body: args.message,
6016
+ kind: "message",
6017
+ timestamp: incomingTimestamp,
6018
+ });
6002
6019
 
6003
6020
  if (!awaitReply) {
6004
6021
  this.#queueBackgroundExchangeInjection([incomingRecord]);
@@ -6024,11 +6041,59 @@ export class AgentSession {
6024
6041
  timestamp: Date.now(),
6025
6042
  };
6026
6043
  void this.#emitSessionEvent({ type: "irc_message", message: replyRecord });
6044
+ this.#forwardIrcRelayToMain({
6045
+ from: this.#agentId ?? "?",
6046
+ to: args.from,
6047
+ body: replyText,
6048
+ kind: "reply",
6049
+ timestamp: replyRecord.timestamp,
6050
+ });
6027
6051
  this.#queueBackgroundExchangeInjection([incomingRecord, replyRecord]);
6028
6052
 
6029
6053
  return { replyText };
6030
6054
  }
6031
6055
 
6056
+ /**
6057
+ * Forward an IRC exchange observation to the main agent's session UI so the
6058
+ * user can see every IRC conversation in the main transcript, even when the
6059
+ * main agent is not a direct participant. The relay record is display-only:
6060
+ * it is NOT injected into the main agent's persisted history.
6061
+ */
6062
+ #forwardIrcRelayToMain(args: {
6063
+ from: string;
6064
+ to: string;
6065
+ body: string;
6066
+ kind: "message" | "reply";
6067
+ timestamp: number;
6068
+ }): void {
6069
+ const registry = this.#agentRegistry;
6070
+ if (!registry) return;
6071
+ // If this session is the main agent, the local emit already reached the main UI.
6072
+ if (this.#agentId === MAIN_AGENT_ID) return;
6073
+ const mainRef = registry.get(MAIN_AGENT_ID);
6074
+ const mainSession = mainRef?.session;
6075
+ if (!mainSession || mainSession === this) return;
6076
+ const arrow = args.kind === "reply" ? "\u2192 (auto)" : "\u2192";
6077
+ const relayRecord: CustomMessage = {
6078
+ role: "custom",
6079
+ customType: "irc:relay",
6080
+ content: `[IRC \`${args.from}\` ${arrow} \`${args.to}\`]\n\n${args.body}`,
6081
+ display: true,
6082
+ details: { from: args.from, to: args.to, body: args.body, kind: args.kind },
6083
+ attribution: "agent",
6084
+ timestamp: args.timestamp,
6085
+ };
6086
+ mainSession.emitIrcRelayObservation(relayRecord);
6087
+ }
6088
+
6089
+ /**
6090
+ * Emit an IRC relay observation event on this session for UI rendering only.
6091
+ * Does not persist the record to history. Public so other sessions can forward.
6092
+ */
6093
+ emitIrcRelayObservation(record: CustomMessage): void {
6094
+ void this.#emitSessionEvent({ type: "irc_message", message: record });
6095
+ }
6096
+
6032
6097
  /**
6033
6098
  * Run a single ephemeral side-channel turn against this session's current
6034
6099
  * model + system prompt + history. No tools are used; the side request
@@ -384,6 +384,8 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
384
384
  export interface SystemPromptToolMetadata {
385
385
  label: string;
386
386
  description: string;
387
+ /** Tool name the model sees on the provider wire. Defaults to the internal tool name. */
388
+ wireName?: string;
387
389
  }
388
390
 
389
391
  export function buildSystemPromptToolMetadata(
@@ -394,12 +396,16 @@ export function buildSystemPromptToolMetadata(
394
396
  Array.from(tools.entries(), ([name, tool]) => {
395
397
  const toolRecord = tool as AgentTool & { label?: string; description?: string };
396
398
  const override = overrides[name];
399
+ const wireName =
400
+ override?.wireName ??
401
+ (typeof toolRecord.customWireName === "string" ? toolRecord.customWireName : undefined);
397
402
  return [
398
403
  name,
399
404
  {
400
405
  label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
401
406
  description:
402
407
  override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
408
+ wireName,
403
409
  },
404
410
  ] as const;
405
411
  }),
@@ -570,14 +576,17 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
570
576
  }
571
577
  }
572
578
 
573
- // Build tool descriptions for system prompt rendering
579
+ // Build tool descriptions for system prompt rendering.
580
+ const toolPromptNames = new Map<string, string>(toolNames.map(name => [name, tools?.get(name)?.wireName ?? name]));
581
+ const toolRefs = Object.fromEntries(toolPromptNames.entries());
574
582
  const toolInfo = toolNames.map(name => ({
575
- name,
583
+ name: toolPromptNames.get(name) ?? name,
584
+ internalName: name,
576
585
  label: tools?.get(name)?.label ?? "",
577
586
  description: tools?.get(name)?.description ?? "",
578
587
  }));
579
588
 
580
- // Filter skills to only include those with read tool
589
+ // Filter skills to only include those with read tool.
581
590
  const hasRead = tools?.has("read");
582
591
  const filteredSkills = hasRead ? skills : [];
583
592
 
@@ -589,6 +598,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
589
598
  const injectedAlwaysApplyRules = dedupeAlwaysApplyRules(alwaysApplyRules, promptSources);
590
599
 
591
600
  const environment = await logger.time("getEnvironmentInfo", getEnvironmentInfo);
601
+ const reportToolIssueToolName = toolPromptNames.get("report_tool_issue") ?? "report_tool_issue";
592
602
  const data = {
593
603
  systemPromptCustomization: effectiveSystemPromptCustomization,
594
604
  customPrompt: resolvedCustomPrompt,
@@ -596,6 +606,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
596
606
  tools: toolNames,
597
607
  toolInfo,
598
608
  repeatToolDescriptions,
609
+ toolRefs,
599
610
  environment,
600
611
  contextFiles,
601
612
  agentsMdSearch,
@@ -617,8 +628,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
617
628
 
618
629
  // When autoqa is active the report_tool_issue tool is in the tool set — nudge the agent.
619
630
  if (toolNames.includes("report_tool_issue")) {
620
- rendered +=
621
- "\n\n<critical>\nThe `report_tool_issue` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call `report_tool_issue` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>";
631
+ rendered += `\n\n<critical>\nThe \`${reportToolIssueToolName}\` tool is available for automated QA. If ANY tool you call returns output that is unexpected, incorrect, malformed, or otherwise inconsistent with what you anticipated given the tool's described behavior and your parameters, call \`${reportToolIssueToolName}\` with the tool name and a concise description of the discrepancy. Do not hesitate to report — false positives are acceptable.\n</critical>`;
622
632
  }
623
633
 
624
634
  return rendered;
@@ -3,6 +3,7 @@
3
3
  *
4
4
  * Runs each subagent on the main thread and forwards AgentEvents for progress tracking.
5
5
  */
6
+
6
7
  import path from "node:path";
7
8
  import type { AgentEvent, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
9
  import { logger, prompt, untilAborted } from "@oh-my-pi/pi-utils";
@@ -16,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
16
17
  import type { CustomTool } from "../extensibility/custom-tools/types";
17
18
  import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
18
19
  import type { Skill } from "../extensibility/skills";
20
+ import type { LocalProtocolOptions } from "../internal-urls";
19
21
  import { callTool } from "../mcp/client";
20
22
  import type { MCPManager } from "../mcp/manager";
21
23
  import subagentSystemPromptTemplate from "../prompts/system/subagent-system-prompt.md" with { type: "text" };
@@ -159,6 +161,8 @@ export interface ExecutorOptions {
159
161
  authStorage?: AuthStorage;
160
162
  modelRegistry?: ModelRegistry;
161
163
  settings?: Settings;
164
+ /** Override local:// protocol options so subagent shares parent's local:// root */
165
+ localProtocolOptions?: LocalProtocolOptions;
162
166
  }
163
167
 
164
168
  function parseStringifiedJson(value: unknown): unknown {
@@ -987,6 +991,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
987
991
  enableMCP,
988
992
  mcpManager: options.mcpManager,
989
993
  customTools: mcpProxyTools.length > 0 ? mcpProxyTools : undefined,
994
+ localProtocolOptions: options.localProtocolOptions,
990
995
  });
991
996
 
992
997
  activeSession = session;
package/src/task/index.ts CHANGED
@@ -28,6 +28,7 @@ import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type:
28
28
  import { formatBytes, formatDuration } from "../tools/render-utils";
29
29
  // Import review tools for side effects (registers subagent tool handlers)
30
30
  import "../tools/review";
31
+ import type { LocalProtocolOptions } from "../internal-urls";
31
32
  import { generateCommitMessage } from "../utils/commit-message-generator";
32
33
  import * as git from "../utils/git";
33
34
  import { discoverAgents, getAgent } from "./discovery";
@@ -567,7 +568,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
567
568
  }
568
569
 
569
570
  const planModeState = this.session.getPlanModeState?.();
570
- const planModeTools = ["read", "grep", "find", "ls", "lsp", "web_search"];
571
+ const planModeTools = ["read", "search", "find", "lsp", "web_search"];
571
572
  const effectiveAgent: typeof agent = planModeState?.enabled
572
573
  ? {
573
574
  ...agent,
@@ -715,6 +716,12 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
715
716
  const tempArtifactsDir = artifactsDir ? null : path.join(os.tmpdir(), `omp-task-${Snowflake.next()}`);
716
717
  const effectiveArtifactsDir = artifactsDir || tempArtifactsDir!;
717
718
 
719
+ // Share the parent session's local:// root with subagents so they read/write the same scratch space
720
+ const localProtocolOptions: LocalProtocolOptions = {
721
+ getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
722
+ getSessionId: this.session.getSessionId ?? (() => null),
723
+ };
724
+
718
725
  // Initialize progress tracking
719
726
  const progressMap = new Map<number, AgentProgress>();
720
727
 
@@ -856,6 +863,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
856
863
  contextFiles,
857
864
  skills: availableSkills,
858
865
  promptTemplates,
866
+ localProtocolOptions,
859
867
  });
860
868
  }
861
869
 
@@ -909,6 +917,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
909
917
  contextFiles,
910
918
  skills: availableSkills,
911
919
  promptTemplates,
920
+ localProtocolOptions,
912
921
  });
913
922
  if (mergeMode === "branch" && result.exitCode === 0) {
914
923
  try {
@@ -1,4 +1,3 @@
1
- import * as path from "node:path";
2
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
2
  import { type AstReplaceChange, astEdit } from "@oh-my-pi/pi-natives";
4
3
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -16,6 +15,7 @@ import { createFileRecorder, formatResultPath } from "./file-recorder";
16
15
  import { formatGroupedFiles } from "./grouped-file-output";
17
16
  import type { OutputMeta } from "./output-meta";
18
17
  import {
18
+ formatPathRelativeToCwd,
19
19
  hasGlobPathChars,
20
20
  normalizePathLikeInput,
21
21
  parseSearchPath,
@@ -106,10 +106,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
106
106
  const normalizedRewrites = Object.fromEntries(ops);
107
107
  const maxFiles = $envpos("PI_MAX_AST_FILES", 1000);
108
108
 
109
- const formatScopePath = (targetPath: string): string => {
110
- const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
111
- return relative.length === 0 ? "." : relative;
112
- };
109
+ const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
113
110
  let searchPath: string | undefined;
114
111
  let scopePath: string | undefined;
115
112
  let globFilter: string | undefined;
@@ -164,7 +161,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
164
161
  });
165
162
 
166
163
  const dedupedParseErrors = dedupeParseErrors(result.parseErrors);
167
- const formatPath = (filePath: string): string => formatResultPath(filePath, isDirectory);
164
+ const formatPath = (filePath: string): string =>
165
+ formatResultPath(filePath, isDirectory, resolvedSearchPath, this.session.cwd);
168
166
 
169
167
  const { record: recordFile, list: fileList } = createFileRecorder();
170
168
  const fileReplacementCounts = new Map<string, number>();
@@ -1,4 +1,3 @@
1
- import * as path from "node:path";
2
1
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
3
2
  import { type AstFindMatch, astGrep } from "@oh-my-pi/pi-natives";
4
3
  import type { Component } from "@oh-my-pi/pi-tui";
@@ -16,6 +15,7 @@ import { formatGroupedFiles } from "./grouped-file-output";
16
15
  import { formatMatchLine } from "./match-line-format";
17
16
  import type { OutputMeta } from "./output-meta";
18
17
  import {
18
+ formatPathRelativeToCwd,
19
19
  hasGlobPathChars,
20
20
  normalizePathLikeInput,
21
21
  parseSearchPath,
@@ -87,10 +87,7 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
87
87
  if (!Number.isFinite(skip) || skip < 0) {
88
88
  throw new ToolError("skip must be a non-negative number");
89
89
  }
90
- const formatScopePath = (targetPath: string): string => {
91
- const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
92
- return relative.length === 0 ? "." : relative;
93
- };
90
+ const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
94
91
  let searchPath: string | undefined;
95
92
  let scopePath: string | undefined;
96
93
  let globFilter: string | undefined;
@@ -147,7 +144,8 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
147
144
  return parseError?.[1] ?? error;
148
145
  });
149
146
  const dedupedParseErrors = dedupeParseErrors(normalizedParseErrors);
150
- const formatPath = (filePath: string): string => formatResultPath(filePath, isDirectory);
147
+ const formatPath = (filePath: string): string =>
148
+ formatResultPath(filePath, isDirectory, resolvedSearchPath, this.session.cwd);
151
149
 
152
150
  const { record: recordFile, list: fileList } = createFileRecorder();
153
151
  const fileMatchCounts = new Map<string, number>();
package/src/tools/bash.ts CHANGED
@@ -269,7 +269,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
269
269
  autoBackgroundThresholdSeconds: Math.max(0, Math.floor(this.#autoBackgroundThresholdMs / 1000)),
270
270
  hasAstGrep: this.session.settings.get("astGrep.enabled"),
271
271
  hasAstEdit: this.session.settings.get("astEdit.enabled"),
272
- hasGrep: this.session.settings.get("grep.enabled"),
272
+ hasSearch: this.session.settings.get("search.enabled"),
273
273
  hasFind: this.session.settings.get("find.enabled"),
274
274
  });
275
275
  }
@@ -1,4 +1,5 @@
1
1
  import * as path from "node:path";
2
+ import { formatPathRelativeToCwd } from "./path-utils";
2
3
 
3
4
  /**
4
5
  * Creates a deduplicating recorder for relative file paths.
@@ -22,14 +23,13 @@ export function createFileRecorder(): {
22
23
  }
23
24
 
24
25
  /**
25
- * Strip a leading slash and, when the search scope is a directory, normalize
26
- * Windows-style separators. For single-file scopes, fall back to the basename
27
- * so tool output does not leak absolute paths.
26
+ * Strip native virtual-root prefixes and format file paths relative to cwd when
27
+ * they are inside cwd. Paths outside cwd remain absolute.
28
28
  */
29
- export function formatResultPath(filePath: string, isDirectory: boolean): string {
29
+ export function formatResultPath(filePath: string, isDirectory: boolean, basePath: string, cwd: string): string {
30
30
  const cleanPath = filePath.startsWith("/") ? filePath.slice(1) : filePath;
31
31
  if (isDirectory) {
32
- return cleanPath.replace(/\\/g, "/");
32
+ return formatPathRelativeToCwd(path.resolve(basePath, cleanPath), cwd);
33
33
  }
34
- return path.basename(cleanPath);
34
+ return formatPathRelativeToCwd(basePath, cwd);
35
35
  }
package/src/tools/find.ts CHANGED
@@ -23,7 +23,13 @@ import {
23
23
  import type { ToolSession } from ".";
24
24
  import { applyListLimit } from "./list-limit";
25
25
  import { formatFullOutputReference, type OutputMeta } from "./output-meta";
26
- import { normalizePathLikeInput, parseFindPattern, resolveMultiFindPattern, resolveToCwd } from "./path-utils";
26
+ import {
27
+ formatPathRelativeToCwd,
28
+ normalizePathLikeInput,
29
+ parseFindPattern,
30
+ resolveMultiFindPattern,
31
+ resolveToCwd,
32
+ } from "./path-utils";
27
33
  import { formatCount, formatEmptyMessage, formatErrorMessage, PREVIEW_LIMITS } from "./render-utils";
28
34
  import { ToolAbortError, ToolError, throwIfAborted } from "./tool-errors";
29
35
  import { toolResult } from "./tool-result";
@@ -101,10 +107,7 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
101
107
  const { pattern, limit, hidden } = params;
102
108
 
103
109
  return untilAborted(signal, async () => {
104
- const formatScopePath = (targetPath: string): string => {
105
- const relative = path.relative(this.session.cwd, targetPath).replace(/\\/g, "/");
106
- return relative.length === 0 ? "." : relative;
107
- };
110
+ const formatScopePath = (targetPath: string): string => formatPathRelativeToCwd(targetPath, this.session.cwd);
108
111
  const normalizedPattern = normalizePathLikeInput(pattern).replace(/\\/g, "/");
109
112
  if (!normalizedPattern) {
110
113
  throw new ToolError("Pattern must not be empty");
@@ -132,14 +135,9 @@ export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
132
135
  const formatMatchPath = (matchPath: string, fileType?: natives.FileType): string => {
133
136
  const hadTrailingSlash = matchPath.endsWith("/") || matchPath.endsWith("\\");
134
137
  const absolutePath = path.isAbsolute(matchPath) ? matchPath : path.resolve(searchPath, matchPath);
135
- let relativePath = path.relative(this.session.cwd, absolutePath).replace(/\\/g, "/");
136
- if (relativePath.length === 0) {
137
- relativePath = ".";
138
- }
139
- if ((fileType === natives.FileType.Dir || hadTrailingSlash) && !relativePath.endsWith("/")) {
140
- relativePath += "/";
141
- }
142
- return relativePath;
138
+ return formatPathRelativeToCwd(absolutePath, this.session.cwd, {
139
+ trailingSlash: fileType === natives.FileType.Dir || hadTrailingSlash,
140
+ });
143
141
  };
144
142
 
145
143
  const buildResult = (files: string[]): AgentToolResult<FindToolDetails> => {
@@ -18,7 +18,7 @@ import type { ToolChoiceQueue } from "../session/tool-choice-queue";
18
18
  import { TaskTool } from "../task";
19
19
  import type { AgentOutputManager } from "../task/output-manager";
20
20
  import type { EventBus } from "../utils/event-bus";
21
- import { SearchTool } from "../web/search";
21
+ import { WebSearchTool } from "../web/search";
22
22
  import { AskTool } from "./ask";
23
23
  import { AstEditTool } from "./ast-edit";
24
24
  import { AstGrepTool } from "./ast-grep";
@@ -30,7 +30,6 @@ import { DebugTool } from "./debug";
30
30
  import { ExitPlanModeTool } from "./exit-plan-mode";
31
31
  import { FindTool } from "./find";
32
32
  import { GithubTool } from "./gh";
33
- import { GrepTool } from "./grep";
34
33
  import { InspectImageTool } from "./inspect-image";
35
34
  import { IrcTool } from "./irc";
36
35
  import { JobTool } from "./job";
@@ -42,6 +41,7 @@ import { RenderMermaidTool } from "./render-mermaid";
42
41
  import { createReportToolIssueTool, isAutoQaEnabled } from "./report-tool-issue";
43
42
  import { ResolveTool } from "./resolve";
44
43
  import { reportFindingTool } from "./review";
44
+ import { SearchTool } from "./search";
45
45
  import { SearchToolBm25Tool } from "./search-tool-bm25";
46
46
  import { loadSshTool } from "./ssh";
47
47
  import { type TodoPhase, TodoWriteTool } from "./todo-write";
@@ -68,7 +68,6 @@ export * from "./debug";
68
68
  export * from "./exit-plan-mode";
69
69
  export * from "./find";
70
70
  export * from "./gh";
71
- export * from "./grep";
72
71
  export * from "./image-gen";
73
72
  export * from "./inspect-image";
74
73
  export * from "./irc";
@@ -80,6 +79,7 @@ export * from "./render-mermaid";
80
79
  export * from "./report-tool-issue";
81
80
  export * from "./resolve";
82
81
  export * from "./review";
82
+ export * from "./search";
83
83
  export * from "./search-tool-bm25";
84
84
  export * from "./ssh";
85
85
  export * from "./todo-write";
@@ -214,7 +214,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
214
214
  edit: s => new EditTool(s),
215
215
  github: GithubTool.createIf,
216
216
  find: s => new FindTool(s),
217
- grep: s => new GrepTool(s),
217
+ search: s => new SearchTool(s),
218
218
  lsp: LspTool.createIf,
219
219
  notebook: s => new NotebookTool(s),
220
220
  read: s => new ReadTool(s),
@@ -226,7 +226,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
226
226
  job: JobTool.createIf,
227
227
  irc: IrcTool.createIf,
228
228
  todo_write: s => new TodoWriteTool(s),
229
- web_search: s => new SearchTool(s),
229
+ web_search: s => new WebSearchTool(s),
230
230
  search_tool_bm25: SearchToolBm25Tool.createIf,
231
231
  write: s => new WriteTool(s),
232
232
  };
@@ -357,7 +357,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
357
357
  // Auto-include AST counterparts when their text-based sibling is present
358
358
  if (requestedTools) {
359
359
  if (
360
- requestedTools.includes("grep") &&
360
+ requestedTools.includes("search") &&
361
361
  !requestedTools.includes("ast_grep") &&
362
362
  session.settings.get("astGrep.enabled")
363
363
  ) {
@@ -379,7 +379,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
379
379
  if (name === "debug") return session.settings.get("debug.enabled");
380
380
  if (name === "todo_write") return !includeYield && session.settings.get("todo.enabled");
381
381
  if (name === "find") return session.settings.get("find.enabled");
382
- if (name === "grep") return session.settings.get("grep.enabled");
382
+ if (name === "search") return session.settings.get("search.enabled");
383
383
  if (name === "github") return session.settings.get("github.enabled");
384
384
  if (name === "ast_grep") return session.settings.get("astGrep.enabled");
385
385
  if (name === "ast_edit") return session.settings.get("astEdit.enabled");