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,
};
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()
};
}
}
The key responsibilities of this adapter are:
- Validate configuration parameters and reject unsupported setting keys
- Maintain session binding relationships, supporting session resumption
- Map ACP messages to the system's unified format
- Use
ProviderErrorAutoRetryCoordinatorto 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);
}
}
The core functions of the Grain include:
- Use
[PersistentState("reasonix-interop")]to maintain session state - Implement Hero-aware configuration fallback logic
- Track tool lifecycle status
- 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"];
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"
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
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';
}
};
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>
</>
);
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"
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';
}
};
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");
}
}
}
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;
}
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');
}
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();
}
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:
- Clear three-layer architecture separates concerns and reduces complexity
- Dedicated Grain and thin adapter design maintains flexibility
- Graceful degradation and health monitoring improve user experience
- 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.
- Author: newbe36524
- Original URL: https://docs.hagicode.com/go?platform=devto&target=%2Fblog%2F2026-06-09-integrating-reasonix-cli-into-hagicode%2F
- License: Unless otherwise stated, this article is licensed under CC BY-NC-SA. Please retain attribution when sharing.
Top comments (0)