* Make inline citation permanent and add image guide for local search - Removed the `enableInlineCitations` setting from the configuration and related components to simplify citation management. - Updated citation functions to operate without the setting, ensuring consistent behavior across the codebase. - Adjusted tests and documentation to reflect the removal of inline citation handling logic. - Enhanced the `renderCiCMessage` and `processInlineCitations` functions for improved clarity and functionality. * fix: Update web search tool description for clarity - Revised the description of the `webSearch` tool to specify that it searches the internet for information only when explicitly requested by the user. - Enhanced the instructions for using the `webSearch` tool to clarify when it should be utilized, ensuring users understand the intent required for web searches. - Reintroduced the `createGetTagListTool` import to maintain functionality in the `builtinTools` module. * fix: Enhance response handling in AgentPrompt integration tests - Updated the response processing logic in the AgentPrompt integration tests to ensure that `fullResponse` is always a string, accommodating multimodal and array content. - Implemented a function to extract text parts from arrays and handle various content types, improving the robustness of the tests. * Remove websearch related prompt from modelAdapter * fix: Update time query instructions in builtinTools - Added detailed instructions for handling time queries, including the conversion of city or timezone names to UTC offsets. - Clarified when to use the timezoneOffset parameter and emphasized the need for user clarification if the offset cannot be determined.
13 KiB
Tool System Documentation
Overview
The Copilot tool system uses a centralized registry pattern that makes it easy to add new tools, including future MCP (Model Context Protocol) tools. All tools are managed through a singleton ToolRegistry that provides a unified interface for tool discovery, configuration, and execution.
Tool Prompt Architecture
How Tool Instructions Flow to the LLM
The system uses a three-layer approach for providing tool instructions to LLMs:
-
Tool Schema Descriptions (in tool implementations like
ComposerTools.ts)- Defines parameter formats, rules, and validation
- NO XML examples - focuses on data contract only
-
Custom Prompt Instructions (in
builtinTools.ts)- Contains XML
<use_tool>invocation examples - Shows when and how to call the tool
- Contains XML
-
Model-Specific Adaptations (in
modelAdapter.ts)- Last resort for model-specific quirks
Layered Prompt Integration
ContextManagerpromotes user-attached artifacts from earlier turns into L2 (Context Library), so every chain runner starts with the same cacheable system prefix.- When a tool executes during the current turn, its XML payload is prepended to the user message (L3 + L5) using
renderCiCMessage(...). Nothing is injected into the system message, keeping L1/L2 stable. LayerToMessagesConverter.convert(envelope, { includeSystemMessage: true, mergeUserContent: true })materializes the base messages; runners then append tool XML before sending to the model.promptPayloadRecorderinspects the final payload and highlights tool blocks in its layered view, making it easy to debug the L1-L5 structure.
Why Two Layers: Schema vs Custom Instructions
Key Difference: Schema descriptions document parameters, while custom instructions provide XML invocation examples.
-
Clear Separation
- Schema: Parameter documentation (types, formats, rules) - NO XML examples
- Custom Instructions: XML
<use_tool>examples showing how to invoke
-
MCP Compatibility
- External tools provide immutable schemas
- We add custom instructions without modifying their code
-
Example
// Schema (parameter documentation only) salientTerms: z.array(z.string()).describe("Keywords to find in notes"); // Custom Instructions (XML invocation examples) customPromptInstructions: ` Example usage: <use_tool> <name>localSearch</name> <query>piano learning</query> <salientTerms>["piano", "learning"]</salientTerms> </use_tool>`;
Best Practices
-
Schema Descriptions: Parameter Documentation Only
- Document parameter types, formats, and validation rules
- NO XML examples - those belong in custom instructions
- Focus on the data contract
-
Custom Instructions: XML Examples & Usage Patterns
- Provide XML
<use_tool>invocation examples - Show common usage patterns and edge cases
- Include behavioral guidance (when to use vs other tools)
- Provide XML
-
Model Adapters: Model-Specific Fixes
- Only for persistent model-specific failures
- Keep minimal and targeted
localSearch CiC Prompting Flow
- CiC: Corpus in Context https://arxiv.org/pdf/2406.13121
- Instruction First:
CopilotPlusChainRunnernow assembles the localSearch payload viabuildLocalSearchInnerContent, ensuring citation guidance (e.g.,<guidance>rules) tops the XML block before any documents. - Documents Next: Search hits are serialized once through
formatSearchResultsForLLM; the helper simply appends them after guidance, keeping the documents section untouched but clearly separated. - Question Last:
renderCiCMessageformats the final prompt so any context precedes the user's original query; this matches the CiC recommendation for instruction → context → query ordering.- CopilotPlus: Uses
LayerToMessagesConverterwhich adds[User query]:label when merging L3+L5 content from envelope - AutonomousAgent: Uses
ensureCiCOrderingWithQuestionwhich adds[User query]:label to clearly separate tool results from original query in iterative loop - Consistency: Both chains use the same
[User query]:label format for uniform prompting across the codebase
- CopilotPlus: Uses
- Reusable Wrapping:
wrapLocalSearchPayloadcentralizes the<localSearch>tag creation (including optionaltimeRange), making the layout reusable for future chains without copying string glue.
Current Implementation
Core Files
src/tools/ToolRegistry.ts- Central registry for all toolssrc/tools/builtinTools.ts- Built-in tool definitions and initializationsrc/LLMProviders/chainRunner/AutonomousAgentChainRunner.ts- Tool execution in the agentsrc/settings/v2/components/ToolSettingsSection.tsx- Settings UI for tool configuration
Tool Registry Pattern
The ToolRegistry is a singleton that manages all tools:
class ToolRegistry {
static getInstance(): ToolRegistry;
register(definition: ToolDefinition): void;
registerAll(definitions: ToolDefinition[]): void;
getAllTools(): ToolDefinition[];
getEnabledTools(enabledToolIds: Set<string>, vaultAvailable: boolean): SimpleTool<any, any>[];
getToolsByCategory(): Map<string, ToolDefinition[]>;
getConfigurableTools(): ToolDefinition[];
getToolMetadata(id: string): ToolMetadata | undefined;
clear(): void;
}
Tool Definition Structure
interface ToolDefinition {
tool: SimpleTool<any, any>; // The actual tool implementation
metadata: ToolMetadata; // UI and configuration metadata
}
interface ToolMetadata {
id: string; // Unique identifier
displayName: string; // Shown in UI
description: string; // Help text
category: "search" | "time" | "file" | "media" | "mcp" | "custom";
isAlwaysEnabled?: boolean; // If true, not configurable (e.g., time tools)
requiresVault?: boolean; // Needs vault access
customPromptInstructions?: string; // Tool-specific prompts
}
Adding a New Built-in Tool
1. Implement the Tool
Create your tool following the SimpleTool interface:
// Example: New built-in tool
import { z } from "zod";
import { SimpleTool } from "./SimpleTool";
export const myNewTool: SimpleTool<{ input: string }, { result: string }> = {
name: "myNewTool",
description: "Description for the LLM to understand when to use this tool",
schema: z.object({
input: z.string().describe("The input parameter description"),
}),
func: async (params) => {
// Tool implementation
const result = await performOperation(params.input);
return { result };
},
};
2. Add to Built-in Tools
Update src/tools/builtinTools.ts:
export const BUILTIN_TOOLS: ToolDefinition[] = [
// ... existing tools ...
{
tool: myNewTool,
metadata: {
id: "myNewTool",
displayName: "My New Tool",
description: "User-friendly description for settings UI",
category: "custom", // Choose appropriate category
// Optional flags:
isAlwaysEnabled: false, // Set true if tool should always be available
requiresVault: true, // Set true if tool needs vault access
customPromptInstructions: "Special instructions for the AI when using this tool",
},
},
];
3. Update Default Settings (if configurable)
If the tool is configurable (not always-enabled), add its ID to the default enabled tools in src/constants.ts:
autonomousAgentEnabledToolIds: [
"localSearch",
"webSearch",
"pomodoro",
"youtubeTranscription",
"writeToFile",
"myNewTool" // Add your tool ID here
],
Adding MCP Tools (Future Implementation)
1. MCP Tool Wrapper
Create a wrapper to convert MCP tools to the SimpleTool interface:
function createMcpToolWrapper(serverName: string, mcpTool: McpTool): SimpleTool<any, any> {
return {
name: `${serverName}_${mcpTool.name}`,
description: mcpTool.description || `MCP tool from ${serverName}`,
schema: convertMcpSchemaToZod(mcpTool.inputSchema),
func: async (params) => {
// Call the MCP server
const result = await mcpHub.callTool(serverName, mcpTool.name, params);
// Convert MCP response to expected format
return {
result: formatMcpResponse(result),
};
},
};
}
2. Dynamic MCP Tool Registration
Register MCP tools when servers connect:
// In your MCP initialization code
export async function registerMcpServerTools(serverName: string, mcpTools: McpTool[]) {
const registry = ToolRegistry.getInstance();
for (const mcpTool of mcpTools) {
registry.register({
tool: createMcpToolWrapper(serverName, mcpTool),
metadata: {
id: `mcp_${serverName}_${mcpTool.name}`,
displayName: mcpTool.displayName || mcpTool.name,
description: mcpTool.description || `MCP tool from ${serverName}`,
category: "mcp",
// MCP tools are user-configurable by default
isAlwaysEnabled: false,
// Add any MCP-specific prompt instructions
customPromptInstructions: mcpTool.systemPrompt,
},
});
}
}
// When MCP server disconnects
export function unregisterMcpServerTools(serverName: string) {
const registry = ToolRegistry.getInstance();
const allTools = registry.getAllTools();
// Remove tools from this server
const toolsToKeep = allTools.filter((t) => !t.metadata.id.startsWith(`mcp_${serverName}_`));
registry.clear();
registry.registerAll(toolsToKeep);
// Re-initialize built-in tools
initializeBuiltinTools(app.vault);
}
3. Schema Conversion Helper
Convert MCP JSON Schema to Zod schema:
function convertMcpSchemaToZod(jsonSchema: any): z.ZodSchema {
// Basic implementation - extend as needed
if (jsonSchema.type === "object") {
const shape: any = {};
for (const [key, prop] of Object.entries(jsonSchema.properties || {})) {
const propSchema = prop as any;
if (propSchema.type === "string") {
shape[key] = z.string();
if (propSchema.description) {
shape[key] = shape[key].describe(propSchema.description);
}
} else if (propSchema.type === "number") {
shape[key] = z.number();
} else if (propSchema.type === "boolean") {
shape[key] = z.boolean();
} else if (propSchema.type === "array") {
shape[key] = z.array(z.any()); // Simplification
} else if (propSchema.type === "object") {
shape[key] = z.object({});
}
// Handle optional properties
if (!jsonSchema.required?.includes(key)) {
shape[key] = shape[key].optional();
}
}
return z.object(shape);
}
// Fallback for other types
return z.any();
}
4. Settings Storage for MCP Tools
MCP tool preferences are stored in the same array as built-in tools:
// When enabling/disabling MCP tools:
function updateMcpToolSetting(toolId: string, enabled: boolean) {
const settings = getSettings();
const enabledIds = new Set(settings.autonomousAgentEnabledToolIds || []);
if (enabled) {
enabledIds.add(toolId);
} else {
enabledIds.delete(toolId);
}
updateSetting("autonomousAgentEnabledToolIds", Array.from(enabledIds));
}
How the System Works
Tool Discovery Flow
- Initialization:
initializeBuiltinTools()registers all built-in tools - MCP Connection: When MCP servers connect, their tools are dynamically registered
- Settings UI:
ToolSettingsSectioncomponent reads from the registry to generate UI - Tool Execution:
AutonomousAgentChainRunner.getAvailableTools()filters tools based on settings
Tool Call Rendering Roots
React invariant #409 surfaced when tool-call banners attempted to render into React roots that had already been unmounted. To prevent this regression, the plugin routes all banner rendering through the shared manager in src/components/chat-components/toolCallRootManager.tsx.
Manager Responsibilities
- Tracks
{ root, isUnmounting }per message/tool call viawindow.__copilotToolCallRoots. ensureToolCallRootfinalises pending disposals and creates a newcreateRootwhen needed.renderToolCallBannerrenders<ToolCallBanner />into the managed root; components never callroot.renderdirectly.removeToolCallRootandcleanupMessageToolCallRootsschedule unmounts on the next tick and drop entries only after disposal completes.cleanupStaleToolCallRootspurges message IDs older than one hour to avoid leaking historical roots.
Integration Notes
ChatSingleMessage keeps const rootsRef = useRef(getMessageToolCallRoots(messageId)), which provides a stable registry for each message. The component delegates all lifecycle calls to the manager and snapshots rootsRef.current inside effect cleanup to satisfy react-hooks/exhaustive-deps.
Verification
Run the focused test to cover the streaming behaviour and tool-call integration:
npm test -- src/components/chat-components/ChatSingleMessage.test.tsx