WebSocket Patterns for AI Applications
This guide covers advanced WebSocket patterns for building real-time, bidirectional AI applications with Claude Code, including collaborative features, multi-agent coordination, and interactive AI sessions.
Overview
WebSockets provide full-duplex communication channels over a single TCP connection, making them ideal for:
- Real-time collaborative AI coding
- Interactive AI chat sessions with context
- Multi-agent coordination
- Live debugging and pair programming
- Bidirectional data streaming
Core WebSocket Implementation
Server Setup with TypeScript
import { WebSocketServer, WebSocket } from 'ws';
import { Anthropic } from '@anthropic-ai/sdk';
import { v4 as uuidv4 } from 'uuid';
interface Session {
id: string;
userId: string;
ws: WebSocket;
context: Array<{ role: string; content: string }>;
activeStream?: AbortController;
metadata: {
connectedAt: Date;
lastActivity: Date;
model: string;
};
}
class AIWebSocketServer {
private wss: WebSocketServer;
private sessions: Map<string, Session> = new Map();
private anthropic: Anthropic;
private heartbeatInterval: NodeJS.Timeout;
constructor(port: number) {
this.wss = new WebSocketServer({ port });
this.anthropic = new Anthropic();
this.setupServer();
this.startHeartbeat();
}
private setupServer() {
this.wss.on('connection', (ws: WebSocket, request) => {
const sessionId = uuidv4();
const session = this.createSession(sessionId, ws);
this.sessions.set(sessionId, session);
// Send welcome message
this.sendMessage(ws, {
type: 'connection',
sessionId,
timestamp: new Date().toISOString()
});
// Set up event handlers
ws.on('message', (data) => this.handleMessage(session, data));
ws.on('close', () => this.handleDisconnect(sessionId));
ws.on('error', (error) => this.handleError(session, error));
ws.on('pong', () => this.handlePong(session));
});
}
private createSession(id: string, ws: WebSocket): Session {
return {
id,
userId: '', // Set after authentication
ws,
context: [],
metadata: {
connectedAt: new Date(),
lastActivity: new Date(),
model: 'claude-opus-4-20250514'
}
};
}
private async handleMessage(session: Session, data: WebSocket.Data) {
try {
const message = JSON.parse(data.toString());
session.metadata.lastActivity = new Date();
switch (message.type) {
case 'auth':
await this.handleAuth(session, message);
break;
case 'query':
await this.handleQuery(session, message);
break;
case 'stop':
this.handleStop(session);
break;
case 'context':
this.handleContextUpdate(session, message);
break;
case 'collaborate':
await this.handleCollaboration(session, message);
break;
default:
this.sendError(session, `Unknown message type: ${message.type}`);
}
} catch (error) {
this.sendError(session, error.message);
}
}
private async handleQuery(session: Session, message: any) {
// Abort any active stream
if (session.activeStream) {
session.activeStream.abort();
}
const controller = new AbortController();
session.activeStream = controller;
try {
const stream = await this.anthropic.messages.create({
model: session.metadata.model,
messages: [
...session.context,
{ role: 'user', content: message.content }
],
max_tokens: message.maxTokens || 1024,
stream: true
}, {
signal: controller.signal
});
let fullResponse = '';
for await (const event of stream) {
if (event.type === 'content_block_delta') {
fullResponse += event.delta.text;
this.sendMessage(session.ws, {
type: 'stream_delta',
delta: event.delta.text,
index: event.index
});
} else if (event.type === 'message_stop') {
// Update context
session.context.push(
{ role: 'user', content: message.content },
{ role: 'assistant', content: fullResponse }
);
this.sendMessage(session.ws, {
type: 'stream_complete',
fullResponse,
usage: event.usage
});
}
}
} catch (error) {
if (error.name !== 'AbortError') {
this.sendError(session, error.message);
}
} finally {
session.activeStream = undefined;
}
}
private sendMessage(ws: WebSocket, message: any) {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(message));
}
}
private sendError(session: Session, error: string) {
this.sendMessage(session.ws, {
type: 'error',
error,
timestamp: new Date().toISOString()
});
}
private startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
this.sessions.forEach((session) => {
if (session.ws.readyState === WebSocket.OPEN) {
session.ws.ping();
}
});
}, 30000); // 30 seconds
}
shutdown() {
clearInterval(this.heartbeatInterval);
this.sessions.forEach(session => {
session.ws.close();
});
this.wss.close();
}
}Robust Client Implementation
class AIWebSocketClient {
private ws: WebSocket | null = null;
private url: string;
private sessionId: string | null = null;
private reconnectAttempts = 0;
private maxReconnectAttempts = 5;
private reconnectDelay = 1000;
private pingInterval: number | null = null;
private messageQueue: any[] = [];
private isConnected = false;
constructor(url: string) {
this.url = url;
}
connect(options: {
onOpen?: () => void;
onMessage?: (data: any) => void;
onError?: (error: any) => void;
onClose?: () => void;
}) {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
this.isConnected = true;
this.reconnectAttempts = 0;
this.flushMessageQueue();
this.startPingInterval();
options.onOpen?.();
};
this.ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data);
if (data.type === 'connection') {
this.sessionId = data.sessionId;
}
options.onMessage?.(data);
} catch (error) {
console.error('Failed to parse message:', error);
}
};
this.ws.onerror = (error) => {
options.onError?.(error);
};
this.ws.onclose = () => {
this.isConnected = false;
this.stopPingInterval();
options.onClose?.();
this.handleReconnect(options);
};
}
private handleReconnect(options: any) {
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
console.error('Max reconnection attempts reached');
return;
}
this.reconnectAttempts++;
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
console.log(`Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
setTimeout(() => {
this.connect(options);
}, delay);
}
send(message: any) {
const msg = typeof message === 'string' ? message : JSON.stringify(message);
if (this.isConnected && this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(msg);
} else {
// Queue message for later delivery
this.messageQueue.push(msg);
}
}
private flushMessageQueue() {
while (this.messageQueue.length > 0 && this.isConnected) {
const msg = this.messageQueue.shift();
this.ws?.send(msg);
}
}
private startPingInterval() {
this.pingInterval = window.setInterval(() => {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 25000); // 25 seconds
}
private stopPingInterval() {
if (this.pingInterval) {
clearInterval(this.pingInterval);
this.pingInterval = null;
}
}
disconnect() {
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
this.stopPingInterval();
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
}Advanced Patterns
1. Collaborative AI Coding Session
interface CollaborativeSession {
id: string;
participants: Map<string, Participant>;
sharedContext: SharedContext;
aiAssistant: AIAssistant;
}
interface Participant {
id: string;
name: string;
ws: WebSocket;
cursor: CursorPosition;
permissions: Set<Permission>;
}
class CollaborativeAIServer {
private sessions: Map<string, CollaborativeSession> = new Map();
async createCollaborativeSession(hostId: string): Promise<string> {
const sessionId = uuidv4();
const session: CollaborativeSession = {
id: sessionId,
participants: new Map(),
sharedContext: new SharedContext(),
aiAssistant: new AIAssistant()
};
this.sessions.set(sessionId, session);
// Set up AI context observer
session.sharedContext.on('change', async (change) => {
await this.handleContextChange(session, change);
});
return sessionId;
}
async handleCollaborativeAction(
sessionId: string,
participantId: string,
action: any
) {
const session = this.sessions.get(sessionId);
if (!session) return;
const participant = session.participants.get(participantId);
if (!participant) return;
switch (action.type) {
case 'code_edit':
await this.handleCodeEdit(session, participant, action);
break;
case 'ai_request':
await this.handleAIRequest(session, participant, action);
break;
case 'cursor_move':
this.handleCursorMove(session, participant, action);
break;
case 'selection_change':
this.handleSelectionChange(session, participant, action);
break;
}
}
private async handleAIRequest(
session: CollaborativeSession,
participant: Participant,
action: any
) {
// Check permissions
if (!participant.permissions.has('ai_request')) {
this.sendToParticipant(participant, {
type: 'error',
error: 'Insufficient permissions'
});
return;
}
// Get relevant context
const context = await session.sharedContext.getRelevantContext(action.query);
// Stream AI response to all participants
const stream = await session.aiAssistant.streamResponse({
query: action.query,
context,
requestedBy: participant.name
});
for await (const chunk of stream) {
this.broadcastToSession(session, {
type: 'ai_stream',
chunk,
requestedBy: participant.id
});
}
}
private broadcastToSession(
session: CollaborativeSession,
message: any,
excludeParticipant?: string
) {
session.participants.forEach((participant, id) => {
if (id !== excludeParticipant) {
this.sendToParticipant(participant, message);
}
});
}
}
// Client-side collaborative features
class CollaborativeClient {
private ws: AIWebSocketClient;
private editor: CodeEditor;
private cursors: Map<string, RemoteCursor> = new Map();
constructor(editorElement: HTMLElement) {
this.editor = new CodeEditor(editorElement);
this.setupCollaborativeFeatures();
}
private setupCollaborativeFeatures() {
// Track local cursor movements
this.editor.on('cursorMove', (position) => {
this.ws.send({
type: 'cursor_move',
position
});
});
// Track code changes
this.editor.on('change', (change) => {
this.ws.send({
type: 'code_edit',
change
});
});
// Handle remote updates
this.ws = new AIWebSocketClient('wss://collab.example.com');
this.ws.connect({
onMessage: (data) => {
switch (data.type) {
case 'cursor_move':
this.updateRemoteCursor(data.participantId, data.position);
break;
case 'code_edit':
this.applyRemoteEdit(data.change);
break;
case 'ai_stream':
this.handleAIStream(data);
break;
}
}
});
}
requestAIAssistance(query: string) {
this.ws.send({
type: 'ai_request',
query
});
}
}2. Multi-Agent Coordination Pattern
interface AgentMessage {
from: string;
to: string;
type: 'request' | 'response' | 'broadcast';
payload: any;
correlationId: string;
}
class MultiAgentCoordinator {
private agents: Map<string, Agent> = new Map();
private messageRouter: MessageRouter;
constructor() {
this.messageRouter = new MessageRouter();
}
registerAgent(agent: Agent) {
this.agents.set(agent.id, agent);
agent.on('message', (message: AgentMessage) => {
this.routeMessage(message);
});
}
private async routeMessage(message: AgentMessage) {
if (message.type === 'broadcast') {
// Broadcast to all agents except sender
this.agents.forEach((agent, id) => {
if (id !== message.from) {
agent.receive(message);
}
});
} else {
// Direct message
const targetAgent = this.agents.get(message.to);
if (targetAgent) {
targetAgent.receive(message);
}
}
// Log for debugging and auditing
await this.messageRouter.log(message);
}
async coordinateTask(task: ComplexTask) {
// Decompose task into subtasks
const subtasks = await this.decomposeTask(task);
// Create execution plan
const plan = this.createExecutionPlan(subtasks);
// Execute plan with agent coordination
const results = await this.executePlan(plan);
return this.aggregateResults(results);
}
private async executePlan(plan: ExecutionPlan) {
const results: Map<string, any> = new Map();
for (const step of plan.steps) {
const agent = this.selectAgent(step.requirements);
const result = await this.executeWithAgent(agent, step, (progress) => {
// Stream progress updates
this.broadcastProgress({
step: step.id,
agent: agent.id,
progress
});
});
results.set(step.id, result);
// Update context for dependent steps
this.updateExecutionContext(plan, step.id, result);
}
return results;
}
}
// Specialized agents
class CodeAnalysisAgent extends Agent {
async analyze(code: string, options: AnalysisOptions) {
const ws = this.getWebSocket();
return new Promise((resolve, reject) => {
const correlationId = uuidv4();
ws.send({
type: 'analyze_code',
code,
options,
correlationId
});
this.once(`response:${correlationId}`, (result) => {
resolve(result);
});
setTimeout(() => {
reject(new Error('Analysis timeout'));
}, 30000);
});
}
}3. Interactive Debugging Session
class InteractiveDebugSession {
private ws: WebSocket;
private debugger: AIDebugger;
private breakpoints: Set<Breakpoint> = new Set();
private watchExpressions: Map<string, WatchExpression> = new Map();
async startDebugSession(code: string, error: Error) {
// Initialize AI debugger
this.debugger = new AIDebugger({
model: 'claude-opus-4-20250514',
context: { code, error }
});
// Connect to debug server
this.ws = new WebSocket('wss://debug.example.com');
this.ws.on('message', async (data) => {
const message = JSON.parse(data.toString());
switch (message.type) {
case 'breakpoint_hit':
await this.handleBreakpoint(message);
break;
case 'expression_evaluated':
this.updateWatchExpression(message);
break;
case 'ai_suggestion':
this.displayAISuggestion(message);
break;
}
});
// Start interactive session
await this.initializeSession();
}
private async handleBreakpoint(message: any) {
const { location, variables, callStack } = message;
// Get AI analysis of current state
const analysis = await this.debugger.analyzeState({
location,
variables,
callStack
});
// Stream analysis to client
this.ws.send(JSON.stringify({
type: 'ai_analysis',
analysis,
suggestions: analysis.suggestions
}));
// Set up interactive prompt
this.enableInteractiveMode({
onQuery: async (query) => {
const response = await this.debugger.interactiveQuery({
query,
currentState: { location, variables, callStack }
});
return response;
}
});
}
async stepThrough() {
// AI-guided step-through debugging
const nextStep = await this.debugger.suggestNextStep();
this.ws.send(JSON.stringify({
type: 'step',
action: nextStep.action,
reasoning: nextStep.reasoning
}));
}
}Performance Optimization
1. Message Compression
import pako from 'pako';
class CompressedWebSocket {
private ws: WebSocket;
private compressionThreshold = 1024; // 1KB
send(data: any) {
const message = JSON.stringify(data);
if (message.length > this.compressionThreshold) {
const compressed = pako.deflate(message);
this.ws.send(new Blob([compressed], {
type: 'application/octet-stream'
}));
} else {
this.ws.send(message);
}
}
private setupDecompression() {
this.ws.onmessage = async (event) => {
let data;
if (event.data instanceof Blob) {
const arrayBuffer = await event.data.arrayBuffer();
const decompressed = pako.inflate(new Uint8Array(arrayBuffer), {
to: 'string'
});
data = JSON.parse(decompressed);
} else {
data = JSON.parse(event.data);
}
this.handleMessage(data);
};
}
}2. Connection Pooling
class WebSocketPool {
private pool: WebSocket[] = [];
private activeConnections: Map<string, WebSocket> = new Map();
private maxPoolSize = 10;
private url: string;
constructor(url: string, poolSize: number = 5) {
this.url = url;
this.maxPoolSize = poolSize;
this.initializePool();
}
private initializePool() {
for (let i = 0; i < this.maxPoolSize; i++) {
this.createConnection();
}
}
private createConnection(): WebSocket {
const ws = new WebSocket(this.url);
ws.onopen = () => {
this.pool.push(ws);
};
ws.onclose = () => {
this.removeFromPool(ws);
// Replace closed connection
if (this.pool.length < this.maxPoolSize) {
this.createConnection();
}
};
return ws;
}
async getConnection(): Promise<WebSocket> {
// Wait for available connection
while (this.pool.length === 0) {
await new Promise(resolve => setTimeout(resolve, 100));
}
return this.pool.shift()!;
}
releaseConnection(ws: WebSocket) {
if (ws.readyState === WebSocket.OPEN) {
this.pool.push(ws);
} else {
this.createConnection();
}
}
}Security Best Practices
1. Authentication and Authorization
class SecureWebSocketServer {
private authenticate = async (ws: WebSocket, token: string): Promise<User> => {
try {
const user = await this.verifyToken(token);
if (!user) {
ws.send(JSON.stringify({
type: 'auth_error',
error: 'Invalid token'
}));
ws.close(1008, 'Invalid token');
throw new Error('Authentication failed');
}
return user;
} catch (error) {
ws.close(1008, 'Authentication failed');
throw error;
}
};
private authorizeAction = (user: User, action: string): boolean => {
const permissions = this.getPermissions(user);
return permissions.has(action);
};
handleConnection(ws: WebSocket, request: IncomingMessage) {
const token = this.extractToken(request);
this.authenticate(ws, token).then(user => {
const session = this.createAuthenticatedSession(user, ws);
ws.on('message', (data) => {
const message = JSON.parse(data.toString());
if (!this.authorizeAction(user, message.type)) {
ws.send(JSON.stringify({
type: 'error',
error: 'Unauthorized action'
}));
return;
}
this.handleAuthorizedMessage(session, message);
});
}).catch(error => {
console.error('Auth error:', error);
});
}
}2. Rate Limiting
class RateLimitedWebSocket {
private rateLimiter: Map<string, RateLimit> = new Map();
private checkRateLimit(sessionId: string): boolean {
const limit = this.rateLimiter.get(sessionId) || this.createRateLimit();
const now = Date.now();
const windowStart = now - limit.windowMs;
// Remove old entries
limit.requests = limit.requests.filter(time => time > windowStart);
if (limit.requests.length >= limit.maxRequests) {
return false;
}
limit.requests.push(now);
this.rateLimiter.set(sessionId, limit);
return true;
}
private createRateLimit(): RateLimit {
return {
requests: [],
maxRequests: 100,
windowMs: 60000 // 1 minute
};
}
}Testing WebSocket Applications
// WebSocket test utilities
class WebSocketTestClient {
private messages: any[] = [];
private ws: WebSocket;
async connect(url: string): Promise<void> {
return new Promise((resolve, reject) => {
this.ws = new WebSocket(url);
this.ws.onopen = () => resolve();
this.ws.onerror = (error) => reject(error);
this.ws.onmessage = (event) => {
this.messages.push(JSON.parse(event.data));
};
});
}
async sendAndWait(message: any, expectedType: string, timeout = 5000): Promise<any> {
this.ws.send(JSON.stringify(message));
const start = Date.now();
while (Date.now() - start < timeout) {
const response = this.messages.find(m => m.type === expectedType);
if (response) {
return response;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
throw new Error(`Timeout waiting for ${expectedType}`);
}
disconnect() {
this.ws.close();
}
}
// Example test
describe('WebSocket AI Server', () => {
let server: AIWebSocketServer;
let client: WebSocketTestClient;
beforeEach(async () => {
server = new AIWebSocketServer(8080);
client = new WebSocketTestClient();
await client.connect('ws://localhost:8080');
});
afterEach(() => {
client.disconnect();
server.shutdown();
});
it('should handle AI queries', async () => {
const response = await client.sendAndWait({
type: 'query',
content: 'Hello, Claude!'
}, 'stream_complete');
expect(response.fullResponse).toBeTruthy();
expect(response.usage).toBeDefined();
});
});