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