@oh-my-pi/pi-coding-agent 14.7.1 → 14.7.2
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 +15 -0
- package/package.json +7 -7
- package/src/config/model-equivalence.ts +1 -0
- package/src/config/model-registry.ts +108 -22
- package/src/config/settings-schema.ts +36 -1
- package/src/discovery/helpers.ts +4 -3
- package/src/edit/index.ts +1 -0
- package/src/eval/py/gateway-coordinator.ts +2 -3
- package/src/eval/py/runtime.ts +1 -0
- package/src/internal-urls/docs-index.generated.ts +1 -1
- package/src/lsp/index.ts +2 -0
- package/src/mcp/discoverable-tool-metadata.ts +24 -202
- package/src/modes/components/extensions/extension-dashboard.ts +26 -2
- package/src/modes/components/extensions/state-manager.ts +41 -0
- package/src/modes/controllers/selector-controller.ts +3 -0
- package/src/modes/interactive-mode.ts +26 -1
- package/src/prompts/tools/search-tool-bm25.md +14 -14
- package/src/prompts/tools/todo-write.md +1 -0
- package/src/sdk.ts +69 -8
- package/src/session/agent-session.ts +177 -1
- package/src/slash-commands/builtin-registry.ts +11 -0
- package/src/task/index.ts +2 -0
- package/src/tool-discovery/tool-index.ts +377 -0
- package/src/tools/ask.ts +2 -0
- package/src/tools/ast-edit.ts +2 -0
- package/src/tools/ast-grep.ts +2 -0
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser.ts +2 -0
- package/src/tools/calculator.ts +2 -0
- package/src/tools/checkpoint.ts +4 -0
- package/src/tools/debug.ts +2 -0
- package/src/tools/eval.ts +2 -0
- package/src/tools/find.ts +2 -0
- package/src/tools/gh.ts +2 -0
- package/src/tools/hindsight-recall.ts +2 -0
- package/src/tools/hindsight-reflect.ts +2 -0
- package/src/tools/hindsight-retain.ts +2 -0
- package/src/tools/index.ts +74 -14
- package/src/tools/inspect-image.ts +2 -0
- package/src/tools/irc.ts +2 -1
- package/src/tools/job.ts +2 -1
- package/src/tools/notebook.ts +2 -0
- package/src/tools/read.ts +1 -0
- package/src/tools/recipe/index.ts +2 -0
- package/src/tools/render-mermaid.ts +2 -0
- package/src/tools/search-tool-bm25.ts +128 -42
- package/src/tools/search.ts +2 -0
- package/src/tools/ssh.ts +2 -0
- package/src/tools/todo-write.ts +2 -1
- package/src/tools/write.ts +2 -0
- package/src/web/search/index.ts +2 -0
- package/src/web/search/providers/searxng.ts +8 -0
package/src/tools/index.ts
CHANGED
|
@@ -10,13 +10,13 @@ import type { Skill } from "../extensibility/skills";
|
|
|
10
10
|
import type { HindsightSessionState } from "../hindsight/state";
|
|
11
11
|
import type { InternalUrlRouter } from "../internal-urls";
|
|
12
12
|
import { LspTool } from "../lsp";
|
|
13
|
-
import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
14
13
|
import type { PlanModeState } from "../plan-mode/state";
|
|
15
14
|
import type { AgentRegistry } from "../registry/agent-registry";
|
|
16
15
|
import type { CustomMessage } from "../session/messages";
|
|
17
16
|
import type { ToolChoiceQueue } from "../session/tool-choice-queue";
|
|
18
17
|
import { TaskTool } from "../task";
|
|
19
18
|
import type { AgentOutputManager } from "../task/output-manager";
|
|
19
|
+
import type { DiscoverableTool, DiscoverableToolSearchIndex } from "../tool-discovery/tool-index";
|
|
20
20
|
import type { EventBus } from "../utils/event-bus";
|
|
21
21
|
import { WebSearchTool } from "../web/search";
|
|
22
22
|
import { AskTool } from "./ask";
|
|
@@ -105,6 +105,12 @@ export type ContextFileEntry = {
|
|
|
105
105
|
};
|
|
106
106
|
|
|
107
107
|
export type { DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
108
|
+
export type {
|
|
109
|
+
DiscoverableTool,
|
|
110
|
+
DiscoverableToolSearchIndex,
|
|
111
|
+
DiscoverableToolSearchResult,
|
|
112
|
+
DiscoverableToolSource,
|
|
113
|
+
} from "../tool-discovery/tool-index";
|
|
108
114
|
|
|
109
115
|
/** Session context for tool factories */
|
|
110
116
|
export interface ToolSession {
|
|
@@ -184,14 +190,29 @@ export interface ToolSession {
|
|
|
184
190
|
setTodoPhases?: (phases: TodoPhase[]) => void;
|
|
185
191
|
/** Whether MCP tool discovery is active for this session. */
|
|
186
192
|
isMCPDiscoveryEnabled?: () => boolean;
|
|
187
|
-
/** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
193
|
+
/** Get hidden-but-discoverable MCP tools for search_tool_bm25 prompts and fallbacks.
|
|
194
|
+
* @deprecated Use getDiscoverableTools with source filter instead. */
|
|
195
|
+
getDiscoverableMCPTools?: () => import("../mcp/discoverable-tool-metadata").DiscoverableMCPTool[];
|
|
196
|
+
/** Get the cached discoverable MCP search index for search_tool_bm25 execution.
|
|
197
|
+
* @deprecated Use getDiscoverableToolSearchIndex instead. */
|
|
198
|
+
getDiscoverableMCPSearchIndex?: () => import("../tool-discovery/tool-index").DiscoverableMCPSearchIndex;
|
|
191
199
|
/** Get MCP tools activated by prior search_tool_bm25 calls. */
|
|
192
200
|
getSelectedMCPToolNames?: () => string[];
|
|
193
201
|
/** Merge MCP tool selections into the active session tool set. */
|
|
194
202
|
activateDiscoveredMCPTools?: (toolNames: string[]) => Promise<string[]>;
|
|
203
|
+
// ── Generic tool discovery (unified — covers built-in + MCP + extension) ──
|
|
204
|
+
/** Whether any form of tool discovery is active (tools.discoveryMode !== "off" or mcp.discoveryMode). */
|
|
205
|
+
isToolDiscoveryEnabled?: () => boolean;
|
|
206
|
+
/** Get all hidden-but-discoverable tools for search_tool_bm25 prompts. */
|
|
207
|
+
getDiscoverableTools?: (filter?: {
|
|
208
|
+
source?: import("../tool-discovery/tool-index").DiscoverableToolSource;
|
|
209
|
+
}) => DiscoverableTool[];
|
|
210
|
+
/** Get the cached generic discoverable search index. */
|
|
211
|
+
getDiscoverableToolSearchIndex?: () => DiscoverableToolSearchIndex;
|
|
212
|
+
/** Get tool names activated by prior search_tool_bm25 calls (all sources). */
|
|
213
|
+
getSelectedDiscoveredToolNames?: () => string[];
|
|
214
|
+
/** Merge tool selections into the active session tool set. */
|
|
215
|
+
activateDiscoveredTools?: (toolNames: string[]) => Promise<string[]>;
|
|
195
216
|
/** The tool-choice queue used to force forthcoming tool invocations and carry invocation handlers. */
|
|
196
217
|
getToolChoiceQueue?(): ToolChoiceQueue;
|
|
197
218
|
/** Build a model-provider-specific ToolChoice that targets the named tool, or undefined if unsupported. */
|
|
@@ -209,25 +230,48 @@ export interface ToolSession {
|
|
|
209
230
|
queueDeferredMessage?(message: CustomMessage): void;
|
|
210
231
|
}
|
|
211
232
|
|
|
212
|
-
type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
233
|
+
export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool | null>;
|
|
213
234
|
|
|
235
|
+
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
|
236
|
+
|
|
237
|
+
/** Default essential tool names when tools.essentialOverride is empty. */
|
|
238
|
+
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the active essential built-in tool names from settings.
|
|
242
|
+
* Returns `tools.essentialOverride` if non-empty (filtered to known built-ins),
|
|
243
|
+
* otherwise `DEFAULT_ESSENTIAL_TOOL_NAMES`.
|
|
244
|
+
*/
|
|
245
|
+
export function computeEssentialBuiltinNames(settings: Settings): string[] {
|
|
246
|
+
const override = settings.get("tools.essentialOverride") ?? [];
|
|
247
|
+
const cleaned = override.map(name => name.trim()).filter(Boolean);
|
|
248
|
+
if (cleaned.length > 0) {
|
|
249
|
+
return cleaned.filter(name => name in BUILTIN_TOOLS);
|
|
250
|
+
}
|
|
251
|
+
return [...DEFAULT_ESSENTIAL_TOOL_NAMES];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Public callable factory map. External callers may invoke `BUILTIN_TOOLS.read(session)` or
|
|
256
|
+
* `BUILTIN_TOOLS[name](session)` to construct a tool directly.
|
|
257
|
+
*/
|
|
214
258
|
export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
|
259
|
+
read: s => new ReadTool(s),
|
|
260
|
+
bash: s => new BashTool(s),
|
|
261
|
+
edit: s => new EditTool(s),
|
|
215
262
|
ast_grep: s => new AstGrepTool(s),
|
|
216
263
|
ast_edit: s => new AstEditTool(s),
|
|
217
264
|
render_mermaid: s => new RenderMermaidTool(s),
|
|
218
265
|
ask: AskTool.createIf,
|
|
219
|
-
bash: s => new BashTool(s),
|
|
220
266
|
debug: DebugTool.createIf,
|
|
221
267
|
eval: s => new EvalTool(s),
|
|
222
268
|
calc: s => new CalculatorTool(s),
|
|
223
269
|
ssh: loadSshTool,
|
|
224
|
-
edit: s => new EditTool(s),
|
|
225
270
|
github: GithubTool.createIf,
|
|
226
271
|
find: s => new FindTool(s),
|
|
227
272
|
search: s => new SearchTool(s),
|
|
228
273
|
lsp: LspTool.createIf,
|
|
229
274
|
notebook: s => new NotebookTool(s),
|
|
230
|
-
read: s => new ReadTool(s),
|
|
231
275
|
inspect_image: s => new InspectImageTool(s),
|
|
232
276
|
browser: s => new BrowserTool(s),
|
|
233
277
|
checkpoint: CheckpointTool.createIf,
|
|
@@ -299,7 +343,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
299
343
|
const enableLsp = session.enableLsp ?? true;
|
|
300
344
|
const requestedTools =
|
|
301
345
|
toolNames && toolNames.length > 0 ? [...new Set(toolNames.map(name => name.toLowerCase()))] : undefined;
|
|
302
|
-
|
|
346
|
+
const planEnabled = session.settings.get("plan.enabled");
|
|
347
|
+
if (planEnabled && requestedTools && !requestedTools.includes("exit_plan_mode")) {
|
|
303
348
|
requestedTools.push("exit_plan_mode");
|
|
304
349
|
}
|
|
305
350
|
const backends = resolveEvalBackends(session);
|
|
@@ -360,8 +405,20 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
360
405
|
}
|
|
361
406
|
}
|
|
362
407
|
}
|
|
408
|
+
// Resolve effective tool discovery mode.
|
|
409
|
+
// tools.discoveryMode takes precedence; mcp.discoveryMode is a back-compat alias for "mcp-only".
|
|
410
|
+
const toolsDiscoveryMode = session.settings.get("tools.discoveryMode");
|
|
411
|
+
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
|
|
412
|
+
toolsDiscoveryMode !== "off"
|
|
413
|
+
? (toolsDiscoveryMode as "off" | "mcp-only" | "all")
|
|
414
|
+
: session.settings.get("mcp.discoveryMode")
|
|
415
|
+
? "mcp-only"
|
|
416
|
+
: "off";
|
|
417
|
+
const discoveryActive = effectiveDiscoveryMode !== "off";
|
|
418
|
+
|
|
363
419
|
const allTools: Record<string, ToolFactory> = { ...BUILTIN_TOOLS, ...HIDDEN_TOOLS };
|
|
364
420
|
const isToolAllowed = (name: string) => {
|
|
421
|
+
if (name === "exit_plan_mode") return planEnabled;
|
|
365
422
|
if (name === "lsp") return enableLsp && session.settings.get("lsp.enabled");
|
|
366
423
|
if (name === "bash") return true;
|
|
367
424
|
if (name === "eval") return allowEval;
|
|
@@ -376,7 +433,8 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
376
433
|
if (name === "notebook") return session.settings.get("notebook.enabled");
|
|
377
434
|
if (name === "inspect_image") return session.settings.get("inspect_image.enabled");
|
|
378
435
|
if (name === "web_search") return session.settings.get("web_search.enabled");
|
|
379
|
-
|
|
436
|
+
// search_tool_bm25 is allowed when either legacy mcp.discoveryMode or new tools.discoveryMode is active.
|
|
437
|
+
if (name === "search_tool_bm25") return discoveryActive;
|
|
380
438
|
if (name === "calc") return session.settings.get("calc.enabled");
|
|
381
439
|
if (name === "browser") return session.settings.get("browser.enabled");
|
|
382
440
|
if (name === "checkpoint" || name === "rewind") return session.settings.get("checkpoint.enabled");
|
|
@@ -401,14 +459,16 @@ export async function createTools(session: ToolSession, toolNames?: string[]): P
|
|
|
401
459
|
filteredRequestedTools !== undefined
|
|
402
460
|
? filteredRequestedTools.filter(name => name !== "resolve").map(name => [name, allTools[name]] as const)
|
|
403
461
|
: [
|
|
404
|
-
...Object.entries(BUILTIN_TOOLS)
|
|
462
|
+
...Object.entries(BUILTIN_TOOLS)
|
|
463
|
+
.filter(([name]) => isToolAllowed(name))
|
|
464
|
+
.map(([name, factory]) => [name, factory] as const),
|
|
405
465
|
...(includeYield ? ([["yield", HIDDEN_TOOLS.yield]] as const) : []),
|
|
406
|
-
...([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const),
|
|
466
|
+
...(planEnabled ? ([["exit_plan_mode", HIDDEN_TOOLS.exit_plan_mode]] as const) : []),
|
|
407
467
|
];
|
|
408
468
|
|
|
409
469
|
const baseResults = await Promise.all(
|
|
410
470
|
baseEntries.map(async ([name, factory]) => {
|
|
411
|
-
const tool = await logger.time(`createTools:${name}`, factory, session);
|
|
471
|
+
const tool = await logger.time(`createTools:${name}`, factory as ToolFactory, session);
|
|
412
472
|
return tool ? wrapToolWithMetaNotice(tool) : null;
|
|
413
473
|
}),
|
|
414
474
|
);
|
|
@@ -41,6 +41,8 @@ function extractResponseText(message: AssistantMessage): string {
|
|
|
41
41
|
export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
|
|
42
42
|
readonly name = "inspect_image";
|
|
43
43
|
readonly label = "InspectImage";
|
|
44
|
+
readonly loadMode = "discoverable";
|
|
45
|
+
readonly summary = "Describe or analyze an image file";
|
|
44
46
|
readonly description: string;
|
|
45
47
|
readonly parameters = inspectImageSchema;
|
|
46
48
|
readonly strict = false;
|
package/src/tools/irc.ts
CHANGED
|
@@ -74,10 +74,11 @@ export interface IrcDetails {
|
|
|
74
74
|
export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
|
75
75
|
readonly name = "irc";
|
|
76
76
|
readonly label = "IRC";
|
|
77
|
+
readonly summary = "Send and receive messages between agents over IRC-like channels";
|
|
77
78
|
readonly description: string;
|
|
78
79
|
readonly parameters = ircSchema;
|
|
79
80
|
readonly strict = true;
|
|
80
|
-
|
|
81
|
+
readonly loadMode = "discoverable";
|
|
81
82
|
constructor(private readonly session: ToolSession) {
|
|
82
83
|
this.description = prompt.render(ircDescription);
|
|
83
84
|
}
|
package/src/tools/job.ts
CHANGED
|
@@ -76,10 +76,11 @@ export interface JobToolDetails {
|
|
|
76
76
|
export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
|
77
77
|
readonly name = "job";
|
|
78
78
|
readonly label = "Job";
|
|
79
|
+
readonly summary = "Manage long-running background jobs (async bash/python)";
|
|
79
80
|
readonly description: string;
|
|
80
81
|
readonly parameters = jobSchema;
|
|
81
82
|
readonly strict = true;
|
|
82
|
-
|
|
83
|
+
readonly loadMode = "discoverable";
|
|
83
84
|
constructor(private readonly session: ToolSession) {
|
|
84
85
|
this.description = prompt.render(jobDescription);
|
|
85
86
|
}
|
package/src/tools/notebook.ts
CHANGED
|
@@ -64,6 +64,8 @@ type NotebookParams = Static<typeof notebookSchema>;
|
|
|
64
64
|
export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
|
|
65
65
|
readonly name = "notebook";
|
|
66
66
|
readonly label = "Notebook";
|
|
67
|
+
readonly loadMode = "discoverable";
|
|
68
|
+
readonly summary = "Read and execute Jupyter notebooks";
|
|
67
69
|
readonly description = "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based.";
|
|
68
70
|
readonly parameters = notebookSchema;
|
|
69
71
|
readonly strict = true;
|
package/src/tools/read.ts
CHANGED
|
@@ -512,6 +512,7 @@ interface ResolvedSqliteReadPath {
|
|
|
512
512
|
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
|
513
513
|
readonly name = "read";
|
|
514
514
|
readonly label = "Read";
|
|
515
|
+
readonly loadMode = "essential";
|
|
515
516
|
readonly description: string;
|
|
516
517
|
readonly parameters = readSchema;
|
|
517
518
|
readonly nonAbortable = true;
|
|
@@ -33,6 +33,8 @@ export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetail
|
|
|
33
33
|
readonly parameters = recipeSchema;
|
|
34
34
|
readonly strict = true;
|
|
35
35
|
readonly concurrency = "exclusive";
|
|
36
|
+
readonly loadMode = "discoverable";
|
|
37
|
+
readonly summary = "Execute a saved bash recipe (multi-step shell command preset)";
|
|
36
38
|
readonly mergeCallAndResult = true;
|
|
37
39
|
readonly inline = true;
|
|
38
40
|
readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
|
|
@@ -35,9 +35,11 @@ export interface RenderMermaidToolDetails {
|
|
|
35
35
|
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
|
|
36
36
|
readonly name = "render_mermaid";
|
|
37
37
|
readonly label = "RenderMermaid";
|
|
38
|
+
readonly summary = "Render a Mermaid diagram to an image";
|
|
38
39
|
readonly description: string;
|
|
39
40
|
readonly parameters = renderMermaidSchema;
|
|
40
41
|
readonly strict = true;
|
|
42
|
+
readonly loadMode = "discoverable";
|
|
41
43
|
|
|
42
44
|
constructor(private readonly session: ToolSession) {
|
|
43
45
|
this.description = prompt.render(renderMermaidDescription);
|
|
@@ -3,21 +3,27 @@ import { type Component, Text } from "@oh-my-pi/pi-tui";
|
|
|
3
3
|
import { prompt } from "@oh-my-pi/pi-utils";
|
|
4
4
|
import { type Static, Type } from "@sinclair/typebox";
|
|
5
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
6
|
import type { Theme } from "../modes/theme/theme";
|
|
15
7
|
import searchToolBm25Description from "../prompts/tools/search-tool-bm25.md" with { type: "text" };
|
|
8
|
+
import {
|
|
9
|
+
buildDiscoverableToolSearchIndex,
|
|
10
|
+
type DiscoverableTool,
|
|
11
|
+
type DiscoverableToolSearchIndex,
|
|
12
|
+
formatDiscoverableToolServerSummary,
|
|
13
|
+
searchDiscoverableTools,
|
|
14
|
+
summarizeDiscoverableTools,
|
|
15
|
+
} from "../tool-discovery/tool-index";
|
|
16
16
|
import { renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
|
|
17
17
|
import type { ToolSession } from ".";
|
|
18
18
|
import { formatCount, replaceTabs, TRUNCATE_LENGTHS } from "./render-utils";
|
|
19
19
|
import { ToolError } from "./tool-errors";
|
|
20
20
|
|
|
21
|
+
// Re-export legacy MCP types for back-compat (tests and external callers may reference them)
|
|
22
|
+
export type {
|
|
23
|
+
DiscoverableMCPSearchIndex,
|
|
24
|
+
DiscoverableMCPTool,
|
|
25
|
+
} from "../mcp/discoverable-tool-metadata";
|
|
26
|
+
|
|
21
27
|
const DEFAULT_LIMIT = 8;
|
|
22
28
|
const TOOL_DISCOVERY_TITLE = "Tool Discovery";
|
|
23
29
|
const COLLAPSED_MATCH_LIMIT = 5;
|
|
@@ -25,7 +31,10 @@ const MATCH_LABEL_LEN = 72;
|
|
|
25
31
|
const MATCH_DESCRIPTION_LEN = 96;
|
|
26
32
|
|
|
27
33
|
const searchToolBm25Schema = Type.Object({
|
|
28
|
-
query: Type.String({
|
|
34
|
+
query: Type.String({
|
|
35
|
+
description: "tool search query",
|
|
36
|
+
examples: ["kubernetes pod", "image processing", "git commit"],
|
|
37
|
+
}),
|
|
29
38
|
limit: Type.Optional(Type.Integer({ description: "max matches", minimum: 1 })),
|
|
30
39
|
});
|
|
31
40
|
|
|
@@ -50,11 +59,11 @@ export interface SearchToolBm25Details {
|
|
|
50
59
|
tools: SearchToolBm25Match[];
|
|
51
60
|
}
|
|
52
61
|
|
|
53
|
-
function formatMatch(tool:
|
|
62
|
+
function formatMatch(tool: DiscoverableTool, score: number): SearchToolBm25Match {
|
|
54
63
|
return {
|
|
55
64
|
name: tool.name,
|
|
56
65
|
label: tool.label,
|
|
57
|
-
description: tool.
|
|
66
|
+
description: tool.summary,
|
|
58
67
|
server_name: tool.serverName,
|
|
59
68
|
mcp_tool_name: tool.mcpToolName,
|
|
60
69
|
schema_keys: tool.schemaKeys,
|
|
@@ -71,41 +80,99 @@ function buildSearchToolBm25Content(details: SearchToolBm25Details): string {
|
|
|
71
80
|
});
|
|
72
81
|
}
|
|
73
82
|
|
|
74
|
-
|
|
83
|
+
/** Get discoverable tools for description rendering. Falls back to empty array on error. */
|
|
84
|
+
function getDiscoverableToolsForDescription(session: ToolSession): DiscoverableTool[] {
|
|
75
85
|
try {
|
|
76
|
-
|
|
86
|
+
// Prefer generic method; fall back to legacy MCP-only
|
|
87
|
+
if (session.getDiscoverableTools) {
|
|
88
|
+
return session.getDiscoverableTools();
|
|
89
|
+
}
|
|
90
|
+
// Legacy MCP path — adapt DiscoverableMCPTool (with `description`) → DiscoverableTool.
|
|
91
|
+
const legacy = session.getDiscoverableMCPTools?.() ?? [];
|
|
92
|
+
return legacy.map(t => ({
|
|
93
|
+
name: t.name,
|
|
94
|
+
label: t.label,
|
|
95
|
+
summary: t.description,
|
|
96
|
+
source: "mcp" as const,
|
|
97
|
+
serverName: t.serverName,
|
|
98
|
+
mcpToolName: t.mcpToolName,
|
|
99
|
+
schemaKeys: t.schemaKeys,
|
|
100
|
+
}));
|
|
77
101
|
} catch {
|
|
78
102
|
return [];
|
|
79
103
|
}
|
|
80
104
|
}
|
|
81
105
|
|
|
82
|
-
function
|
|
106
|
+
function getDiscoverableToolSearchIndexForExecution(session: ToolSession): DiscoverableToolSearchIndex {
|
|
83
107
|
try {
|
|
84
|
-
|
|
85
|
-
if (
|
|
108
|
+
// Prefer generic cached index
|
|
109
|
+
if (session.getDiscoverableToolSearchIndex) {
|
|
110
|
+
const cached = session.getDiscoverableToolSearchIndex();
|
|
111
|
+
if (cached) return cached;
|
|
112
|
+
}
|
|
113
|
+
// Legacy MCP: use cached MCP index. Its documents expose `tool.description` as well as
|
|
114
|
+
// `tool.summary`, so it is structurally compatible with DiscoverableToolSearchIndex.
|
|
115
|
+
const mcpCached = session.getDiscoverableMCPSearchIndex?.();
|
|
116
|
+
if (mcpCached) return mcpCached as unknown as DiscoverableToolSearchIndex;
|
|
86
117
|
} catch {}
|
|
87
|
-
return
|
|
118
|
+
return buildDiscoverableToolSearchIndex(getDiscoverableToolsForDescription(session));
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Resolve the effective selected tool names (generic or legacy MCP). */
|
|
122
|
+
function getSelectedToolNames(session: ToolSession): string[] {
|
|
123
|
+
if (session.getSelectedDiscoveredToolNames) {
|
|
124
|
+
return session.getSelectedDiscoveredToolNames();
|
|
125
|
+
}
|
|
126
|
+
return session.getSelectedMCPToolNames?.() ?? [];
|
|
88
127
|
}
|
|
89
128
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
129
|
+
/** Activate tools (generic or legacy MCP fallback). */
|
|
130
|
+
async function activateTools(session: ToolSession, toolNames: string[]): Promise<string[]> {
|
|
131
|
+
if (session.activateDiscoveredTools) {
|
|
132
|
+
return session.activateDiscoveredTools(toolNames);
|
|
133
|
+
}
|
|
134
|
+
if (session.activateDiscoveredMCPTools) {
|
|
135
|
+
return session.activateDiscoveredMCPTools(toolNames);
|
|
136
|
+
}
|
|
137
|
+
return [];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
type DiscoveryExecutionSession = ToolSession & {
|
|
141
|
+
_supportsDiscoveryExecution: true;
|
|
94
142
|
};
|
|
95
143
|
|
|
96
|
-
function
|
|
97
|
-
|
|
144
|
+
function supportsToolDiscoveryExecution(session: ToolSession): session is DiscoveryExecutionSession {
|
|
145
|
+
// Supports generic discovery
|
|
146
|
+
if (
|
|
147
|
+
typeof session.isToolDiscoveryEnabled === "function" &&
|
|
148
|
+
typeof session.getSelectedDiscoveredToolNames === "function" &&
|
|
149
|
+
typeof session.activateDiscoveredTools === "function"
|
|
150
|
+
) {
|
|
151
|
+
return true;
|
|
152
|
+
}
|
|
153
|
+
// Supports legacy MCP discovery
|
|
154
|
+
if (
|
|
98
155
|
typeof session.isMCPDiscoveryEnabled === "function" &&
|
|
99
156
|
typeof session.getSelectedMCPToolNames === "function" &&
|
|
100
157
|
typeof session.activateDiscoveredMCPTools === "function"
|
|
101
|
-
)
|
|
158
|
+
) {
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
return false;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function isDiscoveryEnabled(session: ToolSession): boolean {
|
|
165
|
+
if (typeof session.isToolDiscoveryEnabled === "function") {
|
|
166
|
+
return session.isToolDiscoveryEnabled();
|
|
167
|
+
}
|
|
168
|
+
return session.isMCPDiscoveryEnabled?.() ?? false;
|
|
102
169
|
}
|
|
103
170
|
|
|
104
|
-
export function renderSearchToolBm25Description(discoverableTools:
|
|
105
|
-
const summary =
|
|
171
|
+
export function renderSearchToolBm25Description(discoverableTools: DiscoverableTool[] = []): string {
|
|
172
|
+
const summary = summarizeDiscoverableTools(discoverableTools);
|
|
106
173
|
return prompt.render(searchToolBm25Description, {
|
|
107
174
|
discoverableMCPToolCount: summary.toolCount,
|
|
108
|
-
discoverableMCPServerSummaries: summary.servers.map(
|
|
175
|
+
discoverableMCPServerSummaries: summary.servers.map(formatDiscoverableToolServerSummary),
|
|
109
176
|
hasDiscoverableMCPServers: summary.servers.length > 0,
|
|
110
177
|
});
|
|
111
178
|
}
|
|
@@ -134,11 +201,18 @@ function renderFallbackResult(text: string, theme: Theme): Component {
|
|
|
134
201
|
return new Text([header, ...bodyLines].join("\n"), 0, 0);
|
|
135
202
|
}
|
|
136
203
|
|
|
204
|
+
/**
|
|
205
|
+
* SearchToolsTool — wire name `search_tool_bm25` (preserved for persisted session back-compat).
|
|
206
|
+
*
|
|
207
|
+
* When tools.discoveryMode === "all", this covers both MCP tools and built-in discoverable tools.
|
|
208
|
+
* When tools.discoveryMode === "mcp-only" or mcp.discoveryMode === true, only MCP tools are searched.
|
|
209
|
+
*/
|
|
137
210
|
export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
|
|
138
211
|
readonly name = "search_tool_bm25";
|
|
139
|
-
readonly label = "
|
|
212
|
+
readonly label = "SearchTools";
|
|
213
|
+
readonly loadMode = "essential";
|
|
140
214
|
get description(): string {
|
|
141
|
-
return renderSearchToolBm25Description(
|
|
215
|
+
return renderSearchToolBm25Description(getDiscoverableToolsForDescription(this.session));
|
|
142
216
|
}
|
|
143
217
|
readonly parameters = searchToolBm25Schema;
|
|
144
218
|
readonly strict = true;
|
|
@@ -146,8 +220,13 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
146
220
|
constructor(private readonly session: ToolSession) {}
|
|
147
221
|
|
|
148
222
|
static createIf(session: ToolSession): SearchToolBm25Tool | null {
|
|
149
|
-
|
|
150
|
-
|
|
223
|
+
// Active when new tools.discoveryMode is non-"off" or legacy mcp.discoveryMode is true
|
|
224
|
+
const toolsDiscoveryMode = session.settings.get("tools.discoveryMode");
|
|
225
|
+
const active =
|
|
226
|
+
(toolsDiscoveryMode !== undefined && toolsDiscoveryMode !== "off") ||
|
|
227
|
+
session.settings.get("mcp.discoveryMode") === true;
|
|
228
|
+
if (!active) return null;
|
|
229
|
+
return supportsToolDiscoveryExecution(session) ? new SearchToolBm25Tool(session) : null;
|
|
151
230
|
}
|
|
152
231
|
|
|
153
232
|
async execute(
|
|
@@ -157,11 +236,13 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
157
236
|
_onUpdate?: AgentToolUpdateCallback<SearchToolBm25Details>,
|
|
158
237
|
_context?: AgentToolContext,
|
|
159
238
|
): Promise<AgentToolResult<SearchToolBm25Details>> {
|
|
160
|
-
if (!
|
|
161
|
-
throw new ToolError("
|
|
239
|
+
if (!supportsToolDiscoveryExecution(this.session)) {
|
|
240
|
+
throw new ToolError("Tool discovery is unavailable in this session.");
|
|
162
241
|
}
|
|
163
|
-
if (!this.session
|
|
164
|
-
throw new ToolError(
|
|
242
|
+
if (!isDiscoveryEnabled(this.session)) {
|
|
243
|
+
throw new ToolError(
|
|
244
|
+
"Tool discovery is disabled. Enable tools.discoveryMode or mcp.discoveryMode to use search_tool_bm25.",
|
|
245
|
+
);
|
|
165
246
|
}
|
|
166
247
|
|
|
167
248
|
const query = params.query.trim();
|
|
@@ -173,11 +254,11 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
173
254
|
throw new ToolError("Limit must be a positive integer.");
|
|
174
255
|
}
|
|
175
256
|
|
|
176
|
-
const searchIndex =
|
|
177
|
-
const selectedToolNames = new Set(this.session
|
|
178
|
-
let ranked: Array<{ tool:
|
|
257
|
+
const searchIndex = getDiscoverableToolSearchIndexForExecution(this.session);
|
|
258
|
+
const selectedToolNames = new Set(getSelectedToolNames(this.session));
|
|
259
|
+
let ranked: Array<{ tool: DiscoverableTool; score: number }> = [];
|
|
179
260
|
try {
|
|
180
|
-
ranked =
|
|
261
|
+
ranked = searchDiscoverableTools(searchIndex, query, searchIndex.documents.length)
|
|
181
262
|
.filter(result => !selectedToolNames.has(result.tool.name))
|
|
182
263
|
.slice(0, limit);
|
|
183
264
|
} catch (error) {
|
|
@@ -187,14 +268,19 @@ export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema
|
|
|
187
268
|
throw error;
|
|
188
269
|
}
|
|
189
270
|
const activated =
|
|
190
|
-
ranked.length > 0
|
|
271
|
+
ranked.length > 0
|
|
272
|
+
? await activateTools(
|
|
273
|
+
this.session,
|
|
274
|
+
ranked.map(result => result.tool.name),
|
|
275
|
+
)
|
|
276
|
+
: [];
|
|
191
277
|
|
|
192
278
|
const details: SearchToolBm25Details = {
|
|
193
279
|
query,
|
|
194
280
|
limit,
|
|
195
281
|
total_tools: searchIndex.documents.length,
|
|
196
282
|
activated_tools: activated,
|
|
197
|
-
active_selected_tools: this.session
|
|
283
|
+
active_selected_tools: getSelectedToolNames(this.session),
|
|
198
284
|
tools: ranked.map(result => formatMatch(result.tool, result.score)),
|
|
199
285
|
};
|
|
200
286
|
|
|
@@ -252,7 +338,7 @@ export const searchToolBm25Renderer = {
|
|
|
252
338
|
);
|
|
253
339
|
if (details.tools.length === 0) {
|
|
254
340
|
const emptyMessage =
|
|
255
|
-
details.total_tools === 0 ? "No discoverable
|
|
341
|
+
details.total_tools === 0 ? "No discoverable tools are currently loaded." : "No matching tools found.";
|
|
256
342
|
return new Text(`${header}\n${uiTheme.fg("muted", emptyMessage)}`, 0, 0);
|
|
257
343
|
}
|
|
258
344
|
|
package/src/tools/search.ts
CHANGED
|
@@ -80,6 +80,8 @@ type SearchParams = Static<typeof searchSchema>;
|
|
|
80
80
|
export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
|
|
81
81
|
readonly name = "search";
|
|
82
82
|
readonly label = "Search";
|
|
83
|
+
readonly loadMode = "discoverable";
|
|
84
|
+
readonly summary = "Search file contents using ripgrep (fast text search)";
|
|
83
85
|
readonly description: string;
|
|
84
86
|
readonly parameters = searchSchema;
|
|
85
87
|
readonly strict = true;
|
package/src/tools/ssh.ts
CHANGED
|
@@ -120,6 +120,8 @@ type SshToolParams = Static<typeof sshSchema>;
|
|
|
120
120
|
|
|
121
121
|
export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
|
122
122
|
readonly name = "ssh";
|
|
123
|
+
readonly summary = "Execute a command on a remote host over SSH";
|
|
124
|
+
readonly loadMode = "discoverable";
|
|
123
125
|
readonly label = "SSH";
|
|
124
126
|
readonly parameters = sshSchema;
|
|
125
127
|
readonly concurrency = "exclusive";
|
package/src/tools/todo-write.ts
CHANGED
|
@@ -503,11 +503,12 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
|
|
503
503
|
export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
|
|
504
504
|
readonly name = "todo_write";
|
|
505
505
|
readonly label = "Todo Write";
|
|
506
|
+
readonly summary = "Write a structured todo list to track progress within a session";
|
|
506
507
|
readonly description: string;
|
|
507
508
|
readonly parameters = todoWriteSchema;
|
|
508
509
|
readonly concurrency = "exclusive";
|
|
509
510
|
readonly strict = true;
|
|
510
|
-
|
|
511
|
+
readonly loadMode = "discoverable";
|
|
511
512
|
constructor(private readonly session: ToolSession) {
|
|
512
513
|
this.description = prompt.render(todoWriteDescription);
|
|
513
514
|
}
|
package/src/tools/write.ts
CHANGED
|
@@ -166,6 +166,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
|
|
166
166
|
readonly nonAbortable = true;
|
|
167
167
|
readonly strict = true;
|
|
168
168
|
readonly concurrency = "exclusive";
|
|
169
|
+
readonly loadMode = "discoverable";
|
|
170
|
+
readonly summary = "Write content to a file (creates or overwrites)";
|
|
169
171
|
|
|
170
172
|
readonly #writethrough: WritethroughCallback;
|
|
171
173
|
|
package/src/web/search/index.ts
CHANGED
|
@@ -211,6 +211,8 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
|
|
|
211
211
|
readonly description: string;
|
|
212
212
|
readonly parameters = webSearchSchema;
|
|
213
213
|
readonly strict = true;
|
|
214
|
+
readonly loadMode = "discoverable";
|
|
215
|
+
readonly summary = "Search the web for up-to-date information";
|
|
214
216
|
|
|
215
217
|
constructor(_session: ToolSession) {
|
|
216
218
|
this.description = prompt.render(webSearchDescription);
|
|
@@ -119,6 +119,11 @@ function buildBasicAuthValue(username: string, password: string): string {
|
|
|
119
119
|
return Buffer.from(`${username}:${password}`, "utf-8").toString("base64");
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** RFC 7617 forbids C0 and C1 control characters in Basic auth credentials. */
|
|
123
|
+
function hasControlCharacters(value: string): boolean {
|
|
124
|
+
return /[\u0000-\u001F\u007F-\u009F]/u.test(value);
|
|
125
|
+
}
|
|
126
|
+
|
|
122
127
|
/** Find SearXNG authentication from settings or environment. Basic auth takes precedence over bearer tokens. */
|
|
123
128
|
function findAuth(): SearXNGAuth | null {
|
|
124
129
|
const basicUsername = findBasicUsername();
|
|
@@ -132,6 +137,9 @@ function findAuth(): SearXNGAuth | null {
|
|
|
132
137
|
if (basicUsername.includes(":")) {
|
|
133
138
|
throw new Error("SearXNG Basic auth username cannot contain ':' because RFC 7617 uses it as the separator.");
|
|
134
139
|
}
|
|
140
|
+
if (hasControlCharacters(basicUsername) || hasControlCharacters(basicPassword)) {
|
|
141
|
+
throw new Error("SearXNG Basic auth credentials must not contain RFC 7617 control characters.");
|
|
142
|
+
}
|
|
135
143
|
return { type: "basic", value: buildBasicAuthValue(basicUsername, basicPassword) };
|
|
136
144
|
}
|
|
137
145
|
|