Skip to main content

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
    └── ...

Tool Lifecycle

  1. Registration: Tool registered in ToolRegistry
  2. Discovery: Model receives tool definitions (name, description, schema)
  3. Invocation: Model requests tool use with parameters
  4. Validation: Parameters validated against schema and business rules
  5. Confirmation: User approval for dangerous operations (if needed)
  6. Execution: Tool executes and returns results
  7. Response: Results sent back to model

Base Tool Class

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>;
}

Tool Kinds

export enum Kind {
  Read = 'read',    // Read-only operations (no confirmation)
  Edit = 'edit',    // Modifying operations (require confirmation)
  Other = 'other',  // Special tools (custom behavior)
}

Tool Invocation

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>;
}

Tool Result

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;
  };
}

Creating a New Tool

Step 1: Create Tool File

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);
  }
}

Step 2: Add Tool Names

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';
}

Step 3: Register Tool

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;
}

Built-in Tools

Qwen Code includes these built-in tools:

File System 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

Execution Tools

  • run_shell_command - Execute shell commands
See Shell Tool

Delegation Tools

  • task - Delegate to subagents
See Task Tool

Memory Tools

  • save_memory - Save long-term memories
See Memory Tool

Integration Tools

  • MCP tools (dynamically loaded)
  • LSP tools (language server integration)
  • Web fetch and search

Tool Registry

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

  1. Clear Descriptions: Write clear, detailed descriptions for the model
  2. Validation: Validate all parameters thoroughly
  3. Error Handling: Provide helpful error messages
  4. Testing: Write comprehensive tests
  5. Documentation: Document parameters and behavior
  6. Security: Validate paths, sanitize inputs
  7. Performance: Optimize for common cases
  8. Telemetry: Log usage for analytics (respecting privacy)

Next Steps