DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

From Scratch: How to Integrate Reasonix CLI into the HagiCode System

From Scratch: How to Integrate Reasonix CLI into the HagiCode System

This article shares the complete technical practice of integrating Reasonix CLI as a first-class Agent Provider into the HagiCode system, covering three-layer architecture design, key technical decisions, and frontend and backend implementation details.

Background

Reasonix CLI, as it happens, is a pretty interesting thing. It's an AI code assistant tool based on ACP (Agent Communication Protocol), providing powerful streaming and session management capabilities. Actually, in the HagiCode.Libs layer, we've already completed its underlying implementation. It's just that these components are still in an isolated state, like beautiful pearls that haven't been strung into a necklace. Users cannot use it through Hero profession selection, session execution paths, or monitoring panels, which is somewhat regrettable.

The problem we face is: how to elevate Reasonix to the same level as Codex, Hermes, and other first-class Agent Providers, implementing complete backend routing and frontend display? This isn't simply a matter of registering an enum value. It requires building a complete chain from low-level abstraction to user interface. It's like building a house—you can't just lay a foundation and call it done. You have to build the walls and put up the roof.

The challenge of this integration lies in the fact that Reasonix, as a local CLI tool, has its own personality and temperament. For example, it doesn't need a connection string—all parameters are configured by the user at runtime; it might not even be installed, requiring graceful degradation; it's compatible with anthropic series models, but also has its own ACP-specific parameters like effort, budget, and so on. It's like a person with their own unique way of handling things—you can't force it.

After careful architectural design and multiple rounds of discussion, we finally adopted a clear three-layer architecture solution, successfully integrating Reasonix into the system. This solution not only solved the immediate problem but also provided a reusable pattern for subsequent similar CLI Provider integrations. Actually, many things are like this—once you find the right method, the path forward becomes much easier.

About HagiCode

The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an open-source AI code assistant project dedicated to providing developers with powerful code generation, refactoring, and optimization capabilities. During development, we encountered various technical challenges, and integrating Reasonix as a first-class Agent Provider was one of them. If you find the solution shared in this article valuable, it means our engineering practice isn't bad, and HagiCode itself is worth paying attention to.

Core Content

Technical Architecture Design

The system uses a clear three-layer architecture to separate concerns, with each layer having clear responsibility boundaries:

HagiCode.Libs layer: This layer is already complete, providing the abstraction and specific implementation of the CLI provider. It defines the ICliProvider<ReasonixOptions> interface, implements ReasonixProvider to handle ACP streaming and session management, and supports parameters like effort, budget, yolo, transcript, and so on. The responsibility of this layer is to provide stable, reusable low-level capabilities without involving any business logic. It's like the foundation of a house—though invisible, it's very important.

hagicode-core layer: This is the focus of our integration. It's responsible for bridging the low-level abstraction to the system's unified interface. Specific work includes registering the AIProviderType.ReasonixCli = 12 enum value, creating ReasonixCliProvider as a thin adapter to bridge the Libs layer, implementing ReasonixGrain to handle session state and execution flow, and integrating the Hero system for parameter mapping and configuration management. The core of this layer is coordinating components to build a complete business chain. It's like the load-bearing walls of a house, connecting all parts together.

web layer: Responsible for displaying and collecting configurations from users. We need to regenerate OpenAPI types to support the new enum value, implement visual type mapping to give Reasonix its own icon and display name, create a CLI parameter configuration form allowing users to configure various parameters, and add multi-language support. The focus of this layer is user experience and interaction design. It's like the decoration of a house—whether it's done well directly affects how comfortable it is to live in.

Such layered design allows each layer to focus on its own responsibilities, reducing system complexity and facilitating subsequent maintenance and expansion. Actually, many times, clarifying things makes them simpler.

Key Technical Decisions

During implementation, we made several key technical decisions that had important impacts on the final architecture and user experience.

Decision 1: Use a Dedicated Grain

We created an independent ReasonixGrain : IReasonixGrain, IExecutorStreamGrain rather than trying to reuse some shared Grain. This decision follows the established pattern of the system's existing 11 providers. While there might be some code duplication, a dedicated Grain allows us to have fine-grained control over Reasonix's specific features, such as its unique session binding mechanism and ACP message mapping. We also defined an empty response DTO ReasonixResponse as a type discriminator. Although it doesn't contain actual data, it plays an important role in the type system. It's like everyone having their own room—even if empty, it's their own space.

Decision 2: Don't Create a Dedicated Settings Class

Unlike some Providers that need connection strings, all Reasonix configurations are set by the user at runtime and don't require startup validation. Therefore, we didn't create a dedicated Settings class. Instead, we store all configurations in the AIProviderOptions.Providers[ReasonixCli].Settings dictionary. This pattern is consistent with other local CLI Providers like Qoder, Kiro, and Kimi, simplifying code structure and avoiding unnecessary abstraction layers. Supported setting keys include: effort, budgetUsd, transcriptPath, enableYolo, arguments, startupTimeoutMs, reasoning. Sometimes simpler is better.

Decision 3: Provider Strategy Health Monitoring

Reasonix is a CLI installed locally by the user—it might not be installed at all, or might not be in the system PATH. In such cases, we shouldn't directly error out but should handle it gracefully with degradation. We use the Provider strategy to check if the CLI is available through CommandUtil.TryResolveExecutablePath. If the check fails, the UI displays as "unavailable" but doesn't affect other parts of the system. This design makes the system more robust and provides clear feedback to users. After all, no one wants the entire system to crash because of a minor issue.

Decision 4: Economic System Classification

In the HagiCode system, different Providers have different economic system classifications. We decided to let Reasonix use the 'claude' economic system classification by default, since Reasonix itself is compatible with the anthropic series of models. Currently, only Codex and Copilot have dedicated economic system classifications, while other Providers reuse existing classifications. This maintains system simplicity while correctly handling billing and cost statistics. Reusing is also a kind of wisdom—not everything needs to start from scratch.

Decision 5: Model Compatibility

Reasonix supports multiple models through the --model flag, especially the anthropic series. We added compatibility mapping in secondary-professions.index.json, allowing users to select these models in Reasonix. This design respects Reasonix's capabilities while maintaining system consistency. Users don't need to understand the underlying differences to smoothly use various models. Users have it hard enough—let's keep things simple for them.

Backend Implementation Details

Backend implementation is divided into several key parts, each with its own unique technical points.

Enum and Type Registration

First, we need to register the new Provider type in the system:

// AIProviderType.cs
public enum AIProviderType
{
    // ... other providers
    ReasonixCli = 12,
}

// AIProviderTypeExtensions.cs
private static readonly Dictionary<string, AIProviderType> _typeMap = new()
{
    // ... other mappings
    ["Reasonix"] = AIProviderType.ReasonixCli,
    ["reasonix"] = AIProviderType.ReasonixCli,
    ["reasonix-cli"] = AIProviderType.ReasonixCli,
    ["ReasonixCli"] = AIProviderType.ReasonixCli,
};
Enter fullscreen mode Exit fullscreen mode

This enum value needs to coordinate with other concurrent changes to avoid conflicts. We chose the value 12 because it's the next available number. It's like lining up—there has to be some order.

Thin Adapter Implementation

ReasonixCliProvider is the key component connecting the Libs layer and the system's unified interface:

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()
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

The key responsibilities of this adapter are:

  1. Validate configuration parameters and reject unsupported setting keys
  2. Maintain session binding relationships, supporting session resumption
  3. Map ACP messages to the system's unified format
  4. Use ProviderErrorAutoRetryCoordinator to implement automatic retry

It's like a translator, translating one language to another while ensuring the meaning is accurate.

Orleans Grain Implementation

ReasonixGrain is responsible for handling session state and execution flow:

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-aware configuration fallback logic
        var config = await HeroProviderResolver.ResolveAsync(AIProviderType.ReasonixCli, heroId);
        return _aiProviderFactory.CreateProvider(AIProviderType.ReasonixCli, config);
    }
}
Enter fullscreen mode Exit fullscreen mode

The core functions of the Grain include:

  1. Use [PersistentState("reasonix-interop")] to maintain session state
  2. Implement Hero-aware configuration fallback logic
  3. Track tool lifecycle status
  4. Support cancellation token chains, ensuring execution can be interrupted in a timely manner

It's like a housekeeper, arranging things in an orderly manner.

Hero System Integration

The Hero system is HagiCode's profession configuration system, and we need to integrate Reasonix into this system:

// HeroAppService.cs
// Family inference
AIProviderType.ReasonixCli => "reasonix"

// Managed CLI parameters
ManagedCliParameterKeysByProvider[AIProviderType.ReasonixCli] = 
    ["binary", "effort", "budgetUsd", "transcriptPath", "enableYolo", "arguments", "startupTimeoutMs"];

// Managed model parameters
ManagedModelParameterKeysByProvider[AIProviderType.ReasonixCli] = 
    ["model", "reasoning"];
Enter fullscreen mode Exit fullscreen mode

In the profession directory configuration file main-professions.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"
Enter fullscreen mode Exit fullscreen mode

With this configuration, users can see the Reasonix option in the Hero configuration interface and make personalized settings. It's like registering someone's household—with an identity, they can live normally in this society.

Frontend Implementation Details

Frontend implementation is mainly responsible for user interaction and display, also divided into several key parts.

Type Generation and Visual Mapping

First, we need to regenerate OpenAPI types:

npm run generate:api:once
Enter fullscreen mode Exit fullscreen mode

This generates type definitions containing REASONIX_CLI = 'ReasonixCli'. Then in visual mapping:

// executorTypeAdapter.ts
export type ExecutorVisualType = 'Claude' | 'Codex' | 'Copilot' | 'Reasonix' | ...;

export const resolveExecutorVisualTypeFromProviderType = (
  providerType: PCode_Models_AIProviderType
): ExecutorVisualType => {
  switch (providerType) {
    // ... other cases
    case PCode_Models_AIProviderType.REASONIX_CLI:
      return 'Reasonix';
  }
};
Enter fullscreen mode Exit fullscreen mode

This way Reasonix has its own visual type and can display corresponding icons and styles. It's like everyone having their own ID photo.

Configuration Form Implementation

In HeroCliEquipmentForm.tsx, we added a dedicated configuration form for Reasonix:

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>
    </>
  );
Enter fullscreen mode Exit fullscreen mode

This form covers all parameters supported by Reasonix, allowing users to configure according to their needs. It's like tailoring clothes for someone—fit is most important.

Multi-language Support

To enable international users to use it as well, we added multi-language support:

# locales/*/common/hero.yml
profession:
  primary:
    reasonix:
      name: "Reasonix"
      summary: "AI code assistant based on ACP"
      parameters:
        effort: "Computational effort"
        budgetUsd: "Budget (USD)"
        transcriptPath: "Transcript file path"
        enableYolo: "Enable YOLO mode"
Enter fullscreen mode Exit fullscreen mode

After all, if the language doesn't work, even good things can't be used.

Health Monitoring Mapping

The frontend also needs to display Reasonix's health status:

// healthApi.ts
export const MONITORING_CHANNEL_FALLBACKS = {
  // ... other providers
  reasonix: {
    displayName: 'Reasonix',
    icon: 'executor-avatar:Reasonix'
  }
};

export const mapProviderTypeToMonitoringCliId = (
  providerType: PCode_Models_AIProviderType
): string => {
  switch (providerType) {
    // ... other cases
    case PCode_Models_AIProviderType.REASONIX_CLI:
      return 'reasonix';
  }
};
Enter fullscreen mode Exit fullscreen mode

This way users can see Reasonix's status in the monitoring panel. If the CLI is not installed or unavailable, they will receive clear prompts. It's like a doctor examining a patient—catching problems early leads to early treatment.

Best Practices and Considerations

During implementation, we summarized some best practices and points to note.

Parameter Validation

ReasonixCliProvider must strictly validate configuration parameters and reject unsupported setting keys:

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");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

This prevents users from configuring wrong parameters and avoids runtime errors. It's like a goalkeeper—can't let things that shouldn't enter get in.

Session Binding Management

Use ConcurrentDictionary to manage session bindings, supporting session resumption:

_sessionBindings[cessionId] = sessionId;

// Bind existing sessions in subsequent requests
if (_sessionBindings.TryGetValue(cessionId, out var boundSessionId))
{
    options.SessionId = boundSessionId;
}
Enter fullscreen mode Exit fullscreen mode

This design allows users to resume previous sessions after interruption, providing a better experience. It's like remembering a story so you can continue telling it next time.

Graceful Degradation Handling

The frontend should check CLI availability and provide friendly prompts:

const reasonixAvailable = await healthApi.checkCliAvailable('reasonix');
if (!reasonixAvailable) {
  showMessage('Reasonix CLI is not installed, please configure it in the system PATH');
}
Enter fullscreen mode Exit fullscreen mode

Don't let users encounter inexplicable errors—check in advance and provide clear guidance. After all, no one wants to get stuck inexplicably.

Test Coverage

Comprehensive testing is key to quality assurance:

[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();
}
Enter fullscreen mode Exit fullscreen mode

Such unit tests can verify that core functions work correctly, preventing regression errors. It's like doing practice questions before an exam—you need to ensure you've actually mastered it.

Summary

Through this Reasonix integration practice, we successfully elevated a local CLI tool to the system's first-class Agent Provider. Throughout the process, we followed established architecture patterns, made reasonable technical decisions, and ultimately implemented a complete, well-integrated solution with good user experience.

The core value of this solution lies in:

  1. Clear three-layer architecture separates concerns and reduces complexity
  2. Dedicated Grain and thin adapter design maintains flexibility
  3. Graceful degradation and health monitoring improve user experience
  4. Comprehensive parameter validation and session management ensure reliability

For other similar CLI Provider integrations, this solution provides a reusable pattern. We hope this practice can help other developers, and we welcome everyone to exchange experiences in the HagiCode project.

Actually, many things are like this—at first they seem difficult, but as long as you find the right method and take it step by step, you can always solve it. It's like climbing a mountain—the peak looks far away, but as long as you don't give up, you can always climb up.

Summary

Returning to the theme "From Scratch: How to Integrate Reasonix CLI into the HagiCode System," what's truly worth repeatedly confirming isn't scattered techniques, but whether constraint conditions, implementation boundaries, and engineering trade-offs have been clearly understood.

As long as the judgment bases in the article are distilled into stable checklist items, you can make reliable decisions more quickly when facing similar problems in the future.

Original Article & License

Thanks for reading. If this article helped, consider liking, bookmarking, or sharing it.
This article was created with AI assistance and reviewed by the author before publication.

Top comments (0)