← Создание агентов на Cloudflare / agents / runtime / lifecycle
Хранение и синхронизация состояния
Agents предоставляют встроенное управление состоянием с автоматическим сохранением и синхронизацией в реальном времени по всем подключённым клиентам.
Обзор
Состояние в рамках Agent:
- Постоянный - автоматически сохраняется в SQLite, переживает перезапуски и гибернацию
- Синхронизировано - изменения мгновенно рассылаются всем подключённым клиентам WebSocket
- Двунаправленный - состояние могут обновлять как сервер, так и клиенты
- Типобезопасный - полная поддержка TypeScript с generics
- Немедленно согласованный - Чтение собственных записей
- Потокобезопасный - Безопасно при параллельных обновлениях
- Быстро - Состояние размещается там же, где выполняется Agent
Состояние Agent хранится в базе данных SQL, встроенной в каждый отдельный экземпляр Agent. Вы можете взаимодействовать с ним, используя более высокоуровневый this.setState API (рекомендуется), который позволяет синхронизировать состояние и запускать события при его изменении, либо напрямую запрашивая базу данных с помощью this.sql.
import { Agent } from "agents";
export class GameAgent extends Agent {
// Default state for new agents
initialState = {
players: [],
score: 0,
status: "waiting",
};
// React to state changes
onStateChanged(state, source) {
if (source !== "server" && state.players.length >= 2) {
// Client added a player, start the game
this.setState({ ...state, status: "playing" });
}
}
addPlayer(name) {
this.setState({
...this.state,
players: [...this.state.players, name],
});
}
}import { Agent } from "agents";
type GameState = {
players: string[];
score: number;
status: "waiting" | "playing" | "finished";
};
export class GameAgent extends Agent<Env, GameState> {
// Default state for new agents
initialState: GameState = {
players: [],
score: 0,
status: "waiting",
};
// React to state changes
onStateChanged(state: GameState, source: Connection | "server") {
if (source !== "server" && state.players.length >= 2) {
// Client added a player, start the game
this.setState({ ...state, status: "playing" });
}
}
addPlayer(name: string) {
this.setState({
...this.state,
players: [...this.state.players, name],
});
}
}Определение начального состояния
Используйте initialState свойство для определения значений по умолчанию для новых экземпляров агента:
export class ChatAgent extends Agent {
initialState = {
messages: [],
settings: { theme: "dark", notifications: true },
lastActive: null,
};
}type State = {
messages: Message[];
settings: UserSettings;
lastActive: string | null;
};
export class ChatAgent extends Agent<Env, State> {
initialState: State = {
messages: [],
settings: { theme: "dark", notifications: true },
lastActive: null,
};
}Типобезопасность
Второй generic-параметр Agent определяет тип вашего состояния:
// State is fully typed
export class MyAgent extends Agent {
initialState = { count: 0 };
increment() {
// TypeScript knows this.state is MyState
this.setState({ count: this.state.count + 1 });
}
}// State is fully typed
export class MyAgent extends Agent<Env, MyState> {
initialState: MyState = { count: 0 };
increment() {
// TypeScript knows this.state is MyState
this.setState({ count: this.state.count + 1 });
}
}Когда применяется начальное состояние
Начальное состояние применяется лениво при первом обращении, а не при каждом пробуждении:
- Новый агент -
initialStateиспользуется и сохраняется - Существующий агент - Сохранённое состояние загружается из SQLite
- Нет
initialStateопределено -this.stateэтоundefined
class MyAgent extends Agent {
initialState = { count: 0 };
async onStart() {
// Safe to access - returns initialState if new, or persisted state
console.log("Current count:", this.state.count);
}
}class MyAgent extends Agent<Env, { count: number }> {
initialState = { count: 0 };
async onStart() {
// Safe to access - returns initialState if new, or persisted state
console.log("Current count:", this.state.count);
}
}Чтение состояния
Получайте доступ к текущему состоянию через this.state геттер:
class MyAgent extends Agent {
async onRequest(request) {
// Read current state
const { players, status } = this.state;
if (status === "waiting" && players.length < 2) {
return new Response("Waiting for players...");
}
return Response.json(this.state);
}
}class MyAgent extends Agent<
Env,
{ players: string[]; status: "waiting" | "playing" | "finished" }
> {
async onRequest(request: Request) {
// Read current state
const { players, status } = this.state;
if (status === "waiting" && players.length < 2) {
return new Response("Waiting for players...");
}
return Response.json(this.state);
}
}Неопределённое состояние
Если вы не определите initialState, this.state возвращает undefined:
export class MinimalAgent extends Agent {
// No initialState defined
async onConnect(connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}export class MinimalAgent extends Agent {
// No initialState defined
async onConnect(connection: Connection) {
if (!this.state) {
// First time - initialize state
this.setState({ initialized: true });
}
}
}Обновление состояния
Используйте setState() чтобы обновить состояние. Это:
- Сохраняет в SQLite (постоянно)
- Рассылает всем подключённым клиентам (кроме подключений, где
shouldSendProtocolMessagesвозвращеноfalse) - Запускает
onStateChanged()(после broadcast; по возможности)
// Replace entire state
this.setState({
players: ["Alice", "Bob"],
score: 0,
status: "playing",
});
// Update specific fields (spread existing state)
this.setState({
...this.state,
score: this.state.score + 10,
});// Replace entire state
this.setState({
players: ["Alice", "Bob"],
score: 0,
status: "playing",
});
// Update specific fields (spread existing state)
this.setState({
...this.state,
score: this.state.score + 10,
});Состояние должно быть сериализуемым
Состояние хранится в формате JSON, поэтому оно должно быть сериализуемым:
// Good - plain objects, arrays, primitives
this.setState({
items: ["a", "b", "c"],
count: 42,
active: true,
metadata: { key: "value" },
});
// Bad - functions, classes, circular references
// Functions do not serialize
// Dates become strings, lose methods
// Circular references fail
// For dates, use ISO strings
this.setState({
createdAt: new Date().toISOString(),
});// Good - plain objects, arrays, primitives
this.setState({
items: ["a", "b", "c"],
count: 42,
active: true,
metadata: { key: "value" },
});
// Bad - functions, classes, circular references
// Functions do not serialize
// Dates become strings, lose methods
// Circular references fail
// For dates, use ISO strings
this.setState({
createdAt: new Date().toISOString(),
});Реакция на изменения состояния
Переопределить onStateChanged() чтобы реагировать на изменения состояния (уведомления и побочные эффекты):
class MyAgent extends Agent {
onStateChanged(state, source) {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}
}class MyAgent extends Agent<Env, GameState> {
onStateChanged(state: GameState, source: Connection | "server") {
console.log("State updated:", state);
console.log("Updated by:", source === "server" ? "server" : source.id);
}
}Параметр source
source показывает, кто инициировал обновление:
| Значение | Значение |
|---|---|
"server" |
Agent вызвал setState() |
Connection |
Клиент отправил состояние через WebSocket |
Это полезно для:
- Избегание бесконечных циклов (не реагируйте на собственные обновления)
- Валидация ввода клиента
- Запуск побочных эффектов только по действиям клиента
class MyAgent extends Agent {
onStateChanged(state, source) {
// Ignore server-initiated updates
if (source === "server") return;
// A client updated state - validate and process
const connection = source;
console.log(`Client ${connection.id} updated state`);
// Maybe trigger something based on the change
if (state.status === "submitted") {
this.processSubmission(state);
}
}
}class MyAgent extends Agent<
Env,
{ status: "waiting" | "playing" | "finished" }
> {
onStateChanged(state: GameState, source: Connection | "server") {
// Ignore server-initiated updates
if (source === "server") return;
// A client updated state - validate and process
const connection = source;
console.log(`Client ${connection.id} updated state`);
// Maybe trigger something based on the change
if (state.status === "submitted") {
this.processSubmission(state);
}
}
}Типичный паттерн: действия, инициируемые клиентом
class MyAgent extends Agent {
onStateChanged(state, source) {
if (source === "server") return;
// Client added a message
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage && !lastMessage.processed) {
// Process and update
this.setState({
...state,
messages: state.messages.map((m) =>
m.id === lastMessage.id ? { ...m, processed: true } : m,
),
});
}
}
}class MyAgent extends Agent<Env, { messages: Message[] }> {
onStateChanged(state: State, source: Connection | "server") {
if (source === "server") return;
// Client added a message
const lastMessage = state.messages[state.messages.length - 1];
if (lastMessage && !lastMessage.processed) {
// Process and update
this.setState({
...state,
messages: state.messages.map((m) =>
m.id === lastMessage.id ? { ...m, processed: true } : m,
),
});
}
}
}Валидация обновлений состояния
Если вы хотите проверять или отклонять обновления состояния, переопределите validateStateChange():
- Выполняется до сохранения и рассылки
- Должен быть синхронным
- Исключение прерывает обновление
class MyAgent extends Agent {
validateStateChange(nextState, source) {
// Example: reject negative scores
if (nextState.score < 0) {
throw new Error("score cannot be negative");
}
// Example: only allow certain status transitions
if (this.state.status === "finished" && nextState.status !== "finished") {
throw new Error("Cannot restart a finished game");
}
}
}class MyAgent extends Agent<Env, GameState> {
validateStateChange(nextState: GameState, source: Connection | "server") {
// Example: reject negative scores
if (nextState.score < 0) {
throw new Error("score cannot be negative");
}
// Example: only allow certain status transitions
if (this.state.status === "finished" && nextState.status !== "finished") {
throw new Error("Cannot restart a finished game");
}
}
}Синхронизация состояния на стороне клиента
Состояние автоматически синхронизируется с подключёнными клиентами.
React (useAgent)
import { useAgent } from "agents/react";
function GameUI() {
const agent = useAgent({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state, source) => {
console.log("State updated:", state);
},
});
// Push state to agent
const addPlayer = (name) => {
agent.setState({
...agent.state,
players: [...agent.state.players, name],
});
};
return <div>Players: {agent.state?.players.join(", ")}</div>;
}import { useAgent } from "agents/react";
function GameUI() {
const agent = useAgent({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state, source) => {
console.log("State updated:", state);
}
});
// Push state to agent
const addPlayer = (name: string) => {
agent.setState({
...agent.state,
players: [...agent.state.players, name]
});
};
return <div>Players: {agent.state?.players.join(", ")}</div>;
}Vanilla JS (AgentClient)
import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state) => {
document.getElementById("score").textContent = state.score;
},
});
// Push state update
client.setState({ ...client.state, score: 100 });import { AgentClient } from "agents/client";
const client = new AgentClient({
agent: "game-agent",
name: "room-123",
onStateUpdate: (state) => {
document.getElementById("score").textContent = state.score;
},
});
// Push state update
client.setState({ ...client.state, score: 100 });Поток состояния
flowchart TD
subgraph Agent
S["this.state<br/>(persisted in SQLite)"]
end
subgraph Clients
C1["Client 1"]
C2["Client 2"]
C3["Client 3"]
end
C1 & C2 & C3 -->|setState| S
S -->|broadcast via WebSocket| C1 & C2 & C3
Состояние из Workflows
При использовании Workflows, вы можете обновлять состояние агента из шагов workflow:
// In your workflow
class MyWorkflow extends Workflow {
async run(event, step) {
// Replace entire state
await step.updateAgentState({ status: "processing", progress: 0 });
// Merge partial updates (preserves other fields)
await step.mergeAgentState({ progress: 50 });
// Reset to initialState
await step.resetAgentState();
return result;
}
}// In your workflow
class MyWorkflow extends Workflow<Env> {
async run(event: AgentWorkflowEvent, step: AgentWorkflowStep) {
// Replace entire state
await step.updateAgentState({ status: "processing", progress: 0 });
// Merge partial updates (preserves other fields)
await step.mergeAgentState({ progress: 50 });
// Reset to initialState
await step.resetAgentState();
return result;
}
}Это устойчивые операции: они сохраняются, даже если workflow повторяет попытку.
SQL API
У каждого отдельного экземпляра Agent есть своя база данных SQL (SQLite), которая работает в том же контексте, что и сам Agent. Это означает, что вставка или запрос данных внутри вашего Agent происходит практически без задержек: Agent не нужно отправлять запрос через континент или через весь мир, чтобы получить доступ к своим собственным данным.
Вы можете обращаться к SQL API из любого метода Agent через this.sql. SQL API принимает template-литералы:
export class MyAgent extends Agent {
async onRequest(request) {
let userId = new URL(request.url).searchParams.get("userId");
// 'users' is just an example here: you can create arbitrary tables and define your own schemas
// within each Agent's database using SQL (SQLite syntax).
let [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}export class MyAgent extends Agent {
async onRequest(request: Request) {
let userId = new URL(request.url).searchParams.get("userId");
// 'users' is just an example here: you can create arbitrary tables and define your own schemas
// within each Agent's database using SQL (SQLite syntax).
let [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}Вы также можете передать запросу аргумент типа TypeScript, который будет использован для вывода типа результата:
export class MyAgent extends Agent {
async onRequest(request) {
let userId = new URL(request.url).searchParams.get("userId");
// Supply the type parameter to the query when calling this.sql
// This assumes the results returns one or more User rows with "id", "name", and "email" columns
const [user] = this.sql`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}type User = {
id: string;
name: string;
email: string;
};
export class MyAgent extends Agent {
async onRequest(request: Request) {
let userId = new URL(request.url).searchParams.get("userId");
// Supply the type parameter to the query when calling this.sql
// This assumes the results returns one or more User rows with "id", "name", and "email" columns
const [user] = this.sql<User>`SELECT * FROM users WHERE id = ${userId}`;
return Response.json(user);
}
}Вам не нужно указывать тип массива (User[] или Array<User>), как this.sql всегда возвращает массив указанного типа.
SQL API, доступный агенту, похож на тот, внутри Durable Objects. Вы можете использовать те же SQL-запросы с базой данных Agent. Создавайте таблицы и делайте запросы к данным точно так же, как с Durable Objects или D1.
Рекомендации
Делайте состояние компактным
Состояние рассылается всем клиентам при каждом изменении. Для больших объёмов данных:
// Bad - storing large arrays in state
initialState = {
allMessages: [] // Could grow to thousands of items
};
// Good - store in SQL, keep state light
initialState = {
messageCount: 0,
lastMessageId: null
};
// Query SQL for full data
async getMessages(limit = 50) {
return this.sql`SELECT * FROM messages ORDER BY created_at DESC LIMIT ${limit}`;
}Оптимистичные обновления
Для отзывчивых интерфейсов обновляйте состояние клиента немедленно:
// Client-side
function sendMessage(text) {
const optimisticMessage = {
id: crypto.randomUUID(),
text,
pending: true,
};
// Update immediately
agent.setState({
...agent.state,
messages: [...agent.state.messages, optimisticMessage],
});
// Server will confirm/update
}
// Server-side
class MyAgent extends Agent {
onStateChanged(state, source) {
if (source === "server") return;
const pendingMessages = state.messages.filter((m) => m.pending);
for (const msg of pendingMessages) {
// Validate and confirm
this.setState({
...state,
messages: state.messages.map((m) =>
m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m,
),
});
}
}
}// Client-side
function sendMessage(text: string) {
const optimisticMessage = {
id: crypto.randomUUID(),
text,
pending: true,
};
// Update immediately
agent.setState({
...agent.state,
messages: [...agent.state.messages, optimisticMessage],
});
// Server will confirm/update
}
// Server-side
class MyAgent extends Agent<Env, { messages: Message[] }> {
onStateChanged(state: GameState, source: Connection | "server") {
if (source === "server") return;
const pendingMessages = state.messages.filter((m) => m.pending);
for (const msg of pendingMessages) {
// Validate and confirm
this.setState({
...state,
messages: state.messages.map((m) =>
m.id === msg.id ? { ...m, pending: false, timestamp: Date.now() } : m,
),
});
}
}
}State и SQL
| Используйте State для | Используйте SQL для |
|---|---|
| Состояние UI (загрузка, выбранные элементы) | Исторические данные |
| Счётчики в реальном времени | Большие коллекции |
| Данные активной сессии | Отношения |
| Конфигурация | Запрашиваемые данные |
export class ChatAgent extends Agent {
// State: current UI state
initialState = {
typing: [],
unreadCount: 0,
activeUsers: [],
};
// SQL: message history
async getMessages(limit = 100) {
return this.sql`
SELECT * FROM messages
ORDER BY created_at DESC
LIMIT ${limit}
`;
}
async saveMessage(message) {
this.sql`
INSERT INTO messages (id, text, user_id, created_at)
VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})
`;
// Update state for real-time UI
this.setState({
...this.state,
unreadCount: this.state.unreadCount + 1,
});
}
}export class ChatAgent extends Agent {
// State: current UI state
initialState = {
typing: [],
unreadCount: 0,
activeUsers: [],
};
// SQL: message history
async getMessages(limit = 100) {
return this.sql`
SELECT * FROM messages
ORDER BY created_at DESC
LIMIT ${limit}
`;
}
async saveMessage(message: Message) {
this.sql`
INSERT INTO messages (id, text, user_id, created_at)
VALUES (${message.id}, ${message.text}, ${message.userId}, ${Date.now()})
`;
// Update state for real-time UI
this.setState({
...this.state,
unreadCount: this.state.unreadCount + 1,
});
}
}Избегайте бесконечных циклов
Следите, чтобы обновления состояния не запускались в ответ на ваши же собственные обновления:
// Bad - infinite loop
onStateChanged(state: State) {
this.setState({ ...state, lastUpdated: Date.now() });
}
// Good - check source
onStateChanged(state: State, source: Connection | "server") {
if (source === "server") return; // Do not react to own updates
this.setState({ ...state, lastUpdated: Date.now() });
}Используйте состояние Agent в качестве контекста модели
Вы можете сочетать API состояния и SQL API вашего Agent с его возможностью вызывать модели ИИ чтобы включать исторический контекст в промпты модели. Современные большие языковые модели (LLM) часто имеют очень большие контекстные окна (до миллионов токенов), что позволяет напрямую добавлять релевантный контекст в промпт.
Например, можно использовать встроенную SQL-базу данных Agent, чтобы извлечь историю, сделать по ней запрос к модели и добавить результат в историю перед следующим вызовом модели:
export class ReasoningAgent extends Agent {
async callReasoningModel(prompt) {
let result = this
.sql`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`;
let context = [];
for (const row of result) {
context.push(row.entry);
}
const systemPrompt = prompt.system || "You are a helpful assistant.";
const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;
try {
const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
});
// Store the response in history
this
.sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;
return response.response;
} catch (error) {
console.error("Error calling reasoning model:", error);
throw error;
}
}
}interface Env {
AI: Ai;
}
export class ReasoningAgent extends Agent<Env> {
async callReasoningModel(prompt: Prompt) {
let result = this
.sql<History>`SELECT * FROM history WHERE user = ${prompt.userId} ORDER BY timestamp DESC LIMIT 1000`;
let context = [];
for (const row of result) {
context.push(row.entry);
}
const systemPrompt = prompt.system || "You are a helpful assistant.";
const userPrompt = `${prompt.user}\n\nUser history:\n${context.join("\n")}`;
try {
const response = await this.env.AI.run("@cf/zai-org/glm-4.7-flash", {
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: userPrompt },
],
});
// Store the response in history
this
.sql`INSERT INTO history (timestamp, user, entry) VALUES (${new Date()}, ${prompt.userId}, ${response.response})`;
return response.response;
} catch (error) {
console.error("Error calling reasoning model:", error);
throw error;
}
}
}Это работает потому, что у каждого экземпляра Agent есть собственная база данных, и состояние, хранящееся в этой базе, доступно только этому Agent: будь то работа от имени отдельного пользователя, комнаты или канала либо инструмента глубокого исследования. По умолчанию вам не нужно управлять конкуренцией доступа или обращаться по сети к централизованной базе данных, чтобы получать и сохранять состояние.
Справочник по API
Свойства
| Свойство | Тип | Описание |
|---|---|---|
state |
State |
Текущее состояние (геттер) |
initialState |
State |
Состояние по умолчанию для новых агентов |
Методы
| Метод | Подпись | Описание |
|---|---|---|
setState |
(state: State) => void |
Обновите состояние, сохраните и разошлите |
onStateChanged |
(state: State, source: Connection | "server") => void |
Вызывается при изменении состояния |
validateStateChange |
(nextState: State, source: Connection | "server") => void |
Валидируйте перед сохранением (при отклонении выбрасывайте исключение) |
Методы шагов Workflow
| Метод | Описание |
|---|---|
step.updateAgentState(state) |
Заменить состояние агента из workflow |
step.mergeAgentState(partial) |
Слить частичное состояние из workflow |
step.resetAgentState() |
Сбросить до initialState из workflow |