| name | hermes-agentmesh-async-bus |
| description | Build cross-device async multi-agent systems using Redis message bus with 0-SSH deployment |
| triggers | ["set up async message bus for multi-agent systems","deploy Hermes AgentMesh across devices","configure Redis-backed agent communication","fix agent timeout issues with async messaging","run cross-device agent collaboration without SSH","build peer-to-peer agent mesh network","orchestrate multi-agent debate asynchronously","troubleshoot AgentMesh worker deployment"] |
Hermes AgentMesh Async Bus
Skill by ara.so — Hermes Skills collection.
What It Does
Hermes-AgentMesh is a Redis-backed async message bus that solves HTTP timeout crashes for long-running multi-agent tasks. Instead of synchronous requests.post() calls that timeout after 5-10 minutes, agents communicate via Redis queues ("mailboxes") that persist tasks indefinitely.
Key capabilities:
- 0-SSH cross-device deployment — workers run as systemd/LaunchAgent daemons
- Timeout elimination — 15-minute+ agent tasks work reliably
- Framework agnostic — works with Hermes, OpenClaw, LangGraph, AutoGen, CrewAI, custom agents
- Natural persistence — tasks survive server restarts
- Decoupled architecture — sender doesn't block waiting for receiver
Architecture:
Mac mini (central hub) Remote nodes (X230i, Pi, WSL)
├── Redis :6379 (0.0.0.0) └── worker_node.py (systemd)
├── Hermes LLM API :8642 └── reads inbox:nodename
├── HTTP file server :8080 └── writes outbox:orchestrator
└── worker_macmini (LaunchAgent)
Installation
Prerequisites
- 1 central machine (Mac mini recommended) running Redis + Hermes Gateway
- 1+ remote nodes (Linux/Mac/WSL) on same LAN
- Python 3.8+ with
redis, requests, python-dotenv
Step 1: Mac Mini (Central Hub)
sudo sed -i.bak \
-e 's/^bind 127.0.0.1 ::1$/bind 0.0.0.0/' \
-e 's/^protected-mode yes$/protected-mode no/' \
/opt/homebrew/etc/redis.conf
brew services restart redis
redis-cli ping
pip3 install redis requests python-dotenv
git clone https://github.com/seleman66eeddwegger3-art/hermes-agentmesh.git
cd hermes-agentmesh
mkdir -p ~/.hermes/async_bus
cp .env.example ~/.hermes/async_bus/.env_common
nano ~/.hermes/async_bus/.env_common
Edit .env_common:
REDIS_HOST=192.168.1.100
REDIS_PORT=6379
REDIS_DB=0
REDIS_PROTOCOL=2
API_URL=http://192.168.1.100:8642/chat
API_KEY=your_hermes_api_key_from_~/.hermes/.env
NODE_NAME=macmini
cp worker_node.py ~/.hermes/async_bus/
cp orchestrator_async.py ~/.hermes/async_bus/
cp deploy/macos-launchd-worker.plist ~/Library/LaunchAgents/ai.hermes.async_bus_worker.plist
sed -i '' 's/<NODE_NAME>/macmini/g' ~/Library/LaunchAgents/ai.hermes.async_bus_worker.plist
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.hermes.async_bus_worker.plist
launchctl kickstart gui/$UID/ai.hermes.async_bus_worker
launchctl print gui/$UID/ai.hermes.async_bus_worker | grep state
cp deploy/macos-launchd-serve.plist ~/Library/LaunchAgents/ai.hermes.async_bus_serve.plist
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.hermes.async_bus_serve.plist
Step 2: Remote Nodes (Linux/WSL)
mkdir -p ~/.hermes/async_bus ~/.config/systemd/user
MAC_MINI_IP=192.168.1.100
curl -s http://$MAC_MINI_IP:8080/worker_node.py -o ~/.hermes/async_bus/worker_node.py
curl -s http://$MAC_MINI_IP:8080/orchestrator_async.py -o ~/.hermes/async_bus/orchestrator_async.py
curl -s http://$MAC_MINI_IP:8080/.env_common -o ~/.hermes/async_bus/.env_common
echo "NODE_NAME=node99" >> ~/.hermes/async_bus/.env_common
curl -s http://$MAC_MINI_IP:8080/deploy/linux-systemd-worker.service \
-o ~/.config/systemd/user/async_bus_worker.service
sed -i 's/<NODE_NAME>/node99/g' ~/.config/systemd/user/async_bus_worker.service
systemctl --user daemon-reload
systemctl --user enable --now async_bus_worker
systemctl --user status async_bus_worker
journalctl --user -u async_bus_worker -f
Core Concepts
Message Bus Protocol
4-step async protocol:
- Task push — Orchestrator pushes to
inbox:<NODE_NAME>
- Worker poll — Node's worker daemon blocks on
BRPOP inbox:<NODE_NAME>
- LLM execution — Worker calls Hermes API with task payload
- Result return — Worker pushes to
outbox:orchestrator
Task JSON format:
{
"turn": 1,
"node": "node99",
"messages": [
{"role": "system", "content": "You are agent 99 in a debate."},
{"role": "user", "content": "What's your position on topic X?"}
]
}
Mailbox Naming Convention
inbox:<NODE_NAME>
outbox:orchestrator
Key Usage Patterns
Pattern 1: Run Multi-Agent Debate
TOPIC = "Should AI agents use async messaging over HTTP?"
MAX_TURNS = 3
NODES = ["macmini", "node99", "nodepi"]
cd ~/.hermes/async_bus
source .env_common
python3 orchestrator_async.py
What happens:
- Orchestrator pushes tasks to
inbox:macmini, inbox:node99, inbox:nodepi
- Each node's worker daemon picks up task via
BRPOP
- Workers call Hermes LLM API (5-15 min per turn)
- Workers push responses to
outbox:orchestrator
- Orchestrator collects results, starts next turn
- Final report written to
async_debate_<timestamp>.md
Pattern 2: Custom Agent Integration
For non-Hermes frameworks (LangGraph, AutoGen, etc.):
import redis
import json
import os
r = redis.Redis(
host=os.getenv("REDIS_HOST"),
port=int(os.getenv("REDIS_PORT")),
db=int(os.getenv("REDIS_DB")),
protocol=2
)
def worker_loop(node_name):
inbox = f"inbox:{node_name}"
while True:
_, task_raw = r.brpop(inbox, timeout=0)
task = json.loads(task_raw)
result = your_agent_framework.run(
messages=task["messages"],
context=task.get("context", {})
)
response = {
"turn": task["turn"],
"node": node_name,
"response": result
}
r.lpush("outbox:orchestrator", json.dumps(response))
Pattern 3: Push Task from Orchestrator
import redis
import json
import os
r = redis.Redis(
host=os.getenv("REDIS_HOST"),
port=int(os.getenv("REDIS_PORT")),
db=int(os.getenv("REDIS_DB")),
protocol=2
)
def dispatch_task(node_name, turn, messages):
task = {
"turn": turn,
"node": node_name,
"messages": messages
}
inbox = f"inbox:{node_name}"
r.lpush(inbox, json.dumps(task))
print(f"✓ Pushed turn {turn} to {inbox}")
dispatch_task("node99", 1, [
{"role": "system", "content": "You are a code reviewer."},
{"role": "user", "content": "Review this function: def foo(): pass"}
])
Pattern 4: Collect Results
import redis
import json
import time
r = redis.Redis(host=os.getenv("REDIS_HOST"), protocol=2)
def collect_results(expected_count, timeout=600):
results = []
start = time.time()
while len(results) < expected_count:
remaining = timeout - (time.time() - start)
if remaining <= 0:
break
item = r.brpop("outbox:orchestrator", timeout=int(remaining))
if item:
_, result_raw = item
results.append(json.loads(result_raw))
return results
responses = collect_results(3, timeout=900)
for resp in responses:
print(f"{resp['node']}: {resp['response'][:100]}...")
Configuration
Environment Variables
Required in .env_common:
REDIS_HOST=192.168.1.100
REDIS_PORT=6379
REDIS_DB=0
REDIS_PROTOCOL=2
API_URL=http://192.168.1.100:8642/chat
API_KEY=${HERMES_API_KEY}
NODE_NAME=macmini
LaunchAgent Configuration (macOS)
~/Library/LaunchAgents/ai.hermes.async_bus_worker.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>ai.hermes.async_bus_worker</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>-c</string>
<string>source ~/.hermes/async_bus/.env_common && exec /usr/local/bin/python3 -u ~/.hermes/async_bus/worker_node.py</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/Users/youruser/.hermes/async_bus/worker_macmini.log</string>
<key>StandardErrorPath</key>
<string>/Users/youruser/.hermes/async_bus/worker_macmini.err</string>
</dict>
</plist>
Systemd Configuration (Linux)
~/.config/systemd/user/async_bus_worker.service:
[Unit]
Description=Hermes AgentMesh Worker (node99)
After=network.target
[Service]
Type=simple
WorkingDirectory=%h/.hermes/async_bus
EnvironmentFile=%h/.hermes/async_bus/.env_common
ExecStart=/usr/bin/python3 -u %h/.hermes/async_bus/worker_node.py
Restart=always
RestartSec=10
StandardOutput=append:%h/.hermes/async_bus/worker_node99.log
StandardError=append:%h/.hermes/async_bus/worker_node99.err
[Install]
WantedBy=default.target
Troubleshooting
Worker Not Starting
launchctl list | grep async_bus
launchctl print gui/$UID/ai.hermes.async_bus_worker
systemctl --user status async_bus_worker
journalctl --user -u async_bus_worker -n 50
launchctl bootout gui/$UID/ai.hermes.async_bus_worker
launchctl bootstrap gui/$UID ~/Library/LaunchAgents/ai.hermes.async_bus_worker.plist
Redis Connection Refused
redis-cli CONFIG GET bind
redis-cli -h 192.168.1.100 ping
sudo nano /opt/homebrew/etc/redis.conf
brew services restart redis
RESP3 Protocol Errors
Symptom: ResponseError: wrong number of arguments or connection hangs
Cause: redis-py 5.x defaults to RESP3, which has bugs with certain commands
Fix:
echo "REDIS_PROTOCOL=2" >> ~/.hermes/async_bus/.env_common
sed -i 's/protocol=3/protocol=2/g' ~/.hermes/async_bus/worker_node.py
Tasks Stuck in Queue
redis-cli LLEN inbox:macmini
redis-cli LLEN inbox:node99
redis-cli LLEN outbox:orchestrator
redis-cli LINDEX inbox:node99 0
redis-cli DEL inbox:node99
systemctl --user restart async_bus_worker
Long Task Timeout (HTTP API)
Not a bug — AgentMesh is designed for 15-min+ tasks. If Hermes API itself times out:
response = requests.post(
API_URL,
json=payload,
headers={"X-API-Key": API_KEY},
timeout=1800
)
Worker Stops After SSH Logout (Linux)
Cause: systemd user services stop when session ends
Fix: Enable lingering
sudo loginctl enable-linger $USER
loginctl show-user $USER | grep Linger
File Server 404 (Cross-Device Deploy)
launchctl list | grep serve
curl http://localhost:8080/worker_node.py
launchctl kickstart gui/$UID/ai.hermes.async_bus_serve
sudo /usr/libexec/ApplicationFirewall/socketfilterfw --getglobalstate
Advanced Patterns
Custom Timeout Per Task
task = {
"turn": 1,
"node": "node99",
"timeout": 600,
"messages": [...]
}
r.lpush("inbox:node99", json.dumps(task))
task = json.loads(task_raw)
timeout = task.get("timeout", 1800)
response = requests.post(API_URL, json=payload, timeout=timeout)
Multi-Round Deliberation
for turn in range(1, MAX_TURNS + 1):
for node in NODES:
dispatch_task(node, turn, build_messages(turn, history))
results = collect_results(len(NODES), timeout=1800)
history.extend(results)
write_checkpoint(turn, results)
Priority Queues
r.lpush("inbox:node99:high", json.dumps(urgent_task))
r.lpush("inbox:node99:low", json.dumps(batch_task))
while True:
item = r.brpop(["inbox:node99:high", "inbox:node99:low"], timeout=0)
_, task_raw = item
process(task_raw)
Health Monitoring
def worker_with_heartbeat(node_name):
while True:
r.setex(f"heartbeat:{node_name}", 60, "alive")
item = r.brpop(f"inbox:{node_name}", timeout=30)
if item:
_, task = item
process(task)
def check_node_alive(node_name):
return r.exists(f"heartbeat:{node_name}") == 1
Performance Notes
- Latency: Redis BRPOP typically <1ms on LAN
- Throughput: 1000+ tasks/sec with pipelining
- Durability: Redis AOF persistence recommended for critical tasks
- Scalability: Tested with 5 nodes, 50+ concurrent tasks
Further Reading