@oh-my-pi/pi-coding-agent 13.12.6 → 13.12.8

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.
package/src/sdk.ts CHANGED
@@ -65,6 +65,11 @@ import {
65
65
  } from "./internal-urls";
66
66
  import { disposeAllKernelSessions } from "./ipy/executor";
67
67
  import { discoverAndLoadMCPTools, type MCPManager, type MCPToolsLoadResult } from "./mcp";
68
+ import {
69
+ collectDiscoverableMCPTools,
70
+ formatDiscoverableMCPToolServerSummary,
71
+ summarizeDiscoverableMCPTools,
72
+ } from "./mcp/discoverable-tool-metadata";
68
73
  import { buildMemoryToolDeveloperInstructions, getMemoryRoot, startMemoryStartupTask } from "./memories";
69
74
  import asyncResultTemplate from "./prompts/tools/async-result.md" with { type: "text" };
70
75
  import { collectEnvSecrets, loadSecrets, obfuscateMessages, SecretObfuscator } from "./secrets";
@@ -76,6 +81,7 @@ import { closeAllConnections } from "./ssh/connection-manager";
76
81
  import { unmountAll } from "./ssh/sshfs-mount";
77
82
  import {
78
83
  buildSystemPrompt as buildSystemPromptInternal,
84
+ buildSystemPromptToolMetadata,
79
85
  loadProjectContextFiles as loadContextFilesInternal,
80
86
  } from "./system-prompt";
81
87
  import { AgentOutputManager } from "./task/output-manager";
@@ -95,6 +101,7 @@ import {
95
101
  PythonTool,
96
102
  ReadTool,
97
103
  ResolveTool,
104
+ renderSearchToolBm25Description,
98
105
  setPreferredCodeSearchProvider,
99
106
  setPreferredImageProvider,
100
107
  setPreferredSearchProvider,
@@ -184,7 +191,7 @@ export interface CreateAgentSessionOptions {
184
191
  /** Parent task ID prefix for nested artifact naming (e.g., "6-Extensions") */
185
192
  parentTaskPrefix?: string;
186
193
 
187
- /** Session manager. Default: SessionManager.create(cwd) */
194
+ /** Session manager. Default: session stored under the configured agentDir sessions root */
188
195
  sessionManager?: SessionManager;
189
196
 
190
197
  /** Settings instance. Default: Settings.init({ cwd, agentDir }) */
@@ -649,7 +656,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
649
656
  setPreferredImageProvider(imageProvider);
650
657
  }
651
658
 
652
- const sessionManager = options.sessionManager ?? logger.time("sessionManager", SessionManager.create, cwd);
659
+ const sessionManager =
660
+ options.sessionManager ??
661
+ logger.time("sessionManager", () =>
662
+ SessionManager.create(cwd, SessionManager.getDefaultSessionDir(cwd, agentDir)),
663
+ );
653
664
  const sessionId = sessionManager.getSessionId();
654
665
  const modelApiKeyAvailability = new Map<string, boolean>();
655
666
  const getModelAvailabilityKey = (candidate: Model): string =>
@@ -864,6 +875,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
864
875
  getCompactContext: () => session.formatCompactContext(),
865
876
  getTodoPhases: () => session.getTodoPhases(),
866
877
  setTodoPhases: phases => session.setTodoPhases(phases),
878
+ isMCPDiscoveryEnabled: () => session.isMCPDiscoveryEnabled(),
879
+ getDiscoverableMCPTools: () => session.getDiscoverableMCPTools(),
880
+ getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
881
+ getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
882
+ activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
867
883
  getCheckpointState: () => session.getCheckpointState(),
868
884
  setCheckpointState: state => session.setCheckpointState(state ?? undefined),
869
885
  allocateOutputArtifact: async toolType => {
@@ -1189,6 +1205,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1189
1205
  const intentField = settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1" ? INTENT_FIELD : undefined;
1190
1206
  const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
1191
1207
  toolContextStore.setToolNames(toolNames);
1208
+ const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
1209
+ const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
1210
+ const hasDiscoverableMCPTools =
1211
+ mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableMCPTools.length > 0;
1212
+ const promptTools = buildSystemPromptToolMetadata(tools, {
1213
+ search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1214
+ });
1192
1215
  const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
1193
1216
 
1194
1217
  // Build combined append prompt: memory instructions + MCP server instructions
@@ -1214,14 +1237,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1214
1237
  cwd,
1215
1238
  skills,
1216
1239
  contextFiles,
1217
- tools,
1240
+ tools: promptTools,
1218
1241
  toolNames,
1219
1242
  rules: rulebookRules,
1220
1243
  skillsSettings: settings.getGroup("skills"),
1221
1244
  appendSystemPrompt: appendPrompt,
1222
1245
  repeatToolDescriptions,
1223
- eagerTasks,
1224
1246
  intentField,
1247
+ mcpDiscoveryMode: hasDiscoverableMCPTools,
1248
+ mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1249
+ eagerTasks,
1225
1250
  });
1226
1251
 
1227
1252
  if (options.systemPrompt === undefined) {
@@ -1232,15 +1257,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1232
1257
  cwd,
1233
1258
  skills,
1234
1259
  contextFiles,
1235
- tools,
1260
+ tools: promptTools,
1236
1261
  toolNames,
1237
1262
  rules: rulebookRules,
1238
1263
  skillsSettings: settings.getGroup("skills"),
1239
1264
  customPrompt: options.systemPrompt,
1240
1265
  appendSystemPrompt: appendPrompt,
1241
1266
  repeatToolDescriptions,
1242
- eagerTasks,
1243
1267
  intentField,
1268
+ mcpDiscoveryMode: hasDiscoverableMCPTools,
1269
+ mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
1270
+ eagerTasks,
1244
1271
  });
1245
1272
  }
1246
1273
  return options.systemPrompt(defaultPrompt);
@@ -1250,9 +1277,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1250
1277
  const requestedToolNames = options.toolNames?.map(name => name.toLowerCase()) ?? toolNamesFromRegistry;
1251
1278
  const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
1252
1279
  const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
1253
- const initialToolNames = includeExitPlanMode
1280
+ const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false;
1281
+ const requestedActiveToolNames = includeExitPlanMode
1254
1282
  ? normalizedRequested
1255
1283
  : normalizedRequested.filter(name => name !== "exit_plan_mode");
1284
+ const explicitlyRequestedMCPToolNames = options.toolNames
1285
+ ? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
1286
+ : [];
1287
+ const initialToolNames = mcpDiscoveryEnabled
1288
+ ? [...requestedActiveToolNames.filter(name => !name.startsWith("mcp_")), ...explicitlyRequestedMCPToolNames]
1289
+ : [...requestedActiveToolNames];
1290
+ const initialSelectedMCPToolNames = mcpDiscoveryEnabled ? [...explicitlyRequestedMCPToolNames] : [];
1256
1291
 
1257
1292
  // Custom tools and extension-registered tools are always included regardless of toolNames filter
1258
1293
  const alwaysInclude: string[] = [
@@ -1260,6 +1295,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1260
1295
  ...registeredTools.map(t => t.definition.name),
1261
1296
  ];
1262
1297
  for (const name of alwaysInclude) {
1298
+ if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
1299
+ continue;
1300
+ }
1263
1301
  if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
1264
1302
  initialToolNames.push(name);
1265
1303
  }
@@ -1440,6 +1478,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1440
1478
  onPayload,
1441
1479
  convertToLlm: convertToLlmFinal,
1442
1480
  rebuildSystemPrompt,
1481
+ mcpDiscoveryEnabled,
1482
+ initialSelectedMCPToolNames,
1443
1483
  ttsrManager,
1444
1484
  obfuscator,
1445
1485
  asyncJobManager,
@@ -87,6 +87,13 @@ import type { Skill, SkillWarning } from "../extensibility/skills";
87
87
  import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
88
88
  import { resolveLocalUrlToPath } from "../internal-urls";
89
89
  import { executePython as executePythonCommand, type PythonResult } from "../ipy/executor";
90
+ import {
91
+ buildDiscoverableMCPSearchIndex,
92
+ collectDiscoverableMCPTools,
93
+ type DiscoverableMCPSearchIndex,
94
+ type DiscoverableMCPTool,
95
+ isMCPToolName,
96
+ } from "../mcp/discoverable-tool-metadata";
90
97
  import { getCurrentThemeName, theme } from "../modes/theme/theme";
91
98
  import { normalizeDiff, normalizeToLF, ParseError, previewPatch, stripBom } from "../patch";
92
99
  import type { PlanModeState } from "../plan-mode/state";
@@ -206,6 +213,10 @@ export interface AgentSessionConfig {
206
213
  convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
207
214
  /** System prompt builder that can consider tool availability */
208
215
  rebuildSystemPrompt?: (toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>;
216
+ /** Enable hidden-by-default MCP tool discovery for this session. */
217
+ mcpDiscoveryEnabled?: boolean;
218
+ /** MCP tool names previously selected via discovery in this session. */
219
+ initialSelectedMCPToolNames?: string[];
209
220
  /** TTSR manager for time-traveling stream rules */
210
221
  ttsrManager?: TtsrManager;
211
222
  /** Secret obfuscator for deobfuscating streaming edit content */
@@ -400,6 +411,10 @@ export class AgentSession {
400
411
  #convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
401
412
  #rebuildSystemPrompt: ((toolNames: string[], tools: Map<string, AgentTool>) => Promise<string>) | undefined;
402
413
  #baseSystemPrompt: string;
414
+ #mcpDiscoveryEnabled = false;
415
+ #discoverableMCPTools = new Map<string, DiscoverableMCPTool>();
416
+ #discoverableMCPSearchIndex: DiscoverableMCPSearchIndex | null = null;
417
+ #selectedMCPToolNames = new Set<string>();
403
418
 
404
419
  // TTSR manager for time-traveling stream rules
405
420
  #ttsrManager: TtsrManager | undefined = undefined;
@@ -446,6 +461,10 @@ export class AgentSession {
446
461
  this.#convertToLlm = config.convertToLlm ?? convertToLlm;
447
462
  this.#rebuildSystemPrompt = config.rebuildSystemPrompt;
448
463
  this.#baseSystemPrompt = this.agent.state.systemPrompt;
464
+ this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
465
+ this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
466
+ this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
467
+ this.#pruneSelectedMCPToolNames();
449
468
  this.#ttsrManager = config.ttsrManager;
450
469
  this.#obfuscator = config.obfuscator;
451
470
  this.agent.providerSessionState = this.#providerSessionState;
@@ -1604,6 +1623,36 @@ export class AgentSession {
1604
1623
  return this.#retryAttempt;
1605
1624
  }
1606
1625
 
1626
+ #collectDiscoverableMCPToolsFromRegistry(): Map<string, DiscoverableMCPTool> {
1627
+ return new Map(collectDiscoverableMCPTools(this.#toolRegistry.values()).map(tool => [tool.name, tool] as const));
1628
+ }
1629
+
1630
+ #setDiscoverableMCPTools(discoverableMCPTools: Map<string, DiscoverableMCPTool>): void {
1631
+ this.#discoverableMCPTools = discoverableMCPTools;
1632
+ this.#discoverableMCPSearchIndex = null;
1633
+ }
1634
+
1635
+ #pruneSelectedMCPToolNames(): void {
1636
+ for (const name of Array.from(this.#selectedMCPToolNames)) {
1637
+ if (!this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
1638
+ this.#selectedMCPToolNames.delete(name);
1639
+ }
1640
+ }
1641
+ }
1642
+
1643
+ #getVisibleMCPToolNames(): string[] {
1644
+ if (!this.#mcpDiscoveryEnabled) {
1645
+ return Array.from(this.#toolRegistry.keys()).filter(name => isMCPToolName(name));
1646
+ }
1647
+ return Array.from(this.#selectedMCPToolNames).filter(
1648
+ name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
1649
+ );
1650
+ }
1651
+
1652
+ #getActiveNonMCPToolNames(): string[] {
1653
+ return this.getActiveToolNames().filter(name => !isMCPToolName(name) && this.#toolRegistry.has(name));
1654
+ }
1655
+
1607
1656
  /**
1608
1657
  * Get the names of currently active tools.
1609
1658
  * Returns the names of tools currently set on the agent.
@@ -1631,11 +1680,52 @@ export class AgentSession {
1631
1680
  return Array.from(this.#toolRegistry.keys());
1632
1681
  }
1633
1682
 
1683
+ isMCPDiscoveryEnabled(): boolean {
1684
+ return this.#mcpDiscoveryEnabled;
1685
+ }
1686
+
1687
+ getDiscoverableMCPTools(): DiscoverableMCPTool[] {
1688
+ return Array.from(this.#discoverableMCPTools.values());
1689
+ }
1690
+
1691
+ getDiscoverableMCPSearchIndex(): DiscoverableMCPSearchIndex {
1692
+ if (!this.#discoverableMCPSearchIndex) {
1693
+ this.#discoverableMCPSearchIndex = buildDiscoverableMCPSearchIndex(this.#discoverableMCPTools.values());
1694
+ }
1695
+ return this.#discoverableMCPSearchIndex;
1696
+ }
1697
+
1698
+ getSelectedMCPToolNames(): string[] {
1699
+ if (!this.#mcpDiscoveryEnabled) {
1700
+ return this.getActiveToolNames().filter(name => isMCPToolName(name) && this.#toolRegistry.has(name));
1701
+ }
1702
+ return Array.from(this.#selectedMCPToolNames).filter(
1703
+ name => this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
1704
+ );
1705
+ }
1706
+
1707
+ async activateDiscoveredMCPTools(toolNames: string[]): Promise<string[]> {
1708
+ const activated: string[] = [];
1709
+ for (const name of toolNames) {
1710
+ if (!isMCPToolName(name) || !this.#discoverableMCPTools.has(name) || !this.#toolRegistry.has(name)) {
1711
+ continue;
1712
+ }
1713
+ this.#selectedMCPToolNames.add(name);
1714
+ activated.push(name);
1715
+ }
1716
+ if (activated.length === 0) {
1717
+ return [];
1718
+ }
1719
+ const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.#getVisibleMCPToolNames()];
1720
+ await this.setActiveToolsByName(nextActive);
1721
+ return [...new Set(activated)];
1722
+ }
1723
+
1634
1724
  /**
1635
1725
  * Set active tools by name.
1636
1726
  * Only tools in the registry can be enabled. Unknown tool names are ignored.
1637
1727
  * Also rebuilds the system prompt to reflect the new tool set.
1638
- * Changes take effect on the next agent turn.
1728
+ * Changes take effect before the next model call.
1639
1729
  */
1640
1730
  async setActiveToolsByName(toolNames: string[]): Promise<void> {
1641
1731
  const tools: AgentTool[] = [];
@@ -1647,6 +1737,13 @@ export class AgentSession {
1647
1737
  validToolNames.push(name);
1648
1738
  }
1649
1739
  }
1740
+ if (this.#mcpDiscoveryEnabled) {
1741
+ this.#selectedMCPToolNames = new Set(
1742
+ validToolNames.filter(
1743
+ name => isMCPToolName(name) && this.#discoverableMCPTools.has(name) && this.#toolRegistry.has(name),
1744
+ ),
1745
+ );
1746
+ }
1650
1747
  this.agent.setTools(tools);
1651
1748
 
1652
1749
  // Rebuild base system prompt with new tool set
@@ -1665,14 +1762,13 @@ export class AgentSession {
1665
1762
  }
1666
1763
 
1667
1764
  /**
1668
- * Replace MCP tools in the registry and activate the latest MCP tool set immediately.
1765
+ * Replace MCP tools in the registry and recompute the visible MCP tool set immediately.
1669
1766
  * This allows /mcp add/remove/reauth to take effect without restarting the session.
1670
1767
  */
1671
1768
  async refreshMCPTools(mcpTools: CustomTool[]): Promise<void> {
1672
- const prefix = "mcp_";
1673
1769
  const existingNames = Array.from(this.#toolRegistry.keys());
1674
1770
  for (const name of existingNames) {
1675
- if (name.startsWith(prefix)) {
1771
+ if (isMCPToolName(name)) {
1676
1772
  this.#toolRegistry.delete(name);
1677
1773
  }
1678
1774
  }
@@ -1696,17 +1792,10 @@ export class AgentSession {
1696
1792
  this.#toolRegistry.set(finalTool.name, finalTool);
1697
1793
  }
1698
1794
 
1699
- const currentActive = this.getActiveToolNames().filter(
1700
- name => !name.startsWith(prefix) && this.#toolRegistry.has(name),
1701
- );
1702
- const mcpToolNames = Array.from(this.#toolRegistry.keys()).filter(name => name.startsWith(prefix));
1703
- const nextActive = [...currentActive];
1704
- for (const name of mcpToolNames) {
1705
- if (!nextActive.includes(name)) {
1706
- nextActive.push(name);
1707
- }
1708
- }
1795
+ this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
1796
+ this.#pruneSelectedMCPToolNames();
1709
1797
 
1798
+ const nextActive = [...this.#getActiveNonMCPToolNames(), ...this.getSelectedMCPToolNames()];
1710
1799
  await this.setActiveToolsByName(nextActive);
1711
1800
  }
1712
1801
 
@@ -21,6 +21,8 @@ import {
21
21
  isEnoent,
22
22
  logger,
23
23
  parseJsonlLenient,
24
+ pathIsWithin,
25
+ resolveEquivalentPath,
24
26
  Snowflake,
25
27
  toError,
26
28
  } from "@oh-my-pi/pi-utils";
@@ -345,7 +347,7 @@ export function migrateSessionEntries(entries: FileEntry[]): void {
345
347
  migrateToCurrentVersion(entries);
346
348
  }
347
349
 
348
- let sessionDirsMigrated = false;
350
+ const migratedSessionRoots = new Set<string>();
349
351
 
350
352
  /**
351
353
  * Merge or rename a legacy session directory into its canonical target.
@@ -375,29 +377,36 @@ function encodeLegacyAbsoluteSessionDirName(cwd: string): string {
375
377
  return `--${resolvedCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
376
378
  }
377
379
 
378
- function pathIsWithin(root: string, candidate: string): boolean {
379
- const relative = path.relative(root, candidate);
380
- return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
381
- }
382
-
383
380
  function encodeRelativeSessionDirName(prefix: string, root: string, cwd: string): string {
384
381
  const relative = path.relative(root, cwd).replace(/[/\\:]/g, "-");
385
- return relative ? `${prefix}-${relative}` : prefix;
382
+ return relative ? (prefix.endsWith("-") ? `${prefix}${relative}` : `${prefix}-${relative}`) : prefix;
383
+ }
384
+
385
+ function getDefaultSessionDirName(cwd: string): { encodedDirName: string; resolvedCwd: string } {
386
+ const resolvedCwd = path.resolve(cwd);
387
+ const canonicalCwd = resolveEquivalentPath(resolvedCwd);
388
+ const home = resolveEquivalentPath(os.homedir());
389
+ const tempRoot = resolveEquivalentPath(os.tmpdir());
390
+ const encodedDirName = pathIsWithin(home, canonicalCwd)
391
+ ? encodeRelativeSessionDirName("-", home, canonicalCwd)
392
+ : pathIsWithin(tempRoot, canonicalCwd)
393
+ ? encodeRelativeSessionDirName("-tmp", tempRoot, canonicalCwd)
394
+ : encodeLegacyAbsoluteSessionDirName(canonicalCwd);
395
+ return { encodedDirName, resolvedCwd };
386
396
  }
387
397
 
388
398
  /**
389
399
  * Migrate old `--<home-encoded>-*--` session dirs to the new `-*` format.
390
- * Runs once on first access, best-effort.
400
+ * Runs once per sessions root on first access, best-effort.
391
401
  */
392
- function migrateHomeSessionDirs(): void {
393
- if (sessionDirsMigrated) return;
394
- sessionDirsMigrated = true;
402
+ function migrateHomeSessionDirs(sessionsRoot: string): void {
403
+ if (migratedSessionRoots.has(sessionsRoot)) return;
404
+ migratedSessionRoots.add(sessionsRoot);
395
405
 
396
406
  const home = os.homedir();
397
407
  const homeEncoded = home.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-");
398
408
  const oldPrefix = `--${homeEncoded}-`;
399
409
  const oldExact = `--${homeEncoded}--`;
400
- const sessionsRoot = getSessionsDir();
401
410
 
402
411
  let entries: string[];
403
412
  try {
@@ -428,8 +437,8 @@ function migrateHomeSessionDirs(): void {
428
437
  }
429
438
  }
430
439
 
431
- function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string): void {
432
- const legacyDir = path.join(getSessionsDir(), encodeLegacyAbsoluteSessionDirName(cwd));
440
+ function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string, sessionsRoot: string): void {
441
+ const legacyDir = path.join(sessionsRoot, encodeLegacyAbsoluteSessionDirName(cwd));
433
442
  if (legacyDir === sessionDir || !fs.existsSync(legacyDir)) return;
434
443
 
435
444
  try {
@@ -439,6 +448,15 @@ function migrateLegacyAbsoluteSessionDir(cwd: string, sessionDir: string): void
439
448
  }
440
449
  }
441
450
 
451
+ function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | undefined {
452
+ const currentDirName = path.basename(sessionDir);
453
+ const { encodedDirName } = getDefaultSessionDirName(cwd);
454
+ if (currentDirName !== encodedDirName && currentDirName !== encodeLegacyAbsoluteSessionDirName(cwd)) {
455
+ return undefined;
456
+ }
457
+ return path.dirname(sessionDir);
458
+ }
459
+
442
460
  /** Exported for compaction.test.ts */
443
461
  export function parseSessionEntries(content: string): FileEntry[] {
444
462
  return parseJsonlLenient<FileEntry>(content);
@@ -633,34 +651,20 @@ export function buildSessionContext(
633
651
  return { messages, thinkingLevel, serviceTier, models, injectedTtsrRules, mode, modeData };
634
652
  }
635
653
 
636
- /**
637
- * Encode a cwd into a safe directory name for session storage.
638
- * Home-relative paths use single-dash format: `/Users/x/Projects/pi` → `-Projects-pi`
639
- * Temp-root paths use `-tmp-` prefixes: `/tmp/foo` → `-tmp-foo`
640
- * Other absolute paths keep the legacy double-dash format for compatibility.
641
- */
642
- function encodeSessionDirName(cwd: string): string {
643
- const resolvedCwd = path.resolve(cwd);
644
- const home = path.resolve(os.homedir());
645
- if (pathIsWithin(home, resolvedCwd)) {
646
- return encodeRelativeSessionDirName("-", home, resolvedCwd);
647
- }
648
- const tempRoot = path.resolve(os.tmpdir());
649
- if (pathIsWithin(tempRoot, resolvedCwd)) {
650
- return encodeRelativeSessionDirName("-tmp", tempRoot, resolvedCwd);
651
- }
652
- return encodeLegacyAbsoluteSessionDirName(resolvedCwd);
653
- }
654
-
655
654
  /**
656
655
  * Compute the default session directory for a cwd.
657
- * Encodes cwd into a safe directory name under ~/.omp/agent/sessions/.
656
+ * Classifies cwd by canonical location so symlink/alias paths resolve to the
657
+ * same home-relative or temp-root directory names as their real targets.
658
658
  */
659
- function getDefaultSessionDir(cwd: string, storage: SessionStorage): string {
660
- const resolvedCwd = path.resolve(cwd);
661
- migrateHomeSessionDirs();
662
- const sessionDir = path.join(getSessionsDir(), encodeSessionDirName(resolvedCwd));
663
- migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir);
659
+ function computeDefaultSessionDir(
660
+ cwd: string,
661
+ storage: SessionStorage,
662
+ sessionsRoot: string = getSessionsDir(),
663
+ ): string {
664
+ const { encodedDirName, resolvedCwd } = getDefaultSessionDirName(cwd);
665
+ migrateHomeSessionDirs(sessionsRoot);
666
+ const sessionDir = path.join(sessionsRoot, encodedDirName);
667
+ migrateLegacyAbsoluteSessionDir(resolvedCwd, sessionDir, sessionsRoot);
664
668
  storage.ensureDirSync(sessionDir);
665
669
  return sessionDir;
666
670
  }
@@ -1322,7 +1326,8 @@ export async function resolveResumableSession(
1322
1326
  sessionDir?: string,
1323
1327
  storage: SessionStorage = new FileSessionStorage(),
1324
1328
  ): Promise<ResolvedSessionMatch | undefined> {
1325
- const localSessions = await SessionManager.list(cwd, sessionDir, storage);
1329
+ const localSessionDir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
1330
+ const localSessions = await SessionManager.list(cwd, localSessionDir, storage);
1326
1331
  const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
1327
1332
  if (localMatch) {
1328
1333
  return { session: localMatch, scope: "local" };
@@ -1489,7 +1494,10 @@ export class SessionManager {
1489
1494
  const resolvedCwd = path.resolve(newCwd);
1490
1495
  if (resolvedCwd === this.cwd) return;
1491
1496
 
1492
- const newSessionDir = getDefaultSessionDir(resolvedCwd, this.storage);
1497
+ const managedSessionsRoot = resolveManagedSessionRoot(this.sessionDir, this.cwd);
1498
+ const newSessionDir = managedSessionsRoot
1499
+ ? computeDefaultSessionDir(resolvedCwd, this.storage, managedSessionsRoot)
1500
+ : computeDefaultSessionDir(resolvedCwd, this.storage);
1493
1501
  let hadSessionFile = false;
1494
1502
 
1495
1503
  if (this.persist && this.#sessionFile) {
@@ -2446,13 +2454,24 @@ export class SessionManager {
2446
2454
  return undefined;
2447
2455
  }
2448
2456
 
2457
+ /**
2458
+ * Resolve the canonical default session directory for a cwd.
2459
+ */
2460
+ static getDefaultSessionDir(
2461
+ cwd: string,
2462
+ agentDir?: string,
2463
+ storage: SessionStorage = new FileSessionStorage(),
2464
+ ): string {
2465
+ return computeDefaultSessionDir(cwd, storage, getSessionsDir(agentDir));
2466
+ }
2467
+
2449
2468
  /**
2450
2469
  * Create a new session.
2451
2470
  * @param cwd Working directory (stored in session header)
2452
2471
  * @param sessionDir Optional session directory. If omitted, uses default (~/.omp/agent/sessions/<encoded-cwd>/).
2453
2472
  */
2454
2473
  static create(cwd: string, sessionDir?: string, storage: SessionStorage = new FileSessionStorage()): SessionManager {
2455
- const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
2474
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2456
2475
  const manager = new SessionManager(cwd, dir, true, storage);
2457
2476
  manager.#initNewSession();
2458
2477
  return manager;
@@ -2468,7 +2487,7 @@ export class SessionManager {
2468
2487
  sessionDir?: string,
2469
2488
  storage: SessionStorage = new FileSessionStorage(),
2470
2489
  ): Promise<SessionManager> {
2471
- const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
2490
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2472
2491
  const manager = new SessionManager(cwd, dir, true, storage);
2473
2492
  const forkEntries = structuredClone(await loadEntriesFromFile(sourcePath, storage)) as FileEntry[];
2474
2493
  migrateToCurrentVersion(forkEntries);
@@ -2516,7 +2535,7 @@ export class SessionManager {
2516
2535
  sessionDir?: string,
2517
2536
  storage: SessionStorage = new FileSessionStorage(),
2518
2537
  ): Promise<SessionManager> {
2519
- const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
2538
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2520
2539
  // Prefer terminal-scoped breadcrumb (handles concurrent sessions correctly)
2521
2540
  const terminalSession = await readTerminalBreadcrumb(cwd);
2522
2541
  const mostRecent = terminalSession ?? (await findMostRecentSession(dir, storage));
@@ -2549,7 +2568,7 @@ export class SessionManager {
2549
2568
  sessionDir?: string,
2550
2569
  storage: SessionStorage = new FileSessionStorage(),
2551
2570
  ): Promise<SessionInfo[]> {
2552
- const dir = sessionDir ?? getDefaultSessionDir(cwd, storage);
2571
+ const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
2553
2572
  try {
2554
2573
  const files = storage.listFilesSync(dir, "*.jsonl");
2555
2574
  return await collectSessionsFromFiles(files, storage);
@@ -5,6 +5,7 @@
5
5
  import * as fs from "node:fs";
6
6
  import * as os from "node:os";
7
7
  import * as path from "node:path";
8
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
8
9
  import { $env, getGpuCachePath, getProjectDir, hasFsCode, isEnoent, logger } from "@oh-my-pi/pi-utils";
9
10
  import { $ } from "bun";
10
11
  import { contextFileCapability } from "./capability/context-file";
@@ -315,11 +316,36 @@ export async function loadSystemPromptFiles(options: LoadContextFilesOptions = {
315
316
  return parts.join("\n\n");
316
317
  }
317
318
 
319
+ export interface SystemPromptToolMetadata {
320
+ label: string;
321
+ description: string;
322
+ }
323
+
324
+ export function buildSystemPromptToolMetadata(
325
+ tools: Map<string, AgentTool>,
326
+ overrides: Partial<Record<string, Partial<SystemPromptToolMetadata>>> = {},
327
+ ): Map<string, SystemPromptToolMetadata> {
328
+ return new Map(
329
+ Array.from(tools.entries(), ([name, tool]) => {
330
+ const toolRecord = tool as AgentTool & { label?: string; description?: string };
331
+ const override = overrides[name];
332
+ return [
333
+ name,
334
+ {
335
+ label: override?.label ?? (typeof toolRecord.label === "string" ? toolRecord.label : ""),
336
+ description:
337
+ override?.description ?? (typeof toolRecord.description === "string" ? toolRecord.description : ""),
338
+ },
339
+ ] as const;
340
+ }),
341
+ );
342
+ }
343
+
318
344
  export interface BuildSystemPromptOptions {
319
345
  /** Custom system prompt (replaces default). */
320
346
  customPrompt?: string;
321
347
  /** Tools to include in prompt. */
322
- tools?: Map<string, { description: string; label: string }>;
348
+ tools?: Map<string, SystemPromptToolMetadata>;
323
349
  /** Tool names to include in prompt. */
324
350
  toolNames?: string[];
325
351
  /** Text to append to system prompt. */
@@ -338,6 +364,10 @@ export interface BuildSystemPromptOptions {
338
364
  rules?: Array<{ name: string; description?: string; path: string; globs?: string[] }>;
339
365
  /** Intent field name injected into every tool schema. If set, explains the field in the prompt. */
340
366
  intentField?: string;
367
+ /** Whether MCP tool discovery is active for this prompt build. */
368
+ mcpDiscoveryMode?: boolean;
369
+ /** Discoverable MCP server summaries to advertise when discovery mode is active. */
370
+ mcpDiscoveryServerSummaries?: string[];
341
371
  /** Encourage the agent to delegate via tasks unless changes are trivial. */
342
372
  eagerTasks?: boolean;
343
373
  }
@@ -360,6 +390,8 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
360
390
  skills: providedSkills,
361
391
  rules,
362
392
  intentField,
393
+ mcpDiscoveryMode = false,
394
+ mcpDiscoveryServerSummaries = [],
363
395
  eagerTasks = false,
364
396
  } = options;
365
397
  const resolvedCwd = cwd ?? getProjectDir();
@@ -494,6 +526,9 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
494
526
  cwd: promptCwd,
495
527
  intentTracing: !!intentField,
496
528
  intentField: intentField ?? "",
529
+ mcpDiscoveryMode,
530
+ hasMCPDiscoveryServers: mcpDiscoveryServerSummaries.length > 0,
531
+ mcpDiscoveryServerSummaries,
497
532
  eagerTasks,
498
533
  };
499
534
  return renderPromptTemplate(resolvedCustomPrompt ? customSystemPromptTemplate : systemPromptTemplate, data);
@@ -17,6 +17,7 @@ import type { OutputMeta } from "./output-meta";
17
17
  import {
18
18
  combineSearchGlobs,
19
19
  hasGlobPathChars,
20
+ normalizePathLikeInput,
20
21
  parseSearchPath,
21
22
  resolveMultiSearchPath,
22
23
  resolveToCwd,
@@ -110,8 +111,8 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
110
111
  };
111
112
  let searchPath: string | undefined;
112
113
  let scopePath: string | undefined;
113
- let globFilter = params.glob?.trim() || undefined;
114
- const rawPath = params.path?.trim();
114
+ let globFilter = params.glob ? normalizePathLikeInput(params.glob) || undefined : undefined;
115
+ const rawPath = params.path ? normalizePathLikeInput(params.path) || undefined : undefined;
115
116
  if (rawPath) {
116
117
  const internalRouter = this.session.internalRouter;
117
118
  if (internalRouter?.canHandle(rawPath)) {