fix(coding-agent): support dynamic tool registration and tool prompt snippets closes #1720

This commit is contained in:
Mario Zechner 2026-03-02 22:32:07 +01:00
parent ca5510158d
commit bc2fa8d6d0
12 changed files with 285 additions and 47 deletions

View file

@ -2,8 +2,13 @@
## [Unreleased] ## [Unreleased]
### Added
- Added optional `promptSnippet` to `ToolDefinition` for one-line entries in the default system prompt's `Available tools` section. Active extension tools now appear there, including tools registered dynamically after startup.
### Fixed ### Fixed
- Fixed `pi.registerTool()` dynamic registration after session initialization. Tools registered in `session_start` and later handlers now refresh immediately, become active, and are visible to the LLM without `/reload` ([#1720](https://github.com/badlogic/pi-mono/issues/1720))
- Fixed session message persistence ordering by serializing `AgentSession` event processing, preventing `toolResult` entries from being written before their corresponding assistant tool-call messages when extension handlers are asynchronous ([#1717](https://github.com/badlogic/pi-mono/issues/1717)) - Fixed session message persistence ordering by serializing `AgentSession` event processing, preventing `toolResult` entries from being written before their corresponding assistant tool-call messages when extension handlers are asynchronous ([#1717](https://github.com/badlogic/pi-mono/issues/1717))
## [0.55.3] - 2026-02-27 ## [0.55.3] - 2026-02-27

View file

@ -880,6 +880,12 @@ Subscribe to events. See [Events](#events) for event types and return values.
Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) for full details. Register a custom tool callable by the LLM. See [Custom Tools](#custom-tools) for full details.
`pi.registerTool()` works both during extension load and after startup. You can call it inside `session_start`, command handlers, or other event handlers. New tools are refreshed immediately in the same session, so they appear in `pi.getAllTools()` and are callable by the LLM without `/reload`.
Use `pi.setActiveTools()` to enable or disable tools (including dynamically added tools) at runtime.
See [dynamic-tools.ts](../examples/extensions/dynamic-tools.ts) for a full example.
```typescript ```typescript
import { Type } from "@sinclair/typebox"; import { Type } from "@sinclair/typebox";
import { StringEnum } from "@mariozechner/pi-ai"; import { StringEnum } from "@mariozechner/pi-ai";
@ -888,6 +894,7 @@ pi.registerTool({
name: "my_tool", name: "my_tool",
label: "My Tool", label: "My Tool",
description: "What this tool does", description: "What this tool does",
promptSnippet: "Summarize or transform text according to action",
parameters: Type.Object({ parameters: Type.Object({
action: StringEnum(["list", "add"] as const), action: StringEnum(["list", "add"] as const),
text: Type.Optional(Type.String()), text: Type.Optional(Type.String()),
@ -1116,7 +1123,7 @@ const result = await pi.exec("git", ["status"], { signal, timeout: 5000 });
### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names) ### pi.getActiveTools() / pi.getAllTools() / pi.setActiveTools(names)
Manage active tools. Manage active tools. This works for both built-in tools and dynamically registered tools.
```typescript ```typescript
const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"] const active = pi.getActiveTools(); // ["read", "bash", "edit", "write"]
@ -1276,6 +1283,8 @@ export default function (pi: ExtensionAPI) {
Register tools the LLM can call via `pi.registerTool()`. Tools appear in the system prompt and can have custom rendering. Register tools the LLM can call via `pi.registerTool()`. Tools appear in the system prompt and can have custom rendering.
Use `promptSnippet` for a short one-line entry in the `Available tools` section in the default system prompt. If omitted, pi falls back to `description`.
Note: Some models are idiots and include the @ prefix in tool path arguments. Built-in tools strip a leading @ before resolving paths. If your custom tool accepts a path, normalize a leading @ as well. Note: Some models are idiots and include the @ prefix in tool path arguments. Built-in tools strip a leading @ before resolving paths. If your custom tool accepts a path, normalize a leading @ as well.
### Tool Definition ### Tool Definition
@ -1289,6 +1298,7 @@ pi.registerTool({
name: "my_tool", name: "my_tool",
label: "My Tool", label: "My Tool",
description: "What this tool does (shown to LLM)", description: "What this tool does (shown to LLM)",
promptSnippet: "List or add items in the project todo list",
parameters: Type.Object({ parameters: Type.Object({
action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility action: StringEnum(["list", "add"] as const), // Use StringEnum for Google compatibility
text: Type.Optional(Type.String()), text: Type.Optional(Type.String()),
@ -1886,6 +1896,7 @@ All examples in [examples/extensions/](../examples/extensions/).
| `question.ts` | Tool with user interaction | `registerTool`, `ui.select` | | `question.ts` | Tool with user interaction | `registerTool`, `ui.select` |
| `questionnaire.ts` | Multi-step wizard tool | `registerTool`, `ui.custom` | | `questionnaire.ts` | Multi-step wizard tool | `registerTool`, `ui.custom` |
| `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events | | `todo.ts` | Stateful tool with persistence | `registerTool`, `appendEntry`, `renderResult`, session events |
| `dynamic-tools.ts` | Register tools after startup and during commands | `registerTool`, `session_start`, `registerCommand` |
| `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` | | `truncated-tool.ts` | Output truncation example | `registerTool`, `truncateHead` |
| `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) | | `tool-override.ts` | Override built-in read tool | `registerTool` (same name as built-in) |
| **Commands** ||| | **Commands** |||

View file

@ -33,6 +33,7 @@ cp permission-gate.ts ~/.pi/agent/extensions/
| `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI | | `question.ts` | Demonstrates `ctx.ui.select()` for asking the user questions with custom UI |
| `questionnaire.ts` | Multi-question input with tab bar navigation between questions | | `questionnaire.ts` | Multi-question input with tab bar navigation between questions |
| `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) | | `tool-override.ts` | Override built-in tools (e.g., add logging/access control to `read`) |
| `dynamic-tools.ts` | Register tools after startup (`session_start`) and at runtime via command |
| `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior | | `built-in-tool-renderer.ts` | Custom compact rendering for built-in tools (read, bash, edit, write) while keeping original behavior |
| `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) | | `minimal-mode.ts` | Override built-in tool rendering for minimal display (only tool calls, no output in collapsed mode) |
| `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) | | `truncated-tool.ts` | Wraps ripgrep with proper output truncation (50KB/2000 lines) |

View file

@ -0,0 +1,73 @@
/**
* Dynamic Tools Extension
*
* Demonstrates registering tools after session initialization.
*
* - Registers one tool during session_start
* - Registers additional tools at runtime via /add-echo-tool <name>
*/
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
import { Type } from "@sinclair/typebox";
const ECHO_PARAMS = Type.Object({
message: Type.String({ description: "Message to echo" }),
});
function normalizeToolName(input: string): string | undefined {
const trimmed = input.trim().toLowerCase();
if (!trimmed) return undefined;
if (!/^[a-z0-9_]+$/.test(trimmed)) return undefined;
return trimmed;
}
export default function dynamicToolsExtension(pi: ExtensionAPI) {
const registeredToolNames = new Set<string>();
const registerEchoTool = (name: string, label: string, prefix: string): boolean => {
if (registeredToolNames.has(name)) {
return false;
}
registeredToolNames.add(name);
pi.registerTool({
name,
label,
description: `Echo a message with prefix: ${prefix}`,
promptSnippet: `Echo back user-provided text with ${prefix.trim()} prefix`,
parameters: ECHO_PARAMS,
async execute(_toolCallId, params) {
return {
content: [{ type: "text", text: `${prefix}${params.message}` }],
details: { tool: name, prefix },
};
},
});
return true;
};
pi.on("session_start", (_event, ctx) => {
registerEchoTool("echo_session", "Echo Session", "[session] ");
ctx.ui.notify("Registered dynamic tool: echo_session", "info");
});
pi.registerCommand("add-echo-tool", {
description: "Register a new echo tool dynamically: /add-echo-tool <tool_name>",
handler: async (args, ctx) => {
const toolName = normalizeToolName(args);
if (!toolName) {
ctx.ui.notify("Usage: /add-echo-tool <tool_name> (lowercase, numbers, underscores)", "warning");
return;
}
const created = registerEchoTool(toolName, `Echo ${toolName}`, `[${toolName}] `);
if (!created) {
ctx.ui.notify(`Tool already registered: ${toolName}`, "warning");
return;
}
ctx.ui.notify(`Registered dynamic tool: ${toolName}`, "info");
},
});
}

View file

@ -268,6 +268,7 @@ export class AgentSession {
// Tool registry for extension getTools/setTools // Tool registry for extension getTools/setTools
private _toolRegistry: Map<string, AgentTool> = new Map(); private _toolRegistry: Map<string, AgentTool> = new Map();
private _toolPromptSnippets: Map<string, string> = new Map();
// Base system prompt (without extension appends) - used to apply fresh appends each turn // Base system prompt (without extension appends) - used to apply fresh appends each turn
private _baseSystemPrompt = ""; private _baseSystemPrompt = "";
@ -682,8 +683,25 @@ export class AgentSession {
return this._resourceLoader.getPrompts().prompts; return this._resourceLoader.getPrompts().prompts;
} }
private _normalizePromptSnippet(text: string | undefined): string | undefined {
if (!text) return undefined;
const oneLine = text
.replace(/[\r\n]+/g, " ")
.replace(/\s+/g, " ")
.trim();
return oneLine.length > 0 ? oneLine : undefined;
}
private _rebuildSystemPrompt(toolNames: string[]): string { private _rebuildSystemPrompt(toolNames: string[]): string {
const validToolNames = toolNames.filter((name) => this._baseToolRegistry.has(name)); const validToolNames = toolNames.filter((name) => this._toolRegistry.has(name));
const toolSnippets: Record<string, string> = {};
for (const name of validToolNames) {
const snippet = this._toolPromptSnippets.get(name);
if (snippet) {
toolSnippets[name] = snippet;
}
}
const loaderSystemPrompt = this._resourceLoader.getSystemPrompt(); const loaderSystemPrompt = this._resourceLoader.getSystemPrompt();
const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt(); const loaderAppendSystemPrompt = this._resourceLoader.getAppendSystemPrompt();
const appendSystemPrompt = const appendSystemPrompt =
@ -698,6 +716,7 @@ export class AgentSession {
customPrompt: loaderSystemPrompt, customPrompt: loaderSystemPrompt,
appendSystemPrompt, appendSystemPrompt,
selectedTools: validToolNames, selectedTools: validToolNames,
toolSnippets,
}); });
} }
@ -1934,6 +1953,7 @@ export class AgentSession {
getActiveTools: () => this.getActiveToolNames(), getActiveTools: () => this.getActiveToolNames(),
getAllTools: () => this.getAllTools(), getAllTools: () => this.getAllTools(),
setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames), setActiveTools: (toolNames) => this.setActiveToolsByName(toolNames),
refreshTools: () => this._refreshToolRegistry(),
getCommands, getCommands,
setModel: async (model) => { setModel: async (model) => {
const key = await this.modelRegistry.getApiKey(model); const key = await this.modelRegistry.getApiKey(model);
@ -1969,6 +1989,60 @@ export class AgentSession {
); );
} }
private _refreshToolRegistry(options?: { activeToolNames?: string[]; includeAllExtensionTools?: boolean }): void {
const previousRegistryNames = new Set(this._toolRegistry.keys());
const previousActiveToolNames = this.getActiveToolNames();
const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
const allCustomTools = [
...registeredTools,
...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
];
this._toolPromptSnippets = new Map(
allCustomTools
.map((registeredTool) => {
const snippet = this._normalizePromptSnippet(
registeredTool.definition.promptSnippet ?? registeredTool.definition.description,
);
return snippet ? ([registeredTool.definition.name, snippet] as const) : undefined;
})
.filter((entry): entry is readonly [string, string] => entry !== undefined),
);
const wrappedExtensionTools = this._extensionRunner
? wrapRegisteredTools(allCustomTools, this._extensionRunner)
: [];
const toolRegistry = new Map(this._baseToolRegistry);
for (const tool of wrappedExtensionTools as AgentTool[]) {
toolRegistry.set(tool.name, tool);
}
if (this._extensionRunner) {
const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
} else {
this._toolRegistry = toolRegistry;
}
const nextActiveToolNames = options?.activeToolNames
? [...options.activeToolNames]
: [...previousActiveToolNames];
if (options?.includeAllExtensionTools) {
for (const tool of wrappedExtensionTools) {
nextActiveToolNames.push(tool.name);
}
} else if (!options?.activeToolNames) {
for (const toolName of this._toolRegistry.keys()) {
if (!previousRegistryNames.has(toolName)) {
nextActiveToolNames.push(toolName);
}
}
}
this.setActiveToolsByName([...new Set(nextActiveToolNames)]);
}
private _buildRuntime(options: { private _buildRuntime(options: {
activeToolNames?: string[]; activeToolNames?: string[];
flagValues?: Map<string, boolean | string>; flagValues?: Map<string, boolean | string>;
@ -2012,52 +2086,14 @@ export class AgentSession {
this._applyExtensionBindings(this._extensionRunner); this._applyExtensionBindings(this._extensionRunner);
} }
const registeredTools = this._extensionRunner?.getAllRegisteredTools() ?? [];
const allCustomTools = [
...registeredTools,
...this._customTools.map((def) => ({ definition: def, extensionPath: "<sdk>" })),
];
const wrappedExtensionTools = this._extensionRunner
? wrapRegisteredTools(allCustomTools, this._extensionRunner)
: [];
const toolRegistry = new Map(this._baseToolRegistry);
for (const tool of wrappedExtensionTools as AgentTool[]) {
toolRegistry.set(tool.name, tool);
}
const defaultActiveToolNames = this._baseToolsOverride const defaultActiveToolNames = this._baseToolsOverride
? Object.keys(this._baseToolsOverride) ? Object.keys(this._baseToolsOverride)
: ["read", "bash", "edit", "write"]; : ["read", "bash", "edit", "write"];
const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames; const baseActiveToolNames = options.activeToolNames ?? defaultActiveToolNames;
const activeToolNameSet = new Set<string>(baseActiveToolNames); this._refreshToolRegistry({
if (options.includeAllExtensionTools) { activeToolNames: baseActiveToolNames,
for (const tool of wrappedExtensionTools as AgentTool[]) { includeAllExtensionTools: options.includeAllExtensionTools,
activeToolNameSet.add(tool.name); });
}
}
const extensionToolNames = new Set(wrappedExtensionTools.map((tool) => tool.name));
const activeBaseTools = Array.from(activeToolNameSet)
.filter((name) => this._baseToolRegistry.has(name) && !extensionToolNames.has(name))
.map((name) => this._baseToolRegistry.get(name) as AgentTool);
const activeExtensionTools = wrappedExtensionTools.filter((tool) => activeToolNameSet.has(tool.name));
const activeToolsArray: AgentTool[] = [...activeBaseTools, ...activeExtensionTools];
if (this._extensionRunner) {
const wrappedActiveTools = wrapToolsWithExtensions(activeToolsArray, this._extensionRunner);
this.agent.setTools(wrappedActiveTools as AgentTool[]);
const wrappedAllTools = wrapToolsWithExtensions(Array.from(toolRegistry.values()), this._extensionRunner);
this._toolRegistry = new Map(wrappedAllTools.map((tool) => [tool.name, tool]));
} else {
this.agent.setTools(activeToolsArray);
this._toolRegistry = toolRegistry;
}
const systemPromptToolNames = Array.from(activeToolNameSet).filter((name) => this._baseToolRegistry.has(name));
this._baseSystemPrompt = this._rebuildSystemPrompt(systemPromptToolNames);
this.agent.setSystemPrompt(this._baseSystemPrompt);
} }
async reload(): Promise<void> { async reload(): Promise<void> {

View file

@ -119,6 +119,8 @@ export function createExtensionRuntime(): ExtensionRuntime {
getActiveTools: notInitialized, getActiveTools: notInitialized,
getAllTools: notInitialized, getAllTools: notInitialized,
setActiveTools: notInitialized, setActiveTools: notInitialized,
// registerTool() is valid during extension load; refresh is only needed post-bind.
refreshTools: () => {},
getCommands: notInitialized, getCommands: notInitialized,
setModel: () => Promise.reject(new Error("Extension runtime not initialized")), setModel: () => Promise.reject(new Error("Extension runtime not initialized")),
getThinkingLevel: notInitialized, getThinkingLevel: notInitialized,
@ -162,6 +164,7 @@ function createExtensionAPI(
definition: tool, definition: tool,
extensionPath: extension.path, extensionPath: extension.path,
}); });
runtime.refreshTools();
}, },
registerCommand(name: string, options: Omit<RegisteredCommand, "name">): void { registerCommand(name: string, options: Omit<RegisteredCommand, "name">): void {

View file

@ -244,6 +244,7 @@ export class ExtensionRunner {
this.runtime.getActiveTools = actions.getActiveTools; this.runtime.getActiveTools = actions.getActiveTools;
this.runtime.getAllTools = actions.getAllTools; this.runtime.getAllTools = actions.getAllTools;
this.runtime.setActiveTools = actions.setActiveTools; this.runtime.setActiveTools = actions.setActiveTools;
this.runtime.refreshTools = actions.refreshTools;
this.runtime.getCommands = actions.getCommands; this.runtime.getCommands = actions.getCommands;
this.runtime.setModel = actions.setModel; this.runtime.setModel = actions.setModel;
this.runtime.getThinkingLevel = actions.getThinkingLevel; this.runtime.getThinkingLevel = actions.getThinkingLevel;

View file

@ -339,6 +339,8 @@ export interface ToolDefinition<TParams extends TSchema = TSchema, TDetails = un
label: string; label: string;
/** Description for LLM */ /** Description for LLM */
description: string; description: string;
/** Optional one-line snippet for the Available tools section in the default system prompt. Falls back to description when omitted. */
promptSnippet?: string;
/** Parameter schema (TypeBox) */ /** Parameter schema (TypeBox) */
parameters: TParams; parameters: TParams;
@ -1251,6 +1253,8 @@ export type GetCommandsHandler = () => SlashCommandInfo[];
export type SetActiveToolsHandler = (toolNames: string[]) => void; export type SetActiveToolsHandler = (toolNames: string[]) => void;
export type RefreshToolsHandler = () => void;
export type SetModelHandler = (model: Model<any>) => Promise<boolean>; export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
export type GetThinkingLevelHandler = () => ThinkingLevel; export type GetThinkingLevelHandler = () => ThinkingLevel;
@ -1291,6 +1295,7 @@ export interface ExtensionActions {
getActiveTools: GetActiveToolsHandler; getActiveTools: GetActiveToolsHandler;
getAllTools: GetAllToolsHandler; getAllTools: GetAllToolsHandler;
setActiveTools: SetActiveToolsHandler; setActiveTools: SetActiveToolsHandler;
refreshTools: RefreshToolsHandler;
getCommands: GetCommandsHandler; getCommands: GetCommandsHandler;
setModel: SetModelHandler; setModel: SetModelHandler;
getThinkingLevel: GetThinkingLevelHandler; getThinkingLevel: GetThinkingLevelHandler;

View file

@ -21,6 +21,8 @@ export interface BuildSystemPromptOptions {
customPrompt?: string; customPrompt?: string;
/** Tools to include in prompt. Default: [read, bash, edit, write] */ /** Tools to include in prompt. Default: [read, bash, edit, write] */
selectedTools?: string[]; selectedTools?: string[];
/** Optional one-line tool snippets keyed by tool name. */
toolSnippets?: Record<string, string>;
/** Text to append to system prompt. */ /** Text to append to system prompt. */
appendSystemPrompt?: string; appendSystemPrompt?: string;
/** Working directory. Default: process.cwd() */ /** Working directory. Default: process.cwd() */
@ -36,6 +38,7 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const { const {
customPrompt, customPrompt,
selectedTools, selectedTools,
toolSnippets,
appendSystemPrompt, appendSystemPrompt,
cwd, cwd,
contextFiles: providedContextFiles, contextFiles: providedContextFiles,
@ -94,9 +97,18 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
const docsPath = getDocsPath(); const docsPath = getDocsPath();
const examplesPath = getExamplesPath(); const examplesPath = getExamplesPath();
// Build tools list based on selected tools (only built-in tools with known descriptions) // Build tools list based on selected tools.
const tools = (selectedTools || ["read", "bash", "edit", "write"]).filter((t) => t in toolDescriptions); // Built-ins use toolDescriptions. Custom tools can provide one-line snippets.
const toolsList = tools.length > 0 ? tools.map((t) => `- ${t}: ${toolDescriptions[t]}`).join("\n") : "(none)"; const tools = selectedTools || ["read", "bash", "edit", "write"];
const toolsList =
tools.length > 0
? tools
.map((name) => {
const snippet = toolSnippets?.[name] ?? toolDescriptions[name] ?? name;
return `- ${name}: ${snippet}`;
})
.join("\n")
: "(none)";
// Build guidelines based on which tools are actually available // Build guidelines based on which tools are actually available
const guidelinesList: string[] = []; const guidelinesList: string[] = [];

View file

@ -0,0 +1,75 @@
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { getModel } from "@mariozechner/pi-ai";
import { Type } from "@sinclair/typebox";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { DefaultResourceLoader } from "../src/core/resource-loader.js";
import { createAgentSession } from "../src/core/sdk.js";
import { SessionManager } from "../src/core/session-manager.js";
import { SettingsManager } from "../src/core/settings-manager.js";
describe("AgentSession dynamic tool registration", () => {
let tempDir: string;
let agentDir: string;
beforeEach(() => {
tempDir = join(tmpdir(), `pi-dynamic-tool-test-${Date.now()}-${Math.random().toString(36).slice(2)}`);
agentDir = join(tempDir, "agent");
mkdirSync(agentDir, { recursive: true });
});
afterEach(() => {
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true, force: true });
}
});
it("refreshes tool registry when tools are registered after initialization", async () => {
const settingsManager = SettingsManager.create(tempDir, agentDir);
const sessionManager = SessionManager.inMemory();
const resourceLoader = new DefaultResourceLoader({
cwd: tempDir,
agentDir,
settingsManager,
extensionFactories: [
(pi) => {
pi.on("session_start", () => {
pi.registerTool({
name: "dynamic_tool",
label: "Dynamic Tool",
description: "Tool registered from session_start",
promptSnippet: "Run dynamic test behavior",
parameters: Type.Object({}),
execute: async () => ({
content: [{ type: "text", text: "ok" }],
details: {},
}),
});
});
},
],
});
await resourceLoader.reload();
const { session } = await createAgentSession({
cwd: tempDir,
agentDir,
model: getModel("anthropic", "claude-sonnet-4-5")!,
settingsManager,
sessionManager,
resourceLoader,
});
expect(session.getAllTools().map((tool) => tool.name)).not.toContain("dynamic_tool");
await session.bindExtensions({});
expect(session.getAllTools().map((tool) => tool.name)).toContain("dynamic_tool");
expect(session.getActiveToolNames()).toContain("dynamic_tool");
expect(session.systemPrompt).toContain("- dynamic_tool: Run dynamic test behavior");
session.dispose();
});
});

View file

@ -60,6 +60,7 @@ describe("ExtensionRunner", () => {
getActiveTools: () => [], getActiveTools: () => [],
getAllTools: () => [], getAllTools: () => [],
setActiveTools: () => {}, setActiveTools: () => {},
refreshTools: () => {},
getCommands: () => [], getCommands: () => [],
setModel: async () => false, setModel: async () => false,
getThinkingLevel: () => "off", getThinkingLevel: () => "off",

View file

@ -37,4 +37,19 @@ describe("buildSystemPrompt", () => {
expect(prompt).toContain("- write:"); expect(prompt).toContain("- write:");
}); });
}); });
describe("custom tool snippets", () => {
test("includes custom tools in available tools section", () => {
const prompt = buildSystemPrompt({
selectedTools: ["read", "dynamic_tool"],
toolSnippets: {
dynamic_tool: "Run dynamic test behavior",
},
contextFiles: [],
skills: [],
});
expect(prompt).toContain("- dynamic_tool: Run dynamic test behavior");
});
});
}); });