# 核心模块设计 — 工具系统
> 所属项目: 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)