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

13 KiB
Raw Blame History

核心模块设计 — 工具系统

所属项目: 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;
}

子类只需实现 NameCategoryGetInputSchemaIsConcurrencySafeIsReadOnlyExecuteAsync。验证逻辑通过重写 GetValidator() 返回一个 FluentValidation 的 IValidator<TInput> 实例来接入。


7.3 BashTool 完整实现

BashTool 是最核心也最复杂的工具,展示了工具系统的完整实现模式。

原始设计意图: 原始 BashTool.tsx 使用 Node.js 的 child_process.spawn 执行命令,通过 Promise 管理超时,并通过 IBackgroundTaskManager 支持后台执行。.NET 版本使用 ProcessWaitForExitAsync + .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,该工具通过命令前缀分析判断是否为只读操作(如 lscatgrep 等),只读命令在某些权限模式下可以跳过确认。


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 个 AgentTriggersV2Todo 等 feature flag 控制
Swarm/Plan/Worktree 7 个 始终注册,但某些工具在特定模式外会返回错误
MCP 工具 动态 从已连接的 MCP 服务器获取,同名工具被内置工具覆盖

去重策略说明

内置工具名称集合构建为 HashSet<string>MCP 工具逐一检查。若名称已存在于集合中,则该 MCP 工具被静默跳过。这一策略确保:

  1. 外部 MCP 服务器无法意外替换 BashFileRead 等核心工具
  2. 用户可以通过 MCP 扩展新工具,但不能降低现有工具的可靠性

参考资料