feat(gateway): add Slate Agent Hub as messaging platform#5957
Open
handsdiff wants to merge 9 commits into
Open
feat(gateway): add Slate Agent Hub as messaging platform#5957handsdiff wants to merge 9 commits into
handsdiff wants to merge 9 commits into
Conversation
|
Friendly bump on this PR in case it fell through the cracks \u2014 would love a review when someone has a minute. Thanks! |
6e1e3c3 to
1cab3fa
Compare
177c2cb to
60990eb
Compare
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
Changes Made
gateway/platforms/hub.py(NEW) —HubAdapterwith WebSocket lifecycle, REST send, exponential backoff reconnect, self-message filtering, persistent httpx client, andcheck_hub_requirements()gateway/config.py—Platform.HUBenum,get_connected_platforms()branch,_apply_env_overrides()forHUB_AGENT_ID/HUB_AGENT_SECRET/HUB_WS_URL/HUB_API_BASE/HUB_HOME_CHANNELenv varsgateway/run.py—_create_adapter()registration with requirements check, auth bypass for Hub (Hub manages its own auth via agent secrets),Platform.HUBadded to_UPDATE_ALLOWED_PLATFORMStools/send_message_tool.py— Hub inplatform_map, routing in_send_to_platform(), standalone_send_hub()for cron/tool use without gateway runningcron/scheduler.py—"hub"in_KNOWN_DELIVERY_PLATFORMSandplatform_maptoolsets.py—"hermes-hub"toolset with_HERMES_CORE_TOOLS, included in"hermes-gateway"agent/prompt_builder.py—PLATFORM_HINTS["hub"]for agent-to-agent contexttests/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 hintswebsite/docs/user-guide/messaging/hub.md(NEW) — full setup guide with registration, env vars, config.yaml, MCP meta-tool, reconnection behavior, and configuration referencecli-config.yaml.example— Hub platform config section and Hub MCP server exampleKey design decisions
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 inplatform_env_map/platform_allow_all_map.send()andget_chat_info()accept both"hub:brain"and"brain"— the adapter creates sessions withhub:prefix, but cron delivery strips the platform prefix before callingsend().connect(), closed indisconnect(). Falls back to a one-shot client if called before connect or after disconnect.How to Test
POST https://admin.slate.ceo/oc/brain/agents/register~/.hermes/config.yaml:HUB_AGENT_ID=your-agent HUB_AGENT_SECRET=your-secrethermes gateway[Hub] Connected to wss://...in logspytest tests/gateway/test_hub.py -vChecklist
Code
feat(gateway):)tests/gateway/test_hub.py)Documentation & Housekeeping
website/docs/user-guide/messaging/hub.md(full setup guide)cli-config.yaml.example— Hub platform config section + Hub MCP server example