@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/CHANGELOG.md +39 -0
- package/package.json +7 -7
- package/src/config/keybindings.ts +1 -4
- package/src/config/settings-schema.ts +10 -3
- package/src/discovery/claude.ts +1 -0
- package/src/discovery/codex.ts +4 -0
- package/src/discovery/cursor.ts +1 -0
- package/src/discovery/gemini.ts +1 -0
- package/src/discovery/vscode.ts +1 -0
- package/src/discovery/windsurf.ts +1 -0
- package/src/mcp/discoverable-tool-metadata.ts +192 -0
- package/src/modes/components/status-line/segments.ts +13 -5
- package/src/modes/controllers/command-controller.ts +8 -46
- package/src/modes/controllers/input-controller.ts +1 -0
- package/src/modes/prompt-action-autocomplete.ts +19 -3
- package/src/modes/utils/hotkeys-markdown.ts +57 -0
- package/src/prompts/system/system-prompt.md +7 -0
- package/src/prompts/tools/search-tool-bm25.md +34 -0
- package/src/sdk.ts +47 -7
- package/src/session/agent-session.ts +103 -14
- package/src/session/session-manager.ts +64 -45
- package/src/system-prompt.ts +36 -1
- package/src/tools/ast-edit.ts +3 -2
- package/src/tools/ast-grep.ts +13 -3
- package/src/tools/find.ts +2 -2
- package/src/tools/grep.ts +3 -2
- package/src/tools/index.ts +17 -0
- package/src/tools/path-utils.ts +4 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/search-tool-bm25.ts +278 -0
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:
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
1700
|
-
|
|
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
|
-
|
|
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 (
|
|
394
|
-
|
|
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(
|
|
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
|
-
*
|
|
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
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
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
|
|
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
|
|
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);
|
package/src/system-prompt.ts
CHANGED
|
@@ -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,
|
|
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);
|
package/src/tools/ast-edit.ts
CHANGED
|
@@ -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
|
|
114
|
-
const rawPath = params.path
|
|
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)) {
|