DEV Community

Hagicode
Hagicode

Posted on • Originally published at docs.hagicode.com

Hermes Agent Integration Practice: From Protocol to Production

Hermes Agent Integration Practice: From Protocol to Production

Sharing our complete experience integrating Hermes Agent into HagiCode, including core insights on ACP protocol adaptation, session pool management, and frontend-backend contract synchronization.

Background

While building the AI-assisted coding platform HagiCode, the team needed to integrate an Agent framework capable of running both locally and scaling to the cloud. After research, Nous Research's Hermes Agent was selected as the underlying engine for our comprehensive Agent.

Technology selection is neither particularly hard nor simple. After all, there are quite a few competitive Agent frameworks on the market, but Hermes's ACP protocol and tool system really stand out, perfectly aligning with HagiCode's "have it all" scenario—local development, team collaboration, and cloud scalability. But truly integrating Hermes into a production system requires solving a series of engineering challenges—this is no trivial matter.

HagiCode's tech stack is built on Orleans for distributed systems, with React + TypeScript on the frontend. Integrating Hermes requires maintaining existing architectural consistency while making Hermes a "first-class citizen" executor alongside ClaudeCode, OpenCode, and others. Easy to say, but in practice... well, you know.

This article shares our practical experience integrating Hermes Agent in the HagiCode project, hoping to provide reference for teams with similar needs. After all, there's no need for others to step into the same potholes we've already fallen into.

About HagiCode

The solution shared in this article comes from our practical experience in the HagiCode project. HagiCode is an AI-driven coding assistance platform supporting unified integration and management of multiple AI Providers. During the Hermes Agent integration, we designed a universal Provider abstraction layer enabling seamless integration of new Agent types into our existing system.

If you're interested in HagiCode, welcome to visit GitHub to learn more. More eyes, more strength—that's all.

Architecture Design

Layered Design Approach

HagiCode's Hermes integration adopts a clear layered architecture with each layer having distinct responsibilities:

Backend Core Layer

  • HermesCliProvider: Implements IAIProvider interface as the unified AI Provider entry point
  • HermesPlatformConfiguration: Manages Hermes executable path, parameters, authentication and other configurations
  • ICliProvider<HermesOptions>: Low-level CLI abstraction provided by HagiCode.Libs, handling subprocess lifecycle

Transport Layer

  • StdioAcpTransport: Communicates with Hermes ACP subprocess via standard input/output
  • ACP protocol methods: initialize, authenticate, session/new, session/prompt

Runtime Layer

  • HermesGrain: Orleans Grain implementation handling distributed session execution
  • CliAcpSessionPool: Session pool reusing ACP subprocesses to avoid frequent startup overhead

Frontend Layer

  • ExecutorAvatar: Hermes visual identity and icon
  • executorTypeAdapter: Provider type mapping logic
  • SignalR real-time messaging: Maintains Hermes identity consistency in message streams

This layered design enables independent evolution of each layer—for example, adding a new transport method (like WebSocket) in the future would only require modifying the transport layer. After all, who wants to overhaul the entire system just to change a transport method? Too exhausting.

Unified Interface Abstraction

All AI Providers implement the IAIProvider interface, the core design of HagiCode's architecture:

public interface IAIProvider
{
    string Name { get; }
    ProviderCapabilities Capabilities { get; }

    IAsyncEnumerable<AIStreamingChunk> StreamAsync(
        AIRequest request,
        CancellationToken cancellationToken = default);

    Task<AIResponse> ExecuteAsync(
        AIRequest request,
        CancellationToken cancellationToken = default);
}
Enter fullscreen mode Exit fullscreen mode

HermesCliProvider implements this interface, standing on equal footing with ClaudeCodeProvider, OpenCodeProvider, and others. Benefits of this design:

  1. Replaceability: Switching Providers doesn't affect upper-layer business logic
  2. Testability: Easy to Mock Providers for unit testing
  3. Extensibility: New Providers only need to implement the interface

In the end, interfaces are like rules—rules that let everyone coexist harmoniously, each playing to their strengths without interference. Isn't that a kind of beauty?

Core Implementation

Provider Layer Implementation

HermesCliProvider is the heart of the entire integration, coordinating various components to complete an AI call:

public sealed class HermesCliProvider : IAIProvider, IVersionedAIProvider
{
    private readonly ICliProvider<LibsHermesOptions> _provider;
    private readonly ConcurrentDictionary<string, string> _sessionBindings;

    public ProviderCapabilities Capabilities { get; } = new()
    {
        SupportsStreaming = true,
        SupportsTools = true,
        SupportsSystemMessages = true,
        SupportsArtifacts = false
    };

    public async IAsyncEnumerable<AIStreamingChunk> StreamAsync(
        AIRequest request,
        [EnumeratorCancellation] CancellationToken cancellationToken = default)
    {
        // 1. Resolve session binding key
        var bindingKey = ResolveBindingKey(request.CessionId);

        // 2. Get or create Hermes session through session pool
        var options = new HermesOptions
        {
            ExecutablePath = _platformConfiguration.ExecutablePath,
            Arguments = _platformConfiguration.Arguments,
            SessionId = _sessionBindings.TryGetValue(bindingKey, out var sessionId) ? sessionId : null,
            WorkingDirectory = request.WorkingDirectory,
            Model = request.Model
        };

        // 3. Execute and collect streaming response
        await foreach (var message in _provider.ExecuteAsync(options, request.Prompt, cancellationToken))
        {
            // 4. Map ACP message to AIStreamingChunk
            if (_responseMapper.TryConvertToStreamingChunk(message, out var chunk))
            {
                yield return chunk;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key design points here:

  1. Session binding: Binding multiple requests to the same Hermes subprocess through CessionId for context continuity in multi-turn conversations
  2. Response mapping: Converting Hermes ACP message format to unified AIStreamingChunk format
  3. Streaming processing: Using IAsyncEnumerable for true streaming response support

Session binding is like human relationships—once a connection is established, subsequent communication has context without starting from scratch each time. Just need to maintain the relationship well, or it'll break.

ACP Protocol Adaptation

Hermes uses ACP (Agent Communication Protocol), different from traditional HTTP APIs. ACP is a standard input/output based protocol with several characteristics:

  1. Startup marker: Hermes process outputs //ready marker after startup
  2. Dynamic authentication: Authentication methods are not fixed, requiring protocol negotiation
  3. Session reuse: Reusing established sessions through SessionId
  4. Response fragmentation: Complete responses may be scattered across multiple session/update notifications

HagiCode handles these characteristics through StdioAcpTransport:

public class StdioAcpTransport
{
    public async Task InitializeAsync(CancellationToken cancellationToken)
    {
        // Wait for //ready marker
        var readyLine = await _outputReader.ReadLineAsync(cancellationToken);
        if (readyLine != "//ready")
        {
            throw new InvalidOperationException("Hermes did not send ready signal");
        }

        // Send initialize request
        await SendRequestAsync(new
        {
            jsonrpc = "2.0",
            id = 1,
            method = "initialize",
            @params = new
            {
                protocolVersion = "2024-11-05",
                capabilities = new { },
                clientInfo = new { name = "HagiCode", version = "1.0.0" }
            }
        }, cancellationToken);
    }
}
Enter fullscreen mode Exit fullscreen mode

Protocols are like tacit understanding between people—with it, communication flows smoothly. But building that understanding takes time—running in is unavoidable for anyone.

Session Pool Management

Frequent Hermes subprocess startup has significant overhead, so we implemented a session pool mechanism:

services.AddSingleton(static _ =>
{
    var registry = new CliProviderPoolConfigurationRegistry();
    registry.Register("hermes", new CliPoolSettings
    {
        MaxActiveSessions = 50,
        IdleTimeout = TimeSpan.FromMinutes(10)
    });
    return registry;
});
Enter fullscreen mode Exit fullscreen mode

Key session pool parameters:

  • MaxActiveSessions: Controls concurrency ceiling to avoid resource exhaustion
  • IdleTimeout: Idle timeout balancing startup cost and memory usage

In practice we found:

  1. Idle timeout set too short causes frequent restarts, too long occupies memory
  2. Concurrency ceiling needs adjustment based on actual load; too large may cause system lag
  3. Need to monitor session pool usage for timely parameter adjustment

It's like many life choices—too aggressive leads to problems, too conservative misses opportunities. Just finding a balance.

Frontend Integration

Type Mapping

The frontend needs to correctly identify Hermes Provider and display corresponding visual elements:

// executorTypeAdapter.ts
export const resolveExecutorVisualTypeFromProviderType = (
  providerType: PCode_Models_AIProviderType | null | undefined
): ExecutorVisualType => {
  switch (providerType) {
    case PCode_Models_AIProviderType.HERMES_CLI:
      return 'Hermes';
    default:
      return 'Unknown';
  }
};
Enter fullscreen mode Exit fullscreen mode

Visual Presentation

Hermes has dedicated icon and color identity:

// ExecutorAvatar.tsx
const renderExecutorGlyph = (executorType: ExecutorVisualType, iconSize: number) => {
  switch (executorType) {
    case 'Hermes':
      return (
        <svg viewBox="0 0 24 24" fill="none" className="h-4 w-4">
          <rect x="4" y="4" width="16" height="16" rx="4" fill="currentColor" opacity="0.16" />
          <path d="M8 7v10M16 7v10M8 12h8" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
        </svg>
      );
    default:
      return <DefaultAvatar />;
  }
};
Enter fullscreen mode Exit fullscreen mode

After all, beautiful things deserve beautiful presentation. But for that beauty to be seen, it relies on our frontend developers' efforts.

Contract Synchronization

Frontend and backend maintain contract consistency through OpenAPI generation. The backend defines the AIProviderType enum:

public enum AIProviderType
{
    Unknown,
    ClaudeCode,
    OpenCode,
    HermesCli  // New addition
}
Enter fullscreen mode Exit fullscreen mode

The frontend generates corresponding TypeScript types through OpenAPI, ensuring enum value consistency. This is key to avoiding "Unknown" displays on the frontend.

Contracts are like promises—once agreed upon, they must be kept, or you'll face awkward situations like "Unknown".

Configuration Management

Hermes configuration is managed through appsettings.json:

{
  "Providers": {
    "HermesCli": {
      "ExecutablePath": "hermes",
      "Arguments": "acp",
      "StartupTimeoutMs": 10000,
      "ClientName": "HagiCode",
      "Authentication": {
        "PreferredMethodId": "api-key",
        "MethodInfo": {
          "api-key": "your-api-key-here"
        }
      },
      "SessionDefaults": {
        "Model": "claude-sonnet-4-20250514",
        "ModeId": "default"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

This configuration-driven design brings flexibility:

  • Can override executable paths for development and testing
  • Can customize startup parameters to adapt to different Hermes versions
  • Can configure authentication information supporting multiple authentication methods

Configuration is like multiple choice questions in life—with enough options, you can always find what fits. But sometimes too many choices cause decision paralysis.

Practical Experience

Health Check

Implementing a reliable Provider requires comprehensive health checks:

public async Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default)
{
    var response = await ExecuteAsync(new AIRequest
    {
        Prompt = "Reply with exactly PONG.",
        CessionId = null,
        AllowedTools = Array.Empty<string>(),
        WorkingDirectory = ResolveWorkingDirectory(null)
    }, cancellationToken);

    var success = string.Equals(response.Content.Trim(), "PONG", StringComparison.OrdinalIgnoreCase);
    return new ProviderTestResult
    {
        ProviderName = Name,
        Success = success,
        ResponseTimeMs = stopwatch.ElapsedMilliseconds,
        ErrorMessage = success ? null : $"Unexpected Hermes ping response: '{response.Content}'."
    };
}
Enter fullscreen mode Exit fullscreen mode

Health checks require attention:

  1. Use simple test cases avoiding complex scenarios
  2. Set reasonable timeout values
  3. Log response times for performance analysis

Like people need physical exams, systems need health checks—early detection and treatment prevents major issues later.

Validation Tools

HagiCode provides a dedicated console for validating Hermes integration:

# Basic validation
HagiCode.Libs.Hermes.Console --test-provider

# Complete suite (including repository analysis)
HagiCode.Libs.Hermes.Console --test-provider-full --repo .

# Custom executable
HagiCode.Libs.Hermes.Console --test-provider-full --executable /path/to/hermes
Enter fullscreen mode Exit fullscreen mode

This tool is very useful during development for quickly validating integration correctness. After all, who wants to think about testing only when problems arise?

Common Issue Handling

Authentication Failure

  • Check if Authentication.PreferredMethodId matches authentication methods actually supported by Hermes
  • Confirm authentication information format is correct (API Key, Bearer Token, etc.)

Session Timeout

  • Increase StartupTimeoutMs value
  • Check MCP server reachability
  • Review system resource usage

Incomplete Response

  • Ensure proper aggregation of session/update notifications and final results
  • Check streaming cancellation logic
  • Verify error handling completeness

Frontend Shows Unknown

  • Confirm OpenAPI generation includes HermesCli enum value
  • Check if type mapping is correct
  • Clear browser cache and regenerate types

Problems are inevitable. When they arise, don't panic—investigate the cause slowly and you'll find a solution. After all, there are always more solutions than difficulties.

Performance Optimization Recommendations

  1. Use session pool: Reuse ACP subprocesses to reduce startup overhead
  2. Set timeouts reasonably: Balance memory and startup costs
  3. Reuse session IDs: Use same CessionId for batch tasks
  4. Configure MCP on-demand: Avoid unnecessary tool calls

Performance is like efficiency in life—doing it right halves the effort, doing it wrong doubles the effort. But finding that "right" point requires experience and luck.

Summary

Integrating Hermes Agent into production systems requires consideration across multiple levels:

  1. Architecture level: Design unified Provider interface implementing replaceable component architecture
  2. Protocol level: Properly handle ACP protocol peculiarities like startup markers, dynamic authentication
  3. Performance level: Reuse resources through session pools balancing startup cost and memory usage
  4. Frontend level: Ensure contract synchronization providing consistent visual experience

HagiCode's practice shows that through good layered design and configuration driving, complex Agent systems can be seamlessly integrated into existing architecture.

These principles sound simple, but in practice you'll encounter various problems. No matter—solved problems become experience, unsolved ones become lessons, both valuable.

Beautiful things or people don't need to be possessed—as long as they remain beautiful, simply appreciating their beauty is enough. Technology is similar: as long as it makes the system better, which framework or protocol is used doesn't really matter...

References

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)