feat(coding-agent): added loadMode and summary to AgentTool discovery
- Added optional `loadMode` and `summary` fields to `AgentTool` and related type declarations. - Added `loadMode` and `summary` metadata to built-in tool classes for discoverable/essential behavior. - Replaced `BUILTIN_TOOL_METADATA` with per-tool fields in discovery code paths. - Updated `search_tool_bm25` and discovery indexing to use each tool's `summary` text. - Updated discovery tests to validate tool `loadMode` and summary completeness.
This commit is contained in:
@@ -1,6 +1,10 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
|
||||
- Added `loadMode` option to `AgentTool` to mark built-in tools as `essential` for initial loading or `discoverable` for search activation
|
||||
- Added optional `summary` field to `AgentTool` definitions for one-line text used in tool discovery indexes
|
||||
|
||||
## [14.7.0] - 2026-05-04
|
||||
### Breaking Changes
|
||||
|
||||
@@ -248,6 +248,10 @@ export interface AgentTool<TParameters extends TSchema = TSchema, TDetails = any
|
||||
hidden?: boolean;
|
||||
/** If true, tool can stage a pending action that requires explicit resolution via the resolve tool. */
|
||||
deferrable?: boolean;
|
||||
/** Built-in tool loading behavior. "essential" loads initially; "discoverable" can be activated by tool search. */
|
||||
loadMode?: "essential" | "discoverable";
|
||||
/** Short one-line summary used for tool discovery indexes. */
|
||||
summary?: string;
|
||||
/** If true, tool execution ignores abort signals (runs to completion) */
|
||||
nonAbortable?: boolean;
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
### Breaking Changes
|
||||
|
||||
- Removed the exported `BUILTIN_TOOL_METADATA` API, including `BuiltinEntry`-style metadata exports and discoverable-built-in helper exports, which will break consumers relying on those symbols
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated discoverable tool search (`search_tool_bm25` and related discovery metadata) to read each tool’s own `summary` field when present, improving discoverability descriptions for built-in tools
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -7822,4 +7829,4 @@ Initial public release.
|
||||
- Git branch display in footer
|
||||
- Message queueing during streaming responses
|
||||
- OAuth integration for Gmail and Google Calendar access
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
- HTML export with syntax highlighting and collapsible sections
|
||||
@@ -246,6 +246,7 @@ async function executeSinglePathEntries(
|
||||
export class EditTool implements AgentTool<TInput> {
|
||||
readonly name = "edit";
|
||||
readonly label = "Edit";
|
||||
readonly loadMode = "essential";
|
||||
readonly nonAbortable = true;
|
||||
readonly concurrency = "exclusive";
|
||||
readonly strict = true;
|
||||
|
||||
@@ -1169,6 +1169,8 @@ export function createLspWritethrough(cwd: string, options?: WritethroughOptions
|
||||
export class LspTool implements AgentTool<typeof lspSchema, LspToolDetails, Theme> {
|
||||
readonly name = "lsp";
|
||||
readonly label = "LSP";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Query LSP (language server) for diagnostics, hover info, and references";
|
||||
readonly description: string;
|
||||
readonly parameters = lspSchema;
|
||||
readonly renderCall = renderCall;
|
||||
|
||||
@@ -116,7 +116,6 @@ import {
|
||||
} from "./tool-discovery/tool-index";
|
||||
import {
|
||||
BashTool,
|
||||
BUILTIN_TOOL_METADATA,
|
||||
BUILTIN_TOOLS,
|
||||
computeEssentialBuiltinNames,
|
||||
createTools,
|
||||
@@ -284,7 +283,6 @@ export {
|
||||
// Individual tool classes (for custom usage)
|
||||
BashTool,
|
||||
// Tool classes and factories
|
||||
BUILTIN_TOOL_METADATA,
|
||||
BUILTIN_TOOLS,
|
||||
createTools,
|
||||
EditTool,
|
||||
@@ -1358,19 +1356,13 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
toolContextStore.setToolNames(toolNames);
|
||||
const discoverableMCPTools = mcpDiscoveryEnabled ? collectDiscoverableMCPTools(tools.values()) : [];
|
||||
const activeToolNames = new Set(toolNames);
|
||||
const builtinSummaryMap = new Map(
|
||||
Object.entries(BUILTIN_TOOL_METADATA)
|
||||
.filter(([, meta]) => typeof meta.summary === "string")
|
||||
.map(([name, meta]) => [name, meta.summary!] as const),
|
||||
);
|
||||
const discoverableBuiltinTools: DiscoverableTool[] =
|
||||
effectiveDiscoveryMode === "all"
|
||||
? collectDiscoverableTools(
|
||||
Array.from(tools.values()).filter(tool => {
|
||||
const meta = BUILTIN_TOOL_METADATA[tool.name];
|
||||
return meta?.loadMode === "discoverable" && !activeToolNames.has(tool.name);
|
||||
}),
|
||||
{ source: "builtin", summaryMap: builtinSummaryMap },
|
||||
Array.from(tools.values()).filter(
|
||||
tool => tool.loadMode === "discoverable" && !activeToolNames.has(tool.name),
|
||||
),
|
||||
{ source: "builtin" },
|
||||
)
|
||||
: [];
|
||||
const discoverableToolsForDesc: DiscoverableTool[] = [
|
||||
@@ -1527,9 +1519,9 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
||||
// activation persistence is a follow-up). MCP names won't collide with built-in names.
|
||||
const restoredDiscoveredNames = new Set(existingSession.selectedMCPToolNames);
|
||||
initialToolNames = initialToolNames.filter(name => {
|
||||
const meta = BUILTIN_TOOL_METADATA[name];
|
||||
if (!meta) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
||||
if (meta.loadMode === "essential") return true;
|
||||
const tool = toolRegistry.get(name);
|
||||
if (!tool?.loadMode) return true; // not a built-in — leave MCP/custom/extension to existing logic
|
||||
if (tool.loadMode === "essential") return true;
|
||||
if (essentialBuiltinNames.has(name)) return true;
|
||||
if (explicitlyRequestedToolNames.has(name)) return true;
|
||||
if (restoredDiscoveredNames.has(name)) return true;
|
||||
|
||||
@@ -137,7 +137,6 @@ import {
|
||||
} from "../tool-discovery/tool-index";
|
||||
import { assertEditableFile } from "../tools/auto-generated-guard";
|
||||
import type { CheckpointState } from "../tools/checkpoint";
|
||||
import { BUILTIN_TOOL_METADATA } from "../tools/index";
|
||||
import { outputMeta } from "../tools/output-meta";
|
||||
import { normalizeLocalScheme, resolveToCwd } from "../tools/path-utils";
|
||||
import { isAutoQaEnabled } from "../tools/report-tool-issue";
|
||||
@@ -2307,20 +2306,17 @@ export class AgentSession {
|
||||
return filter?.source ? allTools.filter(t => t.source === filter.source) : allTools;
|
||||
}
|
||||
|
||||
/** Collect built-in tools the model can discover via search_tool_bm25. Restricted to entries
|
||||
* whose `BUILTIN_TOOL_METADATA[name].loadMode === "discoverable"`. This keeps hidden/internal
|
||||
* tools (resolve, yield, exit_plan_mode, report_finding, report_tool_issue) out of the index
|
||||
/** Collect built-in tools the model can discover via search_tool_bm25. Restricted to tool
|
||||
* definitions whose `loadMode === "discoverable"`. This keeps hidden/internal tools
|
||||
* (resolve, yield, exit_plan_mode, report_finding, report_tool_issue) out of the index
|
||||
* and avoids mislabeling extension/custom default-inactive tools as built-ins. */
|
||||
#collectDiscoverableBuiltinTools(): DiscoverableTool[] {
|
||||
const activeNames = new Set(this.getActiveToolNames());
|
||||
const result: DiscoverableTool[] = [];
|
||||
for (const [name, meta] of Object.entries(BUILTIN_TOOL_METADATA)) {
|
||||
if (meta.loadMode !== "discoverable") continue;
|
||||
if (activeNames.has(name)) continue;
|
||||
const tool = this.#toolRegistry.get(name);
|
||||
if (!tool) continue;
|
||||
const summaryMap = meta.summary ? new Map([[name, meta.summary]]) : undefined;
|
||||
const collected = collectDiscoverableTools([tool], { source: "builtin", summaryMap });
|
||||
for (const tool of this.#toolRegistry.values()) {
|
||||
if (tool.loadMode !== "discoverable") continue;
|
||||
if (activeNames.has(tool.name)) continue;
|
||||
const collected = collectDiscoverableTools([tool], { source: "builtin" });
|
||||
result.push(...collected);
|
||||
}
|
||||
return result;
|
||||
|
||||
@@ -195,7 +195,9 @@ function validateTaskModeParams(simpleMode: TaskSimpleMode, params: TaskParams):
|
||||
export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
||||
readonly name = "task";
|
||||
readonly label = "Task";
|
||||
readonly summary = "Spawn a subagent to complete a parallel task";
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly renderResult = renderResult;
|
||||
readonly #discoveredAgents: AgentDefinition[];
|
||||
readonly #blockedAgent: string | undefined;
|
||||
|
||||
@@ -156,12 +156,19 @@ export function getDiscoverableTool(
|
||||
label?: string;
|
||||
description?: string;
|
||||
mcpServerName?: string;
|
||||
summary?: string;
|
||||
mcpToolName?: string;
|
||||
parameters?: unknown;
|
||||
};
|
||||
const source: DiscoverableToolSource = overrides?.source ?? (isMCPToolName(tool.name) ? "mcp" : "builtin");
|
||||
const rawSummary =
|
||||
typeof overrides?.summary === "string"
|
||||
? overrides.summary
|
||||
: typeof toolRecord.summary === "string"
|
||||
? toolRecord.summary
|
||||
: undefined;
|
||||
const rawDescription = typeof toolRecord.description === "string" ? toolRecord.description : "";
|
||||
const summary = overrides?.summary ?? rawDescription.slice(0, 200);
|
||||
const summary = rawSummary ?? rawDescription.slice(0, 200);
|
||||
return {
|
||||
name: tool.name,
|
||||
label: typeof toolRecord.label === "string" ? toolRecord.label : tool.name,
|
||||
|
||||
@@ -379,9 +379,11 @@ type AskParams = AskToolInput;
|
||||
export class AskTool implements AgentTool<typeof askSchema, AskToolDetails> {
|
||||
readonly name = "ask";
|
||||
readonly label = "Ask";
|
||||
readonly summary = "Ask the user a clarifying question";
|
||||
readonly description: string;
|
||||
readonly parameters = askSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(askDescription);
|
||||
|
||||
@@ -167,10 +167,12 @@ export interface AstEditToolDetails {
|
||||
export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
|
||||
readonly name = "ast_edit";
|
||||
readonly label = "AST Edit";
|
||||
readonly summary = "Perform AST-aware code edits (structural refactoring)";
|
||||
readonly description: string;
|
||||
readonly parameters = astEditSchema;
|
||||
readonly strict = true;
|
||||
readonly deferrable = true;
|
||||
readonly loadMode = "discoverable";
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(astEditDescription);
|
||||
}
|
||||
|
||||
@@ -122,9 +122,11 @@ export interface AstGrepToolDetails {
|
||||
export class AstGrepTool implements AgentTool<typeof astGrepSchema, AstGrepToolDetails> {
|
||||
readonly name = "ast_grep";
|
||||
readonly label = "AST Grep";
|
||||
readonly summary = "Search code with AST patterns (structural grep)";
|
||||
readonly description: string;
|
||||
readonly parameters = astGrepSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(astGrepDescription);
|
||||
|
||||
@@ -236,6 +236,7 @@ function formatTimeoutClampNotice(requestedTimeoutSec: number, effectiveTimeoutS
|
||||
export class BashTool implements AgentTool<BashToolSchema, BashToolDetails> {
|
||||
readonly name = "bash";
|
||||
readonly label = "Bash";
|
||||
readonly loadMode = "essential";
|
||||
readonly description: string;
|
||||
readonly parameters: BashToolSchema;
|
||||
readonly concurrency = "exclusive";
|
||||
|
||||
@@ -110,6 +110,8 @@ function resolveBrowserKind(params: BrowserParams, session: ToolSession): Browse
|
||||
export class BrowserTool implements AgentTool<typeof browserSchema, BrowserToolDetails> {
|
||||
readonly name = "browser";
|
||||
readonly label = "Browser";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Control a headless browser to navigate and interact with web pages";
|
||||
readonly parameters = browserSchema;
|
||||
readonly strict = true;
|
||||
|
||||
|
||||
@@ -396,6 +396,8 @@ type CalculatorParams = Static<typeof calculatorSchema>;
|
||||
export class CalculatorTool implements AgentTool<typeof calculatorSchema, CalculatorToolDetails> {
|
||||
readonly name = "calc";
|
||||
readonly label = "Calc";
|
||||
readonly summary = "Evaluate a mathematical expression";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly description: string;
|
||||
readonly parameters = calculatorSchema;
|
||||
readonly strict = true;
|
||||
|
||||
@@ -49,9 +49,11 @@ function isTopLevelSession(session: ToolSession): boolean {
|
||||
export class CheckpointTool implements AgentTool<typeof checkpointSchema, CheckpointToolDetails> {
|
||||
readonly name = "checkpoint";
|
||||
readonly label = "Checkpoint";
|
||||
readonly summary = "Create a git-based checkpoint to save and restore session state";
|
||||
readonly description: string;
|
||||
readonly parameters = checkpointSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly intent = (args: Partial<CheckpointParams>) => (args.goal ? `checkpointing: ${args.goal}` : "checkpointing");
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
@@ -92,9 +94,11 @@ export class CheckpointTool implements AgentTool<typeof checkpointSchema, Checkp
|
||||
export class RewindTool implements AgentTool<typeof rewindSchema, RewindToolDetails> {
|
||||
readonly name = "rewind";
|
||||
readonly label = "Rewind";
|
||||
readonly summary = "Rewind to a previously created checkpoint";
|
||||
readonly description: string;
|
||||
readonly parameters = rewindSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly intent = (): string => "rewinding";
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
|
||||
@@ -597,10 +597,12 @@ export const debugToolRenderer = {
|
||||
export class DebugTool implements AgentTool<typeof debugSchema, DebugToolDetails> {
|
||||
readonly name = "debug";
|
||||
readonly label = "Debug";
|
||||
readonly summary = "Debug a running process with DAP (debugger adapter protocol)";
|
||||
readonly description: string;
|
||||
readonly parameters = debugSchema;
|
||||
readonly strict = true;
|
||||
readonly concurrency = "exclusive";
|
||||
readonly loadMode = "discoverable";
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(debugDescription);
|
||||
|
||||
@@ -207,6 +207,8 @@ async function resolveBackend(
|
||||
|
||||
export class EvalTool implements AgentTool<typeof evalSchema> {
|
||||
readonly name = "eval";
|
||||
readonly summary = "Execute Python or JavaScript code in an in-process eval backend";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly label = "Eval";
|
||||
get description(): string {
|
||||
if (!this.session) return getEvalToolDescription();
|
||||
|
||||
@@ -88,6 +88,8 @@ export interface FindToolOptions {
|
||||
|
||||
export class FindTool implements AgentTool<typeof findSchema, FindToolDetails> {
|
||||
readonly name = "find";
|
||||
readonly summary = "Find files and directories matching a glob pattern";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly label = "Find";
|
||||
readonly description: string;
|
||||
readonly parameters = findSchema;
|
||||
|
||||
@@ -2087,6 +2087,8 @@ function buildTextResult(
|
||||
|
||||
export class GithubTool implements AgentTool<typeof githubSchema, GhToolDetails> {
|
||||
readonly name = "github";
|
||||
readonly summary = "Interact with GitHub issues, pull requests, and repositories";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly label = "GitHub";
|
||||
readonly description = prompt.render(githubDescription);
|
||||
readonly parameters = githubSchema;
|
||||
|
||||
@@ -19,6 +19,8 @@ export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSche
|
||||
readonly description = recallDescription;
|
||||
readonly parameters = hindsightRecallSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Search hindsight memory for relevant prior context";
|
||||
|
||||
constructor(private readonly session: ToolSession) {}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSc
|
||||
readonly description = reflectDescription;
|
||||
readonly parameters = hindsightReflectSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Reflect on recent work and write hindsight memory";
|
||||
|
||||
constructor(private readonly session: ToolSession) {}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSche
|
||||
readonly description = retainDescription;
|
||||
readonly parameters = hindsightRetainSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Store important facts in hindsight memory";
|
||||
|
||||
constructor(private readonly session: ToolSession) {}
|
||||
|
||||
|
||||
@@ -234,22 +234,6 @@ export type ToolFactory = (session: ToolSession) => Tool | null | Promise<Tool |
|
||||
|
||||
export type BuiltinToolLoadMode = "essential" | "discoverable";
|
||||
|
||||
/** Per-tool discovery metadata for built-ins. Kept separate from BUILTIN_TOOLS so the public
|
||||
* factory map remains directly callable (`BUILTIN_TOOLS.read(session)`). */
|
||||
export interface BuiltinToolMetadata {
|
||||
/** "essential" tools are always loaded regardless of discovery mode.
|
||||
* "discoverable" tools are hidden behind search_tool_bm25 when tools.discoveryMode === "all". */
|
||||
loadMode: BuiltinToolLoadMode;
|
||||
/** Short one-line summary for the BM25 corpus (optional; falls back to tool description). */
|
||||
summary?: string;
|
||||
}
|
||||
|
||||
/** @deprecated Internal shape used by older callers. Prefer BUILTIN_TOOLS (factory) +
|
||||
* BUILTIN_TOOL_METADATA (loadMode/summary). */
|
||||
export interface BuiltinEntry extends BuiltinToolMetadata {
|
||||
factory: ToolFactory;
|
||||
}
|
||||
|
||||
/** Default essential tool names when tools.essentialOverride is empty. */
|
||||
export const DEFAULT_ESSENTIAL_TOOL_NAMES: readonly string[] = ["read", "bash", "edit"] as const;
|
||||
|
||||
@@ -262,7 +246,7 @@ export function computeEssentialBuiltinNames(settings: Settings): string[] {
|
||||
const override = settings.get("tools.essentialOverride") ?? [];
|
||||
const cleaned = override.map(name => name.trim()).filter(Boolean);
|
||||
if (cleaned.length > 0) {
|
||||
return cleaned.filter(name => name in BUILTIN_TOOL_METADATA);
|
||||
return cleaned.filter(name => name in BUILTIN_TOOLS);
|
||||
}
|
||||
return [...DEFAULT_ESSENTIAL_TOOL_NAMES];
|
||||
}
|
||||
@@ -305,57 +289,6 @@ export const BUILTIN_TOOLS: Record<string, ToolFactory> = {
|
||||
reflect: HindsightReflectTool.createIf,
|
||||
};
|
||||
|
||||
/**
|
||||
* Per-tool discovery metadata. Keys must align with BUILTIN_TOOLS. Used by initial-tool filtering
|
||||
* (sdk.ts) and discovery-corpus collection (search-tool-bm25). Kept separate so the public
|
||||
* BUILTIN_TOOLS map remains a directly callable factory record.
|
||||
*/
|
||||
export const BUILTIN_TOOL_METADATA: Record<string, BuiltinToolMetadata> = {
|
||||
read: { loadMode: "essential" },
|
||||
bash: { loadMode: "essential" },
|
||||
edit: { loadMode: "essential" },
|
||||
ast_grep: { loadMode: "discoverable", summary: "Search code with AST patterns (structural grep)" },
|
||||
ast_edit: { loadMode: "discoverable", summary: "Perform AST-aware code edits (structural refactoring)" },
|
||||
render_mermaid: { loadMode: "discoverable", summary: "Render a Mermaid diagram to an image" },
|
||||
ask: { loadMode: "discoverable", summary: "Ask the user a clarifying question" },
|
||||
debug: { loadMode: "discoverable", summary: "Debug a running process with DAP (debugger adapter protocol)" },
|
||||
eval: { loadMode: "discoverable", summary: "Execute Python or JavaScript code in an in-process eval backend" },
|
||||
calc: { loadMode: "discoverable", summary: "Evaluate a mathematical expression" },
|
||||
ssh: { loadMode: "discoverable", summary: "Execute a command on a remote host over SSH" },
|
||||
github: { loadMode: "discoverable", summary: "Interact with GitHub issues, pull requests, and repositories" },
|
||||
find: { loadMode: "discoverable", summary: "Find files and directories matching a glob pattern" },
|
||||
search: { loadMode: "discoverable", summary: "Search file contents using ripgrep (fast text search)" },
|
||||
lsp: {
|
||||
loadMode: "discoverable",
|
||||
summary: "Query LSP (language server) for diagnostics, hover info, and references",
|
||||
},
|
||||
notebook: { loadMode: "discoverable", summary: "Read and execute Jupyter notebooks" },
|
||||
inspect_image: { loadMode: "discoverable", summary: "Describe or analyze an image file" },
|
||||
browser: {
|
||||
loadMode: "discoverable",
|
||||
summary: "Control a headless browser to navigate and interact with web pages",
|
||||
},
|
||||
checkpoint: {
|
||||
loadMode: "discoverable",
|
||||
summary: "Create a git-based checkpoint to save and restore session state",
|
||||
},
|
||||
rewind: { loadMode: "discoverable", summary: "Rewind to a previously created checkpoint" },
|
||||
task: { loadMode: "discoverable", summary: "Spawn a subagent to complete a parallel task" },
|
||||
job: { loadMode: "discoverable", summary: "Manage long-running background jobs (async bash/python)" },
|
||||
recipe: { loadMode: "discoverable", summary: "Execute a saved bash recipe (multi-step shell command preset)" },
|
||||
irc: { loadMode: "discoverable", summary: "Send and receive messages between agents over IRC-like channels" },
|
||||
todo_write: {
|
||||
loadMode: "discoverable",
|
||||
summary: "Write a structured todo list to track progress within a session",
|
||||
},
|
||||
web_search: { loadMode: "discoverable", summary: "Search the web for up-to-date information" },
|
||||
search_tool_bm25: { loadMode: "essential" },
|
||||
write: { loadMode: "discoverable", summary: "Write content to a file (creates or overwrites)" },
|
||||
retain: { loadMode: "discoverable", summary: "Store important facts in hindsight memory" },
|
||||
recall: { loadMode: "discoverable", summary: "Search hindsight memory for relevant prior context" },
|
||||
reflect: { loadMode: "discoverable", summary: "Reflect on recent work and write hindsight memory" },
|
||||
};
|
||||
|
||||
export const HIDDEN_TOOLS: Record<string, ToolFactory> = {
|
||||
yield: s => new YieldTool(s),
|
||||
report_finding: () => reportFindingTool,
|
||||
@@ -371,32 +304,6 @@ export interface EvalBackendsAllowance {
|
||||
js: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all built-in tools whose metadata `loadMode` is "discoverable".
|
||||
* Used by search_tool_bm25 to build the BM25 corpus when tools.discoveryMode === "all".
|
||||
* Pass an optional exclusion set to filter out names that are already active.
|
||||
*/
|
||||
export function getBuiltinDiscoverableEntries(
|
||||
excludeNames?: ReadonlySet<string>,
|
||||
): Array<{ name: string; entry: BuiltinEntry }> {
|
||||
return Object.entries(BUILTIN_TOOL_METADATA)
|
||||
.filter(([name, meta]) => meta.loadMode === "discoverable" && !excludeNames?.has(name))
|
||||
.map(([name, meta]) => {
|
||||
const factory = BUILTIN_TOOLS[name];
|
||||
if (!factory) {
|
||||
throw new Error(`BUILTIN_TOOL_METADATA["${name}"] has no matching factory in BUILTIN_TOOLS`);
|
||||
}
|
||||
return { name, entry: { factory, ...meta } satisfies BuiltinEntry };
|
||||
});
|
||||
}
|
||||
|
||||
/** Names of all built-ins whose metadata `loadMode` is "discoverable". */
|
||||
export function getBuiltinDiscoverableNames(): string[] {
|
||||
return Object.entries(BUILTIN_TOOL_METADATA)
|
||||
.filter(([, meta]) => meta.loadMode === "discoverable")
|
||||
.map(([name]) => name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse PI_PY / PI_JS environment variables. Each is a boolean flag; unset
|
||||
* means "not specified, defer to settings". Returns null when neither is set
|
||||
|
||||
@@ -41,6 +41,8 @@ function extractResponseText(message: AssistantMessage): string {
|
||||
export class InspectImageTool implements AgentTool<typeof inspectImageSchema, InspectImageToolDetails> {
|
||||
readonly name = "inspect_image";
|
||||
readonly label = "InspectImage";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Describe or analyze an image file";
|
||||
readonly description: string;
|
||||
readonly parameters = inspectImageSchema;
|
||||
readonly strict = false;
|
||||
|
||||
@@ -74,10 +74,11 @@ export interface IrcDetails {
|
||||
export class IrcTool implements AgentTool<typeof ircSchema, IrcDetails> {
|
||||
readonly name = "irc";
|
||||
readonly label = "IRC";
|
||||
readonly summary = "Send and receive messages between agents over IRC-like channels";
|
||||
readonly description: string;
|
||||
readonly parameters = ircSchema;
|
||||
readonly strict = true;
|
||||
|
||||
readonly loadMode = "discoverable";
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(ircDescription);
|
||||
}
|
||||
|
||||
@@ -76,10 +76,11 @@ export interface JobToolDetails {
|
||||
export class JobTool implements AgentTool<typeof jobSchema, JobToolDetails> {
|
||||
readonly name = "job";
|
||||
readonly label = "Job";
|
||||
readonly summary = "Manage long-running background jobs (async bash/python)";
|
||||
readonly description: string;
|
||||
readonly parameters = jobSchema;
|
||||
readonly strict = true;
|
||||
|
||||
readonly loadMode = "discoverable";
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(jobDescription);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,8 @@ type NotebookParams = Static<typeof notebookSchema>;
|
||||
export class NotebookTool implements AgentTool<typeof notebookSchema, NotebookToolDetails> {
|
||||
readonly name = "notebook";
|
||||
readonly label = "Notebook";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Read and execute Jupyter notebooks";
|
||||
readonly description = "Edit, insert, or delete cells in Jupyter notebooks (.ipynb). cell_index is 0-based.";
|
||||
readonly parameters = notebookSchema;
|
||||
readonly strict = true;
|
||||
|
||||
@@ -512,6 +512,7 @@ interface ResolvedSqliteReadPath {
|
||||
export class ReadTool implements AgentTool<typeof readSchema, ReadToolDetails> {
|
||||
readonly name = "read";
|
||||
readonly label = "Read";
|
||||
readonly loadMode = "essential";
|
||||
readonly description: string;
|
||||
readonly parameters = readSchema;
|
||||
readonly nonAbortable = true;
|
||||
|
||||
@@ -33,6 +33,8 @@ export class RecipeTool implements AgentTool<typeof recipeSchema, BashToolDetail
|
||||
readonly parameters = recipeSchema;
|
||||
readonly strict = true;
|
||||
readonly concurrency = "exclusive";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Execute a saved bash recipe (multi-step shell command preset)";
|
||||
readonly mergeCallAndResult = true;
|
||||
readonly inline = true;
|
||||
readonly renderCall: (args: RecipeRenderArgs, options: RenderResultOptions, uiTheme: Theme) => Component;
|
||||
|
||||
@@ -35,9 +35,11 @@ export interface RenderMermaidToolDetails {
|
||||
export class RenderMermaidTool implements AgentTool<typeof renderMermaidSchema, RenderMermaidToolDetails> {
|
||||
readonly name = "render_mermaid";
|
||||
readonly label = "RenderMermaid";
|
||||
readonly summary = "Render a Mermaid diagram to an image";
|
||||
readonly description: string;
|
||||
readonly parameters = renderMermaidSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(renderMermaidDescription);
|
||||
|
||||
@@ -210,6 +210,7 @@ function renderFallbackResult(text: string, theme: Theme): Component {
|
||||
export class SearchToolBm25Tool implements AgentTool<typeof searchToolBm25Schema, SearchToolBm25Details> {
|
||||
readonly name = "search_tool_bm25";
|
||||
readonly label = "SearchTools";
|
||||
readonly loadMode = "essential";
|
||||
get description(): string {
|
||||
return renderSearchToolBm25Description(getDiscoverableToolsForDescription(this.session));
|
||||
}
|
||||
|
||||
@@ -80,6 +80,8 @@ type SearchParams = Static<typeof searchSchema>;
|
||||
export class SearchTool implements AgentTool<typeof searchSchema, SearchToolDetails> {
|
||||
readonly name = "search";
|
||||
readonly label = "Search";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Search file contents using ripgrep (fast text search)";
|
||||
readonly description: string;
|
||||
readonly parameters = searchSchema;
|
||||
readonly strict = true;
|
||||
|
||||
@@ -120,6 +120,8 @@ type SshToolParams = Static<typeof sshSchema>;
|
||||
|
||||
export class SshTool implements AgentTool<typeof sshSchema, SSHToolDetails> {
|
||||
readonly name = "ssh";
|
||||
readonly summary = "Execute a command on a remote host over SSH";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly label = "SSH";
|
||||
readonly parameters = sshSchema;
|
||||
readonly concurrency = "exclusive";
|
||||
|
||||
@@ -503,11 +503,12 @@ function formatSummary(phases: TodoPhase[], errors: string[]): string {
|
||||
export class TodoWriteTool implements AgentTool<typeof todoWriteSchema, TodoWriteToolDetails> {
|
||||
readonly name = "todo_write";
|
||||
readonly label = "Todo Write";
|
||||
readonly summary = "Write a structured todo list to track progress within a session";
|
||||
readonly description: string;
|
||||
readonly parameters = todoWriteSchema;
|
||||
readonly concurrency = "exclusive";
|
||||
readonly strict = true;
|
||||
|
||||
readonly loadMode = "discoverable";
|
||||
constructor(private readonly session: ToolSession) {
|
||||
this.description = prompt.render(todoWriteDescription);
|
||||
}
|
||||
|
||||
@@ -166,6 +166,8 @@ export class WriteTool implements AgentTool<typeof writeSchema, WriteToolDetails
|
||||
readonly nonAbortable = true;
|
||||
readonly strict = true;
|
||||
readonly concurrency = "exclusive";
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Write content to a file (creates or overwrites)";
|
||||
|
||||
readonly #writethrough: WritethroughCallback;
|
||||
|
||||
|
||||
@@ -211,6 +211,8 @@ export class WebSearchTool implements AgentTool<typeof webSearchSchema, SearchRe
|
||||
readonly description: string;
|
||||
readonly parameters = webSearchSchema;
|
||||
readonly strict = true;
|
||||
readonly loadMode = "discoverable";
|
||||
readonly summary = "Search the web for up-to-date information";
|
||||
|
||||
constructor(_session: ToolSession) {
|
||||
this.description = prompt.render(webSearchDescription);
|
||||
|
||||
@@ -52,6 +52,7 @@ describe("AgentSession manual retry", () => {
|
||||
beforeEach(async () => {
|
||||
tempDir = TempDir.createSync("@pi-manual-retry-");
|
||||
authStorage = await AuthStorage.create(path.join(tempDir.path(), "testauth.db"));
|
||||
authStorage.setRuntimeApiKey("anthropic", "test-key");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -915,7 +915,10 @@ describe("AgentSession MCP discovery", () => {
|
||||
// ── Findings #4: built-in discovery is restricted to declared discoverable ─
|
||||
it("getDiscoverableTools({source:'builtin'}) excludes hidden and non-declared registry tools", () => {
|
||||
const readTool = createBasicTool("read", "Read");
|
||||
readTool.loadMode = "essential";
|
||||
const findTool = createBasicTool("find", "Find");
|
||||
findTool.loadMode = "discoverable";
|
||||
findTool.summary = "Find files and directories matching a glob pattern";
|
||||
const resolveTool = createBasicTool("resolve", "Resolve"); // hidden — must be excluded
|
||||
const customTool = createBasicTool("custom_inactive", "Custom"); // not in metadata — must be excluded
|
||||
const toolRegistry = new Map([
|
||||
@@ -942,7 +945,7 @@ describe("AgentSession MCP discovery", () => {
|
||||
const names = builtin.map(t => t.name);
|
||||
expect(names).toContain("find"); // declared discoverable AND present in registry
|
||||
expect(names).not.toContain("read"); // already active
|
||||
expect(names).not.toContain("resolve"); // hidden — not in BUILTIN_TOOL_METADATA
|
||||
expect(names).not.toContain("custom_inactive"); // unknown — not in BUILTIN_TOOL_METADATA
|
||||
expect(names).not.toContain("resolve"); // hidden — no discoverable loadMode
|
||||
expect(names).not.toContain("custom_inactive"); // unknown — no discoverable loadMode
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,65 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { Settings } from "../../src/config/settings";
|
||||
import type { ToolSession } from "../../src/tools/index";
|
||||
import {
|
||||
BUILTIN_TOOL_METADATA,
|
||||
AskTool,
|
||||
BUILTIN_TOOLS,
|
||||
computeEssentialBuiltinNames,
|
||||
createTools,
|
||||
DEFAULT_ESSENTIAL_TOOL_NAMES,
|
||||
IrcTool,
|
||||
JobTool,
|
||||
RecipeTool,
|
||||
SshTool,
|
||||
} from "../../src/tools/index";
|
||||
|
||||
const allToolsSettings = Settings.isolated({
|
||||
"astGrep.enabled": true,
|
||||
"astEdit.enabled": true,
|
||||
"renderMermaid.enabled": true,
|
||||
"debug.enabled": true,
|
||||
"find.enabled": true,
|
||||
"search.enabled": true,
|
||||
"github.enabled": true,
|
||||
"lsp.enabled": true,
|
||||
"notebook.enabled": true,
|
||||
"inspect_image.enabled": true,
|
||||
"web_search.enabled": true,
|
||||
"calc.enabled": true,
|
||||
"browser.enabled": true,
|
||||
"checkpoint.enabled": true,
|
||||
"irc.enabled": true,
|
||||
"recipe.enabled": true,
|
||||
"todo.enabled": true,
|
||||
"memory.backend": "hindsight",
|
||||
"tools.discoveryMode": "all",
|
||||
});
|
||||
|
||||
const toolSession: ToolSession = {
|
||||
cwd: "/tmp/test",
|
||||
hasUI: false,
|
||||
getSessionFile: () => null,
|
||||
getSessionSpawns: () => null,
|
||||
settings: allToolsSettings,
|
||||
isToolDiscoveryEnabled: () => true,
|
||||
getSelectedDiscoveredToolNames: () => [],
|
||||
activateDiscoveredTools: async names => names,
|
||||
};
|
||||
|
||||
async function getToolMetadata(): Promise<Map<string, { loadMode?: string; summary?: string }>> {
|
||||
const tools = await createTools(toolSession, Object.keys(BUILTIN_TOOLS));
|
||||
const metadata = new Map(tools.map(tool => [tool.name, { loadMode: tool.loadMode, summary: tool.summary }]));
|
||||
for (const tool of [
|
||||
new AskTool({ ...toolSession, hasUI: true }),
|
||||
new SshTool(toolSession, [], new Map(), ""),
|
||||
new JobTool(toolSession),
|
||||
new RecipeTool(toolSession, []),
|
||||
new IrcTool(toolSession),
|
||||
]) {
|
||||
metadata.set(tool.name, { loadMode: tool.loadMode, summary: tool.summary });
|
||||
}
|
||||
return metadata;
|
||||
}
|
||||
describe("BUILTIN_TOOLS public factory map", () => {
|
||||
it("exposes callable tool factories (back-compat for external SDK callers)", () => {
|
||||
// External callers may invoke BUILTIN_TOOLS.read(session) directly. Verify the value
|
||||
@@ -16,31 +69,23 @@ describe("BUILTIN_TOOLS public factory map", () => {
|
||||
expect(typeof BUILTIN_TOOLS.edit).toBe("function");
|
||||
});
|
||||
|
||||
it("has a callable factory for every metadata entry", () => {
|
||||
for (const name of Object.keys(BUILTIN_TOOL_METADATA)) {
|
||||
expect(typeof BUILTIN_TOOLS[name]).toBe("function");
|
||||
}
|
||||
});
|
||||
|
||||
it("has metadata for every factory entry", () => {
|
||||
for (const name of Object.keys(BUILTIN_TOOLS)) {
|
||||
expect(BUILTIN_TOOL_METADATA[name]).toBeDefined();
|
||||
}
|
||||
it("sets loading fields on tool definitions without wrapping factories", async () => {
|
||||
const metadata = await getToolMetadata();
|
||||
const missing = Object.keys(BUILTIN_TOOLS).filter(name => metadata.get(name)?.loadMode === undefined);
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("BUILTIN_TOOL_METADATA loadMode annotations", () => {
|
||||
it("marks read, bash, edit as essential", () => {
|
||||
expect(BUILTIN_TOOL_METADATA.read?.loadMode).toBe("essential");
|
||||
expect(BUILTIN_TOOL_METADATA.bash?.loadMode).toBe("essential");
|
||||
expect(BUILTIN_TOOL_METADATA.edit?.loadMode).toBe("essential");
|
||||
describe("built-in tool loadMode annotations", () => {
|
||||
it("marks read, bash, edit, and search_tool_bm25 as essential", async () => {
|
||||
const metadata = await getToolMetadata();
|
||||
expect(metadata.get("read")?.loadMode).toBe("essential");
|
||||
expect(metadata.get("bash")?.loadMode).toBe("essential");
|
||||
expect(metadata.get("edit")?.loadMode).toBe("essential");
|
||||
expect(metadata.get("search_tool_bm25")?.loadMode).toBe("essential");
|
||||
});
|
||||
|
||||
it("marks search_tool_bm25 as essential (discovery tool itself)", () => {
|
||||
expect(BUILTIN_TOOL_METADATA.search_tool_bm25?.loadMode).toBe("essential");
|
||||
});
|
||||
|
||||
it("marks non-essential tools as discoverable", () => {
|
||||
it("marks non-essential tools as discoverable", async () => {
|
||||
const discoverableExpected = [
|
||||
"ast_grep",
|
||||
"ast_edit",
|
||||
@@ -70,14 +115,15 @@ describe("BUILTIN_TOOL_METADATA loadMode annotations", () => {
|
||||
"recall",
|
||||
"reflect",
|
||||
];
|
||||
for (const name of discoverableExpected) {
|
||||
expect(BUILTIN_TOOL_METADATA[name]?.loadMode).toBe("discoverable");
|
||||
}
|
||||
const metadata = await getToolMetadata();
|
||||
const missing = discoverableExpected.filter(name => metadata.get(name)?.loadMode !== "discoverable");
|
||||
expect(missing).toEqual([]);
|
||||
});
|
||||
|
||||
it("provides a summary for every discoverable tool", () => {
|
||||
it("provides a summary for every discoverable tool", async () => {
|
||||
const missing: string[] = [];
|
||||
for (const [name, meta] of Object.entries(BUILTIN_TOOL_METADATA)) {
|
||||
const metadata = await getToolMetadata();
|
||||
for (const [name, meta] of metadata) {
|
||||
if (meta.loadMode === "discoverable" && !meta.summary) {
|
||||
missing.push(name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user