fix(sdk-py): align communicate transport with current Relaycast API#813
fix(sdk-py): align communicate transport with current Relaycast API#813khaliqgant merged 2 commits intomainfrom
Conversation
The Python communicate transport had drifted from the hosted API:
endpoints, response envelopes, and the auth model were all stale, so
example apps (e.g. Supportly) failed at agent registration.
Endpoint + payload changes:
- POST /v1/agents/register → POST /v1/agents (workspace key, body
{name, type})
- DELETE /v1/agents/{agent_id} → DELETE /v1/agents/{name}
- POST /v1/messages/channel → POST /v1/channels/{name}/messages (agent
token, body {text}; sender derived from token)
- POST /v1/messages/dm → POST /v1/dm (agent token, body {to, text})
- POST /v1/messages/reply → POST /v1/messages/{id}/replies (agent token)
- WS /v1/ws/{agent_id}?token=X → WS /v1/ws?token=X
- {ok, data, error} response envelope is now unwrapped centrally
Auth model:
- Workspace API key for admin ops (register, list, unregister)
- Per-agent token for agent ops (post, dm, reply, ws)
WebSocket dispatch now translates message.created / thread.reply /
dm.received / group_dm.received events into the SDK's flat Message
shape, and subscribes to RelayConfig.channels on connect so channel
events actually arrive. The legacy {type:"message", sender, ...} shape
is still accepted for the in-process mock server.
check_inbox() is preserved as a no-op returning [] — the hosted
/v1/inbox now returns unread metadata only, and real delivery flows
through the WebSocket. send_http() takes a new retry=False knob so
unregister_agent() doesn't block shutdown when the API 5xxs on cleanup.
Mock server in conftest.py is rewritten to mirror the new contract
(routes, envelope, auth split). Test assertions that encoded request
payloads and the inbox-returns-messages contract are updated to match.
Verified:
- 211 communicate tests pass (2 pre-existing skips)
- Supportly vanilla customer-service example runs end-to-end against
api.relaycast.dev: triage → DM escalation → specialist resolution →
resolution DM → clean shutdown
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d6d6e895ea
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await self.send_http( | ||
| "DELETE", | ||
| f"/v1/agents/{quote(self.agent_name, safe='')}", | ||
| retry=False, | ||
| ) | ||
| self.agent_id = None | ||
| self.token = None |
There was a problem hiding this comment.
Clear local identity when unregister request fails
unregister_agent() now performs a best-effort DELETE with retry=False, but agent_id/token are only cleared after that call succeeds. When the API returns a 5xx (the commit message notes this can happen on delete), disconnect() suppresses the exception and leaves stale credentials cached; a later connect() reuses them via register_agent()'s early return and can get stuck with invalid auth instead of re-registering. Move the local state reset into a finally (or clear before the request) so reconnects can recover after failed cleanup.
Useful? React with 👍 / 👎.
| await self._ensure_registered() | ||
| payload = await self.send_http("GET", f"/v1/inbox/{self.agent_id}") | ||
| return [self._message_from_payload(item) for item in payload.get("messages", [])] | ||
| return [] |
There was a problem hiding this comment.
Preserve inbox polling for websocket-fallback mode
This change turns check_inbox() into an unconditional empty result, which effectively disables the non-WebSocket receive path. In Relay._ensure_connected(), websocket failures explicitly fall back to polling (check_inbox() in inbox(), peek(), and _poll_loop()), so agents in environments where WS is blocked will now never receive messages. Even if hosted /v1/inbox only returns metadata, this method should still surface deliverable messages (or an alternative fetch path) instead of silently dropping all fallback delivery.
Useful? React with 👍 / 👎.
Summary
The Python
agent_relay.communicate.transporthad drifted off the productionapi.relaycast.devsurface across endpoints, response envelopes, auth model, and WebSocket dispatch. Example apps (e.g. Supportly) failed at agent registration with a 404. This PR realigns the transport with the current API and updates the in-process mock + tests to match.Endpoint + payload changes
POST /v1/agents/register→POST /v1/agents(workspace key, body{name, type})DELETE /v1/agents/{agent_id}→DELETE /v1/agents/{name}POST /v1/messages/channel→POST /v1/channels/{name}/messages(agent token, body{text}; sender derived from token)POST /v1/messages/dm→POST /v1/dm(agent token, body{to, text})POST /v1/messages/reply→POST /v1/messages/{id}/replies(agent token)WS /v1/ws/{agent_id}?token=X→WS /v1/ws?token=X{ok, data, error}envelope is now unwrapped centrallyAuth model
WebSocket
message.created/thread.reply/dm.received/group_dm.receivedinto the SDK's flatMessageRelayConfig.channelson connect so channel events actually arrive{type:"message", sender, text}shape still accepted (for the in-process mock)Behaviour preserved
check_inbox()is a no-op returning[]— the hosted/v1/inboxreturns unread metadata, not message bodies. Real delivery flows through the WebSocket.send_http(retry=False)knob;unregister_agentuses it so cleanup doesn't block shutdown when the API 5xxs (separate server-side bug —DELETE /v1/agents/{name}500s on cascading delete for some agents).Test plan
pytest tests/communicate/— 211 passed, 2 pre-existing skipsvanilla/main.pyruns end-to-end againstapi.relaycast.dev: triage → DM escalation → specialist resolution → resolution DM back → clean shutdownDELETE /v1/agents/{name}server-side 500 on cascading delete🤖 Generated with Claude Code