INTEGRITY Документация

WebSockets

Agents поддерживают соединения WebSocket для двусторонней связи в реальном времени. Эта страница посвящена обработке WebSocket на стороне сервера. Про подключение на стороне клиента читайте в Клиентский SDK.

Хуки жизненного цикла

У Agents есть несколько хуков жизненного цикла, которые срабатывают в разные моменты:

Хук При вызове
onStart(props?) Один раз при первом запуске агента (до установления каких-либо подключений)
onRequest(request) Когда получен HTTP-запрос (не WebSocket)
onConnect(connection, ctx) Когда устанавливается новое WebSocket-соединение
onMessage(connection, message) Когда получено сообщение WebSocket
onClose(connection, code, reason, wasClean) Когда WebSocket-соединение закрывается
onError(connection, error) Когда в соединении возникает ошибка WebSocket
onError(error) Когда возникает ошибка на уровне сервера (не привязанная к конкретному соединению)
shouldSendProtocolMessages(connection, ctx) Нужно ли отправлять протокольные сообщения (identity, state, MCP) в это соединение. По умолчанию: true

onStart

onStart() вызывается один раз при первом запуске агента, до установления каких-либо соединений:

export class MyAgent extends Agent {
	async onStart() {
		// Initialize resources
		console.log(`Agent ${this.name} starting...`);

		// Load data from storage
		const savedData = this.sql`SELECT * FROM cache`;
		for (const row of savedData) {
			// Rebuild in-memory state from persistent storage
		}
	}

	onConnect(connection) {
		// By the time connections arrive, onStart has completed
	}
}
export class MyAgent extends Agent {
	async onStart() {
		// Initialize resources
		console.log(`Agent ${this.name} starting...`);

		// Load data from storage
		const savedData = this.sql`SELECT * FROM cache`;
		for (const row of savedData) {
			// Rebuild in-memory state from persistent storage
		}
	}

	onConnect(connection: Connection) {
		// By the time connections arrive, onStart has completed
	}
}

Обработка соединений

Определите onConnect и onMessage методы в вашем Agent, чтобы принимать соединения WebSocket:

import { Agent, Connection, ConnectionContext, WSMessage } from "agents";

export class ChatAgent extends Agent {
	async onConnect(connection, ctx) {
		// Connections are automatically accepted
		// Access the original request for auth, headers, cookies
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");

		if (!token) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on this connection
		connection.setState({ authenticated: true });
	}

	async onMessage(connection, message) {
		if (typeof message === "string") {
			// Handle text message
			const data = JSON.parse(message);
			connection.send(JSON.stringify({ received: data }));
		}
	}
}
import { Agent, Connection, ConnectionContext, WSMessage } from "agents";

export class ChatAgent extends Agent {
	async onConnect(connection: Connection, ctx: ConnectionContext) {
		// Connections are automatically accepted
		// Access the original request for auth, headers, cookies
		const url = new URL(ctx.request.url);
		const token = url.searchParams.get("token");

		if (!token) {
			connection.close(4001, "Unauthorized");
			return;
		}

		// Store user info on this connection
		connection.setState({ authenticated: true });
	}

	async onMessage(connection: Connection, message: WSMessage) {
		if (typeof message === "string") {
			// Handle text message
			const data = JSON.parse(message);
			connection.send(JSON.stringify({ received: data }));
		}
	}
}

Объект подключения

У каждого подключённого клиента есть уникальный Connection объект:

Свойство/Метод Тип Описание
id string Уникальный идентификатор этого соединения
uri string | null URL исходного запроса на обновление WebSocket. Сохраняется при гибернации
state State Объект состояния для каждого соединения
setState(state) void Обновите состояние соединения
send(message) void Отправить сообщение этому клиенту
close(code?, reason?) void Закрыть соединение
tags readonly string[] Теги, назначенные через getConnectionTags. Всегда включает ID подключения в качестве первого тега
server string Имя экземпляра агента (то же, что и this.name в Agent)

Состояние для каждого соединения

Сохраняйте данные, специфичные для каждого подключения (информация о пользователе, предпочтения и т. д.):

export class ChatAgent extends Agent {
	async onConnect(connection, ctx) {
		const userId = new URL(ctx.request.url).searchParams.get("userId");

		connection.setState({
			userId: userId || "anonymous",
			role: "user",
			joinedAt: Date.now(),
		});
	}

	async onMessage(connection, message) {
		// Access connection-specific state
		console.log(`Message from ${connection.state.userId}`);
	}
}
interface ConnectionState {
	userId: string;
	role: "admin" | "user";
	joinedAt: number;
}

export class ChatAgent extends Agent {
	async onConnect(
		connection: Connection<ConnectionState>,
		ctx: ConnectionContext,
	) {
		const userId = new URL(ctx.request.url).searchParams.get("userId");

		connection.setState({
			userId: userId || "anonymous",
			role: "user",
			joinedAt: Date.now(),
		});
	}

	async onMessage(connection: Connection<ConnectionState>, message: WSMessage) {
		// Access connection-specific state
		console.log(`Message from ${connection.state.userId}`);
	}
}

Рассылка всем клиентам

Используйте this.broadcast() чтобы отправить сообщение всем подключённым клиентам:

export class ChatAgent extends Agent {
	async onMessage(connection, message) {
		// Broadcast to all connected clients
		this.broadcast(
			JSON.stringify({
				from: connection.id,
				message: message,
				timestamp: Date.now(),
			}),
		);
	}

	// Broadcast from any method
	async notifyAll(event, data) {
		this.broadcast(JSON.stringify({ event, data }));
	}
}
export class ChatAgent extends Agent {
	async onMessage(connection: Connection, message: WSMessage) {
		// Broadcast to all connected clients
		this.broadcast(
			JSON.stringify({
				from: connection.id,
				message: message,
				timestamp: Date.now(),
			}),
		);
	}

	// Broadcast from any method
	async notifyAll(event: string, data: unknown) {
		this.broadcast(JSON.stringify({ event, data }));
	}
}

Исключение подключений

Передайте массив ID подключений, которые нужно исключить из рассылки:

// Broadcast to everyone except the sender
this.broadcast(
	JSON.stringify({ type: "user-typing", userId: "123" }),
	[connection.id], // Do not send to the originator
);
// Broadcast to everyone except the sender
this.broadcast(
	JSON.stringify({ type: "user-typing", userId: "123" }),
	[connection.id], // Do not send to the originator
);

Теги подключения

Помечайте подключения тегами для удобной фильтрации. Переопределите getConnectionTags() чтобы назначать теги при установке соединения:

export class ChatAgent extends Agent {
	getConnectionTags(connection, ctx) {
		const url = new URL(ctx.request.url);
		const role = url.searchParams.get("role");

		const tags = [];
		if (role === "admin") tags.push("admin");
		if (role === "moderator") tags.push("moderator");

		return tags; // Up to 9 tags, max 256 chars each
	}

	// Later, broadcast only to admins
	notifyAdmins(message) {
		for (const conn of this.getConnections("admin")) {
			conn.send(message);
		}
	}
}
export class ChatAgent extends Agent {
	getConnectionTags(connection: Connection, ctx: ConnectionContext): string[] {
		const url = new URL(ctx.request.url);
		const role = url.searchParams.get("role");

		const tags: string[] = [];
		if (role === "admin") tags.push("admin");
		if (role === "moderator") tags.push("moderator");

		return tags; // Up to 9 tags, max 256 chars each
	}

	// Later, broadcast only to admins
	notifyAdmins(message: string) {
		for (const conn of this.getConnections("admin")) {
			conn.send(message);
		}
	}
}

Методы управления подключением

Метод Подпись Описание
getConnections (tag?: string) => Iterable<Connection> Получить все подключения, опционально по тегу
getConnection (id: string) => Connection | undefined Получить подключение по ID
getConnectionTags (connection, ctx) => string[] Переопределите, чтобы маркировать соединения
broadcast (message, without?: string[]) => void Отправить всем подключениям
isConnectionReadonly (connection) => boolean Проверяет, является ли соединение только для чтения
isConnectionProtocolEnabled (connection) => boolean Проверяет, включены ли для этого соединения сообщения протокола

Обработка двоичных данных

Сообщения могут быть строками или бинарными данными (ArrayBuffer / ArrayBufferView):

export class FileAgent extends Agent {
	async onMessage(connection, message) {
		if (message instanceof ArrayBuffer) {
			// Handle binary upload
			const bytes = new Uint8Array(message);
			await this.processFile(bytes);
			connection.send(
				JSON.stringify({ status: "received", size: bytes.length }),
			);
		} else if (typeof message === "string") {
			// Handle text command
			const command = JSON.parse(message);
			// ...
		}
	}
}
export class FileAgent extends Agent {
	async onMessage(connection: Connection, message: WSMessage) {
		if (message instanceof ArrayBuffer) {
			// Handle binary upload
			const bytes = new Uint8Array(message);
			await this.processFile(bytes);
			connection.send(
				JSON.stringify({ status: "received", size: bytes.length }),
			);
		} else if (typeof message === "string") {
			// Handle text command
			const command = JSON.parse(message);
			// ...
		}
	}
}

Обработка ошибок и закрытия

Обрабатывайте ошибки соединения и отключения. Обработчик onError метод имеет две перегрузки: одна для ошибок соединения WebSocket, другая для ошибок уровня сервера:

export class ChatAgent extends Agent {
	// WebSocket connection error

	// Server-level error (not tied to a specific connection)

	onError(connectionOrError, error) {
		if (error) {
			console.error(`Connection ${connectionOrError.id} error:`, error);
		} else {
			console.error("Server error:", connectionOrError);
		}
	}

	async onClose(connection, code, reason, wasClean) {
		console.log(`Connection ${connection.id} closed: ${code} ${reason}`);

		this.broadcast(
			JSON.stringify({
				event: "user-left",
				userId: connection.state?.userId,
			}),
		);
	}
}
export class ChatAgent extends Agent {
	// WebSocket connection error
	onError(connection: Connection, error: unknown): void;
	// Server-level error (not tied to a specific connection)
	onError(error: unknown): void;
	onError(connectionOrError: Connection | unknown, error?: unknown) {
		if (error) {
			console.error(
				`Connection ${(connectionOrError as Connection).id} error:`,
				error,
			);
		} else {
			console.error("Server error:", connectionOrError);
		}
	}

	async onClose(
		connection: Connection,
		code: number,
		reason: string,
		wasClean: boolean,
	) {
		console.log(`Connection ${connection.id} closed: ${code} ${reason}`);

		this.broadcast(
			JSON.stringify({
				event: "user-left",
				userId: connection.state?.userId,
			}),
		);
	}
}

По умолчанию onError реализация записывает ошибку в лог и снова её выбрасывает. Переопределите её, если нужно добавить собственную обработку ошибок, отчётность или логику восстановления.

Типы сообщений

Тип Описание
string Текстовое сообщение (обычно JSON)
ArrayBuffer Двоичные данные
ArrayBufferView Типизированное представление массива для двоичных данных

Гибернация

Agents поддерживают гибернацию: они могут засыпать при бездействии и просыпаться по мере необходимости. Это экономит ресурсы, сохраняя при этом соединения WebSocket.

Включение гибернации

Гибернация включена по умолчанию. Чтобы отключить её:

export class AlwaysOnAgent extends Agent {
	static options = { hibernate: false };
}
export class AlwaysOnAgent extends Agent {
	static options = { hibernate: false };
}

Как работает гибернация

  1. Agent активен, обрабатывает подключения
  2. После периода бездействия без сообщений агент переходит в спящий режим (засыпает)
  3. Соединения WebSocket остаются открытыми (это обрабатывает Cloudflare)
  4. Когда приходит сообщение, агент просыпается
  5. onMessage вызывается как обычно

Что сохраняется при гибернации

Сохраняется Не сохраняется
this.state (состояние агента) Переменные в памяти
connection.state Таймеры/интервалы
Данные SQLite (this.sql) Незавершённые Promise
Метаданные подключения Локальные кэши

Сохраняйте важные данные в this.state или SQLite, а не в свойствах класса:

export class MyAgent extends Agent {
	initialState = { counter: 0 };

	// Do not do this - lost on hibernation
	localCounter = 0;

	onMessage(connection, message) {
		// Persists across hibernation
		this.setState({ counter: this.state.counter + 1 });

		// Lost after hibernation
		this.localCounter++;
	}
}
export class MyAgent extends Agent<Env, { counter: number }> {
	initialState = { counter: 0 };

	// Do not do this - lost on hibernation
	private localCounter = 0;

	onMessage(connection: Connection, message: WSMessage) {
		// Persists across hibernation
		this.setState({ counter: this.state.counter + 1 });

		// Lost after hibernation
		this.localCounter++;
	}
}

Типичные паттерны

Отслеживание присутствия

Отслеживайте, кто сейчас на связи, с помощью состояния для каждого соединения. Состояние соединения автоматически очищается при отключении пользователей:

export class PresenceAgent extends Agent {
	onConnect(connection, ctx) {
		const url = new URL(ctx.request.url);
		const name = url.searchParams.get("name") || "Anonymous";

		connection.setState({
			name,
			joinedAt: Date.now(),
			lastSeen: Date.now(),
		});

		// Send current presence to new user
		connection.send(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);

		// Notify others that someone joined
		this.broadcastPresence();
	}

	onClose(connection) {
		// No manual cleanup needed - connection state is automatically gone
		this.broadcastPresence();
	}

	onMessage(connection, message) {
		if (message === "ping") {
			connection.setState((prev) => ({
				...prev,
				lastSeen: Date.now(),
			}));
			connection.send("pong");
		}
	}

	getPresence() {
		const users = {};
		for (const conn of this.getConnections()) {
			if (conn.state) {
				users[conn.id] = {
					name: conn.state.name,
					lastSeen: conn.state.lastSeen,
				};
			}
		}
		return users;
	}

	broadcastPresence() {
		this.broadcast(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);
	}
}
type UserState = {
	name: string;
	joinedAt: number;
	lastSeen: number;
};

export class PresenceAgent extends Agent {
	onConnect(connection: Connection<UserState>, ctx: ConnectionContext) {
		const url = new URL(ctx.request.url);
		const name = url.searchParams.get("name") || "Anonymous";

		connection.setState({
			name,
			joinedAt: Date.now(),
			lastSeen: Date.now(),
		});

		// Send current presence to new user
		connection.send(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);

		// Notify others that someone joined
		this.broadcastPresence();
	}

	onClose(connection: Connection) {
		// No manual cleanup needed - connection state is automatically gone
		this.broadcastPresence();
	}

	onMessage(connection: Connection<UserState>, message: WSMessage) {
		if (message === "ping") {
			connection.setState((prev) => ({
				...prev!,
				lastSeen: Date.now(),
			}));
			connection.send("pong");
		}
	}

	private getPresence() {
		const users: Record<string, { name: string; lastSeen: number }> = {};
		for (const conn of this.getConnections<UserState>()) {
			if (conn.state) {
				users[conn.id] = {
					name: conn.state.name,
					lastSeen: conn.state.lastSeen,
				};
			}
		}
		return users;
	}

	private broadcastPresence() {
		this.broadcast(
			JSON.stringify({
				type: "presence",
				users: this.getPresence(),
			}),
		);
	}
}

Чат-комната с рассылкой

export class ChatRoom extends Agent {
	onConnect(connection, ctx) {
		const url = new URL(ctx.request.url);
		const username = url.searchParams.get("username") || "Anonymous";

		connection.setState({ username });

		// Notify others
		this.broadcast(
			JSON.stringify({
				type: "join",
				user: username,
				timestamp: Date.now(),
			}),
			[connection.id], // Do not send to the joining user
		);
	}

	onMessage(connection, message) {
		if (typeof message !== "string") return;

		const { username } = connection.state;

		this.broadcast(
			JSON.stringify({
				type: "message",
				user: username,
				text: message,
				timestamp: Date.now(),
			}),
		);
	}

	onClose(connection) {
		const { username } = connection.state || {};
		if (username) {
			this.broadcast(
				JSON.stringify({
					type: "leave",
					user: username,
					timestamp: Date.now(),
				}),
			);
		}
	}
}
type Message = {
	type: "message" | "join" | "leave";
	user: string;
	text?: string;
	timestamp: number;
};

export class ChatRoom extends Agent {
	onConnect(connection: Connection, ctx: ConnectionContext) {
		const url = new URL(ctx.request.url);
		const username = url.searchParams.get("username") || "Anonymous";

		connection.setState({ username });

		// Notify others
		this.broadcast(
			JSON.stringify({
				type: "join",
				user: username,
				timestamp: Date.now(),
			} satisfies Message),
			[connection.id], // Do not send to the joining user
		);
	}

	onMessage(connection: Connection, message: WSMessage) {
		if (typeof message !== "string") return;

		const { username } = connection.state as { username: string };

		this.broadcast(
			JSON.stringify({
				type: "message",
				user: username,
				text: message,
				timestamp: Date.now(),
			} satisfies Message),
		);
	}

	onClose(connection: Connection) {
		const { username } = (connection.state as { username: string }) || {};
		if (username) {
			this.broadcast(
				JSON.stringify({
					type: "leave",
					user: username,
					timestamp: Date.now(),
				} satisfies Message),
			);
		}
	}
}

Подавление сообщений протокола

По умолчанию агенты отправляют JSON текстовые фреймы (идентичность, синхронизация состояния, списки MCP-серверов) на каждое подключение. Переопределите shouldSendProtocolMessages чтобы подавлять их для отдельных соединений, например для клиентов, которые работают только с бинарными данными и не умеют обрабатывать текстовые JSON-кадры:

export class IoTAgent extends Agent {
	shouldSendProtocolMessages(connection, ctx) {
		const url = new URL(ctx.request.url);
		return url.searchParams.get("protocol") !== "binary";
	}
}
export class IoTAgent extends Agent {
	shouldSendProtocolMessages(
		connection: Connection,
		ctx: ConnectionContext,
	): boolean {
		const url = new URL(ctx.request.url);
		return url.searchParams.get("protocol") !== "binary";
	}
}

Когда это возвращает false, соединение не получает фреймы с идентичностью, состоянием или списком серверов MCP: ни при подключении, ни через рассылки. Соединение по-прежнему может отправлять и принимать обычные сообщения, использовать RPC и участвовать в любой коммуникации, не относящейся к протоколу.

Используйте isConnectionProtocolEnabled(connection) чтобы проверить статус любого соединения во время выполнения.

Свойства Agent

Эти свойства доступны в this внутри любого метода агента:

Свойство Тип Описание
this.name string Имя экземпляра этого агента
this.state State Текущее состояние агента (лениво загружаемое из SQLite)
this.env Env Привязки окружения Worker
this.ctx DurableObjectState Контекст Durable Object (хранилище, будильники и т. д.)
this.sql тег шаблона SQL template-тег для выполнения запросов к хранилищу SQLite агента
this.mcp MCPClientManager Менеджер MCP-клиента для подключения к внешним MCP-серверам

Подключение со стороны клиентов

Для подключений из браузера используйте клиентский SDK Agents:

См. Клиентский SDK для полной документации.

Следующие шаги

Синхронизация состояния

Синхронизация состояния между агентами и клиентами.

Вызываемые методы

RPC поверх WebSocket для вызовов методов.

Аутентификация между доменами

Защищайте WebSocket-соединения между доменами.