13 KiB
核心模块设计 — 工具系统
所属项目: free-code .NET 10 重写 原始代码来源:
../../src/tools.ts,../../src/tools/原始设计意图: 定义 Agent 可调用工具的接口体系,提供工具基类复用逻辑,并统一管理内置工具与 MCP 工具的组装与去重 上级文档: 核心模块设计总览 交叉参考: 查询引擎 (QueryEngine) | 基础设施设计 — MCP 协议集成
概述
工具系统是 Agent 行为能力的载体。每一个工具对应 Agent 可以执行的一类操作,从执行 shell 命令(BashTool)到读写文件、搜索代码、访问网络,再到调用外部 MCP 服务。
原始 TypeScript 实现在 tools.ts 注册工具,在 ../../src/tools/ 目录下实现每个工具,工具本身是对象字面量或类实例。.NET 重写引入了完整的接口层次和抽象基类,使工具实现更标准化,并通过 DI 容器统一管理生命周期。
7.1 核心接口
ITool — 工具基础接口
/// <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> — 泛型工具接口
/// <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);
}
支撑类型
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 集成点。
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) 实现相同语义。
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 输入/输出类型
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 工具组装为统一的工具池,并应用去重和权限过滤规则。
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 工具被静默跳过。这一策略确保:
- 外部 MCP 服务器无法意外替换
Bash、FileRead等核心工具 - 用户可以通过 MCP 扩展新工具,但不能降低现有工具的可靠性