从零开始:如何将 Reasonix CLI 集成到 HagiCode 系统中

从零开始:如何将 Reasonix CLI 集成到 HagiCode 系统中

本文分享了将 Reasonix CLI 作为一等 Agent Provider 集成到 HagiCode 系统的完整技术实践,涵盖三层架构设计、关键技术决策和前后端实现细节。

背景

Reasonix CLI,说起来也是个挺有意思的东西。它是一个基于 ACP(Agent Communication Protocol)的 AI 代码助手工具,提供了强大的流式传输和会话管理能力。其实在 HagiCode.Libs 层,我们已经把它的底层实现都搞定了,只是这些组件还处在孤立的状态,就像一个个漂亮的珍珠,还没串成项链。用户无法通过 Hero 职业选择、会话执行链路或监控面板来使用它,这多少有点可惜。

我们面临的问题是:如何将 Reasonix 提升为与 Codex、Hermes 等同等级的一等 Agent Provider,实现完整的后端路由和前端展示?这可不是简单地注册一个枚举值就能了事的,而是需要构建从底层抽象到用户界面的完整链路。就像盖房子,不能只打个地基就完事了,总得把墙砌起来,屋顶盖上去。

这个集成的挑战在于,Reasonix 作为一个本地 CLI 工具,有自己的性格和脾气。比如它不需要连接字符串,所有参数都是用户运行时配置;它可能根本没有安装,需要优雅降级处理;它兼容 anthropic 系列模型,但又有自己的 effort、budget 等 ACP 特有参数。这就像一个人,有自己独特的处事方式,不能硬来。

经过仔细的架构设计和多轮讨论,我们最终采用了一套清晰的三层架构方案,将 Reasonix 成功集成到系统中。这套方案不仅解决了眼前的问题,也为后续类似 CLI Provider 的集成提供了可复用的模式。其实很多事情就是这样,一旦找到了正确的方法,后面的路就好走多了。

关于 HagiCode

本文分享的方案来自我们在 HagiCode 项目中的实践经验。HagiCode 是一个开源的 AI 代码助手项目,致力于为开发者提供强大的代码生成、重构和优化能力。在开发过程中,我们遇到了各种各样的技术挑战,将 Reasonix 集成为一等 Agent Provider 就是其中之一。如果你觉得本文分享的方案有价值,说明我们的工程实践还不错,那么 HagiCode 本身也值得关注一下。

核心内容

技术架构设计

系统采用了清晰的三层架构来分离关注点,每层都有明确的职责边界:

HagiCode.Libs 层 :这一层已经完成,提供了 CLI provider 的抽象和具体实现。它定义了 ICliProvider<ReasonixOptions> 接口,实现了 ReasonixProvider 来处理 ACP 流式传输和会话管理,同时支持 effort、budget、yolo、transcript 等参数。这一层的职责是提供稳定、可复用的底层能力,不涉及任何业务逻辑。就像房子的地基,虽然看不见,但很重要。

hagicode-core 层 :这是我们本次集成的重点。它负责将底层抽象桥接到系统的统一接口上。具体工作包括注册 AIProviderType.ReasonixCli = 12 枚举值,创建 ReasonixCliProvider 作为 thin adapter 桥接 Libs 层,实现 ReasonixGrain 处理会话状态和执行流,以及集成 Hero 系统进行参数映射和配置管理。这一层的核心是协调各组件,构建完整的业务链路。就像房子的承重墙,把各个部分连接起来。

web 层:负责向用户展示和收集配置。我们需要重新生成 OpenAPI 类型以支持新枚举值,实现视觉类型映射让 Reasonix 有自己的图标和显示名称,创建 CLI 参数配置表单让用户可以配置各个参数,以及添加多语言支持。这一层的重点是用户体验和交互设计。就像房子的装修,做得好不好直接影响住得舒不舒服。

这样的分层设计让每一层都专注于自己的职责,降低了系统的复杂度,也便于后续维护和扩展。其实很多时候,把事情分清楚,反而会更简单。

关键技术决策

在实现过程中,我们做了几个关键的技术决策,这些决策对最终的架构和用户体验都有重要影响。

决策一:使用专用 Grain

我们创建了独立的 ReasonixGrain : IReasonixGrain, IExecutorStreamGrain,而不是尝试复用某个共享的 Grain。这个决策遵循了系统现有的 11 个 provider 的既定模式。虽然看起来可能会有些代码重复,但专用 Grain 让我们可以针对 Reasonix 的特性做精细化的控制,比如它特有的会话绑定机制和 ACP 消息映射。我们还定义了一个空的响应 DTO ReasonixResponse 作为类型区分符,虽然它不包含实际数据,但在类型系统中起到了重要的作用。就像每个人都有自己的房间,哪怕空着,也是自己的空间。

决策二:不创建专用 Settings 类

与某些需要连接字符串的 Provider 不同,Reasonix 的所有配置都是用户运行时设置的,不需要启动时验证。因此我们没有创建专用的 Settings 类,而是将所有配置存储在 AIProviderOptions.Providers[ReasonixCli].Settings 字典中。这种模式与 Qoder、Kiro、Kimi 等其他本地 CLI Provider 保持一致,简化了代码结构,也避免了不必要的抽象层。支持的设置键包括:effortbudgetUsdtranscriptPathenableYoloargumentsstartupTimeoutMsreasoning。有时候简单点,反而更好。

决策三:Provider 策略健康监控

Reasonix 是用户本地安装的 CLI,可能根本没有安装,或者不在系统 PATH 中。这种情况下,我们不应该直接报错,而是应该优雅地降级处理。我们使用了 Provider 策略,通过 CommandUtil.TryResolveExecutablePath 来检查 CLI 是否可用。如果检查失败,UI 会显示为"不可用",但不会影响系统的其他部分。这种设计让系统更加健壮,也给用户清晰的反馈。毕竟谁也不会希望因为一个小问题,整个系统都挂了。

决策四:经济系统分类

在 HagiCode 系统中,不同的 Provider 有不同的经济系统分类。我们决定让 Reasonix 默认使用 'claude' 经济系统分类,因为 Reasonix 本身兼容 anthropic 系列模型。目前只有 Codex 和 Copilot 有专用的经济系统分类,其他 Provider 都是复用现有的分类。这样既保持了系统的简洁性,又能正确处理计费和成本统计。复用也是个智慧,不需要什么都从头来。

决策五:模型兼容性

Reasonix 通过 --model 标志支持多种模型,特别是 anthropic 系列。我们在 secondary-professions.index.json 中添加了兼容性映射,让用户可以在 Reasonix 中选择这些模型。这种设计既尊重了 Reasonix 的能力,又保持了系统的一致性,用户无需理解底层的区别,就能顺畅地使用各种模型。用户也不容易,还是让他们简单点好。

后端实现细节

后端实现分为几个关键部分,每部分都有其独特的技术要点。

枚举和类型注册

首先,我们需要在系统中注册新的 Provider 类型:

csharp 复制代码
// AIProviderType.cs
public enum AIProviderType
{
    // ... 其他 provider
    ReasonixCli = 12,
}

// AIProviderTypeExtensions.cs
private static readonly Dictionary<string, AIProviderType> _typeMap = new()
{
    // ... 其他映射
    ["Reasonix"] = AIProviderType.ReasonixCli,
    ["reasonix"] = AIProviderType.ReasonixCli,
    ["reasonix-cli"] = AIProviderType.ReasonixCli,
    ["ReasonixCli"] = AIProviderType.ReasonixCli,
};

这个枚举值需要与其他并发变更协调,避免冲突。我们选择了 12 这个值,因为它是下一个可用的编号。就像排队一样,总得有个先后顺序。

Thin Adapter 实现

ReasonixCliProvider 是连接 Libs 层和系统统一接口的关键组件:

csharp 复制代码
public sealed class ReasonixCliProvider : IAIProvider, IVersionedAIProvider, IAsyncDisposable
{
    private static readonly IReadOnlyList<string> SupportedSettingKeys =
    [
        "effort",
        "budgetUsd",
        "transcriptPath",
        "enableYolo",
        "arguments",
        "startupTimeoutMs",
        "reasoning"
    ];

    private readonly ICliProvider<ReasonixOptions> _provider;
    private readonly ConcurrentDictionary<string, string> _sessionBindings = new(StringComparer.Ordinal);

    public async IAsyncEnumerable<AIStreamingChunk> StreamCoreAsync(
        AIRequest request,
        string? sessionId = null,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        var options = BuildOptions(request, sessionId);
        await foreach (var message in _provider.StreamAsync(options, cancellationToken))
        {
            yield return MapToStreamingChunk(message);
        }
    }

    private ReasonixOptions BuildOptions(AIRequest request, string? sessionId)
    {
        return new ReasonixOptions
        {
            ExecutablePath = GetExecutablePath(),
            WorkingDirectory = GetWorkingDirectory(),
            Model = _config.Model,
            Effort = _config.Settings.GetValueOrDefault("effort", "medium"),
            Budget = _config.Settings.GetValueOrDefault("budgetUsd", 10.0),
            Yolo = _config.Settings.GetValueOrDefault("enableYolo", false),
            TranscriptPath = _config.Settings.GetValueOrDefault("transcriptPath"),
            Arguments = _config.Settings.GetValueOrDefault("arguments", ""),
            StartupTimeout = GetStartupTimeout(),
            EnvironmentVariables = _environmentVariables,
            CessionId = sessionId ?? GetCessionId()
        };
    }
}

这个 adapter 的关键职责是:

  1. 验证配置参数,拒绝不支持的设置键
  2. 维护会话绑定关系,支持会话恢复
  3. 将 ACP 消息映射到系统的统一格式
  4. 使用 ProviderErrorAutoRetryCoordinator 实现自动重试

就像一个翻译官,把一种语言翻译成另一种语言,还得保证意思准确无误。

Orleans Grain 实现

ReasonixGrain 负责处理会话状态和执行流:

csharp 复制代码
public class ReasonixGrain : Grain, IReasonixGrain, IExecutorStreamGrain
{
    private readonly Dictionary<string, ExecutorToolLifecycleStatus> _toolLifecycleState =
        new(StringComparer.Ordinal);

    public IAsyncEnumerable<ReasonixResponse> ExecuteCommandStreamAsync(
        string command,
        string? heroId = null,
        CancellationToken token = default,
        string? executionMessageId = null,
        string? systemMessage = null,
        Dictionary<string, string>? requestSettings = null)
    {
        var request = BuildRequest(command, isEdit: false, heroId, executionMessageId, systemMessage, requestSettings);
        return SendAsync(request, heroId, token);
    }

    private async IAsyncEnumerable<ReasonixResponse> SendAsync(
        AIRequest request,
        string? heroId,
        [EnumeratorCancellation] CancellationToken token)
    {
        _cancellationTokenSource = new CancellationTokenSource();
        var linkedToken = CancellationTokenSource.CreateLinkedTokenSource(token, _cancellationTokenSource.Token);

        var provider = await ResolveReasonixProviderAsync(heroId);

        await foreach (var chunk in provider.StreamAsync(request, linkedToken.Token))
        {
            var response = BuildChunkResponse(chunk);
            yield return response;
        }
    }

    private async Task<IAIProvider> ResolveReasonixProviderAsync(string? heroId)
    {
        // Hero 感知的配置回退逻辑
        var config = await HeroProviderResolver.ResolveAsync(AIProviderType.ReasonixCli, heroId);
        return _aiProviderFactory.CreateProvider(AIProviderType.ReasonixCli, config);
    }
}

Grain 的核心功能包括:

  1. 使用 [PersistentState("reasonix-interop")] 维护会话状态
  2. 实现 Hero 感知的配置回退逻辑
  3. 追踪工具生命周期状态
  4. 支持取消令牌链,确保执行可以及时中断

这就像一个管家,把事情安排得井井有条。

Hero 系统集成

Hero 系统是 HagiCode 的职业配置系统,我们需要将 Reasonix 集成到这个体系中:

csharp 复制代码
// HeroAppService.cs
// Family 推断
AIProviderType.ReasonixCli => "reasonix"

// 托管的 CLI 参数
ManagedCliParameterKeysByProvider[AIProviderType.ReasonixCli] = 
    ["binary", "effort", "budgetUsd", "transcriptPath", "enableYolo", "arguments", "startupTimeoutMs"];

// 托管的模型参数
ManagedModelParameterKeysByProvider[AIProviderType.ReasonixCli] = 
    ["model", "reasoning"];

在职业目录配置文件 main-professions.yaml 中:

yaml 复制代码
- Id: "profession-reasonix"
  Name: "Reasonix"
  Family: "reasonix"
  Summary: "hero.professionCopy.primary.reasonix.summary"
  Icon: "executor-avatar:Reasonix"
  SourceLabel: "hero.professionCopy.sources.aiProvidersReasonixCli"
  ProviderType: "ReasonixCli"
  SortOrder: 130
  DefaultEnabled: true
  DefaultParameters:
    binary: "reasonix"
    effort: "medium"
    enableYolo: "false"
    startupTimeoutMs: "15000"

这样配置后,用户就可以在 Hero 配置界面看到 Reasonix 选项,并进行个性化设置。就像给一个人登记户口,有了身份,才能在这个社会里正常生活。

前端实现细节

前端实现主要负责用户交互和展示,同样分为几个关键部分。

类型生成和视觉映射

首先需要重新生成 OpenAPI 类型:

bash 复制代码
npm run generate:api:once

这会生成包含 REASONIX_CLI = 'ReasonixCli' 的类型定义。然后在视觉映射中:

typescript 复制代码
// executorTypeAdapter.ts
export type ExecutorVisualType = 'Claude' | 'Codex' | 'Copilot' | 'Reasonix' | ...;

export const resolveExecutorVisualTypeFromProviderType = (
  providerType: PCode_Models_AIProviderType
): ExecutorVisualType => {
  switch (providerType) {
    // ... 其他 case
    case PCode_Models_AIProviderType.REASONIX_CLI:
      return 'Reasonix';
  }
};

这样 Reasonix 就有自己的视觉类型,可以显示对应的图标和样式。就像每个人都有自己的身份证照片。

配置表单实现

HeroCliEquipmentForm.tsx 中,我们为 Reasonix 添加了专门的配置表单:

typescript 复制代码
case PCode_Models_AIProviderType.REASONIX_CLI:
  return (
    <>
      <Form.Item name="binary">
        <Input />
      </Form.Item>
      <Form.Item name="effort">
        <Select>
          <Select.Option value="none">None</Select.Option>
          <Select.Option value="low">Low</Select.Option>
          <Select.Option value="medium">Medium</Select.Option>
          <Select.Option value="high">High</Select.Option>
        </Select>
      </Form.Item>
      <Form.Item name="budgetUsd">
        <InputNumber />
      </Form.Item>
      <Form.Item name="transcriptPath">
        <Input />
      </Form.Item>
      <Form.Item name="enableYolo">
        <Switch />
      </Form.Item>
      <Form.Item name="arguments">
        <Input />
      </Form.Item>
      <Form.Item name="startupTimeoutMs">
        <InputNumber />
      </Form.Item>
    </>
  );

这个表单涵盖了所有 Reasonix 支持的参数,用户可以根据自己的需求进行配置。就像给一个人量身定做衣服,合身最重要。

多语言支持

为了让国际用户也能使用,我们添加了多语言支持:

yaml 复制代码
# locales/*/common/hero.yml
profession:
  primary:
    reasonix:
      name: "Reasonix"
      summary: "基于 ACP 的 AI 代码助手"
      parameters:
        effort: "计算力投入"
        budgetUsd: "预算 (USD)"
        transcriptPath: "转录文件路径"
        enableYolo: "启用 YOLO 模式"

毕竟语言不通,再好的东西也没法用。

健康监控映射

前端还需要展示 Reasonix 的健康状态:

typescript 复制代码
// healthApi.ts
export const MONITORING_CHANNEL_FALLBACKS = {
  // ... 其他 provider
  reasonix: {
    displayName: 'Reasonix',
    icon: 'executor-avatar:Reasonix'
  }
};

export const mapProviderTypeToMonitoringCliId = (
  providerType: PCode_Models_AIProviderType
): string => {
  switch (providerType) {
    // ... 其他 case
    case PCode_Models_AIProviderType.REASONIX_CLI:
      return 'reasonix';
  }
};

这样用户可以在监控面板中看到 Reasonix 的状态,如果 CLI 未安装或不可用,会得到清晰的提示。就像医生给病人检查,有问题早发现早治疗。

最佳实践和注意事项

在实现过程中,我们总结了一些最佳实践和需要注意的地方。

参数验证

ReasonixCliProvider 必须严格验证配置参数,拒绝不支持的设置键:

csharp 复制代码
public void ValidateConfigurationOverrides(Dictionary<string, string?> overrides)
{
    foreach (var key in overrides.Keys)
    {
        if (!SupportedSettingKeys.Contains(key))
        {
            throw new HeroProviderConfigurationException(
                $"Unsupported setting key '{key}' for Reasonix provider");
        }
    }
}

这样可以防止用户配置错误的参数,避免运行时错误。就像守门员,不能让不该进的东西混进来。

会话绑定管理

使用 ConcurrentDictionary 管理会话绑定,支持会话恢复:

csharp 复制代码
_sessionBindings[cessionId] = sessionId;

// 在后续请求中绑定已存在的会话
if (_sessionBindings.TryGetValue(cessionId, out var boundSessionId))
{
    options.SessionId = boundSessionId;
}

这种设计让用户可以在中断后恢复之前的会话,提供更好的体验。就像把一个故事记住,下次还能继续讲。

优雅降级处理

前端应该检查 CLI 可用性,并给出友好的提示:

typescript 复制代码
const reasonixAvailable = await healthApi.checkCliAvailable('reasonix');
if (!reasonixAvailable) {
  showMessage('Reasonix CLI 未安装,请在系统 PATH 中配置');
}

不要让用户遇到莫名其妙的错误,提前检查并给出明确的指导。毕竟谁也不愿意莫名其妙地被卡住。

测试覆盖

完善的测试是质量保证的关键:

csharp 复制代码
[Fact]
public async Task ExecuteCommandStreamAsync_WithValidCommand_StreamsReasonixResponse()
{
    // Arrange
    var grain = _grainFactory.GetGrain<IReasonixGrain>("test-cession");
    var responses = new List<ReasonixResponse>();

    // Act
    await foreach (var response in grain.ExecuteCommandStreamAsync("help"))
    {
        responses.Add(response);
    }

    // Assert
    responses.Should().NotBeEmpty();
    responses.All(r => r.Kind == ExecutorResponseKind.Content).Should().BeTrue();
}

这样的单元测试可以验证核心功能是否正常工作,防止回归错误。就像考试前做练习题,总得确保自己真的掌握了。

总结

通过这次 Reasonix 的集成实践,我们成功地将一个本地 CLI 工具提升为系统的一等 Agent Provider。整个过程中,我们遵循了既定的架构模式,做出了合理的技术决策,最终实现了一个功能完整、用户体验良好的集成方案。

这套方案的核心价值在于:

  1. 清晰的三层架构分离了关注点,降低了复杂度
  2. 专用 Grain 和 thin adapter 的设计保持了灵活性
  3. 优雅降级和健康监控提升了用户体验
  4. 完善的参数验证和会话管理保证了可靠性

对于其他类似 CLI Provider 的集成,这套方案提供了可复用的模式。我们希望这次实践能够帮助到其他开发者,也欢迎大家到 HagiCode 项目中交流经验。

其实很多事情都是这样,一开始看着难,但只要方法对了,一步一步来,总能解决的。就像登山,看着山顶很远,但只要不放弃,总能爬上去。

总结

回到"从零开始:如何将 Reasonix CLI 集成到 HagiCode 系统中"这个主题,真正值得反复确认的不是零散技巧,而是约束条件、实现边界和工程取舍是否已经看清。

只要把文中的判断依据沉淀成稳定的检查项,后续面对类似问题时就能更快做出可靠决策。

原文与版权说明

感谢您的阅读,如果您觉得本文有用,欢迎点赞、收藏和分享支持。

本内容采用人工智能辅助协作,最终内容由作者审核并确认。