← Создание агентов на Cloudflare / agents / runtime / communication
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 };
}Как работает гибернация
- Agent активен, обрабатывает подключения
- После периода бездействия без сообщений агент переходит в спящий режим (засыпает)
- Соединения WebSocket остаются открытыми (это обрабатывает Cloudflare)
- Когда приходит сообщение, агент просыпается
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:
- Vanilla JS:
AgentClientотagents/client - React:
useAgentхук изagents/react
См. Клиентский SDK для полной документации.