# 核心模块设计 — 工具系统 > 所属项目: free-code .NET 10 重写 > 原始代码来源: `../../src/tools.ts`, `../../src/tools/` > 原始设计意图: 定义 Agent 可调用工具的接口体系,提供工具基类复用逻辑,并统一管理内置工具与 MCP 工具的组装与去重 > 上级文档: [核心模块设计总览](核心模块设计.md) > 交叉参考: [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md) | [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md) --- ## 概述 工具系统是 Agent 行为能力的载体。每一个工具对应 Agent 可以执行的一类操作,从执行 shell 命令(`BashTool`)到读写文件、搜索代码、访问网络,再到调用外部 MCP 服务。 原始 TypeScript 实现在 `tools.ts` 注册工具,在 `../../src/tools/` 目录下实现每个工具,工具本身是对象字面量或类实例。.NET 重写引入了完整的接口层次和抽象基类,使工具实现更标准化,并通过 DI 容器统一管理生命周期。 --- ## 7.1 核心接口 ### ITool — 工具基础接口 ```csharp /// Agent工具基础接口 public interface ITool { string Name { get; } string[]? Aliases { get; } string? SearchHint { get; } ToolCategory Category { get; } bool IsEnabled(); JsonElement GetInputSchema(); Task GetDescriptionAsync(object? input = null); bool IsConcurrencySafe(object input); bool IsReadOnly(object input); } ``` ### ITool\ — 泛型工具接口 ```csharp /// 泛型工具接口 public interface ITool : ITool where TInput : class { Task> ExecuteAsync( TInput input, ToolExecutionContext context, CancellationToken ct = default); Task ValidateInputAsync(TInput input); Task CheckPermissionAsync(TInput input, ToolExecutionContext context); } ``` ### 支撑类型 ```csharp public record ToolResult( T Data, bool IsError = false, string? ErrorMessage = null, List? SideMessages = null ); public record ToolExecutionContext( string WorkingDirectory, PermissionMode PermissionMode, IReadOnlyList AdditionalWorkingDirectories, IPermissionEngine PermissionEngine, ILspClientManager LspManager, IBackgroundTaskManager TaskManager, IServiceProvider Services ); public enum ToolCategory { FileSystem, Shell, Agent, Web, Lsp, Mcp, UserInteraction, Todo, Task, PlanMode, AgentSwarm, Worktree, Config } ``` `ToolExecutionContext` 是执行时上下文,包含工具执行所需的所有环境信息。工具实现通过 `context` 参数访问当前工作目录、权限引擎、LSP 客户端等,而不是直接依赖全局状态。 --- ## 7.2 工具基类 ToolBase\ `ToolBase` 为工具实现提供默认行为,减少每个具体工具需要编写的样板代码。 **原始设计意图:** 原始 TypeScript 工具通过对象字面量共享一些约定,但没有强制继承关系。.NET 的抽象基类在编译期保证所有工具遵循统一接口,并为验证逻辑提供可选的 FluentValidation 集成点。 ```csharp public abstract class ToolBase : ITool where TInput : class { public abstract string Name { get; } public virtual string[]? Aliases => null; public virtual string? SearchHint => null; public abstract ToolCategory Category { get; } public virtual bool IsEnabled() => true; public abstract JsonElement GetInputSchema(); public virtual Task GetDescriptionAsync(object? input = null) => Task.FromResult($"Execute {Name}"); public abstract bool IsConcurrencySafe(TInput input); public abstract bool IsReadOnly(TInput input); public abstract Task> ExecuteAsync( TInput input, ToolExecutionContext context, CancellationToken ct); public virtual Task ValidateInputAsync(TInput input) { var validator = GetValidator(); if (validator != null) { var result = validator.Validate(input); return Task.FromResult(result.IsValid ? ValidationResult.Success() : ValidationResult.Failure(result.Errors.Select(e => e.ErrorMessage))); } return Task.FromResult(ValidationResult.Success()); } public virtual Task CheckPermissionAsync( TInput input, ToolExecutionContext context) => Task.FromResult(PermissionResult.Allowed()); protected virtual IValidator? GetValidator() => null; } ``` 子类只需实现 `Name`、`Category`、`GetInputSchema`、`IsConcurrencySafe`、`IsReadOnly` 和 `ExecuteAsync`。验证逻辑通过重写 `GetValidator()` 返回一个 FluentValidation 的 `IValidator` 实例来接入。 --- ## 7.3 BashTool 完整实现 `BashTool` 是最核心也最复杂的工具,展示了工具系统的完整实现模式。 **原始设计意图:** 原始 `BashTool.tsx` 使用 Node.js 的 `child_process.spawn` 执行命令,通过 Promise 管理超时,并通过 `IBackgroundTaskManager` 支持后台执行。.NET 版本使用 `Process` 和 `WaitForExitAsync` + `.WaitAsync(timeout)` 实现相同语义。 ```csharp public class BashTool : ToolBase { public override string Name => "Bash"; public override ToolCategory Category => ToolCategory.Shell; private readonly IBackgroundTaskManager _taskManager; private readonly IFeatureFlagService _features; public override async Task> ExecuteAsync( BashToolInput input, ToolExecutionContext context, CancellationToken ct) { var psi = new ProcessStartInfo { FileName = "/bin/bash", Arguments = $"-c \"{input.Command.Replace("\"", "\\\"")}\"", WorkingDirectory = context.WorkingDirectory, RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true, }; // 沙箱 if (!input.DangerouslyDisableSandbox) ApplySandboxRestrictions(psi, context); // 后台执行 if (input.RunInBackground) return await RunInBackgroundAsync(input, psi, ct); // 前台执行 using var process = new Process { StartInfo = psi }; process.Start(); var timeoutMs = input.Timeout ?? 120_000; try { await process.WaitForExitAsync(ct) .WaitAsync(TimeSpan.FromMilliseconds(timeoutMs), ct); } catch (TimeoutException) { process.Kill(entireProcessTree: true); return new ToolResult(new BashToolOutput { Stdout = await process.StandardOutput.ReadToEndAsync(ct), Stderr = await process.StandardError.ReadToEndAsync(ct), ExitCode = -1, Interrupted = true }); } var stdout = await process.StandardOutput.ReadToEndAsync(ct); var stderr = await process.StandardError.ReadToEndAsync(ct); return new ToolResult(new BashToolOutput { Stdout = stdout, Stderr = stderr, ExitCode = process.ExitCode, Interrupted = false }); } private async Task> RunInBackgroundAsync( BashToolInput input, ProcessStartInfo psi, CancellationToken ct) { var task = await _taskManager.CreateShellTaskAsync(input.Command, psi); return new ToolResult(new BashToolOutput { Stdout = $"Background task started: {task.TaskId}", BackgroundTaskId = task.TaskId }); } public override bool IsConcurrencySafe(BashToolInput input) => false; public override bool IsReadOnly(BashToolInput input) => CommandClassifier.IsReadCommand(input.Command); } ``` ### BashTool 输入/输出类型 ```csharp public record BashToolInput { public string Command { get; init; } = ""; public int? Timeout { get; init; } public string? Description { get; init; } public bool RunInBackground { get; init; } public bool DangerouslyDisableSandbox { get; init; } } public record BashToolOutput { public string Stdout { get; init; } = ""; public string Stderr { get; init; } = ""; public int ExitCode { get; init; } public bool Interrupted { get; init; } public string? BackgroundTaskId { get; init; } } ``` `IsConcurrencySafe` 返回 `false` 表示 Bash 命令不能并发执行(防止状态污染)。`IsReadOnly` 委托给 `CommandClassifier.IsReadCommand`,该工具通过命令前缀分析判断是否为只读操作(如 `ls`、`cat`、`grep` 等),只读命令在某些权限模式下可以跳过确认。 --- ## 7.4 ToolRegistry — 工具池组装与 MCP 去重 `ToolRegistry` 负责将内置工具和 MCP 工具组装为统一的工具池,并应用去重和权限过滤规则。 ```csharp public class ToolRegistry : IToolRegistry { private readonly IServiceProvider _services; private readonly IFeatureFlagService _features; private readonly IMcpClientManager _mcpManager; private List? _cachedBaseTools; public async Task> GetToolsAsync( ToolPermissionContext? permissionContext = null) { var baseTools = GetBaseTools(); var mcpTools = await _mcpManager.GetToolsAsync(); var pool = AssembleToolPool(baseTools, mcpTools); if (permissionContext != null) pool = FilterByDenyRules(pool, permissionContext); return pool; } private List GetBaseTools() { if (_cachedBaseTools != null) return _cachedBaseTools; _cachedBaseTools = new List(); // 核心20个工具 (始终可用) _cachedBaseTools.AddRange(new ITool[] { Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), Get(), }); // 条件工具 (feature-flagged) if (_features.IsEnabled(FeatureFlags.AgentTriggers)) { _cachedBaseTools.Add(Get()); _cachedBaseTools.Add(Get()); } if (_features.IsEnabled(FeatureFlags.V2Todo)) { _cachedBaseTools.AddRange(new ITool[] { Get(), Get(), Get(), Get(), Get(), }); } // Swarm + PlanMode + Worktree (始终注册) _cachedBaseTools.AddRange(new ITool[] { Get(), Get(), Get(), Get(), Get(), Get(), Get(), }); // 稳定排序 (prompt cache一致性) _cachedBaseTools.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.Ordinal)); return _cachedBaseTools; } /// 组装工具池: 内置优先, MCP去重 private List AssembleToolPool( List baseTools, IReadOnlyList mcpTools) { var pool = new List(baseTools); var baseNames = baseTools.Select(t => t.Name).ToHashSet(); foreach (var mcpTool in mcpTools) { if (!baseNames.Contains(mcpTool.Name)) pool.Add(mcpTool); } return pool; } private T Get() where T : ITool => _services.GetRequiredService(); } ``` ### 工具分类 | 类别 | 数量 | 说明 | |------|------|------| | 核心工具 | 20 个 | 始终可用,不受 feature flag 影响 | | 条件工具 | 7 个 | 由 `AgentTriggers`、`V2Todo` 等 feature flag 控制 | | Swarm/Plan/Worktree | 7 个 | 始终注册,但某些工具在特定模式外会返回错误 | | MCP 工具 | 动态 | 从已连接的 MCP 服务器获取,同名工具被内置工具覆盖 | ### 去重策略说明 内置工具名称集合构建为 `HashSet` 后,MCP 工具逐一检查。若名称已存在于集合中,则该 MCP 工具被静默跳过。这一策略确保: 1. 外部 MCP 服务器无法意外替换 `Bash`、`FileRead` 等核心工具 2. 用户可以通过 MCP 扩展新工具,但不能降低现有工具的可靠性 --- ## 参考资料 - [核心模块设计总览](核心模块设计.md) - [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md) - [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md) - [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)