Agent WebSocket Connection
Agent connections work similarly to app connections but with key differences. Each agent connects with its own token and receives events from multiple apps.
Connection Details
Endpoint: wss://api.relay.ckgworks.com/v1/ws/agent
Authentication: Bearer token in the Authorization header
Authorization: Bearer rla_athena_k9x2p...
Connection Lifecycle
1. Establishing the Connection
Connect with your agent token in the WebSocket handshake:
import asyncio
import websockets
import json
async def connect_to_relay(agent_token):
uri = "wss://api.relay.ckgworks.com/v1/ws/agent"
headers = {
"Authorization": f"Bearer {agent_token}"
}
async with websockets.connect(uri, extra_headers=headers) as websocket:
print("Agent connected to Relay")
await handle_events(websocket)
Relay validates your agent token on the handshake. Invalid or revoked tokens are rejected immediately.
2. Receiving Events
Once connected, you receive event messages from apps:
{
"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",
"task_id": "task-123",
"task_title": "Q2 Roadmap Review"
}
}
Note: session_key does not include the agent_id, because the agent already knows who it is.
3. Responding to Events
Stream tokens and send a final reply:
async def process_event(websocket, event):
event_id = event["event_id"]
session_key = event["session_key"]
payload = event["payload"]
# Process through your AI system
async for token in generate_response(payload):
# Stream each token back to Relay
message = {
"type": "token",
"event_id": event_id,
"token": token
}
await websocket.send(json.dumps(message))
# Send final reply
final_message = {
"type": "reply",
"event_id": event_id,
"content": "Full response text here...",
"done": True,
"metadata": {
"tokens_used": 1500,
"model": "claude-sonnet",
"latency_ms": 2300
}
}
await websocket.send(json.dumps(final_message))
4. Heartbeat and Keep-Alive
Send periodic heartbeats to keep the connection alive:
async def heartbeat(websocket):
while True:
await asyncio.sleep(30)
await websocket.send(json.dumps({"type": "ping"}))
try:
response = await asyncio.wait_for(websocket.recv(), timeout=5)
data = json.loads(response)
if data.get("type") != "pong":
print("Expected pong, got:", data.get("type"))
except asyncio.TimeoutError:
print("No pong received, reconnecting...")
break
5. Reconnection with Exponential Backoff
If the connection drops, reconnect with exponential backoff:
async def connect_with_retry(agent_token, max_retries=5):
retry_count = 0
base_delay = 1
while retry_count < max_retries:
try:
uri = "wss://api.relay.ckgworks.com/v1/ws/agent"
headers = {"Authorization": f"Bearer {agent_token}"}
async with websockets.connect(uri, extra_headers=headers) as ws:
print("Connected successfully")
retry_count = 0
await handle_events(ws)
except Exception as e:
print(f"Connection failed: {e}")
retry_count += 1
if retry_count >= max_retries:
print("Max retries exceeded")
break
delay = min(base_delay * (2 ** (retry_count - 1)), 60)
print(f"Reconnecting in {delay}s...")
await asyncio.sleep(delay)
Node.js Example
const WebSocket = require('ws');
class AgentClient {
constructor(agentToken) {
this.agentToken = agentToken;
this.uri = 'wss://api.relay.ckgworks.com/v1/ws/agent';
this.ws = null;
}
async connect() {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(this.uri, {
headers: {
'Authorization': `Bearer ${this.agentToken}`
}
});
this.ws.on('open', () => {
console.log('Agent connected to Relay');
this.startHeartbeat();
this.setupMessageHandler();
resolve();
});
this.ws.on('error', (error) => {
console.error('Connection error:', error);
reject(error);
});
this.ws.on('close', () => {
console.log('Connection closed');
this.reconnect();
});
});
}
setupMessageHandler() {
this.ws.on('message', (data) => {
const message = JSON.parse(data);
if (message.type === 'ping') {
this.ws.send(JSON.stringify({ type: 'pong' }));
} else if (message.type === 'event') {
this.processEvent(message);
}
});
}
async processEvent(event) {
const { event_id, session_key, payload } = event;
try {
// Stream tokens
const response = await generateResponse(payload);
for await (const token of response) {
this.ws.send(JSON.stringify({
type: 'token',
event_id: event_id,
token: token
}));
}
// Send final reply
this.ws.send(JSON.stringify({
type: 'reply',
event_id: event_id,
content: 'Full response...',
done: true,
metadata: {
tokens_used: 1500,
model: 'claude-sonnet',
latency_ms: 2300
}
}));
} catch (error) {
// Send error
this.ws.send(JSON.stringify({
type: 'error',
event_id: event_id,
error: error.message,
code: 'AGENT_PROCESSING_ERROR'
}));
}
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30 * 1000);
}
async reconnect(retryCount = 0, maxRetries = 5) {
if (retryCount >= maxRetries) {
console.error('Max retries exceeded');
return;
}
const delay = Math.min(1000 * Math.pow(2, retryCount), 60000);
console.log(`Reconnecting in ${delay}ms...`);
setTimeout(() => {
this.connect().catch(() => this.reconnect(retryCount + 1, maxRetries));
}, delay);
}
}
// Usage
const agent = new AgentClient('rla_athena_k9x2p...');
agent.connect();
One Connection Per Agent
Important: Run one WebSocket connection per agent. If you have multiple agents (Athena, Klyve, Future Agent), use the OpenClaw plugin or run multiple agent server processes.
OpenClaw Plugin (one process, multiple agents):
┌─────────────────────────────────────────┐
│ OpenClaw Instance (one process) │
├─────────────────────────────────────────┤
│ Agent: Athena ──(WS)──► Relay │
│ Agent: Klyve ──(WS)──► Relay │
│ Agent: Future ──(WS)──► Relay │
└─────────────────────────────────────────┘
OR
Custom Agent Servers (separate processes):
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ Athena Server │ │ Klyve Server │ │ Future Server │
│ ──(WS)──► Relay │ │ ──(WS)──► Relay │ │ ──(WS)──► Relay │
└──────────────────┘ └──────────────────┘ └──────────────────┘
Session Key Format
When an event arrives, the session_key is formatted as:
relay:{app_id}:{thread_id}
For example:
relay:portal:task-123— event from Portal for task-123relay:academy:lesson-101— event from Academy for lesson-101relay:flow:job-456— event from Flow for job-456
Use this session key to create or resume conversations in your AI system. See Processing Events for details.
Connection Best Practices
Do
- Keep one persistent connection per agent
- Send heartbeats every 30-60 seconds
- Use exponential backoff for reconnection
- Handle all message types (event, ping, pong)
- Respond to events promptly
Don't
- Create new connections for each event
- Hardcode agent tokens
- Ignore heartbeat responses
- Use fixed delays for retries
- Assume events arrive in order
Troubleshooting
Connection rejected immediately?
- Verify agent token prefix is
rla_ - Check token hasn't been rotated (old token expired)
- Confirm you copied the entire token
No events arriving?
- Verify agent is connected (check dashboard)
- Check allowlist — is the app permitted to reach this agent?
- Verify the app is actually registered and sending events
Connection drops frequently?
- Check network stability
- Increase heartbeat frequency (every 15-20 seconds)
- Monitor Relay API logs for issues
"Pong" never arrives?
- Increase timeout from 5 to 10 seconds
- Check firewall/proxy isn't filtering WebSocket frames
- Verify network connectivity to api.relay.ckgworks.com