Documentation Index
Fetch the complete documentation index at: https://mintlify.com/QwenLM/qwen-code/llms.txt
Use this file to discover all available pages before exploring further.
Tools System Architecture
Qwen Code’s tools system provides the AI model with capabilities to interact with the local environment. This guide explains how the tools system works and how to extend it.
Overview
The tools system is built on these core principles:
- Self-describing: Tools define their parameters using JSON Schema
- Type-safe: Full TypeScript type safety throughout
- Extensible: Easy to add new tools
- Secure: Validation and approval mechanisms
- Testable: Comprehensive test coverage
Architecture
Core Components
ToolRegistry
│
├── BaseTool (abstract)
│ ├── name: string
│ ├── displayName: string
│ ├── description: string
│ ├── schema: JSONSchema
│ └── createInvocation(params): ToolInvocation
│
├── BaseDeclarativeTool (extends BaseTool)
│ ├── Kind: Read | Edit | Other
│ ├── validateToolParamValues()
│ └── createInvocation()
│
└── Tool Implementations
├── ReadFileTool
├── WriteFileTool
├── EditTool
├── ShellTool
├── TaskTool
└── ...
- Registration: Tool registered in
ToolRegistry
- Discovery: Model receives tool definitions (name, description, schema)
- Invocation: Model requests tool use with parameters
- Validation: Parameters validated against schema and business rules
- Confirmation: User approval for dangerous operations (if needed)
- Execution: Tool executes and returns results
- Response: Results sent back to model
All tools extend BaseDeclarativeTool:
// packages/core/src/tools/tools.ts
export abstract class BaseDeclarativeTool<
TParams extends object,
TResult extends ToolResult
> {
constructor(
public readonly name: string,
public readonly displayName: string,
public readonly description: string,
public readonly kind: Kind,
public readonly schema: object,
public readonly isOutputMarkdown: boolean = false,
public readonly canUpdateOutput: boolean = false,
) {}
// Validate parameters beyond schema validation
protected validateToolParamValues(params: TParams): string | null {
return null; // Override to add custom validation
}
// Create a tool invocation instance
protected abstract createInvocation(
params: TParams,
): ToolInvocation<TParams, TResult>;
}
export enum Kind {
Read = 'read', // Read-only operations (no confirmation)
Edit = 'edit', // Modifying operations (require confirmation)
Other = 'other', // Special tools (custom behavior)
}
Each tool execution creates a ToolInvocation instance:
export interface ToolInvocation<
TParams extends object,
TResult extends ToolResult
> {
// Get display description for UI
getDescription(): string;
// Get file locations involved (for IDE integration)
toolLocations?(): ToolLocation[];
// Check if confirmation needed
shouldConfirmExecute?(
signal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false>;
// Execute the tool
execute(
signal: AbortSignal,
updateOutput?: (output: ToolResultDisplay) => void,
...args: any[]
): Promise<TResult>;
}
Tools return a ToolResult:
export interface ToolResult {
// Content sent to the model
llmContent: PartUnion;
// Content displayed to user (optional)
returnDisplay?: ToolResultDisplay;
// Error information (optional)
error?: {
message: string;
type: ToolErrorType;
};
}
Create a new file in packages/core/src/tools/:
// packages/core/src/tools/my-tool.ts
import { BaseDeclarativeTool, BaseToolInvocation, Kind } from './tools.js';
import type { ToolInvocation, ToolResult } from './tools.js';
import { ToolNames, ToolDisplayNames } from './tool-names.js';
import type { Config } from '../config/config.js';
// 1. Define parameter interface
export interface MyToolParams {
requiredParam: string;
optionalParam?: number;
}
// 2. Create invocation class
class MyToolInvocation extends BaseToolInvocation<
MyToolParams,
ToolResult
> {
constructor(
private config: Config,
params: MyToolParams,
) {
super(params);
}
getDescription(): string {
return `My tool: ${this.params.requiredParam}`;
}
async execute(signal: AbortSignal): Promise<ToolResult> {
// Implement tool logic
try {
const result = await this.doSomething();
return {
llmContent: `Success: ${result}`,
returnDisplay: result,
};
} catch (error) {
return {
llmContent: `Error: ${error.message}`,
error: {
message: error.message,
type: 'EXECUTION_ERROR',
},
};
}
}
private async doSomething(): Promise<string> {
// Tool implementation
return 'result';
}
}
// 3. Create tool class
export class MyTool extends BaseDeclarativeTool<
MyToolParams,
ToolResult
> {
static readonly Name = 'my_tool';
constructor(private config: Config) {
super(
MyTool.Name,
'MyTool', // Display name
'Description of what my tool does',
Kind.Other, // or Kind.Read, Kind.Edit
{
type: 'object',
properties: {
requiredParam: {
type: 'string',
description: 'Description for the model',
},
optionalParam: {
type: 'number',
description: 'Optional parameter',
},
},
required: ['requiredParam'],
},
);
}
protected override validateToolParamValues(
params: MyToolParams,
): string | null {
// Add custom validation
if (params.requiredParam.length === 0) {
return 'requiredParam must not be empty';
}
return null;
}
protected createInvocation(
params: MyToolParams,
): ToolInvocation<MyToolParams, ToolResult> {
return new MyToolInvocation(this.config, params);
}
}
Update packages/core/src/tools/tool-names.ts:
export class ToolNames {
// ... existing tools ...
static readonly MY_TOOL = 'my_tool';
}
export class ToolDisplayNames {
// ... existing tools ...
static readonly MY_TOOL = 'MyTool';
}
Register in packages/core/src/config/config.ts:
private initializeTools(): void {
// ... existing tools ...
// Add your tool
const myTool = new MyTool(this);
this.toolRegistry.registerTool(myTool);
}
Step 4: Add Tests
Create packages/core/src/tools/my-tool.test.ts:
import { describe, it, expect, beforeEach } from 'vitest';
import { MyTool } from './my-tool.js';
import type { Config } from '../config/config.js';
describe('MyTool', () => {
let tool: MyTool;
let mockConfig: Config;
beforeEach(() => {
mockConfig = {} as Config; // Mock as needed
tool = new MyTool(mockConfig);
});
it('should create invocation', () => {
const params = { requiredParam: 'test' };
const invocation = tool.invoke(params);
expect(invocation).toBeDefined();
});
it('should validate parameters', async () => {
const params = { requiredParam: '' };
const result = await tool.invoke(params).execute(
new AbortController().signal,
);
expect(result.error).toBeDefined();
});
it('should execute successfully', async () => {
const params = { requiredParam: 'test' };
const result = await tool.invoke(params).execute(
new AbortController().signal,
);
expect(result.llmContent).toContain('Success');
});
});
Advanced Features
Confirmation Dialogs
For tools that need user approval:
class MyToolInvocation extends BaseToolInvocation<MyToolParams, ToolResult> {
override async shouldConfirmExecute(
signal: AbortSignal,
): Promise<ToolCallConfirmationDetails | false> {
// Return false for no confirmation needed
if (this.isSafeOperation()) {
return false;
}
// Return confirmation details
return {
type: 'exec',
title: 'Confirm Dangerous Operation',
command: this.params.command,
onConfirm: async (outcome: ToolConfirmationOutcome) => {
if (outcome === ToolConfirmationOutcome.ProceedAlways) {
// User chose "Always allow"
this.addToAllowlist();
}
},
};
}
}
Progress Updates
For long-running operations:
async execute(
signal: AbortSignal,
updateOutput?: (output: ToolResultDisplay) => void,
): Promise<ToolResult> {
for (let i = 0; i < steps.length; i++) {
// Update UI with progress
if (updateOutput) {
updateOutput({
status: `Step ${i + 1}/${steps.length}`,
progress: i / steps.length,
});
}
await this.executeStep(steps[i], signal);
}
return { llmContent: 'Complete' };
}
File Path Validation
Common pattern for file-based tools:
protected override validateToolParamValues(
params: MyToolParams,
): string | null {
const filePath = params.path;
// Check if absolute path
if (!path.isAbsolute(filePath)) {
return `Path must be absolute: ${filePath}`;
}
// Check if within workspace
const workspaceContext = this.config.getWorkspaceContext();
if (!workspaceContext.isPathWithinWorkspace(filePath)) {
return `Path must be within workspace: ${filePath}`;
}
// Check against .qwenignore
const fileService = this.config.getFileService();
if (fileService.shouldQwenIgnoreFile(filePath)) {
return `Path is ignored by .qwenignore: ${filePath}`;
}
return null;
}
Qwen Code includes these built-in tools:
read_file - Read file contents
write_file - Write to files
edit - Edit files with string replacement
glob - Find files by pattern
grep_search - Search file contents
list_directory - List directory contents
See File System Tools
run_shell_command - Execute shell commands
See Shell Tool
task - Delegate to subagents
See Task Tool
save_memory - Save long-term memories
See Memory Tool
- MCP tools (dynamically loaded)
- LSP tools (language server integration)
- Web fetch and search
The ToolRegistry manages all available tools:
// packages/core/src/tools/tool-registry.ts
export class ToolRegistry {
private tools = new Map<string, BaseTool>();
registerTool(tool: BaseTool): void {
this.tools.set(tool.name, tool);
}
getTool(name: string): BaseTool | undefined {
return this.tools.get(name);
}
getAllTools(): BaseTool[] {
return Array.from(this.tools.values());
}
getToolsForModel(): FunctionDeclaration[] {
return this.getAllTools().map(tool => ({
name: tool.name,
description: tool.description,
parameters: tool.schema,
}));
}
}
Best Practices
- Clear Descriptions: Write clear, detailed descriptions for the model
- Validation: Validate all parameters thoroughly
- Error Handling: Provide helpful error messages
- Testing: Write comprehensive tests
- Documentation: Document parameters and behavior
- Security: Validate paths, sanitize inputs
- Performance: Optimize for common cases
- Telemetry: Log usage for analytics (respecting privacy)
Next Steps