# Relay — AI Messaging Bridge # https://docs.relay.ckgworks.com # This file describes the Relay integration protocol for AI coding assistants. ## What is Relay? Relay is an AI messaging bridge that connects applications (Portal, Flow, Studio, Academy, etc.) with AI agents (Athena, Klyve, etc.) over WebSocket. It works like Twilio for AI — apps send events to Relay, Relay delivers them to agents, agents respond in real-time. **Core principle:** Relay never reads payloads, never decides routing, never filters events. Apps send events to specific agents; Relay just delivers. Apps can mention agents, assign tasks to agents, or request AI responses — Relay bridges the conversation. ## Quick Reference - **API Base:** wss://api.relay.ckgworks.com/v1 - **App Token Format:** rlk_{app_id}_{random_string} (example: rlk_portal_x8k2m9p...) - **Agent Token Format:** rla_{agent_id}_{random_string} (example: rla_athena_k9x2p...) - **Transport:** WebSocket only (no HTTP POST, no HMAC, no webhooks) - **Auth:** Query param on WebSocket handshake (?token=rlk_... or ?token=rla_...), then all messages on the connection are trusted - **Max Payload:** 64KB per event - **Event ID:** Generated by Relay (not by the app). Returned in the "accepted" response. IMPORTANT: Apps store ONLY the app token (rlk_) and agent_id mappings. Agent tokens (rla_) are never stored by apps — they belong to the agent. Agent IDs are auto-generated by Relay (e.g., athena_k9p2m3) — apps don't create them. - **Session Keys:** relay:{app_id}:{thread_id} (sent to agent), relay:{agent_id}:{app_id}:{thread_id} (stored by Relay) ## Credentials ### App Token (for applications) - **Format:** rlk_portal_x8k2m9p... - **Where to get:** Register app on Relay dashboard → token shown once → store in .env - **Use:** Authenticate to wss://api.relay.ckgworks.com/v1/ws/app - **Scope:** One token per app, works across all agents app is allowlisted for ### Agent Token (for agents/plugins) - **Format:** rla_athena_k9x2p... - **Where to get:** Register agent on Relay dashboard → token shown once → store in plugin config - **Use:** Authenticate to wss://api.relay.ckgworks.com/v1/ws/agent - **Scope:** One token per agent, one WebSocket connection per agent ## WebSocket Endpoints ### For Apps: Sending Events & Receiving Replies ``` Endpoint: wss://api.relay.ckgworks.com/v1/ws/app Auth: Authorization: Bearer rlk_portal_x8k2m9p... Direction: Bidirectional (send events → receive tokens → receive final reply) ``` ### For Agents: Receiving Events & Sending Replies ``` Endpoint: wss://api.relay.ckgworks.com/v1/ws/agent Auth: Authorization: Bearer rla_athena_k9x2p... Direction: Bidirectional (receive events → send tokens → send final reply) ``` ## Message Types: App ↔ Relay All messages are JSON, sent over WebSocket. ### App → Relay: Send Event ```json { "type": "event", "agent_id": "athena", "thread_id": "task-123", "payload": { "event": "comment.mention", "message": "@athena summarize this task", "sender": { "id": "user-456", "name": "Christian Garcia" }, "task_id": "task-123", "task_title": "Q2 Roadmap Review", "project": "Smiling Group Platform" } } ``` **Fields:** - `type` (string, required): Always "event" - `agent_id` (string, required): Which agent to send to (e.g., "athena", "klyve") - `thread_id` (string, required): Thread/conversation identifier (e.g., task ID, lesson ID, channel ID) - Used to maintain conversation continuity within agent's session TTL - Multiple events with same thread_id are treated as one conversation - `payload` (object, required): Opaque data sent to the agent - Relay never reads this, never validates structure - Can be any JSON structure the app and agent agree on - Max 64KB per event **Response:** Relay sends `accepted` message. ### Relay → App: Event Accepted ```json { "type": "accepted", "event_id": "evt_k9p2m", "agent_id": "athena", "session_key": "relay:athena:portal:task-123", "status": "accepted" } ``` **Fields:** - `type` (string): Always "accepted" - `event_id` (string): Unique ID for this event (used in all subsequent messages) - `agent_id` (string): Which agent will receive this - `session_key` (string): Full session key (for Relay's own tracking) - `status` (string): Always "accepted" **Timing:** Sent immediately after Relay receives the event, before agent receives it. ### Relay → App: Token Stream (Streaming Reply) ```json { "type": "token", "event_id": "evt_k9p2m", "agent_id": "athena", "token": "Here" } { "type": "token", "event_id": "evt_k9p2m", "agent_id": "athena", "token": "'s the" } { "type": "token", "event_id": "evt_k9p2m", "agent_id": "athena", "token": " summary" } ``` **Fields:** - `type` (string): Always "token" - `event_id` (string): Matches the event being replied to - `agent_id` (string): Which agent sent this token - `token` (string): A fragment of the reply (may be a word, part of a word, punctuation, etc.) **Usage:** Apps use this to implement "typing..." indicators. Accumulate tokens to build the full reply. ### Relay → App: Final Reply ```json { "type": "reply", "event_id": "evt_k9p2m", "agent_id": "athena", "thread_id": "task-123", "reply": "Here's the summary of the Q2 Roadmap Review...", "payload": { "event": "comment.mention", "message": "@athena summarize this task", "sender": { "id": "user-456", "name": "Christian Garcia" }, "task_id": "task-123", "task_title": "Q2 Roadmap Review", "project": "Smiling Group Platform" }, "metadata": { "tokens_used": 1500, "model": "claude-sonnet", "latency_ms": 2300, "session_key": "relay:athena:portal:task-123" } } ``` **Fields:** - `type` (string): Always "reply" - `event_id` (string): Matches the event being replied to - `agent_id` (string): Which agent sent this reply - `thread_id` (string): The original thread_id from the event - `reply` (string): Full response text from the agent - You've already received this token-by-token via "token" messages - This is the final complete version - `payload` (object): Echo of original payload (so you know which context was used) - `metadata` (object): Additional information - `tokens_used` (number): How many tokens the AI used - `model` (string): Which AI model was used (e.g., "claude-sonnet") - `latency_ms` (number): End-to-end latency in milliseconds - `session_key` (string): Full session key used **Timing:** Sent after all tokens have been sent. This is the final message for this event. ### Relay → App: Error ```json { "type": "error", "event_id": "evt_k9p2m", "agent_id": "athena", "error": "Agent not connected", "code": "AGENT_OFFLINE" } ``` **Fields:** - `type` (string): Always "error" - `event_id` (string): Matches the event that failed - `agent_id` (string): Which agent failed to respond - `error` (string): Human-readable error message - `code` (string): Machine-readable error code (see Error Codes section) **Timing:** Sent instead of "reply" if something goes wrong. ### App ↔ Relay: Heartbeat (Ping/Pong) ```json { "type": "ping" } { "type": "pong" } ``` **Usage:** Keep connection alive, detect stale connections. **Implementation:** - App sends "ping" every 30 seconds - Relay responds with "pong" - If no "pong" within 5 seconds, connection is dead, reconnect ### App → Relay: Discover Available Agents ```json { "type": "discover" } ``` **Response from Relay:** ```json { "type": "agents", "agents": [ { "agent_id": "athena", "name": "Athena", "description": "Personal EA for general tasks" }, { "agent_id": "klyve", "name": "Klyve", "description": "Technical specialist for code review" } ] } ``` **Usage:** - Apps call this on startup to discover which agents they can reach - Returns only agents the app is allowlisted for - If no allowlist exists for an agent, all apps can see it - Used to auto-create agent user profiles in the app's database ## Message Types: Agent ↔ Relay ### Relay → Agent: Deliver Event ```json { "type": "event", "event_id": "evt_k9p2m", "app_id": "portal", "thread_id": "task-123", "session_key": "relay:portal:task-123", "payload": { "event": "comment.mention", "message": "@athena summarize this task", "sender": { "id": "user-456", "name": "Christian Garcia" }, "task_id": "task-123", "task_title": "Q2 Roadmap Review", "project": "Smiling Group Platform" } } ``` **Fields:** - `type` (string): Always "event" - `event_id` (string): Unique ID for this event (used in all reply messages) - `app_id` (string): Which app sent this (e.g., "portal", "flow") - `thread_id` (string): Original thread ID from the app - `session_key` (string): Session key WITHOUT agent_id (format: relay:{app_id}:{thread_id}) - Use this to create/resume an OpenClaw session - Agent already knows who it is; this key just tracks the conversation - `payload` (object): Original opaque payload from the app **Note:** The `session_key` sent to the agent is `relay:portal:task-123` (no agent_id), because one agent instance = one token = one agent. The agent already knows who it is. ### Agent → Relay: Token Stream (Streaming Response) ```json { "type": "token", "event_id": "evt_k9p2m", "token": "Here" } { "type": "token", "event_id": "evt_k9p2m", "token": "'s the" } { "type": "token", "event_id": "evt_k9p2m", "token": " summary" } ``` **Fields:** - `type` (string): Always "token" - `event_id` (string): Matches the event being replied to - `token` (string): A fragment of the response **Usage:** Send one message per token, as you generate them. Relay forwards each token to the app immediately for real-time streaming. ### Agent → Relay: Final Reply ```json { "type": "reply", "event_id": "evt_k9p2m", "content": "Here's the summary of the Q2 Roadmap Review...", "done": true, "metadata": { "tokens_used": 1500, "model": "claude-sonnet", "latency_ms": 2300 } } ``` **Fields:** - `type` (string): Always "reply" - `event_id` (string): Matches the event being replied to - `content` (string): Full response text (or empty if already sent token-by-token) - If you sent tokens, can leave this empty - Or repeat the full response here - `done` (boolean): Always true, indicates response is complete - `metadata` (object): Optional metadata - `tokens_used` (number): How many tokens you used - `model` (string): Which model you used - `latency_ms` (number): How long the response took **Timing:** Sent after all tokens have been sent and processing is complete. ### Agent → Relay: Error ```json { "type": "error", "event_id": "evt_k9p2m", "error": "Agent session timed out", "code": "AGENT_TIMEOUT" } ``` **Fields:** - `type` (string): Always "error" - `event_id` (string): Matches the event that failed - `error` (string): Human-readable error message - `code` (string): Machine-readable error code (see Error Codes section) **Usage:** Send instead of "reply" if processing fails. ### Agent ↔ Relay: Heartbeat ```json { "type": "ping" } { "type": "pong" } ``` **Implementation:** - Agent sends "ping" every 30 seconds - Relay responds with "pong" - If no "pong" within 5 seconds, connection is dead, reconnect ## Error Codes Complete list of error codes returned by Relay: ### App → Agent Errors (sent to app) | Code | Meaning | Cause | Action | |------|---------|-------|--------| | `AGENT_NOT_FOUND` | Agent doesn't exist | app sent agent_id that doesn't exist in organization | Check agent name spelling, verify agent is registered | | `AGENT_OFFLINE` | Agent not connected | agent WebSocket is down | Wait for agent to reconnect, or restart agent service | | `AGENT_TIMEOUT` | Agent took too long | agent processing exceeded timeout (typically 5+ minutes) | Check agent logs, may indicate agent is hung or overloaded | | `AGENT_ERROR` | Agent returned error | agent sent an error message | Check agent logs to see what error occurred | | `PERMISSION_DENIED` | App not allowlisted | app is not on agent's allowlist | Check agent's allowlist, add app if needed | | `INVALID_AGENT_ID` | Agent ID is invalid format | agent_id contains invalid characters | Use lowercase letters, numbers, hyphens only | | `INVALID_THREAD_ID` | Thread ID is invalid | thread_id is empty or malformed | Provide a valid thread identifier | | `PAYLOAD_TOO_LARGE` | Payload exceeds 64KB | payload > 64000 bytes | Reduce payload size, send only essential context | | `INVALID_JSON` | Malformed JSON | message is not valid JSON | Check JSON syntax, ensure all required fields present | | `RATE_LIMIT_EXCEEDED` | Too many events | app is sending too many events too fast | Back off and retry after the `retry_after_seconds` specified in metadata | ### Agent → App Errors (sent to agent) | Code | Meaning | Cause | Action | |------|---------|-------|--------| | `INVALID_EVENT_ID` | event_id doesn't match | agent sent a reply to unknown event | Check event_id, ensure you're replying to received events | | `INVALID_JSON` | Malformed JSON | message is not valid JSON | Check JSON syntax | | `MESSAGE_TOO_LARGE` | Single token > 4KB | a token fragment is too large | Break response into smaller chunks | ### Connection Errors | Code | Meaning | Cause | Action | |------|---------|-------|--------| | `UNAUTHORIZED` | Invalid token | token is wrong, expired, or rotated | Check token value, use the current (non-rotated) token | | `TOKEN_REVOKED` | Token was revoked | admin deactivated the app/agent | Re-register app/agent to get a new token | | `FORBIDDEN` | Permission denied | wrong endpoint (app using agent endpoint) | Use correct endpoint: /ws/app for apps, /ws/agent for agents | | `INTERNAL_ERROR` | Relay error | something went wrong on Relay side | Retry; if persists, contact Relay support | ## Session Keys ### Format in Relay's Database ``` relay:{agent_id}:{app_id}:{thread_id} ``` **Example:** ``` relay:athena:portal:task-123 relay:klyve:flow:job-456 relay:athena:portal:task-456 (same task, different agent) ``` Relay uses this to track unique conversations across agents and apps. ### Format Sent to Agent ``` relay:{app_id}:{thread_id} ``` **Example:** ``` relay:portal:task-123 relay:flow:job-456 ``` No agent_id is included because one OpenClaw instance = one agent. The agent already knows who it is. ### Session Continuity - **Same thread_id → Same session:** Multiple events with the same thread_id use the same agent session - Portal sends event for task-123 today - Portal sends another event for task-123 tomorrow - Agent picks up where it left off (full conversation history preserved, within TTL) - **Different thread_id → Different session:** Each thread_id is a separate conversation - Portal sends event for task-123 (one session) - Portal sends event for task-456 (completely separate session) - Agent treats these as different conversations - **Per-Agent TTL:** Sessions expire after agent-specific TTL - Athena: 14 days - Klyve: 30 days - After expiry, session is deleted and a new one is created if events continue ## Integration Steps: For Apps ### 1. Get Your App Token - Log in to Relay dashboard (relay.ckgworks.com) - Go to **Apps** → click **Register New App** - Fill in app name and ID (e.g., "Portal", "portal") - Dashboard shows your token: `rlk_portal_x8k2m9p...` - **Copy immediately and store in `.env`** ### 2. Store Token Securely Create `.env` file in your project: ``` # .env RELAY_APP_TOKEN=rlk_portal_x8k2m9p... ``` Add to `.gitignore`: ``` # .gitignore .env .env.local ``` In code, load from environment: ```python import os relay_token = os.getenv("RELAY_APP_TOKEN") ``` ### 3. Connect to Relay WebSocket ```python import asyncio import websockets import json import os async def relay_client(): uri = "wss://api.relay.ckgworks.com/v1/ws/app" token = os.getenv("RELAY_APP_TOKEN") headers = { "Authorization": f"Bearer {token}" } async with websockets.connect(uri, subprotocols=["authorization"], extra_headers=headers) as ws: print("Connected to Relay") # Listen for incoming messages async def listen(): async for message in ws: data = json.loads(message) handle_message(data) # Send keepalive pings async def keepalive(): while True: await asyncio.sleep(30) await ws.send(json.dumps({"type": "ping"})) # Run both concurrently await asyncio.gather(listen(), keepalive()) asyncio.run(relay_client()) ``` ### 4. Detect Agent Mentions In your app, detect when a user mentions an agent: ```python def parse_comment(comment_text): # Check for @mentions if "@athena" in comment_text: return ("athena", comment_text) elif "@klyve" in comment_text: return ("klyve", comment_text) return (None, None) ``` ### 5. Send Event to Relay When you detect an agent mention, send an event: ```python async def send_to_relay(agent_id, thread_id, payload, ws): event = { "type": "event", "agent_id": agent_id, "thread_id": thread_id, "payload": payload } await ws.send(json.dumps(event)) ``` Example usage: ```python await send_to_relay( agent_id="athena", thread_id="task-123", payload={ "event": "comment.mention", "message": "@athena summarize this task", "sender": {"id": "user-456", "name": "Christian Garcia"}, "task_id": "task-123", "task_title": "Q2 Roadmap Review", "project": "Smiling Group Platform" }, ws=ws ) ``` ### 6. Receive Tokens and Collect Response ```python def handle_message(data): msg_type = data.get("type") if msg_type == "accepted": print(f"Event accepted: {data['event_id']}") # Event is now being processed elif msg_type == "token": # Accumulate tokens for streaming UI event_id = data['event_id'] token = data['token'] accumulate_token(event_id, token) push_to_browser_via_sse(token) # Live update elif msg_type == "reply": # Final response event_id = data['event_id'] reply = data['reply'] thread_id = data['thread_id'] save_as_ai_comment(thread_id, reply) elif msg_type == "error": # Something went wrong error_code = data['code'] error_msg = data['error'] show_error_to_user(f"Agent error: {error_msg}") elif msg_type == "pong": # Heartbeat response pass ``` ### 7. Discover Agents on Startup When your app starts, discover which agents you can reach: ```python async def discover_agents(ws): await ws.send(json.dumps({"type": "discover"})) # Wait for response... # Response will be {"type": "agents", "agents": [...]} ``` ### 8. Handle Disconnection If the WebSocket drops, reconnect with exponential backoff: ```python import asyncio async def connect_with_backoff(): max_retries = 10 wait_time = 1 for attempt in range(max_retries): try: async with websockets.connect(uri, extra_headers=headers) as ws: return ws except Exception as e: if attempt < max_retries - 1: print(f"Connection failed, retrying in {wait_time}s") await asyncio.sleep(wait_time) wait_time = min(wait_time * 2, 60) # exponential, max 60s else: raise ``` ## Integration Steps: For Agents ### 1. Get Your Agent Token - Log in to Relay dashboard - Go to **Agents** → click **Register New Agent** - Fill in name and ID (e.g., "Athena", "athena") - Dashboard shows your token: `rla_athena_k9x2p...` - **Copy immediately and store in `.env` or secrets manager** ### 2. Use OpenClaw Relay Plugin (Recommended) The OpenClaw Relay plugin handles the WebSocket connection, session management, and message routing automatically. #### Install Plugin ```bash npm install openclaw-relay-plugin ``` #### Configure Plugin Create `openclaw.plugin.json`: ```json { "type": "relay", "agents": [ { "token": "rla_athena_k9x2p...", "session_ttl_days": 14, "model": "claude-sonnet" }, { "token": "rla_klyve_k9x2p...", "session_ttl_days": 30, "model": "claude-opus" } ] } ``` Or use environment variables: ``` RELAY_AGENT_TOKEN_ATHENA=rla_athena_k9x2p... RELAY_AGENT_TOKEN_KLYVE=rla_klyve_k9x2p... ``` #### Load Plugin In your OpenClaw agent: ```python from openclaw.plugin import load_plugin relay_plugin = load_plugin("relay") relay_plugin.start() # Events will be automatically routed to your agent ``` ### 3. Process Events (Manual Connection) If not using the plugin, handle messages directly: ```python async def relay_agent(): uri = "wss://api.relay.ckgworks.com/v1/ws/agent" token = os.getenv("RELAY_AGENT_TOKEN_ATHENA") headers = {"Authorization": f"Bearer {token}"} async with websockets.connect(uri, extra_headers=headers) as ws: print("Agent connected to Relay") async for message in ws: data = json.loads(message) await handle_event(data, ws) async def handle_event(event, ws): if event["type"] == "event": event_id = event["event_id"] session_key = event["session_key"] payload = event["payload"] # Create/resume agent session session = create_or_resume_session(session_key) # Process with agent reply = await agent.process( payload=payload, session=session ) # Send back tokens as they arrive for token in reply.tokens: await ws.send(json.dumps({ "type": "token", "event_id": event_id, "token": token })) # Send final reply await ws.send(json.dumps({ "type": "reply", "event_id": event_id, "content": reply.full_text, "done": True, "metadata": { "tokens_used": reply.tokens_used, "model": "claude-sonnet" } })) ``` ### 4. Maintain Session Storage Sessions are maintained in the agent service (not in Relay). Relay sends the `session_key`, you store the conversation history: ```python sessions = {} # In-memory storage (or use Redis, database, etc.) def create_or_resume_session(session_key): if session_key in sessions: return sessions[session_key] else: session = { "key": session_key, "messages": [], "created_at": datetime.now() } sessions[session_key] = session return session def save_message(session_key, role, content): session = sessions.get(session_key) if session: session["messages"].append({"role": role, "content": content}) ``` ### 5. Handle Errors If something goes wrong, send an error message back to Relay: ```python async def handle_event(event, ws): try: # ... process ... except Exception as e: await ws.send(json.dumps({ "type": "error", "event_id": event["event_id"], "error": str(e), "code": "AGENT_ERROR" })) ``` ### 6. Implement Heartbeat Send pings to keep connection alive: ```python async def keepalive(ws): while True: await asyncio.sleep(30) await ws.send(json.dumps({"type": "ping"})) # Run concurrently with message handling await asyncio.gather(handle_messages(ws), keepalive(ws)) ``` ## Rules and Constraints ### Transport Rules 1. **All WebSocket.** No HTTP, no HMAC, no webhook callbacks. Everything is persistent WebSocket. 2. **Auth once.** Bearer token on handshake, then trust the channel. No per-message signing. 3. **Opaque payloads.** Relay never reads, validates, or transforms the `payload` field. Apps and agents define structure. 4. **Max 64KB.** Payload must not exceed 64000 bytes. Relay returns error if exceeded. 5. **Session keys.** Format `relay:{app_id}:{thread_id}` sent to agent. Agent uses this to maintain conversation. ### Concurrency Rules 1. **One connection per app.** All events from one app share one WebSocket. 2. **One connection per agent.** All events to one agent share one WebSocket. 3. **Multiple instances.** If you run multiple instances of the same app, each gets its own WebSocket to Relay (same token, multiple connections). ### Lifecycle Rules 1. **Events are fire-and-forget.** App sends event, Relay immediately sends "accepted", then processes async. 2. **Sessions persist within TTL.** Each session_key maintains state until TTL expires. 3. **Token rotation has 1-hour grace.** Old token works for 1 hour after rotation, then invalid. 4. **Reconnection is automatic.** Both apps and agents should reconnect automatically with exponential backoff on disconnect. ### Payload Rules 1. **Payload is required.** Every event must include a payload. 2. **Payload is opaque.** Relay never reads it, never validates it, never filters it. 3. **Payload should have context.** Include enough information for the agent to respond meaningfully. 4. **Don't include secrets.** Never send API keys, passwords, or tokens in payload. ### Error Handling Rules 1. **Errors stop processing.** If an error is sent, the event is not retried automatically. 2. **Apps must handle errors.** When Relay sends an error, the app should notify the user or retry. 3. **Agents must send errors.** If processing fails, send an error message, not silence. ## Complete Example: App Sending Event ```python import asyncio import websockets import json import os class RelayClient: def __init__(self, token): self.token = token self.uri = "wss://api.relay.ckgworks.com/v1/ws/app" self.ws = None async def connect(self): headers = {"Authorization": f"Bearer {self.token}"} self.ws = await websockets.connect( self.uri, subprotocols=["authorization"], extra_headers=headers ) async def send_event(self, agent_id, thread_id, payload): """Send an event to an agent""" event = { "type": "event", "agent_id": agent_id, "thread_id": thread_id, "payload": payload } await self.ws.send(json.dumps(event)) async def listen(self): """Listen for messages from Relay""" async for message in self.ws: yield json.loads(message) async def run(self): """Main event loop""" await self.connect() # Start listening for messages async for message in self.listen(): msg_type = message.get("type") if msg_type == "accepted": print(f"Event {message['event_id']} accepted") elif msg_type == "token": print(f"Token: {message['token']}", end="", flush=True) elif msg_type == "reply": print(f"\nFull reply received: {message['reply']}") # Save to database save_ai_comment(message['thread_id'], message['reply']) elif msg_type == "error": print(f"Error: {message['error']} ({message['code']})") elif msg_type == "pong": pass # Heartbeat response # Usage async def main(): token = os.getenv("RELAY_APP_TOKEN") client = RelayClient(token) # Start the client in the background client_task = asyncio.create_task(client.run()) # Send some events await asyncio.sleep(1) # Wait for connection await client.send_event( agent_id="athena", thread_id="task-123", payload={ "event": "comment.mention", "message": "@athena summarize this task", "task_id": "task-123", "task_title": "Q2 Roadmap Review", "context": "..." } ) # Let it run await client_task asyncio.run(main()) ``` ## Complete Example: Agent Receiving Events ```python import asyncio import websockets import json import os class RelayAgent: def __init__(self, token, agent_name): self.token = token self.agent_name = agent_name self.uri = "wss://api.relay.ckgworks.com/v1/ws/agent" self.ws = None self.sessions = {} async def connect(self): headers = {"Authorization": f"Bearer {self.token}"} self.ws = await websockets.connect( self.uri, extra_headers=headers ) async def send_tokens(self, event_id, response): """Stream response tokens""" for token in response: await self.ws.send(json.dumps({ "type": "token", "event_id": event_id, "token": token })) async def send_reply(self, event_id, full_reply, tokens_used): """Send final reply""" await self.ws.send(json.dumps({ "type": "reply", "event_id": event_id, "content": full_reply, "done": True, "metadata": { "tokens_used": tokens_used, "model": "claude-sonnet" } })) async def process_event(self, event): """Process an incoming event""" event_id = event["event_id"] session_key = event["session_key"] payload = event["payload"] try: # Get or create session if session_key not in self.sessions: self.sessions[session_key] = [] # Use the agent (simplified) response = f"Processed: {payload}" tokens = response.split() # Stream response await self.send_tokens(event_id, tokens) # Send final reply await self.send_reply(event_id, response, len(tokens)) except Exception as e: await self.ws.send(json.dumps({ "type": "error", "event_id": event_id, "error": str(e), "code": "AGENT_ERROR" })) async def listen(self): """Listen for messages""" async for message in self.ws: yield json.loads(message) async def run(self): """Main event loop""" await self.connect() print(f"{self.agent_name} connected to Relay") async for message in self.listen(): if message.get("type") == "event": await self.process_event(message) elif message.get("type") == "pong": pass # Heartbeat response # Usage async def main(): token = os.getenv("RELAY_AGENT_TOKEN_ATHENA") agent = RelayAgent(token, "Athena") await agent.run() asyncio.run(main()) ``` ## Testing Your Integration ### 1. Verify Connection ```bash # Test app connection curl -N -H "Authorization: Bearer rlk_portal_..." \ wss://api.relay.ckgworks.com/v1/ws/app ``` ### 2. Send Test Event Use the Relay dashboard test tool: 1. Go to **Test** tab 2. Select app: "Portal" 3. Select agent: "Athena" 4. Enter payload: `{ "message": "test" }` 5. Click **Send** Check event log to see result. ### 3. Monitor Event Logs Go to Relay dashboard: 1. Click **Event Logs** 2. Filter by app: "Portal" 3. Filter by agent: "Athena" 4. Look for "delivered" status 5. Click events to see details (payload, reply, latency) ## Troubleshooting ### Connection Refused **Problem:** WebSocket connection fails immediately. **Solutions:** - Verify endpoint is correct: `wss://api.relay.ckgworks.com/v1/ws/app` (not `/v1/app`) - Verify token format: `rlk_...` for apps, `rla_...` for agents - Verify token is not expired (rotated tokens have 1-hour grace) - Check firewall allows outbound WSS connections - Check that the token hasn't been revoked (go to dashboard and verify) ### Agent Offline / Not Responding **Problem:** Events sent to agent but no reply received. **Solutions:** - Check agent connection status in dashboard - Verify agent token is correct - Check if agent service is running - Look at agent logs for errors - Try sending a test event from dashboard - Restart agent service and check if it reconnects ### High Latency **Problem:** Events take too long to get a reply. **Solutions:** - Check agent load (is it overloaded?) - Check which AI model is being used (GPT-4 is slower than Sonnet) - Check agent session storage (Redis, database) for slowness - Check network latency between agent and Relay - Check if agent is processing long payloads ### Rejected Events **Problem:** Events return status "rejected". **Solutions:** - Check error code (usually `PERMISSION_DENIED`) - Go to agent's allowlist in dashboard - If app is not listed, click "Add App" and select it - Verify app and agent exist in dashboard - Check org_id matches (apps and agents must be in same organization) ### Lost Events **Problem:** Events sent but no reply received, event disappears. **Solutions:** - This shouldn't happen — all events are logged - Check dashboard event log, filter by app - Verify you're checking the correct organization - Look for error or rejection status - If truly missing, contact Relay support --- ## Final Notes Relay is intentionally simple: WebSocket, Bearer auth, opaque payloads, no routing complexity. Apps decide when to involve agents. Agents respond with tokens and final reply. Relay just bridges the gap. Build your integration with this spec, and use the OpenClaw Relay plugin for agents to handle the heavy lifting. Your AI integration will be live in minutes. For questions, see docs.relay.ckgworks.com or contact support.