Hermes collaboration: make the bots talk to each other¶
TL;DR: A team groups several single instances into one shared channel. Collaboration is the next step: letting those agents hand the conversation to each other by
@mention- and, crucially, stopping them from ping-ponging forever. Hermes has no built-in bot-to-bot turn limiter, so the brake is a prompt contract plus four Discord env knobs.
This page is the concrete recipe behind the "honest status" note in
Hermes teams: direct agent-to-agent awareness
is still evolving upstream. A @mention handoff between two role-scoped
agents in a shared channel has been observed working with the safeguards
below, but upstream documents bot-to-bot conversation as an unsupported
topology with no built-in circuit breaker. Treat this as an experimental
recipe, keep a manual stop path, and validate your exact image/platform
configuration. Below is the two-agent pair used for that test.
What a collaborating pair looks like¶
Two independent single instances (each its own release, bot token, PVC, and
identity - see Hermes teams) share one Discord channel. They differ in
role (injected via config.agent.environment_hint) and may even differ in
model backend - one can run on a LiteLLM proxy while the other uses a
Copilot device-flow login. The shared channel is the context bus; an explicit
@mention in the message body is the handoff signal.
graph TB
Human((Human)) -->|"@mentions either bot"| Channel
subgraph Channel["#team - one shared Discord channel (context bus)"]
direction LR
Planner["planner<br/>role: scope & plan<br/>backend: LiteLLM proxy"]
Builder["builder<br/>role: implement<br/>backend: Copilot device-flow"]
Planner -. "<@builder> in message body<br/>(handoff)" .-> Builder
Builder -. "<@planner> in message body<br/>(handoff back)" .-> Planner
end
Planner -->|OpenAI-compatible| LiteLLM["LiteLLM proxy<br/>→ NIM / OpenAI / Ollama"]
Builder -->|"gho_ token (minted at startup)"| Copilot["GitHub Copilot API"]
classDef agent fill:#3b82f6,color:#fff,stroke:#1d4ed8;
classDef ext fill:#10b981,color:#fff,stroke:#047857;
class Planner,Builder agent;
class LiteLLM,Copilot ext;
Why a pair, not one bot with two personalities: each instance keeps its own
HERMES_HOME, memory, and config - so roles stay cleanly separated and you can
scale the roster (add a reviewer, a researcher) without entangling identities.
This is the same "scale up, then group" rule from Hermes teams, extended
with an explicit handoff protocol.
The handoff protocol (@mention)¶
Each agent is told who its partner is and how to hand over through the
system prompt, using config.agent.environment_hint. The partner is addressed by
its Discord user ID, placed as an explicit <@ID> mention in the message
body (not as a reply-reference - see the loop brake below).
config:
agent:
environment_hint: |
You are "planner", one of two collaborating Hermes agents in this Discord
channel. Your partner is "builder", Discord user ID <BUILDER_BOT_USER_ID>.
To hand the conversation to builder, put an explicit
<@BUILDER_BOT_USER_ID> mention in the BODY of your message. Only mention
builder when you have something substantive to say or genuinely need their
input. When a topic reaches a natural conclusion, do NOT mention builder -
address the human instead and end your turn, so the exchange stops. Never
send a filler or "let me know if you need anything" message that mentions
builder; that only restarts the loop.
The last three sentences are the prompt half of the loop brake. They are not optional politeness - they are what makes a finite conversation finite.
For the visible thread to be a real context bus, every agent also needs one transcript across human and bot senders. Hermes defaults to a separate session per sender, so set this explicitly and retain Discord history backfill:
config:
group_sessions_per_user: false
discord:
history_backfill: true
history_backfill_limit: 50
Without group_sessions_per_user: false, a bot may receive the partner's
mention in a session that does not contain the human's earlier turn. Backfill
recovers visible thread messages missed while the bot was not mentioned; it
does not introduce a hidden file-based coordination channel.
Find a bot's user ID by enabling Developer Mode in Discord and right-clicking the bot → Copy User ID. Each agent's hint references the other agent's ID.
Where the IDs live: declarative hint vs. runtime memory¶
An agent learns "who to mention" through two layers, and a real deployment
uses both. This is observable on a running pair - each agent persists its state
under HERMES_HOME (here /opt/data) on its PVC:
# the declarative layer: injected from the chart, re-seeded every deploy
kubectl exec -n hermes-july deploy/hermes-july -- \
sh -c 'sed -n "/agent:/,/gateway_timeout/p" $HERMES_HOME/config.yaml'
# the runtime layer: what the agent learned from conversation, persisted
kubectl exec -n hermes-july deploy/hermes-july -- \
sh -c 'cat $HERMES_HOME/memories/USER.md'
Layer 1: declarative environment_hint (initial prompt)¶
Set in the chart values (config.agent.environment_hint), rendered into
HERMES_HOME/config.yaml, and injected into every session's system prompt.
This is where the partner bot's ID belongs: it is deterministic and
reproducible - a redeploy always restores it, and a fresh PVC starts correct.
A running july shows its partner (june) wired in exactly as templated:
# $HERMES_HOME/config.yaml (rendered from values)
agent:
environment_hint: |
You are "july" ... Your partner agent is "june", whose Discord user ID is
<JUNE_BOT_USER_ID>. To hand the conversation to june, put an explicit
<@JUNE_BOT_USER_ID> mention in the BODY of your message ...
Layer 2: runtime memory (learned in conversation)¶
Hermes' memory tool writes durable facts it learns mid-conversation to
HERMES_HOME/memories/ (e.g. USER.md), which lives on the PVC and survives
restarts. This is where IDs you teach it at runtime land - typically the
human's ID, or anything discovered after deploy. On the live pair, july
had learned, with no chart change, both the collaboration intent and the human's
mention ID:
# $HERMES_HOME/memories/USER.md (learned, not templated)
User is collaborating with Hermes Agent (june and july) to experiment with a
multi-bot setup on Discord. They have interest in bot-to-bot interactions, task
delegation, ...
§
User '<name>' has the Discord user ID '<YOUR_USER_ID>' for mentions.
Teach an ID in real time (Discord)¶
To seed an ID conversationally instead of (or in addition to) the chart, just tell the agent in the channel and ask it to remember:
You: @july remember this: my Discord user ID is <@YOUR_USER_ID> - mention me
with it when you need a human. Also, your partner june is <@JUNE_BOT_USER_ID>.
july: Got it - saved. I'll @mention you for human input and hand off to june
with <@JUNE_BOT_USER_ID>.
The agent's memory tool persists that to memories/, so it holds across
restarts. Verify it landed with the kubectl exec … cat …/memories/USER.md
command above.
Which layer for which ID?¶
| Fact | Put it in | Why |
|---|---|---|
| Partner bot's user ID | environment_hint (Layer 1) |
Deterministic; must be correct on first boot and after every redeploy: not left to chance discovery. |
| The human's user ID | runtime memory (Layer 2) | Learned naturally in chat; varies per user; no redeploy to add a person. |
| Evolving team context (roles, focus, conventions) | runtime memory, or SOUL.md for a fixed identity |
Grows with the conversation; templating every change is churn. |
| Anything that must survive a fresh PVC | environment_hint / SOUL.md seeding |
Runtime memory is lost if the volume is wiped; the chart re-seeds declaratively. |
Rule of thumb: the handoff wiring (partner bot IDs + loop brake) is infrastructure - keep it declarative so a redeploy is faithful. Who the humans are and what the team is working on is conversation - let memory capture it. The reference pair does exactly this split.
The loop brake (why, and the four knobs)¶
Hermes has no built-in bot-to-bot turn limiter. Two agents that can see and
@mention each other will, by default, ping-pong forever - each reply re-triggers
the partner. The prompt contract above asks them to stop; these four Discord env
vars make "stop" actually possible by ensuring a partner fires only on an
explicit <@id> in the message body, nothing else.
| Env var | Value | Why it matters |
|---|---|---|
DISCORD_ALLOW_BOTS |
mentions |
Respond to another bot only when it @mentions us. off would ignore the partner entirely; all would react to every bot message. This is the knob that enables collaboration at all. |
DISCORD_THREAD_REQUIRE_MENTION |
true |
In a thread both bots already belong to, fire only when mentioned: otherwise every bot reacts to every message in the thread. |
DISCORD_REPLY_TO_MODE |
off |
Don't attach a reply-reference to outgoing messages. Discord counts a reply-reference as a mention (replied_user), so a bot replying to its partner auto-pings it: satisfying ALLOW_BOTS=mentions even with no <@id> in the body. This is the subtle infinite-loop source. |
DISCORD_ALLOW_MENTION_REPLIED_USER |
false |
Belt-and-suspenders for the above: never treat the auto reply-ping as a real mention. |
Why both
REPLY_TO_MODEandALLOW_MENTION_REPLIED_USER? The first stops sending the auto-ping; the second stops acting on one if it arrives by any other path. With both off, the only thing that wakes a partner is a deliberate<@id>in the body - so the "stop mentioning when done" instruction in the prompt is the real, enforceable end condition.
These are real environment variables read directly by the Discord adapter
(os.getenv). Unlike require_mention / allowed_channels, they are not
bridged from the config.yaml discord: block - so they must be set as env /
extraEnv, not under config.
How a turn ends¶
sequenceDiagram
participant H as Human
participant P as planner
participant B as builder
H->>P: "@planner design the auth flow"
activate P
P->>B: posts plan with <@builder> in body
deactivate P
activate B
Note over B: ALLOW_BOTS=mentions →<br/>fires only on explicit body mention
B->>P: posts implementation with <@planner> in body
deactivate B
activate P
P-->>H: topic resolved → addresses HUMAN,<br/>no <@builder> mention
deactivate P
Note over P,B: No mention → partner does NOT fire.<br/>REPLY_TO_MODE=off means the<br/>message carries no auto-ping. Loop ends.
Beyond Discord: Telegram and Slack¶
Everything above uses Discord, the only platform with a live-proven
collaborating pair (and the only one behind the leader-team live evidence in
Hermes teams). The same @mention handoff and the same
four-knob loop brake exist for Telegram and Slack too - Hermes routes bot
gating for all three through one shared authorization path
({PLATFORM}_ALLOW_BOTS), and each platform adapter has its own equivalent of
"don't let a reply silently re-trigger the partner." The sections below give
the verified equivalent knobs and a worked example for each. Neither has been
run through a live multi-bot proof the way the Discord recipe has - treat them
as a well-grounded starting recipe, not a guarantee, and validate in a channel
you can watch before trusting it unattended.
Knob mapping¶
| Purpose | Discord | Telegram | Slack |
|---|---|---|---|
| Only respond to another bot on an explicit mention | DISCORD_ALLOW_BOTS=mentions |
TELEGRAM_ALLOW_BOTS=mentions |
SLACK_ALLOW_BOTS=mentions |
| Require an explicit mention at all (vs. free-response) | DISCORD_REQUIRE_MENTION=true |
TELEGRAM_REQUIRE_MENTION=true |
SLACK_REQUIRE_MENTION=true |
| Stop a thread/topic from auto-triggering once mentioned, without a fresh mention | DISCORD_THREAD_REQUIRE_MENTION=true |
not needed: Telegram groups gate every message on TELEGRAM_REQUIRE_MENTION; there is no separate "sticky thread" memory to close |
SLACK_STRICT_MENTION=true: off by default; Slack otherwise remembers a mentioned thread and keeps auto-responding in it |
| Stop sending a native reply-reference that would silently mention the partner back | DISCORD_REPLY_TO_MODE=off |
TELEGRAM_REPLY_TO_MODE=off |
not applicable: Slack has no separate reply-reference mention; SLACK_STRICT_MENTION=true already closes the "reply in a bot's thread" auto-trigger |
| Never treat an inbound reply-reference as an implicit mention | DISCORD_ALLOW_MENTION_REPLIED_USER=false |
covered by TELEGRAM_REPLY_TO_MODE=off on the sending side: there is no separate inbound flag |
covered by SLACK_STRICT_MENTION=true |
Telegram and Slack each fold what takes four knobs on Discord into three (Telegram) or two (Slack) - the platforms have less surface area for an implicit mention to sneak in, not less protection.
Telegram¶
Telegram mentions a bot by its @username (bots must have one, and it must
end in bot, e.g. @hermes_builder_bot) - there is no numeric <@id> token
to look up. Put the exact @username in environment_hint, matching the
Discord pattern:
config:
agent:
environment_hint: |
You are "planner", one of two collaborating Hermes agents in this
Telegram group. Your partner is "builder", Telegram username
@hermes_builder_bot. To hand the conversation to builder, put an
explicit @hermes_builder_bot mention in the BODY of your message -
never rely on Telegram's native "reply" feature to address them, since
a reply does not carry the same explicit-mention guarantee this recipe
depends on. When a topic reaches a natural conclusion, do NOT mention
builder - address the human instead and end your turn.
group_sessions_per_user: false
extraEnv:
- name: TELEGRAM_HOME_CHANNEL # shared group chat id
value: "<shared-chat-id>"
- name: TELEGRAM_ALLOWED_USERS
value: "<comma-separated-human-ids>"
- name: TELEGRAM_ALLOW_BOTS
value: "mentions"
- name: TELEGRAM_REQUIRE_MENTION
value: "true"
- name: TELEGRAM_REPLY_TO_MODE
value: "off"
TELEGRAM_EXCLUSIVE_BOT_MENTIONS defaults to true and is worth leaving
alone: when a message explicitly @mentions one bot username ending in
bot, every other bot in the group ignores that message outright, even if
it's also present. That is an extra layer this recipe gets for free on
Telegram with no Discord equivalent.
Slack¶
Slack mentions use the exact same <@USER_ID> markup as Discord - copy the
Discord environment_hint pattern verbatim, just swap in the partner's Slack
member ID (find it via View profile → More → Copy member ID):
config:
agent:
environment_hint: |
You are "planner", one of two collaborating Hermes agents in this
Slack channel. Your partner is "builder", Slack user <@U0BUILDERID>.
To hand the conversation to builder, put an explicit <@U0BUILDERID>
mention in the BODY of your message. When a topic reaches a natural
conclusion, do NOT mention builder - address the human instead and end
your turn.
group_sessions_per_user: false
extraEnv:
- name: SLACK_HOME_CHANNEL # shared channel id
value: "<shared-channel-id>"
- name: SLACK_ALLOWED_USERS
value: "<comma-separated-human-ids>"
- name: SLACK_ALLOW_BOTS
value: "mentions"
- name: SLACK_REQUIRE_MENTION
value: "true"
- name: SLACK_STRICT_MENTION
value: "true"
SLACK_STRICT_MENTION is the one knob to get right: Slack's default behavior
remembers a thread once a bot is @mentioned in it and keeps that bot
listening for the rest of the thread with no further mention required - the
same "sticky thread" shape as Discord, and just as capable of quietly turning
into a bot-to-bot loop if the partner is also in that thread. Setting it to
true forces a fresh <@id> on every single turn, which is what this
recipe's "stop when done" instruction actually depends on to work.
Mixed backends in one pair¶
Collaborating agents need not share a model backend - the channel is the only thing they share. The reference pair runs asymmetrically:
| Agent | Provider | Auth | Notes |
|---|---|---|---|
planner |
litellm |
proxy key (OPENAI_API_KEY, sealed) |
talks to a shared LiteLLM proxy as an OpenAI-compatible custom provider; see values-litellm.yaml |
builder |
copilot |
device-flow (no token sealed) | mints a gho_ token at startup via auth.deviceFlow; see values-github-copilot.yaml |
This lets you put a cheap/local model on the high-traffic role and a premium model on the role that needs it, without changing the collaboration wiring.
Configure a multi-agent team comfortably¶
You have a single shared knob set and a per-agent knob set. Keep them separate and the roster stays easy to edit.
Shared by every agent in the team (identical values):
- DISCORD_HOME_CHANNEL: the one shared channel id (the context bus)
- DISCORD_ALLOWED_USERS: who may talk to the team
- the four loop-brake knobs above
- config.group_sessions_per_user: false and Discord history backfill - one
visible thread transcript across human and bot senders
Per agent (unique):
- releaseName / metadata.name: must be unique (the
one rule)
- DISCORD_BOT_TOKEN: one bot per agent
- config.agent.environment_hint: role + the partner's user ID
- model backend + its auth
Option A: one values file per agent (start here)¶
Copy values-multi-agent-collab.yaml
once per agent, change the role/partner-id and bot token, and install side by side:
# planner
helm upgrade --install hermes-planner ./charts/hermes-agent \
--namespace hermes-team --create-namespace \
-f charts/hermes-agent/values-multi-agent-collab.yaml \
--set-string env.DISCORD_BOT_TOKEN='<planner-bot-token>' --wait
# builder: same channel, different bot, partner id swapped in its own file
helm upgrade --install hermes-builder ./charts/hermes-agent \
--namespace hermes-team --create-namespace \
-f charts/hermes-agent/values-builder.yaml \
--set-string env.DISCORD_BOT_TOKEN='<builder-bot-token>' --wait
Option B: ArgoCD ApplicationSet (recommended for 3+)¶
Promote the shared knobs into the template and keep only the per-agent
fields in the generator list - adding a teammate becomes a one-line diff. This
extends the team ApplicationSet in Hermes teams
with the collaboration knobs:
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: hermes-collab-team
namespace: argocd
spec:
generators:
- list:
elements:
- name: planner
partnerId: "<BUILDER_BOT_USER_ID>"
role: "scope and plan the work"
botSecret: hermes-planner-discord-secrets
- name: builder
partnerId: "<PLANNER_BOT_USER_ID>"
role: "implement what planner scopes"
botSecret: hermes-builder-discord-secrets
# add a teammate = add a list entry (and give it a partner to mention)
template:
metadata:
name: 'hermes-{{name}}'
spec:
project: default
source:
repoURL: ghcr.io/jyje/hermes-agent-helm
chart: hermes-agent
targetRevision: '*' # pin to a released version in practice
helm:
releaseName: 'hermes-{{name}}'
valuesObject:
fullnameOverride: 'hermes-{{name}}'
config:
group_sessions_per_user: false
discord:
history_backfill: true
history_backfill_limit: 50
agent:
environment_hint: |
You are "{{name}}". Your job is to {{role}}. Your partner is
Discord user <@{{partnerId}}>. Hand over by putting an explicit
<@{{partnerId}}> mention in the BODY of your message. When the
topic is done, address the human and do NOT mention your
partner, so the exchange stops.
extraEnvFrom:
- secretRef:
name: '{{botSecret}}' # per-agent bot token
extraEnv: # shared across the whole team
- { name: DISCORD_HOME_CHANNEL, value: "<shared-channel-id>" }
- { name: DISCORD_ALLOWED_USERS, value: "<comma-separated-ids>" }
- { name: DISCORD_ALLOW_BOTS, value: "mentions" }
- { name: DISCORD_THREAD_REQUIRE_MENTION, value: "true" }
- { name: DISCORD_REPLY_TO_MODE, value: "off" }
- { name: DISCORD_ALLOW_MENTION_REPLIED_USER, value: "false" }
destination:
server: https://kubernetes.default.svc
namespace: hermes-team
syncPolicy:
syncOptions: [CreateNamespace=true]
A hand-written two-Application version (no ApplicationSet) is in
examples/argocd/hermes-collab-pair.yaml
if you'd rather see both releases spelled out.
Checklist¶
- [ ] One bot per agent, all invited to the same channel, Message Content Intent on.
- [ ] Every agent shares
DISCORD_HOME_CHANNELandDISCORD_ALLOWED_USERS. - [ ] All four loop-brake knobs set on every agent (
env/extraEnv, notconfig). - [ ]
group_sessions_per_user: falseand Discord history backfill set on every agent so the thread, rather than sender-isolated sessions, carries context. - [ ] Each
environment_hintnames the partner's user ID and includes the "stop when done" instruction. - [ ] Unique
releaseName==metadata.nameper agent. - [ ] Mixed backends are fine: only the channel must match.
See also¶
- Setting up a team: a friendlier, from-scratch walkthrough if this is your first team; start there.
- teams.md: group single instances into a team (the prerequisite to this page).
- roadmap.md: the ApplicationSet team pattern and the
hermes-operatorcandidacy. values-multi-agent-collab.yaml·examples/argocd/hermes-collab-pair.yaml- Hermes Messaging gateway docs.