@oh-my-pi/pi-coding-agent 14.9.2 → 14.9.5

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 (97) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/package.json +7 -7
  3. package/scripts/format-prompts.ts +3 -3
  4. package/src/async/job-manager.ts +66 -9
  5. package/src/capability/rule.ts +20 -0
  6. package/src/config/model-registry.ts +13 -0
  7. package/src/config/model-resolver.ts +8 -2
  8. package/src/config/prompt-templates.ts +0 -5
  9. package/src/config/settings-schema.ts +39 -1
  10. package/src/edit/index.ts +8 -0
  11. package/src/edit/renderer.ts +6 -1
  12. package/src/edit/streaming.ts +53 -2
  13. package/src/eval/eval.lark +10 -31
  14. package/src/eval/index.ts +1 -0
  15. package/src/eval/js/context-manager.ts +1 -38
  16. package/src/eval/js/prelude.txt +0 -2
  17. package/src/eval/parse.ts +156 -255
  18. package/src/eval/py/executor.ts +24 -8
  19. package/src/eval/py/index.ts +1 -0
  20. package/src/eval/py/prelude.py +11 -80
  21. package/src/eval/sniff.ts +28 -0
  22. package/src/export/html/template.css +50 -0
  23. package/src/export/html/template.generated.ts +1 -1
  24. package/src/export/html/template.js +229 -17
  25. package/src/extensibility/plugins/loader.ts +31 -6
  26. package/src/extensibility/skills.ts +20 -0
  27. package/src/hashline/constants.ts +20 -0
  28. package/src/hashline/grammar.lark +16 -23
  29. package/src/hashline/hash.ts +4 -34
  30. package/src/hashline/input.ts +16 -2
  31. package/src/hashline/parser.ts +12 -1
  32. package/src/internal-urls/agent-protocol.ts +64 -52
  33. package/src/internal-urls/artifact-protocol.ts +52 -51
  34. package/src/internal-urls/docs-index.generated.ts +34 -1
  35. package/src/internal-urls/index.ts +6 -19
  36. package/src/internal-urls/local-protocol.ts +50 -7
  37. package/src/internal-urls/mcp-protocol.ts +3 -8
  38. package/src/internal-urls/memory-protocol.ts +90 -59
  39. package/src/internal-urls/pi-protocol.ts +1 -0
  40. package/src/internal-urls/router.ts +40 -23
  41. package/src/internal-urls/rule-protocol.ts +3 -20
  42. package/src/internal-urls/skill-protocol.ts +5 -27
  43. package/src/internal-urls/types.ts +18 -2
  44. package/src/main.ts +1 -1
  45. package/src/mcp/manager.ts +17 -0
  46. package/src/modes/components/session-observer-overlay.ts +2 -2
  47. package/src/modes/components/tool-execution.ts +6 -0
  48. package/src/modes/components/tree-selector.ts +4 -0
  49. package/src/modes/controllers/event-controller.ts +23 -2
  50. package/src/modes/controllers/mcp-command-controller.ts +7 -10
  51. package/src/modes/interactive-mode.ts +2 -2
  52. package/src/modes/theme/theme.ts +27 -27
  53. package/src/modes/types.ts +1 -1
  54. package/src/modes/utils/ui-helpers.ts +14 -9
  55. package/src/prompts/commands/orchestrate.md +1 -0
  56. package/src/prompts/system/custom-system-prompt.md +0 -2
  57. package/src/prompts/system/project-prompt.md +10 -0
  58. package/src/prompts/system/subagent-system-prompt.md +18 -9
  59. package/src/prompts/system/subagent-user-prompt.md +1 -10
  60. package/src/prompts/system/system-prompt.md +159 -232
  61. package/src/prompts/tools/ask.md +0 -1
  62. package/src/prompts/tools/bash.md +0 -34
  63. package/src/prompts/tools/eval.md +27 -16
  64. package/src/prompts/tools/github.md +6 -5
  65. package/src/prompts/tools/hashline.md +1 -0
  66. package/src/prompts/tools/job.md +14 -6
  67. package/src/prompts/tools/task.md +20 -3
  68. package/src/registry/agent-registry.ts +2 -1
  69. package/src/sdk.ts +87 -89
  70. package/src/session/agent-session.ts +107 -37
  71. package/src/session/artifacts.ts +7 -4
  72. package/src/session/session-manager.ts +30 -1
  73. package/src/ssh/connection-manager.ts +32 -16
  74. package/src/ssh/sshfs-mount.ts +10 -7
  75. package/src/system-prompt.ts +3 -9
  76. package/src/task/executor.ts +23 -7
  77. package/src/task/index.ts +57 -36
  78. package/src/tool-discovery/tool-index.ts +21 -8
  79. package/src/tools/ast-edit.ts +3 -2
  80. package/src/tools/ast-grep.ts +3 -2
  81. package/src/tools/bash.ts +30 -50
  82. package/src/tools/browser/tab-supervisor.ts +12 -2
  83. package/src/tools/eval.ts +59 -44
  84. package/src/tools/fetch.ts +1 -1
  85. package/src/tools/gh.ts +140 -4
  86. package/src/tools/index.ts +12 -11
  87. package/src/tools/job.ts +48 -12
  88. package/src/tools/path-utils.ts +21 -1
  89. package/src/tools/read.ts +74 -31
  90. package/src/tools/search.ts +16 -3
  91. package/src/tools/todo-write.ts +1 -1
  92. package/src/utils/file-display-mode.ts +11 -5
  93. package/src/web/scrapers/mastodon.ts +1 -1
  94. package/src/web/scrapers/repology.ts +7 -7
  95. package/src/internal-urls/jobs-protocol.ts +0 -119
  96. package/src/task/template.ts +0 -47
  97. package/src/tools/bash-normalize.ts +0 -107
@@ -26,9 +26,11 @@ import submitReminderTemplate from "../prompts/system/subagent-yield-reminder.md
26
26
  import { AgentRegistry } from "../registry/agent-registry";
27
27
  import { createAgentSession, discoverAuthStorage } from "../sdk";
28
28
  import type { AgentSession, AgentSessionEvent } from "../session/agent-session";
29
+ import type { ArtifactManager } from "../session/artifacts";
29
30
  import type { AuthStorage } from "../session/auth-storage";
30
31
  import { SessionManager } from "../session/session-manager";
31
- import { type ContextFileEntry, truncateTail } from "../tools";
32
+ import { truncateTail } from "../session/streaming-output";
33
+ import type { ContextFileEntry } from "../tools";
32
34
  import { jtdToJsonSchema, normalizeSchema } from "../tools/jtd-to-json-schema";
33
35
  import { ToolAbortError } from "../tools/tool-errors";
34
36
  import type { EventBus } from "../utils/event-bus";
@@ -139,6 +141,7 @@ export interface ExecutorOptions {
139
141
  agent: AgentDefinition;
140
142
  task: string;
141
143
  assignment?: string;
144
+ context?: string;
142
145
  description?: string;
143
146
  index: number;
144
147
  id: string;
@@ -171,6 +174,12 @@ export interface ExecutorOptions {
171
174
  settings?: Settings;
172
175
  /** Override local:// protocol options so subagent shares parent's local:// root */
173
176
  localProtocolOptions?: LocalProtocolOptions;
177
+ /**
178
+ * Parent session's ArtifactManager. Subagent adopts it so artifact IDs are
179
+ * unique across the whole agent tree and all artifacts land in the parent's
180
+ * artifacts directory (no per-subagent subdir).
181
+ */
182
+ parentArtifactManager?: ArtifactManager;
174
183
  parentHindsightSessionState?: HindsightSessionState;
175
184
  }
176
185
 
@@ -562,6 +571,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
562
571
 
563
572
  const lspEnabled = enableLsp ?? true;
564
573
  const ircEnabled = subagentSettings.get("irc.enabled") === true;
574
+ const contextFileForPrompt = ircEnabled ? undefined : options.contextFile;
565
575
  const skipPythonPreflight = Array.isArray(toolNames) && !toolNames.includes("eval");
566
576
 
567
577
  const outputChunks: string[] = [];
@@ -974,6 +984,9 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
974
984
  const sessionManager = sessionFile
975
985
  ? await SessionManager.open(sessionFile)
976
986
  : SessionManager.inMemory(worktree ?? cwd);
987
+ if (options.parentArtifactManager) {
988
+ sessionManager.adoptArtifactManager(options.parentArtifactManager);
989
+ }
977
990
 
978
991
  const mcpProxyTools = options.mcpManager ? createMCPProxyTools(options.mcpManager) : [];
979
992
  const enableMCP = !options.mcpManager;
@@ -994,17 +1007,20 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
994
1007
  skills: options.skills,
995
1008
  promptTemplates: options.promptTemplates,
996
1009
  workspaceTree: options.workspaceTree,
997
- systemPrompt: defaultPrompt => [
998
- prompt.render(subagentSystemPromptTemplate, {
999
- base: defaultPrompt.join("\n\n"),
1010
+ systemPrompt: defaultPrompt => {
1011
+ const subagentPrompt = prompt.render(subagentSystemPromptTemplate, {
1000
1012
  agent: agent.systemPrompt,
1013
+ context: options.context?.trim() ?? "",
1001
1014
  worktree: worktree ?? "",
1002
1015
  outputSchema: normalizedOutputSchema,
1003
- contextFile: options.contextFile,
1016
+ contextFile: contextFileForPrompt,
1004
1017
  ircPeers: ircEnabled ? renderIrcPeerRoster(id) : "",
1005
1018
  ircSelfId: ircEnabled ? id : "",
1006
- }),
1007
- ],
1019
+ });
1020
+ return defaultPrompt.length === 0
1021
+ ? [subagentPrompt]
1022
+ : [...defaultPrompt.slice(0, -1), subagentPrompt, defaultPrompt[defaultPrompt.length - 1]];
1023
+ },
1008
1024
  sessionManager,
1009
1025
  hasUI: false,
1010
1026
  spawns: spawnsEnv,
package/src/task/index.ts CHANGED
@@ -20,9 +20,12 @@ import type { Usage } from "@oh-my-pi/pi-ai";
20
20
  import { $env, prompt, Snowflake } from "@oh-my-pi/pi-utils";
21
21
  import type { TSchema } from "@sinclair/typebox";
22
22
  import type { ToolSession } from "..";
23
+ import { AsyncJobManager } from "../async";
23
24
  import { resolveAgentModelPatterns } from "../config/model-resolver";
25
+ import { MCPManager } from "../mcp/manager";
24
26
  import type { Theme } from "../modes/theme/theme";
25
27
  import planModeSubagentPrompt from "../prompts/system/plan-mode-subagent.md" with { type: "text" };
28
+ import subagentUserPromptTemplate from "../prompts/system/subagent-user-prompt.md" with { type: "text" };
26
29
  import taskDescriptionTemplate from "../prompts/tools/task.md" with { type: "text" };
27
30
  import taskSummaryTemplate from "../prompts/tools/task-summary.md" with { type: "text" };
28
31
  import { formatBytes, formatDuration } from "../tools/render-utils";
@@ -38,7 +41,6 @@ import { AgentOutputManager } from "./output-manager";
38
41
  import { mapWithConcurrencyLimit, Semaphore } from "./parallel";
39
42
  import { renderResult, renderCall as renderTaskCall } from "./render";
40
43
  import { getTaskSimpleModeCapabilities, type TaskSimpleMode } from "./simple-mode";
41
- import { renderTemplate } from "./template";
42
44
  import {
43
45
  type AgentDefinition,
44
46
  type AgentProgress,
@@ -65,6 +67,12 @@ import {
65
67
  type WorktreeBaseline,
66
68
  } from "./worktree";
67
69
 
70
+ function renderSubagentUserPrompt(assignment: string, simpleMode: TaskSimpleMode): string {
71
+ return prompt.render(subagentUserPromptTemplate, {
72
+ assignment: assignment.trim(),
73
+ independentMode: simpleMode === "independent",
74
+ });
75
+ }
68
76
  function createUsageTotals(): Usage {
69
77
  return {
70
78
  input: 0,
@@ -135,6 +143,7 @@ function renderDescription(
135
143
  asyncEnabled: boolean,
136
144
  disabledAgents: string[],
137
145
  simpleMode: TaskSimpleMode,
146
+ ircEnabled: boolean,
138
147
  ): string {
139
148
  const filteredAgents = disabledAgents.length > 0 ? agents.filter(a => !disabledAgents.includes(a.name)) : agents;
140
149
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
@@ -145,6 +154,7 @@ function renderDescription(
145
154
  asyncEnabled,
146
155
  contextEnabled,
147
156
  customSchemaEnabled,
157
+ ircEnabled,
148
158
  defaultMode: simpleMode === "default",
149
159
  schemaFreeMode: simpleMode === "schema-free",
150
160
  independentMode: simpleMode === "independent",
@@ -223,6 +233,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
223
233
  this.session.settings.get("async.enabled"),
224
234
  disabledAgents,
225
235
  this.#getTaskSimpleMode(),
236
+ this.session.settings.get("irc.enabled") === true,
226
237
  );
227
238
  }
228
239
  private constructor(
@@ -264,7 +275,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
264
275
  return this.#executeSync(_toolCallId, params, signal, onUpdate);
265
276
  }
266
277
 
267
- const manager = this.session.asyncJobManager;
278
+ const manager = AsyncJobManager.instance();
268
279
  if (!manager) {
269
280
  return {
270
281
  content: [{ type: "text", text: "Async execution is enabled but no async job manager is available." }],
@@ -282,21 +293,19 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
282
293
  const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
283
294
  const fallbackAgentSource =
284
295
  this.#discoveredAgents.find(agent => agent.name === params.agent)?.source ?? "bundled";
285
- const { contextEnabled } = getTaskSimpleModeCapabilities(simpleMode);
286
- const sharedContext = contextEnabled ? params.context : undefined;
287
- const renderedTasks = taskItems.map(taskItem => renderTemplate(sharedContext, taskItem, simpleMode));
288
296
  const progressByTaskId = new Map<string, AgentProgress>();
289
- for (let index = 0; index < renderedTasks.length; index++) {
290
- const renderedTask = renderedTasks[index];
291
- progressByTaskId.set(renderedTask.id, {
297
+ for (let index = 0; index < taskItems.length; index++) {
298
+ const taskItem = taskItems[index];
299
+ const assignment = taskItem.assignment.trim();
300
+ progressByTaskId.set(taskItem.id, {
292
301
  index,
293
- id: renderedTask.id,
302
+ id: taskItem.id,
294
303
  agent: params.agent,
295
304
  agentSource: fallbackAgentSource,
296
305
  status: "pending",
297
- task: renderedTask.task,
298
- assignment: renderedTask.assignment,
299
- description: renderedTask.description,
306
+ task: renderSubagentUserPrompt(assignment, simpleMode),
307
+ assignment,
308
+ description: taskItem.description,
300
309
  recentTools: [],
301
310
  recentOutput: [],
302
311
  toolCount: 0,
@@ -440,6 +449,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
440
449
  },
441
450
  {
442
451
  id: label,
452
+ ownerId: this.session.getAgentId?.() ?? undefined,
443
453
  onProgress: (text, details) => {
444
454
  const progressDetails =
445
455
  (details as TaskToolDetails | undefined) ??
@@ -506,7 +516,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
506
516
  const { agent: agentName, context, schema: outputSchema } = params;
507
517
  const simpleMode = this.#getTaskSimpleMode();
508
518
  const { contextEnabled, customSchemaEnabled } = getTaskSimpleModeCapabilities(simpleMode);
509
- const sharedContext = contextEnabled ? context : undefined;
519
+ const sharedContext = contextEnabled ? context?.trim() : undefined;
510
520
  const isolationMode = this.session.settings.get("task.isolation.mode");
511
521
  const isolationRequested = "isolated" in params ? params.isolated === true : false;
512
522
  const isIsolated = isolationMode !== "none" && isolationRequested;
@@ -725,6 +735,10 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
725
735
  getSessionId: this.session.getSessionId ?? (() => null),
726
736
  };
727
737
 
738
+ // Subagents adopt the parent's ArtifactManager so artifact IDs are unique
739
+ // across the whole tree and outputs land flat in the parent's dir.
740
+ const parentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
741
+
728
742
  // Initialize progress tracking
729
743
  const progressMap = new Map<number, AgentProgress>();
730
744
 
@@ -781,9 +795,11 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
781
795
  };
782
796
  }
783
797
 
784
- // Write parent conversation context for subagents
798
+ // Write parent conversation context for subagents. When IRC is available,
799
+ // subagents should ask live peers instead of reading a stale markdown dump.
785
800
  await fs.mkdir(effectiveArtifactsDir, { recursive: true });
786
- const compactContext = this.session.getCompactContext?.();
801
+ const shouldWriteConversationContext = this.session.settings.get("irc.enabled") !== true;
802
+ const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
787
803
  let contextFilePath: string | undefined;
788
804
  if (compactContext) {
789
805
  contextFilePath = path.join(effectiveArtifactsDir, "context.md");
@@ -802,8 +818,6 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
802
818
  }
803
819
  const tasksWithUniqueIds = tasks.map((t, i) => ({ ...t, id: uniqueIds[i] }));
804
820
 
805
- // Build full prompts using shared context only when the current task mode allows it.
806
- const tasksWithContext = tasksWithUniqueIds.map(t => renderTemplate(sharedContext, t, simpleMode));
807
821
  const availableSkills = [...(this.session.skills ?? [])];
808
822
  const contextFiles = this.session.contextFiles?.filter(
809
823
  file => path.basename(file.path).toLowerCase() !== "agents.md",
@@ -811,34 +825,36 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
811
825
  const promptTemplates = this.session.promptTemplates;
812
826
 
813
827
  // Initialize progress for all tasks
814
- for (let i = 0; i < tasksWithContext.length; i++) {
815
- const t = tasksWithContext[i];
828
+ for (let i = 0; i < tasksWithUniqueIds.length; i++) {
829
+ const taskItem = tasksWithUniqueIds[i];
830
+ const assignment = taskItem.assignment.trim();
816
831
  progressMap.set(i, {
817
832
  index: i,
818
- id: t.id,
833
+ id: taskItem.id,
819
834
  agent: agentName,
820
835
  agentSource: agent.source,
821
836
  status: "pending",
822
- task: t.task,
823
- assignment: t.assignment,
837
+ task: renderSubagentUserPrompt(assignment, simpleMode),
838
+ assignment,
824
839
  recentTools: [],
825
840
  recentOutput: [],
826
841
  toolCount: 0,
827
842
  tokens: 0,
828
843
  durationMs: 0,
829
844
  modelOverride,
830
- description: t.description,
845
+ description: taskItem.description,
831
846
  });
832
847
  }
833
848
  emitProgress();
834
849
 
835
- const runTask = async (task: (typeof tasksWithContext)[number], index: number) => {
850
+ const runTask = async (task: (typeof tasksWithUniqueIds)[number], index: number) => {
836
851
  if (!isIsolated) {
837
852
  return runSubprocess({
838
853
  cwd: this.session.cwd,
839
854
  agent,
840
- task: task.task,
841
- assignment: task.assignment,
855
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
856
+ assignment: task.assignment.trim(),
857
+ context: sharedContext,
842
858
  description: task.description,
843
859
  index,
844
860
  id: task.id,
@@ -863,12 +879,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
863
879
  authStorage: this.session.authStorage,
864
880
  modelRegistry: this.session.modelRegistry,
865
881
  settings: this.session.settings,
866
- mcpManager: this.session.mcpManager,
882
+ mcpManager: MCPManager.instance(),
867
883
  contextFiles,
868
884
  skills: availableSkills,
869
885
  workspaceTree: this.session.workspaceTree,
870
886
  promptTemplates,
871
887
  localProtocolOptions,
888
+ parentArtifactManager,
872
889
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
873
890
  });
874
891
  }
@@ -894,8 +911,9 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
894
911
  cwd: this.session.cwd,
895
912
  worktree: isolationDir,
896
913
  agent,
897
- task: task.task,
898
- assignment: task.assignment,
914
+ task: renderSubagentUserPrompt(task.assignment, simpleMode),
915
+ assignment: task.assignment.trim(),
916
+ context: sharedContext,
899
917
  description: task.description,
900
918
  index,
901
919
  id: task.id,
@@ -920,12 +938,13 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
920
938
  authStorage: this.session.authStorage,
921
939
  modelRegistry: this.session.modelRegistry,
922
940
  settings: this.session.settings,
923
- mcpManager: this.session.mcpManager,
941
+ mcpManager: MCPManager.instance(),
924
942
  contextFiles,
925
943
  skills: availableSkills,
926
944
  workspaceTree: this.session.workspaceTree,
927
945
  promptTemplates,
928
946
  localProtocolOptions,
947
+ parentArtifactManager,
929
948
  parentHindsightSessionState: this.session.getHindsightSessionState?.(),
930
949
  });
931
950
  if (mergeMode === "branch" && result.exitCode === 0) {
@@ -979,13 +998,14 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
979
998
  return result;
980
999
  } catch (err) {
981
1000
  const message = err instanceof Error ? err.message : String(err);
1001
+ const assignment = task.assignment.trim();
982
1002
  return {
983
1003
  index,
984
1004
  id: task.id,
985
1005
  agent: agent.name,
986
1006
  agentSource: agent.source,
987
- task: task.task,
988
- assignment: task.assignment,
1007
+ task: renderSubagentUserPrompt(assignment, simpleMode),
1008
+ assignment,
989
1009
  description: task.description,
990
1010
  exitCode: 1,
991
1011
  output: "",
@@ -1011,7 +1031,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1011
1031
 
1012
1032
  // Execute in parallel with concurrency limit
1013
1033
  const { results: partialResults, aborted } = await mapWithConcurrencyLimit(
1014
- tasksWithContext,
1034
+ tasksWithUniqueIds,
1015
1035
  maxConcurrency,
1016
1036
  runTask,
1017
1037
  signal,
@@ -1022,14 +1042,15 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
1022
1042
  if (result !== undefined) {
1023
1043
  return result;
1024
1044
  }
1025
- const task = tasksWithContext[index];
1045
+ const task = tasksWithUniqueIds[index];
1046
+ const assignment = task.assignment.trim();
1026
1047
  return {
1027
1048
  index,
1028
1049
  id: task.id,
1029
1050
  agent: agentName,
1030
1051
  agentSource: agent.source,
1031
- task: task.task,
1032
- assignment: task.assignment,
1052
+ task: renderSubagentUserPrompt(assignment, simpleMode),
1053
+ assignment,
1033
1054
  description: task.description,
1034
1055
  exitCode: 1,
1035
1056
  output: "",
@@ -89,6 +89,7 @@ export interface DiscoverableMCPSearchResult {
89
89
 
90
90
  const BM25_K1 = 1.2;
91
91
  const BM25_B = 0.75;
92
+ const BM25_DELTA = 1.0;
92
93
  const FIELD_WEIGHTS = {
93
94
  name: 6,
94
95
  label: 4,
@@ -112,13 +113,24 @@ function getSchemaPropertyKeys(parameters: unknown): string[] {
112
113
  }
113
114
 
114
115
  function tokenize(value: string): string[] {
115
- return value
116
- .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
117
- .replace(/[^a-zA-Z0-9]+/g, " ")
118
- .toLowerCase()
119
- .trim()
120
- .split(/\s+/)
121
- .filter(token => token.length > 0);
116
+ return (
117
+ value
118
+ .normalize("NFKD")
119
+ // Drop combining marks (accents) so "café" → "cafe".
120
+ .replace(/\p{M}+/gu, "")
121
+ // Split ACRONYMBoundary: "MCPTool" → "MCP Tool".
122
+ .replace(/(\p{Lu}+)(\p{Lu}\p{Ll})/gu, "$1 $2")
123
+ // Split camelCase / digit→letter: "fooBar" → "foo Bar", "v2Beta" → "v2 Beta".
124
+ .replace(/(\p{Ll}|\p{N})(\p{Lu})/gu, "$1 $2")
125
+ // Everything that isn't a letter or digit becomes a separator. This subsumes markdown
126
+ // punctuation (`|*_`#-~>[]()`), box-drawing glyphs (─│┌), em/en dashes, smart quotes,
127
+ // zero-width spaces, NBSPs, etc.
128
+ .replace(/[^\p{L}\p{N}]+/gu, " ")
129
+ .toLowerCase()
130
+ .trim()
131
+ .split(/\s+/)
132
+ .filter(token => token.length > 0)
133
+ );
122
134
  }
123
135
 
124
136
  function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
@@ -274,7 +286,8 @@ export function searchDiscoverableTools(
274
286
  const documentFrequency = index.documentFrequencies.get(token) ?? 0;
275
287
  const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
276
288
  const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
277
- score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
289
+ score +=
290
+ queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization) + BM25_DELTA);
278
291
  }
279
292
  return { tool: document.tool, score };
280
293
  })
@@ -7,6 +7,7 @@ import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
9
  import { computeLineHash, HL_BODY_SEP } from "../hashline/hash";
10
+ import { InternalUrlRouter } from "../internal-urls";
10
11
  import type { Theme } from "../modes/theme/theme";
11
12
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
12
13
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -213,10 +214,10 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
213
214
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
214
215
  throw new ToolError("`paths` must contain non-empty paths or globs");
215
216
  }
216
- const internalRouter = this.session.internalRouter;
217
+ const internalRouter = InternalUrlRouter.instance();
217
218
  const resolvedPathInputs: string[] = [];
218
219
  for (const rawPath of rawPaths) {
219
- if (!internalRouter?.canHandle(rawPath)) {
220
+ if (!internalRouter.canHandle(rawPath)) {
220
221
  resolvedPathInputs.push(rawPath);
221
222
  continue;
222
223
  }
@@ -6,6 +6,7 @@ import { Text } from "@oh-my-pi/pi-tui";
6
6
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
7
7
  import { type Static, Type } from "@sinclair/typebox";
8
8
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
9
+ import { InternalUrlRouter } from "../internal-urls";
9
10
  import type { Theme } from "../modes/theme/theme";
10
11
  import astGrepDescription from "../prompts/tools/ast-grep.md" with { type: "text" };
11
12
  import { Ellipsis, Hasher, type RenderCache, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
@@ -158,10 +159,10 @@ export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolD
158
159
  if (rawPaths.some(rawPath => rawPath.length === 0)) {
159
160
  throw new ToolError("`paths` must contain non-empty paths or globs");
160
161
  }
161
- const internalRouter = this.session.internalRouter;
162
+ const internalRouter = InternalUrlRouter.instance();
162
163
  const resolvedPathInputs: string[] = [];
163
164
  for (const rawPath of rawPaths) {
164
- if (!internalRouter?.canHandle(rawPath)) {
165
+ if (!internalRouter.canHandle(rawPath)) {
165
166
  resolvedPathInputs.push(rawPath);
166
167
  continue;
167
168
  }
package/src/tools/bash.ts CHANGED
@@ -4,8 +4,10 @@ import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { ImageProtocol, TERMINAL, Text } from "@oh-my-pi/pi-tui";
5
5
  import { $env, getProjectDir, isEnoent, prompt } from "@oh-my-pi/pi-utils";
6
6
  import { Type } from "@sinclair/typebox";
7
+ import { AsyncJobManager } from "../async";
7
8
  import { type BashResult, executeBash } from "../exec/bash-executor";
8
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
+ import { InternalUrlRouter } from "../internal-urls";
9
11
  import { truncateToVisualLines } from "../modes/components/visual-truncate";
10
12
  import type { Theme } from "../modes/theme/theme";
11
13
  import bashDescription from "../prompts/tools/bash.md" with { type: "text" };
@@ -16,7 +18,6 @@ import { getSixelLineMask } from "../utils/sixel";
16
18
  import type { ToolSession } from ".";
17
19
  import { type BashInteractiveResult, runInteractiveBashPty } from "./bash-interactive";
18
20
  import { checkBashInterception } from "./bash-interceptor";
19
- import { applyHeadTail } from "./bash-normalize";
20
21
  import { expandInternalUrls, type InternalUrlExpansionOptions } from "./bash-skill-urls";
21
22
  import { formatStyledTruncationWarning, type OutputMeta } from "./output-meta";
22
23
  import { resolveToCwd } from "./path-utils";
@@ -50,8 +51,7 @@ const bashSchemaBase = Type.Object({
50
51
  ),
51
52
  timeout: Type.Optional(Type.Number({ description: "timeout in seconds", default: 300 })),
52
53
  cwd: Type.Optional(Type.String({ description: "working directory", examples: ["src/", "/tmp"] })),
53
- head: Type.Optional(Type.Number({ description: "first n lines of output" })),
54
- tail: Type.Optional(Type.Number({ description: "last n lines of output" })),
54
+
55
55
  pty: Type.Optional(
56
56
  Type.Boolean({
57
57
  description: "run in pty mode",
@@ -75,8 +75,7 @@ export interface BashToolInput {
75
75
  env?: Record<string, string>;
76
76
  timeout?: number;
77
77
  cwd?: string;
78
- head?: number;
79
- tail?: number;
78
+
80
79
  async?: boolean;
81
80
  pty?: boolean;
82
81
  }
@@ -266,16 +265,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
266
265
  });
267
266
  }
268
267
 
269
- #formatResultOutput(result: BashResult | BashInteractiveResult, headLines?: number, tailLines?: number): string {
270
- let outputText = normalizeResultOutput(result);
271
- const headTailResult = applyHeadTail(outputText, headLines, tailLines);
272
- if (headTailResult.applied) {
273
- outputText = headTailResult.text;
274
- }
275
- if (!outputText) {
276
- outputText = "(no output)";
277
- }
278
- return outputText;
268
+ #formatResultOutput(result: BashResult | BashInteractiveResult): string {
269
+ const outputText = normalizeResultOutput(result);
270
+ return outputText || "(no output)";
279
271
  }
280
272
 
281
273
  #buildResultText(result: BashResult | BashInteractiveResult, timeoutSec: number, outputText: string): string {
@@ -297,11 +289,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
297
289
  #buildCompletedResult(
298
290
  result: BashResult | BashInteractiveResult,
299
291
  timeoutSec: number,
300
- headLines?: number,
301
- tailLines?: number,
302
292
  options: { requestedTimeoutSec?: number; notices?: string[] } = {},
303
293
  ): AgentToolResult<BashToolDetails> {
304
- const outputLines = [this.#formatResultOutput(result, headLines, tailLines)];
294
+ const outputLines = [this.#formatResultOutput(result)];
305
295
  const notices = options.notices?.filter(Boolean) ?? [];
306
296
  if (notices.length > 0) outputLines.push("", ...notices);
307
297
  const outputText = outputLines.join("\n");
@@ -338,7 +328,9 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
338
328
  }
339
329
  lines.push(`Background job ${jobId} started: ${label}`);
340
330
  lines.push("Result will be delivered automatically when complete.");
341
- lines.push(`Use \`job\` (with \`poll\` or \`cancel\`) or \`read jobs://${jobId}\` if needed.`);
331
+ lines.push(
332
+ `You can use \`job\` to poll until complete, but prefer to continue with another task in the meanwhile if it's not blocking.`,
333
+ );
342
334
  return {
343
335
  content: [{ type: "text", text: lines.join("\n") }],
344
336
  details,
@@ -356,13 +348,12 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
356
348
  timeoutSec: number;
357
349
  requestedTimeoutSec?: number;
358
350
  timeoutClampNotice?: string;
359
- headLines?: number;
360
- tailLines?: number;
351
+
361
352
  resolvedEnv?: Record<string, string>;
362
353
  onUpdate?: AgentToolUpdateCallback<BashToolDetails>;
363
354
  startBackgrounded: boolean;
364
355
  }): ManagedBashJobHandle {
365
- const manager = this.session.asyncJobManager;
356
+ const manager = AsyncJobManager.instance();
366
357
  if (!manager) {
367
358
  throw new ToolError("Background job manager unavailable for this session.");
368
359
  }
@@ -394,16 +385,10 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
394
385
  },
395
386
  onMinimizedSave: originalText => saveBashOriginalArtifact(this.session, originalText),
396
387
  });
397
- const finalResult = this.#buildCompletedResult(
398
- result,
399
- options.timeoutSec,
400
- options.headLines,
401
- options.tailLines,
402
- {
403
- requestedTimeoutSec: options.requestedTimeoutSec,
404
- notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
405
- },
406
- );
388
+ const finalResult = this.#buildCompletedResult(result, options.timeoutSec, {
389
+ requestedTimeoutSec: options.requestedTimeoutSec,
390
+ notices: [options.timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
391
+ });
407
392
  const finalText = this.#extractTextResult(finalResult);
408
393
  latestText = finalText;
409
394
  completion.resolve({ kind: "completed", result: finalResult });
@@ -418,6 +403,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
418
403
  }
419
404
  },
420
405
  {
406
+ ownerId: this.session.getAgentId?.() ?? undefined,
421
407
  onProgress: async (text, details) => {
422
408
  latestText = text;
423
409
  await options.onUpdate?.({
@@ -481,8 +467,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
481
467
  env: rawEnv,
482
468
  timeout: rawTimeout = 300,
483
469
  cwd,
484
- head,
485
- tail,
470
+
486
471
  async: asyncRequested = false,
487
472
  pty = false,
488
473
  }: BashToolInput,
@@ -505,10 +490,6 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
505
490
  throw new ToolError("Async bash execution is disabled. Enable async.enabled to use async mode.");
506
491
  }
507
492
 
508
- // Only apply explicit head/tail params from tool input.
509
- const headLines = head;
510
- const tailLines = tail;
511
-
512
493
  // Check both the original command and the cwd-normalized command so
513
494
  // leading `cd ... &&` wrappers do not hide either shell-navigation rules
514
495
  // or the dedicated-tool command that follows the directory change.
@@ -525,7 +506,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
525
506
 
526
507
  const internalUrlOptions: InternalUrlExpansionOptions = {
527
508
  skills: this.session.skills ?? [],
528
- internalRouter: this.session.internalRouter,
509
+ internalRouter: InternalUrlRouter.instance(),
529
510
  localOptions: {
530
511
  getArtifactsDir: this.session.getArtifactsDir,
531
512
  getSessionId: this.session.getSessionId,
@@ -573,7 +554,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
573
554
  const timeoutClampNotice = formatTimeoutClampNotice(requestedTimeoutSec, timeoutSec);
574
555
 
575
556
  if (asyncRequested) {
576
- if (!this.session.asyncJobManager) {
557
+ if (!AsyncJobManager.instance()) {
577
558
  throw new ToolError("Async job manager unavailable for this session.");
578
559
  }
579
560
  const job = this.#startManagedBashJob({
@@ -583,8 +564,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
583
564
  timeoutSec,
584
565
  requestedTimeoutSec,
585
566
  timeoutClampNotice,
586
- headLines,
587
- tailLines,
567
+
588
568
  resolvedEnv,
589
569
  onUpdate,
590
570
  startBackgrounded: true,
@@ -595,7 +575,8 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
595
575
  });
596
576
  }
597
577
 
598
- if (this.#autoBackgroundEnabled && !pty && this.session.asyncJobManager) {
578
+ const autoBgManager = AsyncJobManager.instance();
579
+ if (this.#autoBackgroundEnabled && !pty && autoBgManager) {
599
580
  const autoBackgroundWaitMs = this.#resolveAutoBackgroundWaitMs(timeoutMs);
600
581
  const startBackgrounded = autoBackgroundWaitMs === 0;
601
582
  const job = this.#startManagedBashJob({
@@ -605,8 +586,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
605
586
  timeoutSec,
606
587
  requestedTimeoutSec,
607
588
  timeoutClampNotice,
608
- headLines,
609
- tailLines,
589
+
610
590
  resolvedEnv,
611
591
  onUpdate,
612
592
  startBackgrounded,
@@ -619,16 +599,16 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
619
599
  }
620
600
  const waitResult = await this.#waitForManagedBashJob(job, autoBackgroundWaitMs, signal);
621
601
  if (waitResult.kind === "completed") {
622
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
602
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
623
603
  return waitResult.result;
624
604
  }
625
605
  if (waitResult.kind === "failed") {
626
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
606
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
627
607
  throw waitResult.error;
628
608
  }
629
609
  if (waitResult.kind === "aborted") {
630
- this.session.asyncJobManager.cancel(job.jobId);
631
- this.session.asyncJobManager.acknowledgeDeliveries([job.jobId]);
610
+ autoBgManager.cancel(job.jobId);
611
+ autoBgManager.acknowledgeDeliveries([job.jobId]);
632
612
  throw new ToolAbortError(job.getLatestText() || "Command aborted");
633
613
  }
634
614
  job.setBackgrounded(true);
@@ -675,7 +655,7 @@ export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
675
655
  if (isInteractiveResult(result) && result.timedOut) {
676
656
  throw new ToolError(normalizeResultOutput(result) || `Command timed out after ${timeoutSec} seconds`);
677
657
  }
678
- return this.#buildCompletedResult(result, timeoutSec, headLines, tailLines, {
658
+ return this.#buildCompletedResult(result, timeoutSec, {
679
659
  requestedTimeoutSec,
680
660
  notices: [timeoutClampNotice].filter((notice): notice is string => Boolean(notice)),
681
661
  });