Hermes Agent Integration
The Hermes integration turns a self-hosted Hermes Agent (Nous Research’s autonomous AI agent) into a first-class DonutChat bot that behaves like a Slack-thread bot. A user @mentions the bot (or DMs it), and Hermes answers in a thread under that message — the channel stays clean, the answer streams in live, and follow-up questions inside the thread continue the same conversation.
A small standalone service, hermes-bridge, does the wiring: it consumes the DonutChat bot event stream, forwards each triggering message to Hermes’s OpenAI-compatible API server using one session per thread, and streams the reply back through the bot thread-reply API.
At a glance
Section titled “At a glance”| What you run | A Hermes Agent box + the hermes-bridge service |
| Where it answers | In a thread under the triggering message (Slack-style) |
| Bot transport | DonutChat bot WebSocket stream in, bot thread-reply API out |
| Agent transport | OpenAI-compatible POST /v1/chat/completions with stream: true |
| Session model | One Hermes session per thread (X-Hermes-Session-Id = donutchat:<chat_id>:<parent_message_id>) |
| Replies | Stream progressively (GN → OK) so the thread fills in as the agent thinks |
| Coupling | None — the bridge talks to the same public bot APIs any third-party bot uses |
| Backend | Any OpenAI-compatible endpoint, not just Hermes |
Architecture
Section titled “Architecture”flowchart LR U["User @mentions the bot"] subgraph Backend["DonutChat backend"] ED["event_dispatcher (message.new / thread.reply.new)"] BH["bot_hub WebSocket fan-out"] TR["bot thread-reply API"] TH["ThreadHandler: ThreadReply + streaming"] TP["ThreadPresence viewer gating"] end subgraph Bridge["hermes-bridge"] W["per-thread worker (serial per thread, parallel across threads)"] CB["circuit breaker"] end API["Hermes OpenAI-compatible API :8642"] U -->|posts message| ED ED --> BH BH -->|message.new / thread.reply.new| W W -->|"POST /v1/chat/completions (stream, session donutchat:chat:parent)"| API API -.->|SSE deltas| W W -->|"create GN reply, stream deltas, finalize OK"| TR TR --> TH TH --> TP TP -->|ThreadReplyContentEvent| U CB -.->|guards| W
- A user posts a message that triggers the bot (an
@mention, or any message when the bot’s trigger mode isall). - DonutChat pushes a
message.newevent (or, for a reply inside an existing thread, athread.reply.newevent) over the bot WebSocket stream. - The bridge routes it to a per-thread worker — events in one thread are processed in order, while different threads run in parallel.
- The worker opens a streaming Hermes completion with the thread’s session id, then creates a “generating” placeholder reply in the thread so users see Hermes is working.
- As Hermes streams tokens, the bridge pushes them to the thread (
status: GN); when the agent finishes it finalizes the reply (status: OK). Clients viewing the thread see the answer type out viaThreadReplyContentEvent— the same streaming path AI Direct Answers already use.
Conversation flow
Section titled “Conversation flow”sequenceDiagram actor User participant DC as DonutChat backend participant BR as hermes-bridge participant HM as Hermes Agent User->>DC: @hermes what is the weather? DC-->>BR: message.new (message_id=100, mentions_bot) BR->>HM: POST /v1/chat/completions stream, session donutchat:chat:100 BR->>DC: create thread reply under 100 (status GN) DC-->>User: thread reply appears (Hermes is thinking) loop streaming HM-->>BR: SSE token delta BR->>DC: stream content (status GN) DC-->>User: ThreadReplyContentEvent (live text) end HM-->>BR: DONE BR->>DC: finalize reply (status OK) User->>DC: in-thread follow-up DC-->>BR: thread.reply.new (parent=100) BR->>HM: same session donutchat:chat:100 Note over BR,HM: thread context preserved
Why threads, and why a bridge
Section titled “Why threads, and why a bridge”Threads keep an AI agent from flooding the channel: the answer (which can be long, and streams over many seconds for tool use) lives under the triggering message, and a back-and-forth with the agent stays in one place — exactly how Slack scopes app conversations. Each thread maps to an independent Hermes session, so two parallel questions never cross context.
| Option | Trade-off |
|---|---|
| Bridge service (chosen) | Works with today’s public bot APIs, zero coupling, reusable for any OpenAI-compatible backend. Cost: one more deployable. |
| Native Hermes gateway adapter | First-class (Hermes session store, cron, full toolset) but requires an upstream contribution and ongoing maintenance in a foreign repo. A possible future addition. |
| Hermes webhook toolset | Rejected — its webhooks grant Hermes full tool access (including terminal) per request, the wrong trust model for untrusted chat input. |
What’s supported
Section titled “What’s supported”| Capability | Status |
|---|---|
@mention / trigger-mode replies, answered in a thread | ✅ |
Streaming replies (progressive GN → OK) | ✅ |
| Follow-up turns inside the thread (per-thread Hermes session) | ✅ |
| 1:1 DM with a bot | ✅ |
| BOT badge + markdown rendering on thread replies | ✅ |
| Graceful “agent unavailable” fallback when Hermes is down | ✅ (circuit breaker) |
| Bot directory, interactive components | ⏳ future |
Next steps
Section titled “Next steps”- Deployment — create the bot, run Hermes, and deploy the bridge.
- Configuration — every
hermes-bridgeenvironment variable. - Operations & troubleshooting — circuit breaker, presence, limits, common failures.