|
| 1 | +--- |
| 2 | +title: Hermes Agent client integration |
| 3 | +description: Register the ES.FX.NousResearch.HermesAgent typed client with Ignite, including configuration binding, a live health check, and OpenTelemetry tracing. |
| 4 | +--- |
| 5 | + |
| 6 | +## Overview |
| 7 | + |
| 8 | +The Hermes Agent Spark registers the |
| 9 | +[ES.FX.NousResearch.HermesAgent typed client](../../libraries/hermes-agent-client.md) |
| 10 | +(`IHermesAgentClient`, static bearer-key authentication) into dependency injection, with configuration |
| 11 | +binding, startup validation, a live health check, and OpenTelemetry tracing already wired up. Call |
| 12 | +`builder.IgniteHermesAgentClient()` once and inject `IHermesAgentClient` anywhere in your app. |
| 13 | + |
| 14 | +Under the hood the Spark: |
| 15 | + |
| 16 | +- Binds `HermesAgentClientOptions` (`BaseUrl` + `ApiKey`) from the `Ignite:NousResearch:HermesAgent` |
| 17 | + configuration section with `ValidateOnStart()`, so a bad configuration fails at startup instead of on |
| 18 | + first use. |
| 19 | +- Binds a `HermesAgentClientSparkSettings` (observability toggles) from the |
| 20 | + `Ignite:NousResearch:HermesAgent:Settings` sub-section. |
| 21 | +- Registers the client via the library's `AddHermesAgentClient()` (typed `HttpClient`, keyed-capable, |
| 22 | + bearer-key authentication handler — no OAuth machinery). |
| 23 | +- Adds a **live** health check that calls `GET /v1/capabilities` with the configured API key. |
| 24 | +- Adds the client's `ES.FX.NousResearch.HermesAgent` `ActivitySource` to the Ignite OpenTelemetry |
| 25 | + pipeline. |
| 26 | + |
| 27 | +> [!NOTE] |
| 28 | +> The client surface (chat, responses, runs, jobs, sessions, server discovery), streaming with |
| 29 | +> `await foreach`, and the error model (`HermesAgentApiException`) are documented on the |
| 30 | +> [Hermes Agent API client](../../libraries/hermes-agent-client.md) page. This page covers the Ignite |
| 31 | +> wiring. |
| 32 | +
|
| 33 | +## Install the client |
| 34 | + |
| 35 | +```bash |
| 36 | +dotnet add package ES.FX.Ignite.NousResearch.HermesAgent |
| 37 | +``` |
| 38 | + |
| 39 | +```xml |
| 40 | +<PackageReference Include="ES.FX.Ignite.NousResearch.HermesAgent" /> |
| 41 | +``` |
| 42 | + |
| 43 | +> [!NOTE] |
| 44 | +> ES.FX uses Central Package Management, so `<PackageReference>` in a project that also centralizes |
| 45 | +> versions carries no `Version` attribute. If you install into a standalone consumer, add |
| 46 | +> `Version="…"`. |
| 47 | +
|
| 48 | +## Register the client |
| 49 | + |
| 50 | +Call `IgniteHermesAgentClient` on your host application builder, after `builder.Ignite()`: |
| 51 | + |
| 52 | +```csharp |
| 53 | +var builder = WebApplication.CreateBuilder(args); |
| 54 | + |
| 55 | +builder.Ignite(); |
| 56 | +builder.IgniteHermesAgentClient(); |
| 57 | + |
| 58 | +var app = builder.Build(); |
| 59 | +app.Ignite(); |
| 60 | + |
| 61 | +await app.RunAsync(); |
| 62 | +``` |
| 63 | + |
| 64 | +The full signature is: |
| 65 | + |
| 66 | +```csharp |
| 67 | +public static IHttpClientBuilder IgniteHermesAgentClient( |
| 68 | + this IHostApplicationBuilder builder, |
| 69 | + string? name = null, |
| 70 | + string? serviceKey = null, |
| 71 | + Action<HermesAgentClientSparkSettings>? configureSettings = null, |
| 72 | + Action<HermesAgentClientOptions>? configureOptions = null, |
| 73 | + string configurationSectionPath = HermesAgentClientSpark.ConfigurationSectionPath); |
| 74 | +``` |
| 75 | + |
| 76 | +It returns the underlying `IHttpClientBuilder`, for further customization of the named client. |
| 77 | + |
| 78 | +### What gets registered |
| 79 | + |
| 80 | +| Service | Lifetime | Notes | |
| 81 | +| --- | --- | --- | |
| 82 | +| `IHermesAgentClient` | Transient | The typed client. Keyed when `serviceKey` is set. | |
| 83 | +| `HermesAgentClientSparkSettings` | Singleton (keyed) | The resolved observability settings. | |
| 84 | +| Health check `HermesAgentClient` | — | Live API-key check. See [Health checks](#health-checks). | |
| 85 | +| OpenTelemetry `ActivitySource` | — | `ES.FX.NousResearch.HermesAgent`. See [Tracing](#tracing). | |
| 86 | + |
| 87 | +### Consume the client |
| 88 | + |
| 89 | +```csharp |
| 90 | +public sealed class AgentGateway(IHermesAgentClient hermes) |
| 91 | +{ |
| 92 | + public Task<HermesAgentChatCompletion> AskAsync(HermesAgentChatCompletionRequest request, |
| 93 | + CancellationToken cancellationToken) => |
| 94 | + hermes.Chat.CompleteAsync(request, cancellationToken: cancellationToken); |
| 95 | +} |
| 96 | +``` |
| 97 | + |
| 98 | +> [!WARNING] |
| 99 | +> Calling `IgniteHermesAgentClient` twice with the **same** `serviceKey` throws |
| 100 | +> `ReconfigurationNotSupportedException`. Register each instance exactly once; to talk to multiple |
| 101 | +> Hermes Agent servers, give each a distinct `serviceKey` (see below). |
| 102 | +
|
| 103 | +### Register keyed clients |
| 104 | + |
| 105 | +To connect to more than one Hermes Agent server, register each as a **keyed** service with a distinct |
| 106 | +`serviceKey`, and pass a matching `name` so each reads its own configuration sub-section: |
| 107 | + |
| 108 | +```csharp |
| 109 | +builder.IgniteHermesAgentClient(name: "research", serviceKey: "research"); |
| 110 | +builder.IgniteHermesAgentClient(name: "coding", serviceKey: "coding"); |
| 111 | +``` |
| 112 | + |
| 113 | +- **`name`** selects the configuration sub-section: `name: "research"` reads from |
| 114 | + `Ignite:NousResearch:HermesAgent:research` instead of `Ignite:NousResearch:HermesAgent`. It does not |
| 115 | + affect DI. |
| 116 | +- **`serviceKey`** registers `IHermesAgentClient` as a **keyed** service. Resolve it with |
| 117 | + `[FromKeyedServices("…")]`. When `serviceKey` is `null`, the client is the default (unkeyed) |
| 118 | + registration. |
| 119 | + |
| 120 | +```json |
| 121 | +{ |
| 122 | + "Ignite": { |
| 123 | + "NousResearch": { |
| 124 | + "HermesAgent": { |
| 125 | + "research": { |
| 126 | + "BaseUrl": "http://hermes-research:8642" |
| 127 | + }, |
| 128 | + "coding": { |
| 129 | + "BaseUrl": "http://hermes-coding:8642" |
| 130 | + } |
| 131 | + } |
| 132 | + } |
| 133 | + } |
| 134 | +} |
| 135 | +``` |
| 136 | + |
| 137 | +## Configuration |
| 138 | + |
| 139 | +All Hermes Agent configuration lives under the `Ignite:NousResearch:HermesAgent` section. Both |
| 140 | +delegates (`configureSettings`, `configureOptions`) run **after** configuration is read from |
| 141 | +`appsettings.json`, so a delegate overrides the corresponding JSON values. |
| 142 | + |
| 143 | +### Settings vs options |
| 144 | + |
| 145 | +| Concept | Type | Purpose | Config location | Customized via | |
| 146 | +| --- | --- | --- | --- | --- | |
| 147 | +| **Options** | `HermesAgentClientOptions` | The server address + API key. | `Ignite:NousResearch:HermesAgent` | `configureOptions` | |
| 148 | +| **Settings** | `HermesAgentClientSparkSettings` | Ignite observability toggles. | `Ignite:NousResearch:HermesAgent:Settings` | `configureSettings` | |
| 149 | + |
| 150 | +`HermesAgentClientOptions` members: |
| 151 | + |
| 152 | +| Member | Type | Required | Purpose | |
| 153 | +| --- | --- | --- | --- | |
| 154 | +| `BaseUrl` | `string?` | Yes | Absolute http(s) URL of the Hermes Agent API server (e.g. `http://localhost:8642`). A trailing `/` is appended automatically so relative paths compose. | |
| 155 | +| `ApiKey` | `string?` | Yes | The static server key, sent as `Authorization: Bearer {ApiKey}` on every request. | |
| 156 | + |
| 157 | +Options are validated at startup: a missing or malformed `BaseUrl`, or a missing `ApiKey`, stops the |
| 158 | +host with a clear message. The full options reference lives on the |
| 159 | +[client library page](../../libraries/hermes-agent-client.md#configuration). |
| 160 | + |
| 161 | +`HermesAgentClientSparkSettings` members: |
| 162 | + |
| 163 | +| Member | Type | Default | Purpose | |
| 164 | +| --- | --- | --- | --- | |
| 165 | +| `HealthChecks.Enabled` | `bool` | `true` | Registers the live Hermes Agent health check. | |
| 166 | +| `HealthChecks.Timeout` | `TimeSpan?` | none | Timeout applied to the health check. | |
| 167 | +| `HealthChecks.FailureStatus` | `HealthStatus?` | `Unhealthy` | Reported status when the check fails. | |
| 168 | +| `HealthChecks.Tags` | `string[]` | `[]` | Extra tags added alongside the built-in `HermesAgent` tag. | |
| 169 | +| `Tracing.Enabled` | `bool` | `true` | Adds the `ES.FX.NousResearch.HermesAgent` tracing source. | |
| 170 | + |
| 171 | +> [!NOTE] |
| 172 | +> The Hermes Agent Spark exposes no `Metrics` setting — HTTP-level metrics come from .NET's built-in |
| 173 | +> `http.client.*` instruments, which Ignite's OpenTelemetry pipeline already collects. |
| 174 | +
|
| 175 | +### Configure via appsettings |
| 176 | + |
| 177 | +The server address and API key sit at the section root; the observability toggles nest under a |
| 178 | +`Settings` sub-section: |
| 179 | + |
| 180 | +```json |
| 181 | +{ |
| 182 | + "Ignite": { |
| 183 | + "NousResearch": { |
| 184 | + "HermesAgent": { |
| 185 | + "BaseUrl": "http://localhost:8642", |
| 186 | + "Settings": { |
| 187 | + "HealthChecks": { "Enabled": true }, |
| 188 | + "Tracing": { "Enabled": true } |
| 189 | + } |
| 190 | + } |
| 191 | + } |
| 192 | + } |
| 193 | +} |
| 194 | +``` |
| 195 | + |
| 196 | +> [!WARNING] |
| 197 | +> Do not commit `ApiKey` to source control. Supply it via user secrets or an environment variable — |
| 198 | +> `Ignite__NousResearch__HermesAgent__ApiKey` — or a secret store such as the |
| 199 | +> [Azure Key Vault Secrets Spark](./azure-keyvault-secrets.md). For keyed instances the sub-section |
| 200 | +> joins the path: `Ignite__NousResearch__HermesAgent__research__ApiKey`. |
| 201 | +
|
| 202 | +### Configure with delegates |
| 203 | + |
| 204 | +```csharp |
| 205 | +builder.IgniteHermesAgentClient( |
| 206 | + configureSettings: settings => |
| 207 | + { |
| 208 | + settings.HealthChecks.Timeout = TimeSpan.FromSeconds(5); |
| 209 | + settings.Tracing.Enabled = true; |
| 210 | + }, |
| 211 | + configureOptions: options => |
| 212 | + { |
| 213 | + options.BaseUrl = "http://localhost:8642"; |
| 214 | + }); |
| 215 | +``` |
| 216 | + |
| 217 | +### Configuration section path |
| 218 | + |
| 219 | +The final `configurationSectionPath` parameter overrides the root section. It defaults to |
| 220 | +`HermesAgentClientSpark.ConfigurationSectionPath` (`"Ignite:NousResearch:HermesAgent"`). Most apps |
| 221 | +never change it. |
| 222 | + |
| 223 | +## Health checks |
| 224 | + |
| 225 | +The Spark registers a health check named **`HermesAgentClient`** by default (`HealthChecks.Enabled` is |
| 226 | +`true`). For a keyed registration the name carries the key suffix — e.g. `HermesAgentClient[research]`. |
| 227 | +The check is **live**: it calls `GET /v1/capabilities`, an authenticated endpoint, which verifies DNS, |
| 228 | +TLS, server reachability, and the configured API key in one probe. A healthy result reports the |
| 229 | +server's platform and model — nothing sensitive, since health output can surface on an unauthenticated |
| 230 | +`/health` endpoint. |
| 231 | + |
| 232 | +The check is tagged `HermesAgent`, plus any tags you add via `HealthChecks.Tags`. It surfaces at the |
| 233 | +health endpoints mapped by `app.Ignite()`. |
| 234 | + |
| 235 | +> [!NOTE] |
| 236 | +> Each probe spends one real, authenticated Hermes Agent API request. With aggressive probe intervals, |
| 237 | +> budget accordingly or point only the readiness probe at it. |
| 238 | +
|
| 239 | +Disable it via configuration: |
| 240 | + |
| 241 | +```json |
| 242 | +{ |
| 243 | + "Ignite": { |
| 244 | + "NousResearch": { |
| 245 | + "HermesAgent": { |
| 246 | + "Settings": { |
| 247 | + "HealthChecks": { "Enabled": false } |
| 248 | + } |
| 249 | + } |
| 250 | + } |
| 251 | + } |
| 252 | +} |
| 253 | +``` |
| 254 | + |
| 255 | +Or via the delegate: |
| 256 | + |
| 257 | +```csharp |
| 258 | +builder.IgniteHermesAgentClient(configureSettings: settings => |
| 259 | + settings.HealthChecks.Enabled = false); |
| 260 | +``` |
| 261 | + |
| 262 | +## Observability |
| 263 | + |
| 264 | +### Tracing |
| 265 | + |
| 266 | +Tracing is enabled by default (`Tracing.Enabled` is `true`). The Spark adds the client's |
| 267 | +`ES.FX.NousResearch.HermesAgent` `ActivitySource` to the Ignite OpenTelemetry pipeline, so every |
| 268 | +Hermes Agent operation appears as a `HermesAgent.{Area}.{Operation}` client span — with the underlying |
| 269 | +HTTP span from Ignite's `HttpClient` instrumentation as its child. |
| 270 | + |
| 271 | +Disable it via configuration (`Ignite:NousResearch:HermesAgent:Settings:Tracing:Enabled = false`) or |
| 272 | +the `configureSettings` delegate. |
| 273 | + |
| 274 | +### Resilience |
| 275 | + |
| 276 | +Ignite applies the standard resilience handler to **every** `HttpClient` by default |
| 277 | +(`Ignite:Settings:HttpClient:StandardResilienceHandlerEnabled`), including this one — transient failures and |
| 278 | +`429 Too Many Requests` are retried honoring `Retry-After`, with no Spark-specific wiring. When |
| 279 | +retries are exhausted, the guard still throws a `HermesAgentApiException` carrying the final status |
| 280 | +code. |
| 281 | + |
| 282 | +> [!NOTE] |
| 283 | +> The standard handler's total-request timeout also governs long-lived **streaming** calls |
| 284 | +> (`Chat.StreamAsync`, run event streams, session chat streams). If your agent runs stream for longer |
| 285 | +> than that budget, tune or disable the handler for this client via the returned `IHttpClientBuilder`. |
| 286 | +
|
| 287 | +### Logging |
| 288 | + |
| 289 | +The client logs through the app's configured logging pipeline — including |
| 290 | +[Serilog](./serilog.md) when you enable it — with no extra wiring: `Debug` on success, `Warning` with |
| 291 | +the status code on failure. Response bodies are never logged. |
| 292 | + |
| 293 | +## See also |
| 294 | + |
| 295 | +- [Hermes Agent API client](../../libraries/hermes-agent-client.md) — the client surface, streaming, and error handling. |
| 296 | +- [Ignite overview](../index.md) |
| 297 | +- [Sparks catalog](./index.md) |
| 298 | +- [Azure Key Vault Secrets Spark](./azure-keyvault-secrets.md) — for the API key. |
0 commit comments