Skip to content

fix(sdk-py): align communicate transport with current Relaycast API#813

Merged
khaliqgant merged 2 commits intomainfrom
fix-sdk-py-communicate-transport
May 5, 2026
Merged

fix(sdk-py): align communicate transport with current Relaycast API#813
khaliqgant merged 2 commits intomainfrom
fix-sdk-py-communicate-transport

Conversation

@khaliqgant
Copy link
Copy Markdown
Member

Summary

The Python agent_relay.communicate.transport had drifted off the production api.relaycast.dev surface 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/registerPOST /v1/agents (workspace key, body {name, type})
  • DELETE /v1/agents/{agent_id}DELETE /v1/agents/{name}
  • POST /v1/messages/channelPOST /v1/channels/{name}/messages (agent token, body {text}; sender derived from token)
  • POST /v1/messages/dmPOST /v1/dm (agent token, body {to, text})
  • POST /v1/messages/replyPOST /v1/messages/{id}/replies (agent token)
  • WS /v1/ws/{agent_id}?token=XWS /v1/ws?token=X
  • {ok, data, error} 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, websocket)

WebSocket

  • Translates message.created / thread.reply / dm.received / group_dm.received into the SDK's flat Message
  • Subscribes to RelayConfig.channels on connect so channel events actually arrive
  • Legacy flat {type:"message", sender, text} shape still accepted (for the in-process mock)

Behaviour preserved

  • check_inbox() is a no-op returning [] — the hosted /v1/inbox returns unread metadata, not message bodies. Real delivery flows through the WebSocket.
  • New send_http(retry=False) knob; unregister_agent uses 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 skips
  • Supportly vanilla/main.py runs end-to-end against api.relaycast.dev: triage → DM escalation → specialist resolution → resolution DM back → clean shutdown
  • Reviewer: smoke-test the other framework variants (CrewAI, OpenAI Agents, LangGraph, Google ADK) — they import the same module so should ride along
  • Follow-up issue worth filing: DELETE /v1/agents/{name} server-side 500 on cascading delete

🤖 Generated with Claude Code

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>
@khaliqgant khaliqgant requested a review from willwashburn as a code owner May 4, 2026 20:22
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +182 to 188
await self.send_http(
"DELETE",
f"/v1/agents/{quote(self.agent_name, safe='')}",
retry=False,
)
self.agent_id = None
self.token = None
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +237 to +238
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 []
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

@khaliqgant khaliqgant merged commit 4d2ffc7 into main May 5, 2026
39 checks passed
@khaliqgant khaliqgant deleted the fix-sdk-py-communicate-transport branch May 5, 2026 08:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants