@oh-my-pi/pi-coding-agent 13.12.6 → 13.12.7
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 +11 -0
- package/package.json +7 -7
- 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/prompts/system/system-prompt.md +7 -0
- package/src/prompts/tools/search-tool-bm25.md +34 -0
- package/src/sdk.ts +41 -5
- package/src/session/agent-session.ts +103 -14
- package/src/system-prompt.ts +36 -1
- package/src/tools/index.ts +17 -0
- package/src/tools/renderers.ts +2 -0
- package/src/tools/search-tool-bm25.ts +278 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [13.12.7] - 2026-03-16
|
|
6
|
+
### Changed
|
|
7
|
+
|
|
8
|
+
- Modified `getSelectedMCPToolNames()` to return only active MCP tools in non-discovery sessions, filtering by tool registry availability
|
|
9
|
+
- Updated `search_tool_bm25` tool instantiation to conditionally create the tool only when MCP discovery mode is enabled and execution hooks are available
|
|
10
|
+
- Changed search results to exclude already-selected MCP tools before applying the limit parameter, allowing discovery of additional tools in subsequent searches
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Fixed MCP tool selection tracking to properly distinguish between discovery-enabled and non-discovery sessions, preventing orphaned tool selections after manual deactivation
|
|
15
|
+
|
|
5
16
|
## [13.12.6] - 2026-03-15
|
|
6
17
|
### Changed
|
|
7
18
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "13.12.
|
|
4
|
+
"version": "13.12.7",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -41,12 +41,12 @@
|
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@mozilla/readability": "^0.6",
|
|
44
|
-
"@oh-my-pi/omp-stats": "13.12.
|
|
45
|
-
"@oh-my-pi/pi-agent-core": "13.12.
|
|
46
|
-
"@oh-my-pi/pi-ai": "13.12.
|
|
47
|
-
"@oh-my-pi/pi-natives": "13.12.
|
|
48
|
-
"@oh-my-pi/pi-tui": "13.12.
|
|
49
|
-
"@oh-my-pi/pi-utils": "13.12.
|
|
44
|
+
"@oh-my-pi/omp-stats": "13.12.7",
|
|
45
|
+
"@oh-my-pi/pi-agent-core": "13.12.7",
|
|
46
|
+
"@oh-my-pi/pi-ai": "13.12.7",
|
|
47
|
+
"@oh-my-pi/pi-natives": "13.12.7",
|
|
48
|
+
"@oh-my-pi/pi-tui": "13.12.7",
|
|
49
|
+
"@oh-my-pi/pi-utils": "13.12.7",
|
|
50
50
|
"@sinclair/typebox": "^0.34",
|
|
51
51
|
"@xterm/headless": "^6.0",
|
|
52
52
|
"ajv": "^8.18",
|
|
@@ -1188,6 +1188,16 @@ export const SETTINGS_SCHEMA = {
|
|
|
1188
1188
|
ui: { tab: "tools", label: "MCP Project Config", description: "Load .mcp.json/mcp.json from project root" },
|
|
1189
1189
|
},
|
|
1190
1190
|
|
|
1191
|
+
"mcp.discoveryMode": {
|
|
1192
|
+
type: "boolean",
|
|
1193
|
+
default: false,
|
|
1194
|
+
ui: {
|
|
1195
|
+
tab: "tools",
|
|
1196
|
+
label: "MCP Tool Discovery",
|
|
1197
|
+
description: "Hide MCP tools by default and expose them through a tool discovery tool",
|
|
1198
|
+
},
|
|
1199
|
+
},
|
|
1200
|
+
|
|
1191
1201
|
"mcp.notifications": {
|
|
1192
1202
|
type: "boolean",
|
|
1193
1203
|
default: false,
|
|
@@ -1463,9 +1473,6 @@ export const SETTINGS_SCHEMA = {
|
|
|
1463
1473
|
ui: { tab: "providers", label: "Exa Websets", description: "Webset management and enrichment tools" },
|
|
1464
1474
|
},
|
|
1465
1475
|
|
|
1466
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
1467
|
-
// Advanced settings (no UI)
|
|
1468
|
-
// ────────────────────────────────────────────────────────────────────────
|
|
1469
1476
|
"commit.mapReduceEnabled": { type: "boolean", default: true },
|
|
1470
1477
|
|
|
1471
1478
|
"commit.mapReduceMinFiles": { type: "number", default: 4 },
|
package/src/discovery/claude.ts
CHANGED
|
@@ -89,6 +89,7 @@ async function loadMCPServers(ctx: LoadContext): Promise<LoadResult<MCPServer>>
|
|
|
89
89
|
const serverConfig = config as Record<string, unknown>;
|
|
90
90
|
return {
|
|
91
91
|
name,
|
|
92
|
+
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
92
93
|
command: serverConfig.command as string | undefined,
|
|
93
94
|
args: serverConfig.args as string[] | undefined,
|
|
94
95
|
env: serverConfig.env as Record<string, string> | undefined,
|
package/src/discovery/codex.ts
CHANGED
|
@@ -197,6 +197,10 @@ function extractMCPServersFromToml(toml: Record<string, unknown>): Record<string
|
|
|
197
197
|
}
|
|
198
198
|
// Note: validation of transport vs endpoint is handled by mcpCapability.validate()
|
|
199
199
|
|
|
200
|
+
// Map Codex tool_timeout_sec (seconds) to MCPServer timeout (milliseconds)
|
|
201
|
+
if (typeof config.tool_timeout_sec === "number" && config.tool_timeout_sec > 0) {
|
|
202
|
+
server.timeout = config.tool_timeout_sec * 1000;
|
|
203
|
+
}
|
|
200
204
|
result[name] = server;
|
|
201
205
|
}
|
|
202
206
|
|
package/src/discovery/cursor.ts
CHANGED
|
@@ -65,6 +65,7 @@ function parseMCPServers(
|
|
|
65
65
|
transport: ["stdio", "sse", "http"].includes(serverConfig.type as string)
|
|
66
66
|
? (serverConfig.type as "stdio" | "sse" | "http")
|
|
67
67
|
: undefined,
|
|
68
|
+
timeout: typeof serverConfig.timeout === "number" ? serverConfig.timeout : undefined,
|
|
68
69
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
69
70
|
});
|
|
70
71
|
}
|
package/src/discovery/gemini.ts
CHANGED
|
@@ -110,6 +110,7 @@ async function loadMCPFromSettings(
|
|
|
110
110
|
transport: ["stdio", "sse", "http"].includes(raw.type as string)
|
|
111
111
|
? (raw.type as "stdio" | "sse" | "http")
|
|
112
112
|
: undefined,
|
|
113
|
+
timeout: typeof raw.timeout === "number" ? raw.timeout : undefined,
|
|
113
114
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
114
115
|
} as MCPServer);
|
|
115
116
|
}
|
package/src/discovery/vscode.ts
CHANGED
|
@@ -94,6 +94,7 @@ async function loadMCPConfig(
|
|
|
94
94
|
transport: ["stdio", "sse", "http"].includes(expanded.transport as string)
|
|
95
95
|
? (expanded.transport as "stdio" | "sse" | "http")
|
|
96
96
|
: undefined,
|
|
97
|
+
timeout: typeof expanded.timeout === "number" ? expanded.timeout : undefined,
|
|
97
98
|
_source: createSourceMeta(PROVIDER_ID, path, level),
|
|
98
99
|
};
|
|
99
100
|
|
|
@@ -54,6 +54,7 @@ function parseServerConfig(
|
|
|
54
54
|
url: server.url as string | undefined,
|
|
55
55
|
headers: server.headers as Record<string, string> | undefined,
|
|
56
56
|
transport: server.type as "stdio" | "sse" | "http" | undefined,
|
|
57
|
+
timeout: typeof server.timeout === "number" ? server.timeout : undefined,
|
|
57
58
|
_source: createSourceMeta(PROVIDER_ID, path, scope),
|
|
58
59
|
},
|
|
59
60
|
};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { AgentTool } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
|
|
3
|
+
export interface DiscoverableMCPTool {
|
|
4
|
+
name: string;
|
|
5
|
+
label: string;
|
|
6
|
+
description: string;
|
|
7
|
+
serverName?: string;
|
|
8
|
+
mcpToolName?: string;
|
|
9
|
+
schemaKeys: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface DiscoverableMCPToolServerSummary {
|
|
13
|
+
name: string;
|
|
14
|
+
toolCount: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface DiscoverableMCPToolSummary {
|
|
18
|
+
servers: DiscoverableMCPToolServerSummary[];
|
|
19
|
+
toolCount: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function formatDiscoverableMCPToolServerSummary(server: DiscoverableMCPToolServerSummary): string {
|
|
23
|
+
const toolLabel = server.toolCount === 1 ? "tool" : "tools";
|
|
24
|
+
return `${server.name} (${server.toolCount} ${toolLabel})`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DiscoverableMCPSearchDocument {
|
|
28
|
+
tool: DiscoverableMCPTool;
|
|
29
|
+
termFrequencies: Map<string, number>;
|
|
30
|
+
length: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DiscoverableMCPSearchIndex {
|
|
34
|
+
documents: DiscoverableMCPSearchDocument[];
|
|
35
|
+
averageLength: number;
|
|
36
|
+
documentFrequencies: Map<string, number>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DiscoverableMCPSearchResult {
|
|
40
|
+
tool: DiscoverableMCPTool;
|
|
41
|
+
score: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const BM25_K1 = 1.2;
|
|
45
|
+
const BM25_B = 0.75;
|
|
46
|
+
const FIELD_WEIGHTS = {
|
|
47
|
+
name: 6,
|
|
48
|
+
label: 4,
|
|
49
|
+
serverName: 2,
|
|
50
|
+
mcpToolName: 4,
|
|
51
|
+
description: 2,
|
|
52
|
+
schemaKey: 1,
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
export function isMCPToolName(name: string): boolean {
|
|
56
|
+
return name.startsWith("mcp_");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function getSchemaPropertyKeys(parameters: unknown): string[] {
|
|
60
|
+
if (!parameters || typeof parameters !== "object" || Array.isArray(parameters)) return [];
|
|
61
|
+
const properties = (parameters as { properties?: unknown }).properties;
|
|
62
|
+
if (!properties || typeof properties !== "object" || Array.isArray(properties)) return [];
|
|
63
|
+
return Object.keys(properties as Record<string, unknown>).sort();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function tokenize(value: string): string[] {
|
|
67
|
+
return value
|
|
68
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
69
|
+
.replace(/[^a-zA-Z0-9]+/g, " ")
|
|
70
|
+
.toLowerCase()
|
|
71
|
+
.trim()
|
|
72
|
+
.split(/\s+/)
|
|
73
|
+
.filter(token => token.length > 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function addWeightedTokens(termFrequencies: Map<string, number>, value: string | undefined, weight: number): void {
|
|
77
|
+
if (!value) return;
|
|
78
|
+
for (const token of tokenize(value)) {
|
|
79
|
+
termFrequencies.set(token, (termFrequencies.get(token) ?? 0) + weight);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function buildSearchDocument(tool: DiscoverableMCPTool): DiscoverableMCPSearchDocument {
|
|
84
|
+
const termFrequencies = new Map<string, number>();
|
|
85
|
+
addWeightedTokens(termFrequencies, tool.name, FIELD_WEIGHTS.name);
|
|
86
|
+
addWeightedTokens(termFrequencies, tool.label, FIELD_WEIGHTS.label);
|
|
87
|
+
addWeightedTokens(termFrequencies, tool.serverName, FIELD_WEIGHTS.serverName);
|
|
88
|
+
addWeightedTokens(termFrequencies, tool.mcpToolName, FIELD_WEIGHTS.mcpToolName);
|
|
89
|
+
addWeightedTokens(termFrequencies, tool.description, FIELD_WEIGHTS.description);
|
|
90
|
+
for (const schemaKey of tool.schemaKeys) {
|
|
91
|
+
addWeightedTokens(termFrequencies, schemaKey, FIELD_WEIGHTS.schemaKey);
|
|
92
|
+
}
|
|
93
|
+
const length = Array.from(termFrequencies.values()).reduce((sum, value) => sum + value, 0);
|
|
94
|
+
return { tool, termFrequencies, length };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function getDiscoverableMCPTool(tool: AgentTool): DiscoverableMCPTool | null {
|
|
98
|
+
if (!isMCPToolName(tool.name)) return null;
|
|
99
|
+
const toolRecord = tool as AgentTool & {
|
|
100
|
+
label?: string;
|
|
101
|
+
description?: string;
|
|
102
|
+
mcpServerName?: string;
|
|
103
|
+
mcpToolName?: string;
|
|
104
|
+
parameters?: unknown;
|
|
105
|
+
};
|
|
106
|
+
return {
|
|
107
|
+
name: tool.name,
|
|
108
|
+
label: typeof toolRecord.label === "string" ? toolRecord.label : tool.name,
|
|
109
|
+
description: typeof toolRecord.description === "string" ? toolRecord.description : "",
|
|
110
|
+
serverName: typeof toolRecord.mcpServerName === "string" ? toolRecord.mcpServerName : undefined,
|
|
111
|
+
mcpToolName: typeof toolRecord.mcpToolName === "string" ? toolRecord.mcpToolName : undefined,
|
|
112
|
+
schemaKeys: getSchemaPropertyKeys(toolRecord.parameters),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function collectDiscoverableMCPTools(tools: Iterable<AgentTool>): DiscoverableMCPTool[] {
|
|
117
|
+
const discoverable: DiscoverableMCPTool[] = [];
|
|
118
|
+
for (const tool of tools) {
|
|
119
|
+
const metadata = getDiscoverableMCPTool(tool);
|
|
120
|
+
if (metadata) {
|
|
121
|
+
discoverable.push(metadata);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return discoverable;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function summarizeDiscoverableMCPTools(tools: DiscoverableMCPTool[]): DiscoverableMCPToolSummary {
|
|
128
|
+
const serverToolCounts = new Map<string, number>();
|
|
129
|
+
for (const tool of tools) {
|
|
130
|
+
if (!tool.serverName) continue;
|
|
131
|
+
serverToolCounts.set(tool.serverName, (serverToolCounts.get(tool.serverName) ?? 0) + 1);
|
|
132
|
+
}
|
|
133
|
+
const servers = Array.from(serverToolCounts.entries())
|
|
134
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
135
|
+
.map(([name, toolCount]) => ({ name, toolCount }));
|
|
136
|
+
return {
|
|
137
|
+
servers,
|
|
138
|
+
toolCount: tools.length,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildDiscoverableMCPSearchIndex(tools: Iterable<DiscoverableMCPTool>): DiscoverableMCPSearchIndex {
|
|
143
|
+
const documents = Array.from(tools, buildSearchDocument);
|
|
144
|
+
const averageLength = documents.reduce((sum, document) => sum + document.length, 0) / documents.length || 1;
|
|
145
|
+
const documentFrequencies = new Map<string, number>();
|
|
146
|
+
for (const document of documents) {
|
|
147
|
+
for (const token of new Set(document.termFrequencies.keys())) {
|
|
148
|
+
documentFrequencies.set(token, (documentFrequencies.get(token) ?? 0) + 1);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
documents,
|
|
153
|
+
averageLength,
|
|
154
|
+
documentFrequencies,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export function searchDiscoverableMCPTools(
|
|
159
|
+
index: DiscoverableMCPSearchIndex,
|
|
160
|
+
query: string,
|
|
161
|
+
limit: number,
|
|
162
|
+
): DiscoverableMCPSearchResult[] {
|
|
163
|
+
const queryTokens = tokenize(query);
|
|
164
|
+
if (queryTokens.length === 0) {
|
|
165
|
+
throw new Error("Query must contain at least one letter or number.");
|
|
166
|
+
}
|
|
167
|
+
if (index.documents.length === 0) {
|
|
168
|
+
return [];
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const queryTermCounts = new Map<string, number>();
|
|
172
|
+
for (const token of queryTokens) {
|
|
173
|
+
queryTermCounts.set(token, (queryTermCounts.get(token) ?? 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return index.documents
|
|
177
|
+
.map(document => {
|
|
178
|
+
let score = 0;
|
|
179
|
+
for (const [token, queryTermCount] of queryTermCounts) {
|
|
180
|
+
const termFrequency = document.termFrequencies.get(token) ?? 0;
|
|
181
|
+
if (termFrequency === 0) continue;
|
|
182
|
+
const documentFrequency = index.documentFrequencies.get(token) ?? 0;
|
|
183
|
+
const idf = Math.log(1 + (index.documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
184
|
+
const normalization = BM25_K1 * (1 - BM25_B + BM25_B * (document.length / index.averageLength));
|
|
185
|
+
score += queryTermCount * idf * ((termFrequency * (BM25_K1 + 1)) / (termFrequency + normalization));
|
|
186
|
+
}
|
|
187
|
+
return { tool: document.tool, score };
|
|
188
|
+
})
|
|
189
|
+
.filter(result => result.score > 0)
|
|
190
|
+
.sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name))
|
|
191
|
+
.slice(0, limit);
|
|
192
|
+
}
|
|
@@ -155,6 +155,13 @@ You **MUST** use the following tools, as effectively as possible, to complete th
|
|
|
155
155
|
{{/each}}
|
|
156
156
|
{{/if}}
|
|
157
157
|
|
|
158
|
+
{{#if mcpDiscoveryMode}}
|
|
159
|
+
### MCP tool discovery
|
|
160
|
+
|
|
161
|
+
Some MCP tools are intentionally hidden from the initial tool list.
|
|
162
|
+
{{#if hasMCPDiscoveryServers}}Discoverable MCP servers in this session: {{#list mcpDiscoveryServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
163
|
+
If the task may involve external systems, SaaS APIs, chat, tickets, databases, deployments, or other non-local integrations, you **SHOULD** call `search_tool_bm25` before concluding no such tool exists.
|
|
164
|
+
{{/if}}
|
|
158
165
|
## Precedence
|
|
159
166
|
{{#ifAny (includes tools "python") (includes tools "bash")}}
|
|
160
167
|
Pick the right tool for the job:
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Search hidden MCP tool metadata when MCP tool discovery is enabled.
|
|
2
|
+
|
|
3
|
+
Use this tool to discover MCP tools that are loaded into the session but not exposed to the model by default.
|
|
4
|
+
|
|
5
|
+
{{#if hasDiscoverableMCPServers}}Discoverable MCP servers in this session: {{#list discoverableMCPServerSummaries join=", "}}{{this}}{{/list}}.{{/if}}
|
|
6
|
+
{{#if discoverableMCPToolCount}}Total discoverable MCP tools loaded: {{discoverableMCPToolCount}}.{{/if}}
|
|
7
|
+
Input:
|
|
8
|
+
- `query` — required natural-language or keyword query
|
|
9
|
+
- `limit` — optional maximum number of tools to return and activate (default `8`)
|
|
10
|
+
|
|
11
|
+
Behavior:
|
|
12
|
+
- Searches hidden MCP tool metadata using BM25-style relevance ranking
|
|
13
|
+
- Matches against MCP tool name, server name, description, and input schema keys
|
|
14
|
+
- Activates the top matching MCP tools for the rest of the current session
|
|
15
|
+
- Repeated searches add to the active MCP tool set; they do not remove earlier selections
|
|
16
|
+
- Newly activated MCP tools become available before the next model call in the same overall turn
|
|
17
|
+
|
|
18
|
+
Notes:
|
|
19
|
+
- If you are unsure, start with `limit` between 5 and 10 to see a broader set of tools.
|
|
20
|
+
- `query` is matched against MCP tool metadata fields:
|
|
21
|
+
- `name`
|
|
22
|
+
- `label`
|
|
23
|
+
- `server_name`
|
|
24
|
+
- `mcp_tool_name`
|
|
25
|
+
- `description`
|
|
26
|
+
- input schema property keys (`schema_keys`)
|
|
27
|
+
|
|
28
|
+
This is not repository search, file search, or code search. Use it only for MCP tool discovery.
|
|
29
|
+
|
|
30
|
+
Returns JSON with:
|
|
31
|
+
- `query`
|
|
32
|
+
- `activated_tools` — MCP tools activated by this search call
|
|
33
|
+
- `match_count` — number of ranked matches returned by the search
|
|
34
|
+
- `total_tools`
|
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,
|
|
@@ -864,6 +871,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
864
871
|
getCompactContext: () => session.formatCompactContext(),
|
|
865
872
|
getTodoPhases: () => session.getTodoPhases(),
|
|
866
873
|
setTodoPhases: phases => session.setTodoPhases(phases),
|
|
874
|
+
isMCPDiscoveryEnabled: () => session.isMCPDiscoveryEnabled(),
|
|
875
|
+
getDiscoverableMCPTools: () => session.getDiscoverableMCPTools(),
|
|
876
|
+
getDiscoverableMCPSearchIndex: () => session.getDiscoverableMCPSearchIndex(),
|
|
877
|
+
getSelectedMCPToolNames: () => session.getSelectedMCPToolNames(),
|
|
878
|
+
activateDiscoveredMCPTools: toolNames => session.activateDiscoveredMCPTools(toolNames),
|
|
867
879
|
getCheckpointState: () => session.getCheckpointState(),
|
|
868
880
|
setCheckpointState: state => session.setCheckpointState(state ?? undefined),
|
|
869
881
|
allocateOutputArtifact: async toolType => {
|
|
@@ -1189,6 +1201,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1189
1201
|
const intentField = settings.get("tools.intentTracing") || $env.PI_INTENT_TRACING === "1" ? INTENT_FIELD : undefined;
|
|
1190
1202
|
const rebuildSystemPrompt = async (toolNames: string[], tools: Map<string, AgentTool>): Promise<string> => {
|
|
1191
1203
|
toolContextStore.setToolNames(toolNames);
|
|
1204
|
+
const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
|
|
1205
|
+
const discoverableMCPSummary = summarizeDiscoverableMCPTools(discoverableMCPTools);
|
|
1206
|
+
const hasDiscoverableMCPTools =
|
|
1207
|
+
mcpDiscoveryEnabled && toolNames.includes("search_tool_bm25") && discoverableMCPTools.length > 0;
|
|
1208
|
+
const promptTools = buildSystemPromptToolMetadata(tools, {
|
|
1209
|
+
search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
|
|
1210
|
+
});
|
|
1192
1211
|
const memoryInstructions = await buildMemoryToolDeveloperInstructions(agentDir, settings);
|
|
1193
1212
|
|
|
1194
1213
|
// Build combined append prompt: memory instructions + MCP server instructions
|
|
@@ -1214,14 +1233,16 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1214
1233
|
cwd,
|
|
1215
1234
|
skills,
|
|
1216
1235
|
contextFiles,
|
|
1217
|
-
tools,
|
|
1236
|
+
tools: promptTools,
|
|
1218
1237
|
toolNames,
|
|
1219
1238
|
rules: rulebookRules,
|
|
1220
1239
|
skillsSettings: settings.getGroup("skills"),
|
|
1221
1240
|
appendSystemPrompt: appendPrompt,
|
|
1222
1241
|
repeatToolDescriptions,
|
|
1223
|
-
eagerTasks,
|
|
1224
1242
|
intentField,
|
|
1243
|
+
mcpDiscoveryMode: hasDiscoverableMCPTools,
|
|
1244
|
+
mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
|
|
1245
|
+
eagerTasks,
|
|
1225
1246
|
});
|
|
1226
1247
|
|
|
1227
1248
|
if (options.systemPrompt === undefined) {
|
|
@@ -1232,15 +1253,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1232
1253
|
cwd,
|
|
1233
1254
|
skills,
|
|
1234
1255
|
contextFiles,
|
|
1235
|
-
tools,
|
|
1256
|
+
tools: promptTools,
|
|
1236
1257
|
toolNames,
|
|
1237
1258
|
rules: rulebookRules,
|
|
1238
1259
|
skillsSettings: settings.getGroup("skills"),
|
|
1239
1260
|
customPrompt: options.systemPrompt,
|
|
1240
1261
|
appendSystemPrompt: appendPrompt,
|
|
1241
1262
|
repeatToolDescriptions,
|
|
1242
|
-
eagerTasks,
|
|
1243
1263
|
intentField,
|
|
1264
|
+
mcpDiscoveryMode: hasDiscoverableMCPTools,
|
|
1265
|
+
mcpDiscoveryServerSummaries: discoverableMCPSummary.servers.map(formatDiscoverableMCPToolServerSummary),
|
|
1266
|
+
eagerTasks,
|
|
1244
1267
|
});
|
|
1245
1268
|
}
|
|
1246
1269
|
return options.systemPrompt(defaultPrompt);
|
|
@@ -1250,9 +1273,17 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1250
1273
|
const requestedToolNames = options.toolNames?.map(name => name.toLowerCase()) ?? toolNamesFromRegistry;
|
|
1251
1274
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1252
1275
|
const includeExitPlanMode = requestedToolNames.includes("exit_plan_mode");
|
|
1253
|
-
const
|
|
1276
|
+
const mcpDiscoveryEnabled = settings.get("mcp.discoveryMode") ?? false;
|
|
1277
|
+
const requestedActiveToolNames = includeExitPlanMode
|
|
1254
1278
|
? normalizedRequested
|
|
1255
1279
|
: normalizedRequested.filter(name => name !== "exit_plan_mode");
|
|
1280
|
+
const explicitlyRequestedMCPToolNames = options.toolNames
|
|
1281
|
+
? requestedActiveToolNames.filter(name => name.startsWith("mcp_"))
|
|
1282
|
+
: [];
|
|
1283
|
+
const initialToolNames = mcpDiscoveryEnabled
|
|
1284
|
+
? [...requestedActiveToolNames.filter(name => !name.startsWith("mcp_")), ...explicitlyRequestedMCPToolNames]
|
|
1285
|
+
: [...requestedActiveToolNames];
|
|
1286
|
+
const initialSelectedMCPToolNames = mcpDiscoveryEnabled ? [...explicitlyRequestedMCPToolNames] : [];
|
|
1256
1287
|
|
|
1257
1288
|
// Custom tools and extension-registered tools are always included regardless of toolNames filter
|
|
1258
1289
|
const alwaysInclude: string[] = [
|
|
@@ -1260,6 +1291,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1260
1291
|
...registeredTools.map(t => t.definition.name),
|
|
1261
1292
|
];
|
|
1262
1293
|
for (const name of alwaysInclude) {
|
|
1294
|
+
if (mcpDiscoveryEnabled && name.startsWith("mcp_")) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1263
1297
|
if (toolRegistry.has(name) && !initialToolNames.includes(name)) {
|
|
1264
1298
|
initialToolNames.push(name);
|
|
1265
1299
|
}
|
|
@@ -1440,6 +1474,8 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1440
1474
|
onPayload,
|
|
1441
1475
|
convertToLlm: convertToLlmFinal,
|
|
1442
1476
|
rebuildSystemPrompt,
|
|
1477
|
+
mcpDiscoveryEnabled,
|
|
1478
|
+
initialSelectedMCPToolNames,
|
|
1443
1479
|
ttsrManager,
|
|
1444
1480
|
obfuscator,
|
|
1445
1481
|
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
|
|
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/index.ts
CHANGED
|
@@ -8,6 +8,7 @@ import type { InternalUrlRouter } from "../internal-urls";
|
|
|
8
8
|
import { getPreludeDocs, warmPythonEnvironment } from "../ipy/executor";
|
|
9
9
|
import { checkPythonKernelAvailability } from "../ipy/kernel";
|
|
10
10
|
import { LspTool } from "../lsp";
|
|
11
|
+
import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
11
12
|
import { EditTool } from "../patch";
|
|
12
13
|
import type { PlanModeState } from "../plan-mode/state";
|
|
13
14
|
import { TaskTool } from "../task";
|
|
@@ -35,6 +36,7 @@ import { ReadTool } from "./read";
|
|
|
35
36
|
import { RenderMermaidTool } from "./render-mermaid";
|
|
36
37
|
import { ResolveTool } from "./resolve";
|
|
37
38
|
import { reportFindingTool } from "./review";
|
|
39
|
+
import { SearchToolBm25Tool } from "./search-tool-bm25";
|
|
38
40
|
import { loadSshTool } from "./ssh";
|
|
39
41
|
import { SubmitResultTool } from "./submit-result";
|
|
40
42
|
import { type TodoPhase, TodoWriteTool } from "./todo-write";
|
|
@@ -71,6 +73,7 @@ export * from "./read";
|
|
|
71
73
|
export * from "./render-mermaid";
|
|
72
74
|
export * from "./resolve";
|
|
73
75
|
export * from "./review";
|
|
76
|
+
export * from "./search-tool-bm25";
|
|
74
77
|
export * from "./ssh";
|
|
75
78
|
export * from "./submit-result";
|
|
76
79
|
export * from "./todo-write";
|
|
@@ -85,6 +88,8 @@ export type ContextFileEntry = {
|
|
|
85
88
|
depth?: number;
|
|
86
89
|
};
|
|
87
90
|
|
|
91
|
+
export type { DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
92
|
+
|
|
88
93
|
/** Session context for tool factories */
|
|
89
94
|
export interface ToolSession {
|
|
90
95
|
/** Current working directory */
|
|
@@ -147,6 +152,16 @@ export interface ToolSession {
|
|
|
147
152
|
getTodoPhases?: () => TodoPhase[];
|
|
148
153
|
/** Replace cached todo phases for this session. */
|
|
149
154
|
setTodoPhases?: (phases: TodoPhase[]) => void;
|
|
155
|
+
/** Whether MCP tool discovery is active for this session. */
|
|
156
|
+
isMCPDiscoveryEnabled?: () => boolean;
|
|
157
|
+
/** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks. */
|
|
158
|
+
getDiscoverableMCPTools?: () => DiscoverableMCPTool[];
|
|
159
|
+
/** Get the cached discoverable MCP search index for search_tool_bm25 execution. */
|
|
160
|
+
getDiscoverableMCPSearchIndex?: () => DiscoverableMCPSearchIndex;
|
|
161
|
+
/** Get MCP tools activated by prior search_tool_bm25 calls. */
|
|
162
|
+
getSelectedMCPToolNames?: () => string[];
|
|
163
|
+
/** Merge MCP tool selections into the active session tool set. */
|
|
164
|
+
activateDiscoveredMCPTools?: (toolNames: string[]) => Promise<string[]>;
|
|
150
165
|
/** Pending action store for preview/apply workflows */
|
|
151
166
|
pendingActionStore?: import("./pending-action").PendingActionStore;
|
|
152
167
|
/** Get active checkpoint state if any. */
|
|
@@ -182,6 +197,7 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
|
182
197
|
todo_write: s => new TodoWriteTool(s),
|
|
183
198
|
fetch: s => new FetchTool(s),
|
|
184
199
|
web_search: s => new SearchTool(s),
|
|
200
|
+
search_tool_bm25: SearchToolBm25Tool.createIf,
|
|
185
201
|
write: s => new WriteTool(s),
|
|
186
202
|
};
|
|
187
203
|
|
|
@@ -319,6 +335,7 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
319
335
|
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
320
336
|
if (name === "fetch") return session.settings.get("fetch.enabled");
|
|
321
337
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
338
|
+
if (name === "search_tool_bm25") return session.settings.get("mcp.discoveryMode");
|
|
322
339
|
if (name === "lsp") return session.settings.get("lsp.enabled");
|
|
323
340
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
324
341
|
if (name === "browser") return session.settings.get("browser.enabled");
|
package/src/tools/renderers.ts
CHANGED
|
@@ -23,6 +23,7 @@ import { notebookToolRenderer } from "./notebook";
|
|
|
23
23
|
import { pythonToolRenderer } from "./python";
|
|
24
24
|
import { readToolRenderer } from "./read";
|
|
25
25
|
import { resolveToolRenderer } from "./resolve";
|
|
26
|
+
import { searchToolBm25Renderer } from "./search-tool-bm25";
|
|
26
27
|
import { sshToolRenderer } from "./ssh";
|
|
27
28
|
import { todoWriteToolRenderer } from "./todo-write";
|
|
28
29
|
import { writeToolRenderer } from "./write";
|
|
@@ -55,6 +56,7 @@ export const toolRenderers: Record<string, ToolRenderer> = {
|
|
|
55
56
|
inspect_image: inspectImageToolRenderer as ToolRenderer,
|
|
56
57
|
read: readToolRenderer as ToolRenderer,
|
|
57
58
|
resolve: resolveToolRenderer as ToolRenderer,
|
|
59
|
+
search_tool_bm25: searchToolBm25Renderer as ToolRenderer,
|
|
58
60
|
ssh: sshToolRenderer as ToolRenderer,
|
|
59
61
|
task: taskToolRenderer as ToolRenderer,
|
|
60
62
|
todo_write: todoWriteToolRenderer as ToolRenderer,
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
|
|
2
|
+
import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
3
|
+
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
+
import { renderPromptTemplate } from "../config/prompt-templates";
|
|
5
|
+
import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
6
|
+
import {
|
|
7
|
+
buildDiscoverableMCPSearchIndex,
|
|
8
|
+
type DiscoverableMCPSearchIndex,
|
|
9
|
+
type DiscoverableMCPTool,
|
|
10
|
+
formatDiscoverableMCPToolServerSummary,
|
|
11
|
+
searchDiscoverableMCPTools,
|
|
12
|
+
summarizeDiscoverableMCPTools,
|
|
13
|
+
} from "../mcp/discoverable-tool-metadata";
|
|
14
|
+
import type { Theme } from "../modes/theme/theme";
|
|
15
|
+
import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
|
|
16
|
+
import { renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
17
|
+
import type { ToolSession } from ".";
|
|
18
|
+
import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
19
|
+
import { ToolError } from "./tool-errors";
|
|
20
|
+
|
|
21
|
+
const DEFAULT_LIMIT = 8;
|
|
22
|
+
const TOOL_DISCOVERY_TITLE = "Tool Discovery";
|
|
23
|
+
const COLLAPSED_MATCH_LIMIT = 5;
|
|
24
|
+
const MATCH_LABEL_LEN = 72;
|
|
25
|
+
const MATCH_DESCRIPTION_LEN = 96;
|
|
26
|
+
|
|
27
|
+
const searchToolBm25Schema = Type.Object({
|
|
28
|
+
query: Type.String({ description: "Search query for hidden MCP tool metadata" }),
|
|
29
|
+
limit: Type.Optional(
|
|
30
|
+
Type.Integer({ description: "Max matching tools to activate and return (default 8)", minimum: 1 }),
|
|
31
|
+
),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
type SearchToolBm25Params = Static<typeof searchToolBm25Schema>;
|
|
35
|
+
|
|
36
|
+
interface SearchToolBm25Match {
|
|
37
|
+
name: string;
|
|
38
|
+
label: string;
|
|
39
|
+
description: string;
|
|
40
|
+
server_name?: string;
|
|
41
|
+
mcp_tool_name?: string;
|
|
42
|
+
schema_keys: string[];
|
|
43
|
+
score: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SearchToolBm25Details {
|
|
47
|
+
query: string;
|
|
48
|
+
limit: number;
|
|
49
|
+
total_tools: number;
|
|
50
|
+
activated_tools: string[];
|
|
51
|
+
active_selected_tools: string[];
|
|
52
|
+
tools: SearchToolBm25Match[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function formatMatch(tool: DiscoverableMCPTool, score: number): SearchToolBm25Match {
|
|
56
|
+
return {
|
|
57
|
+
name: tool.name,
|
|
58
|
+
label: tool.label,
|
|
59
|
+
description: tool.description,
|
|
60
|
+
server_name: tool.serverName,
|
|
61
|
+
mcp_tool_name: tool.mcpToolName,
|
|
62
|
+
schema_keys: tool.schemaKeys,
|
|
63
|
+
score: Number(score.toFixed(6)),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function buildSearchToolBm25Content(details: SearchToolBm25Details): string {
|
|
68
|
+
return JSON.stringify({
|
|
69
|
+
query: details.query,
|
|
70
|
+
activated_tools: details.activated_tools,
|
|
71
|
+
match_count: details.tools.length,
|
|
72
|
+
total_tools: details.total_tools,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getDiscoverableMCPToolsForDescription(session: ToolSession): DiscoverableMCPTool[] {
|
|
77
|
+
try {
|
|
78
|
+
return session.getDiscoverableMCPTools?.() ?? [];
|
|
79
|
+
} catch {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function getDiscoverableMCPSearchIndexForExecution(session: ToolSession): DiscoverableMCPSearchIndex {
|
|
85
|
+
try {
|
|
86
|
+
const cached = session.getDiscoverableMCPSearchIndex?.();
|
|
87
|
+
if (cached) return cached;
|
|
88
|
+
} catch {}
|
|
89
|
+
return buildDiscoverableMCPSearchIndex(session.getDiscoverableMCPTools?.() ?? []);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
type MCPDiscoveryExecutionSession = ToolSession & {
|
|
93
|
+
isMCPDiscoveryEnabled: () => boolean;
|
|
94
|
+
getSelectedMCPToolNames: () => string[];
|
|
95
|
+
activateDiscoveredMCPTools: (toolNames: string[]) => Promise<string[]>;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
function supportsMCPToolDiscoveryExecution(session: ToolSession): session is MCPDiscoveryExecutionSession {
|
|
99
|
+
return (
|
|
100
|
+
typeof session.isMCPDiscoveryEnabled === "function" &&
|
|
101
|
+
typeof session.getSelectedMCPToolNames === "function" &&
|
|
102
|
+
typeof session.activateDiscoveredMCPTools === "function"
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function renderSearchToolBm25Description(discoverableTools: DiscoverableMCPTool[] = []): string {
|
|
107
|
+
const summary = summarizeDiscoverableMCPTools(discoverableTools);
|
|
108
|
+
return renderPromptTemplate(searchToolBm25Description, {
|
|
109
|
+
discoverableMCPToolCount: summary.toolCount,
|
|
110
|
+
discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableMCPToolServerSummary),
|
|
111
|
+
hasDiscoverableMCPServers: summary.servers.length > 0,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function renderMatchLines(match: SearchToolBm25Match, theme: Theme): string[] {
|
|
116
|
+
const safeServerName = match.server_name ? replaceTabs(match.server_name) : undefined;
|
|
117
|
+
const safeLabel = replaceTabs(match.label);
|
|
118
|
+
const safeDescription = replaceTabs(match.description.trim());
|
|
119
|
+
const metaParts: string[] = [];
|
|
120
|
+
if (safeServerName) metaParts.push(theme.fg("muted", safeServerName));
|
|
121
|
+
metaParts.push(theme.fg("dim", `score ${match.score.toFixed(3)}`));
|
|
122
|
+
const metaSep = theme.fg("dim", theme.sep.dot);
|
|
123
|
+
const metaSuffix = metaParts.length > 0 ? ` ${metaParts.join(metaSep)}` : "";
|
|
124
|
+
const lines = [`${theme.fg("accent", truncateToWidth(safeLabel, MATCH_LABEL_LEN))}${metaSuffix}`];
|
|
125
|
+
if (safeDescription) {
|
|
126
|
+
lines.push(theme.fg("muted", truncateToWidth(safeDescription, MATCH_DESCRIPTION_LEN)));
|
|
127
|
+
}
|
|
128
|
+
return lines;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function renderFallbackResult(text: string, theme: Theme): Component {
|
|
132
|
+
const header = renderStatusLine({ icon: "warning", title: TOOL_DISCOVERY_TITLE }, theme);
|
|
133
|
+
const bodyLines = (text || "Tool discovery completed")
|
|
134
|
+
.split("\n")
|
|
135
|
+
.map(line => theme.fg("dim", truncateToWidth(replaceTabs(line), TRUNCATE_LENGTHS.LINE)));
|
|
136
|
+
return new Text([header, ...bodyLines].join("\n"), 0, 0);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
|
|
140
|
+
readonly name = "search_tool_bm25";
|
|
141
|
+
readonly label = "SearchToolBm25";
|
|
142
|
+
get description(): string {
|
|
143
|
+
return renderSearchToolBm25Description(getDiscoverableMCPToolsForDescription(this.session));
|
|
144
|
+
}
|
|
145
|
+
readonly parameters = searchToolBm25Schema;
|
|
146
|
+
readonly strict = true;
|
|
147
|
+
|
|
148
|
+
constructor(private readonly session: ToolSession) {}
|
|
149
|
+
|
|
150
|
+
static createIf(session: ToolSession): SearchToolBm25Tool | null {
|
|
151
|
+
if (!session.settings.get("mcp.discoveryMode")) return null;
|
|
152
|
+
return supportsMCPToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async execute(
|
|
156
|
+
_toolCallId: string,
|
|
157
|
+
params: SearchToolBm25Params,
|
|
158
|
+
_signal?: AbortSignal,
|
|
159
|
+
_onUpdate?: AgentToolUpdateCallback<SearchToolBm25Details>,
|
|
160
|
+
_context?: AgentToolContext,
|
|
161
|
+
): Promise<AgentToolResult<SearchToolBm25Details>> {
|
|
162
|
+
if (!supportsMCPToolDiscoveryExecution(this.session)) {
|
|
163
|
+
throw new ToolError("MCP tool discovery is unavailable in this session.");
|
|
164
|
+
}
|
|
165
|
+
if (!this.session.isMCPDiscoveryEnabled()) {
|
|
166
|
+
throw new ToolError("MCP tool discovery is disabled. Enable mcp.discoveryMode to use search_tool_bm25.");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const query = params.query.trim();
|
|
170
|
+
if (query.length === 0) {
|
|
171
|
+
throw new ToolError("Query is required and must not be empty.");
|
|
172
|
+
}
|
|
173
|
+
const limit = params.limit ?? DEFAULT_LIMIT;
|
|
174
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
175
|
+
throw new ToolError("Limit must be a positive integer.");
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const searchIndex = getDiscoverableMCPSearchIndexForExecution(this.session);
|
|
179
|
+
const selectedToolNames = new Set(this.session.getSelectedMCPToolNames());
|
|
180
|
+
let ranked: Array<{ tool: DiscoverableMCPTool; score: number }> = [];
|
|
181
|
+
try {
|
|
182
|
+
ranked = searchDiscoverableMCPTools(searchIndex, query, searchIndex.documents.length)
|
|
183
|
+
.filter(result => !selectedToolNames.has(result.tool.name))
|
|
184
|
+
.slice(0, limit);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
if (error instanceof Error) {
|
|
187
|
+
throw new ToolError(error.message);
|
|
188
|
+
}
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
const activated =
|
|
192
|
+
ranked.length > 0 ? await this.session.activateDiscoveredMCPTools(ranked.map(result => result.tool.name)) : [];
|
|
193
|
+
|
|
194
|
+
const details: SearchToolBm25Details = {
|
|
195
|
+
query,
|
|
196
|
+
limit,
|
|
197
|
+
total_tools: searchIndex.documents.length,
|
|
198
|
+
activated_tools: activated,
|
|
199
|
+
active_selected_tools: this.session.getSelectedMCPToolNames(),
|
|
200
|
+
tools: ranked.map(result => formatMatch(result.tool, result.score)),
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
content: [{ type: "text", text: buildSearchToolBm25Content(details) }],
|
|
205
|
+
details,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export const searchToolBm25Renderer = {
|
|
211
|
+
renderCall(args: SearchToolBm25Params, _options: RenderResultOptions, uiTheme: Theme): Component {
|
|
212
|
+
const query = typeof args.query === "string" ? replaceTabs(args.query.trim()) : "";
|
|
213
|
+
const meta = args.limit ? [`limit:${args.limit}`] : [];
|
|
214
|
+
return new Text(
|
|
215
|
+
renderStatusLine(
|
|
216
|
+
{ icon: "pending", title: TOOL_DISCOVERY_TITLE, description: query || "(empty query)", meta },
|
|
217
|
+
uiTheme,
|
|
218
|
+
),
|
|
219
|
+
0,
|
|
220
|
+
0,
|
|
221
|
+
);
|
|
222
|
+
},
|
|
223
|
+
|
|
224
|
+
renderResult(
|
|
225
|
+
result: { content: Array<{ type: string; text?: string }>; details?: SearchToolBm25Details; isError?: boolean },
|
|
226
|
+
options: RenderResultOptions,
|
|
227
|
+
uiTheme: Theme,
|
|
228
|
+
): Component {
|
|
229
|
+
if (!result.details) {
|
|
230
|
+
const fallbackText = result.content
|
|
231
|
+
.filter(part => part.type === "text")
|
|
232
|
+
.map(part => part.text)
|
|
233
|
+
.filter((text): text is string => typeof text === "string" && text.length > 0)
|
|
234
|
+
.join("\n");
|
|
235
|
+
return renderFallbackResult(fallbackText, uiTheme);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const { details } = result;
|
|
239
|
+
const meta = [
|
|
240
|
+
formatCount("match", details.tools.length),
|
|
241
|
+
`${details.active_selected_tools.length} active`,
|
|
242
|
+
`${details.total_tools} total`,
|
|
243
|
+
`limit:${details.limit}`,
|
|
244
|
+
];
|
|
245
|
+
const safeQuery = replaceTabs(details.query);
|
|
246
|
+
const header = renderStatusLine(
|
|
247
|
+
{
|
|
248
|
+
icon: details.tools.length > 0 ? "success" : "warning",
|
|
249
|
+
title: TOOL_DISCOVERY_TITLE,
|
|
250
|
+
description: truncateToWidth(safeQuery, MATCH_LABEL_LEN),
|
|
251
|
+
meta,
|
|
252
|
+
},
|
|
253
|
+
uiTheme,
|
|
254
|
+
);
|
|
255
|
+
if (details.tools.length === 0) {
|
|
256
|
+
const emptyMessage =
|
|
257
|
+
details.total_tools === 0 ? "No discoverable MCP tools are currently loaded." : "No matching tools found.";
|
|
258
|
+
return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const lines = [header];
|
|
262
|
+
const treeLines = renderTreeList(
|
|
263
|
+
{
|
|
264
|
+
items: details.tools,
|
|
265
|
+
expanded: options.expanded,
|
|
266
|
+
maxCollapsed: COLLAPSED_MATCH_LIMIT,
|
|
267
|
+
itemType: "tool",
|
|
268
|
+
renderItem: match => renderMatchLines(match, uiTheme),
|
|
269
|
+
},
|
|
270
|
+
uiTheme,
|
|
271
|
+
);
|
|
272
|
+
lines.push(...treeLines);
|
|
273
|
+
return new Text(lines.join("\n"), 0, 0);
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
mergeCallAndResult: true,
|
|
277
|
+
inline: true,
|
|
278
|
+
};
|