Skip to content

feat(gateway): add Slate Agent Hub as messaging platform#5957

Open
handsdiff wants to merge 9 commits into
NousResearch:mainfrom
handsdiff:hub-adapter
Open

feat(gateway): add Slate Agent Hub as messaging platform#5957
handsdiff wants to merge 9 commits into
NousResearch:mainfrom
handsdiff:hub-adapter

Conversation

@handsdiff

@handsdiff handsdiff commented Apr 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds Slate Agent Hub as a first-class messaging platform in Hermes — enabling agent-to-agent communication alongside existing user-facing platforms (Telegram, Discord, etc.).

Hub uses an outbound WebSocket for receiving messages and REST API for sending, following the same pattern as the Discord adapter. Each Hub correspondent gets a unique Hermes session (chat_id = "hub:{agent_id}").

Related Issue

N/A — new feature, no existing issue.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)
  • 🔒 Security fix
  • 📝 Documentation update
  • ✅ Tests (adding or improving test coverage)
  • ♻️ Refactor (no behavior change)
  • 🎯 New skill (bundled or hub)

Changes Made

  • gateway/platforms/hub.py (NEW) — HubAdapter with WebSocket lifecycle, REST send, exponential backoff reconnect, self-message filtering, persistent httpx client, and check_hub_requirements()
  • gateway/config.pyPlatform.HUB enum, get_connected_platforms() branch, _apply_env_overrides() for HUB_AGENT_ID/HUB_AGENT_SECRET/HUB_WS_URL/HUB_API_BASE/HUB_HOME_CHANNEL env vars
  • gateway/run.py_create_adapter() registration with requirements check, auth bypass for Hub (Hub manages its own auth via agent secrets), Platform.HUB added to _UPDATE_ALLOWED_PLATFORMS
  • tools/send_message_tool.py — Hub in platform_map, routing in _send_to_platform(), standalone _send_hub() for cron/tool use without gateway running
  • cron/scheduler.py"hub" in _KNOWN_DELIVERY_PLATFORMS and platform_map
  • toolsets.py"hermes-hub" toolset with _HERMES_CORE_TOOLS, included in "hermes-gateway"
  • agent/prompt_builder.pyPLATFORM_HINTS["hub"] for agent-to-agent context
  • tests/gateway/test_hub.py (NEW) — 34 unit tests covering config, adapter factory, auth bypass, send, get_chat_info, inbound message handling, self-message filtering, command detection, WS auth failure, standalone send tool, cron delivery, toolset, and platform hints
  • website/docs/user-guide/messaging/hub.md (NEW) — full setup guide with registration, env vars, config.yaml, MCP meta-tool, reconnection behavior, and configuration reference
  • cli-config.yaml.example — Hub platform config section and Hub MCP server example

Key design decisions

  • Auth bypass: Hub agents authenticate with Hub itself via agent secrets. The per-user allowlist model (TELEGRAM_ALLOWED_USERS, etc.) doesn't apply to agent-to-agent messaging. Hub is added to the bypass tuple alongside HomeAssistant and Webhook, with a comment explaining why it's intentionally NOT in platform_env_map/platform_allow_all_map.
  • chat_id format: send() and get_chat_info() accept both "hub:brain" and "brain" — the adapter creates sessions with hub: prefix, but cron delivery strips the platform prefix before calling send().
  • Persistent HTTP client: Created in connect(), closed in disconnect(). Falls back to a one-shot client if called before connect or after disconnect.
  • All changes are additive — no existing platform behavior is modified.

How to Test

  1. Register an agent on Hub: POST https://admin.slate.ceo/oc/brain/agents/register
  2. Configure in ~/.hermes/config.yaml:
    platforms:
      hub:
        enabled: true
        extra:
          agent_id: "your-agent"
          agent_secret: "your-secret"
    Or via env vars: HUB_AGENT_ID=your-agent HUB_AGENT_SECRET=your-secret
  3. Start gateway: hermes gateway
  4. Verify [Hub] Connected to wss://... in logs
  5. Send a message from another Hub agent → verify Hermes receives and responds
  6. Run tests: pytest tests/gateway/test_hub.py -v

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(gateway):)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this feature (no unrelated commits)
  • I've added tests for my changes (34 unit tests in tests/gateway/test_hub.py)
  • I've tested on my platform: Ubuntu (Debian 13, Linux 6.12)

Documentation & Housekeeping

  • I've updated relevant documentation — website/docs/user-guide/messaging/hub.md (full setup guide)
  • I've updated cli-config.yaml.example — Hub platform config section + Hub MCP server example
  • I've considered cross-platform impact (Windows, macOS) — httpx and websockets are cross-platform; no platform-specific code
  • I've updated tool descriptions/schemas if I changed tool behavior — N/A

@blasai1739217-cmyk

Copy link
Copy Markdown

Friendly bump on this PR in case it fell through the cracks \u2014 would love a review when someone has a minute. Thanks!

handsdiff and others added 9 commits April 23, 2026 23:23
Add HubAdapter for agent-to-agent messaging via the Slate Agent Hub.
Uses outbound WebSocket for receiving messages and REST API for sending,
following the same pattern as the Discord adapter.

Key design decisions:
- Auth bypass in _is_user_authorized() — Hub manages its own auth via
  agent secrets; per-user allowlists don't apply to agent-to-agent
- Persistent httpx client for REST sends, created in connect(), closed
  in disconnect()
- Self-message filtering to prevent reply loops
- chat_id accepts both "hub:{agent_id}" and "{agent_id}" formats for
  compatibility with cron delivery (which strips the platform prefix)

Files:
- gateway/platforms/hub.py (NEW) — HubAdapter + check_hub_requirements()
- gateway/config.py — Platform.HUB enum, get_connected_platforms(),
  _apply_env_overrides() for HUB_AGENT_ID/HUB_AGENT_SECRET env vars
- gateway/run.py — _create_adapter(), auth bypass, _UPDATE_ALLOWED_PLATFORMS
- tools/send_message_tool.py — platform_map + routing + standalone _send_hub()
- cron/scheduler.py — _KNOWN_DELIVERY_PLATFORMS + platform_map
- toolsets.py — hermes-hub toolset + hermes-gateway includes
- agent/prompt_builder.py — PLATFORM_HINTS for hub
- tests/gateway/test_hub.py (NEW) — 34 unit tests

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- cli-config.yaml.example: Hub platform config section and MCP server example
- website/docs/user-guide/messaging/hub.md: full setup guide with registration,
  config options (env vars + config.yaml), MCP meta-tool, and reference table

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move hub.md from position 14 (conflicts with wecom.md) to position 15.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
websockets v15 renamed the connect API. The old websockets.client.connect()
is deprecated. Also disable library-level pings — the gateway blocks the
event loop during agent processing, causing ping timeouts after 20s.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Without this entry, _get_platform_tools crashes with KeyError: 'hub'
when a Hub message arrives because the platform registry has no hub key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hub migrated from admin.slate.ceo/oc/brain to hub.slate.ceo.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Hub is an agent-to-agent messaging platform: every inbound message
triggers a fresh LLM call on the receiving agent. The gateway was
forwarding five kinds of internal traffic over that wire, each of
which made the partner agent respond and trigger another LLM call:

1. status_callback — rate-limit notices, retry/fallback events
   (~6 messages per failed request, 34 emit sites in run_agent.py)
2. "Operation interrupted: ..." strings returned as final_response
   when the agent is interrupted mid-call
3. "API call failed after N retries: ..." strings returned as
   final_response when retries are exhausted
4. interim_assistant_callback — mid-turn commentary
5. stream consumer sending a partial + continuation pair because
   HubAdapter never declared SUPPORTS_MESSAGE_EDITING=False

Under rate-limit pressure paths 1+2+3 compound into an unbounded
feedback loop between paired agents (status → interrupt → failure
string → LLM call → status).

This commit:

- Gates status_callback and interim_assistant_callback on
  source.platform != Platform.HUB, mirroring the existing
  Platform.WEBHOOK exclusion pattern for tool_progress.
- Filters known internal error/interrupt prefixes in
  HubAdapter.send() as a belt-and-suspenders guard for any future
  path that sends final_response over Hub.
- Declares SUPPORTS_MESSAGE_EDITING = False on HubAdapter so the
  gateway stream consumer skips streaming entirely for Hub, sending
  one final message per turn instead of two.

Adds 4 unit tests covering the suppression filter and the
no-editing flag. No new test failures from full gateway suite
(pre-existing 14 failures are unrelated env issues).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hub's inbound → LLM call → auto-reply architecture has no surface for the
LLM to express "this message doesn't warrant a response." The model's
text output IS the reply — producing zero tokens is the only silence
path, and RLHF-trained models essentially never do that. Tars tried to
signal intent by writing "[No response.]" which of course became a real
Hub message the peer then answered. Result: stable 10-second
"Holding." / "Silent." oscillations between agents with nothing left to
say, even after the earlier Honcho / status-suppression fixes.

Port openclaw's NO_REPLY convention (see openclaw
auto-reply/tokens.ts + auto-reply/reply/normalize-reply.ts):

- HubAdapter.send() drops an exact-match NO_REPLY before hitting the
  wire. Mixed content with a trailing NO_REPLY has the token stripped;
  if nothing substantive remains (just whitespace or markdown-emphasis
  chars like `**`) the send is suppressed too.
- build_session_context_prompt() teaches the agent the convention via
  Platform.HUB-specific guidance — when to emit NO_REPLY, the strict
  formatting rules, and concrete use cases (acks, notifications already
  processed, status pings with nothing to add).

The decision stays with the LLM and is visible in its trajectory. No
pre-filter, no architectural rewrite. Telegram / Discord / other
platforms are untouched — they still reply-by-default because humans
expect an answer.

Adds 4 tests: exact-match, mixed-strips-to-empty, trailing-strip-delivers,
prose-mention-passes-through.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The busy-ack message ("⚡ Interrupting current task, I'll respond
shortly") is UX reassurance for a human recipient waiting on their
agent. For an agent peer on Hub, it's just unsolicited semantic
content they try to interpret, which we observed fueling false
"message injection" narratives across the fleet.

Add a WANTS_BUSY_ACK class attribute (default True for back-compat)
and set it to False on HubAdapter. The interrupt + pending-message
queue still run — only the outbound ⚡ message is suppressed when
the recipient is another agent.

Test: tests/gateway/test_busy_session_ack.py ::
test_suppresses_ack_when_adapter_opts_out — verifies no ack, but
interrupt + queue still happen.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants