Skip to main content

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-123
  • relay:academy:lesson-101 — event from Academy for lesson-101
  • relay: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