free-code-dotnet/docs/核心模块设计/核心模块设计-工具系统.md
应文浩wenhao.ying@xiaobao100.com e25ac591a7 init easy-code
2026-04-06 07:24:24 +08:00

360 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 核心模块设计 — 工具系统
> 所属项目: 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
/// <summary>Agent工具基础接口</summary>
public interface ITool
{
string Name { get; }
string[]? Aliases { get; }
string? SearchHint { get; }
ToolCategory Category { get; }
bool IsEnabled();
JsonElement GetInputSchema();
Task<string> GetDescriptionAsync(object? input = null);
bool IsConcurrencySafe(object input);
bool IsReadOnly(object input);
}
```
### ITool\<TInput, TOutput\> — 泛型工具接口
```csharp
/// <summary>泛型工具接口</summary>
public interface ITool<TInput, TOutput> : ITool where TInput : class
{
Task<ToolResult<TOutput>> ExecuteAsync(
TInput input, ToolExecutionContext context, CancellationToken ct = default);
Task<ValidationResult> ValidateInputAsync(TInput input);
Task<PermissionResult> CheckPermissionAsync(TInput input, ToolExecutionContext context);
}
```
### 支撑类型
```csharp
public record ToolResult<T>(
T Data,
bool IsError = false,
string? ErrorMessage = null,
List<Message>? SideMessages = null
);
public record ToolExecutionContext(
string WorkingDirectory,
PermissionMode PermissionMode,
IReadOnlyList<AdditionalWorkingDirectory> 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\<TInput, TOutput\>
`ToolBase` 为工具实现提供默认行为,减少每个具体工具需要编写的样板代码。
**原始设计意图:** 原始 TypeScript 工具通过对象字面量共享一些约定,但没有强制继承关系。.NET 的抽象基类在编译期保证所有工具遵循统一接口,并为验证逻辑提供可选的 FluentValidation 集成点。
```csharp
public abstract class ToolBase<TInput, TOutput> : ITool<TInput, TOutput>
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<string> GetDescriptionAsync(object? input = null)
=> Task.FromResult($"Execute {Name}");
public abstract bool IsConcurrencySafe(TInput input);
public abstract bool IsReadOnly(TInput input);
public abstract Task<ToolResult<TOutput>> ExecuteAsync(
TInput input, ToolExecutionContext context, CancellationToken ct);
public virtual Task<ValidationResult> 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<PermissionResult> CheckPermissionAsync(
TInput input, ToolExecutionContext context)
=> Task.FromResult(PermissionResult.Allowed());
protected virtual IValidator<TInput>? GetValidator() => null;
}
```
子类只需实现 `Name``Category``GetInputSchema``IsConcurrencySafe``IsReadOnly``ExecuteAsync`。验证逻辑通过重写 `GetValidator()` 返回一个 FluentValidation 的 `IValidator<TInput>` 实例来接入。
---
## 7.3 BashTool 完整实现
`BashTool` 是最核心也最复杂的工具,展示了工具系统的完整实现模式。
**原始设计意图:** 原始 `BashTool.tsx` 使用 Node.js 的 `child_process.spawn` 执行命令,通过 Promise 管理超时,并通过 `IBackgroundTaskManager` 支持后台执行。.NET 版本使用 `Process``WaitForExitAsync` + `.WaitAsync(timeout)` 实现相同语义。
```csharp
public class BashTool : ToolBase<BashToolInput, BashToolOutput>
{
public override string Name => "Bash";
public override ToolCategory Category => ToolCategory.Shell;
private readonly IBackgroundTaskManager _taskManager;
private readonly IFeatureFlagService _features;
public override async Task<ToolResult<BashToolOutput>> 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<BashToolOutput>(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<BashToolOutput>(new BashToolOutput
{
Stdout = stdout, Stderr = stderr,
ExitCode = process.ExitCode, Interrupted = false
});
}
private async Task<ToolResult<BashToolOutput>> RunInBackgroundAsync(
BashToolInput input, ProcessStartInfo psi, CancellationToken ct)
{
var task = await _taskManager.CreateShellTaskAsync(input.Command, psi);
return new ToolResult<BashToolOutput>(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<ITool>? _cachedBaseTools;
public async Task<IReadOnlyList<ITool>> 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<ITool> GetBaseTools()
{
if (_cachedBaseTools != null) return _cachedBaseTools;
_cachedBaseTools = new List<ITool>();
// 核心20个工具 (始终可用)
_cachedBaseTools.AddRange(new ITool[] {
Get<FileReadTool>(), Get<FileEditTool>(), Get<FileWriteTool>(),
Get<GlobTool>(), Get<GrepTool>(), Get<BashTool>(),
Get<AgentTool>(), Get<SkillTool>(), Get<TaskOutputTool>(),
Get<WebFetchTool>(), Get<WebSearchTool>(), Get<LspTool>(),
Get<TodoWriteTool>(), Get<AskUserQuestionTool>(), Get<BriefTool>(),
Get<ListMcpResourcesTool>(), Get<ReadMcpResourceTool>(),
Get<ToolSearchTool>(), Get<ConfigTool>(), Get<NotebookEditTool>(),
});
// 条件工具 (feature-flagged)
if (_features.IsEnabled(FeatureFlags.AgentTriggers))
{
_cachedBaseTools.Add(Get<CronTool>());
_cachedBaseTools.Add(Get<MonitorTool>());
}
if (_features.IsEnabled(FeatureFlags.V2Todo))
{
_cachedBaseTools.AddRange(new ITool[] {
Get<TaskCreateTool>(), Get<TaskGetTool>(), Get<TaskUpdateTool>(),
Get<TaskListTool>(), Get<TaskStopTool>(),
});
}
// Swarm + PlanMode + Worktree (始终注册)
_cachedBaseTools.AddRange(new ITool[] {
Get<SendMessageTool>(), Get<TeamCreateTool>(), Get<TeamDeleteTool>(),
Get<EnterPlanModeTool>(), Get<ExitPlanModeTool>(),
Get<EnterWorktreeTool>(), Get<ExitWorktreeTool>(),
});
// 稳定排序 (prompt cache一致性)
_cachedBaseTools.Sort((a, b) =>
string.Compare(a.Name, b.Name, StringComparison.Ordinal));
return _cachedBaseTools;
}
/// <summary>组装工具池: 内置优先, MCP去重</summary>
private List<ITool> AssembleToolPool(
List<ITool> baseTools, IReadOnlyList<ITool> mcpTools)
{
var pool = new List<ITool>(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<T>() where T : ITool => _services.GetRequiredService<T>();
}
```
### 工具分类
| 类别 | 数量 | 说明 |
|------|------|------|
| 核心工具 | 20 个 | 始终可用,不受 feature flag 影响 |
| 条件工具 | 7 个 | 由 `AgentTriggers``V2Todo` 等 feature flag 控制 |
| Swarm/Plan/Worktree | 7 个 | 始终注册,但某些工具在特定模式外会返回错误 |
| MCP 工具 | 动态 | 从已连接的 MCP 服务器获取,同名工具被内置工具覆盖 |
### 去重策略说明
内置工具名称集合构建为 `HashSet<string>`MCP 工具逐一检查。若名称已存在于集合中,则该 MCP 工具被静默跳过。这一策略确保:
1. 外部 MCP 服务器无法意外替换 `Bash``FileRead` 等核心工具
2. 用户可以通过 MCP 扩展新工具,但不能降低现有工具的可靠性
---
## 参考资料
- [核心模块设计总览](核心模块设计.md)
- [查询引擎 (QueryEngine)](核心模块设计-查询引擎-QueryEngine.md)
- [基础设施设计 — MCP 协议集成](../基础设施设计/基础设施设计-MCP协议集成.md)
- [原始代码映射 — 核心模块](reference/原始代码映射-核心模块.md)