← Vytvářejte agenty na Cloudflare / agents / runtime / lifecycle
Ukládání a synchronizace stavu
Agents poskytují vestavěnou správu stavu s automatickou perzistencí a synchronizací v reálném čase napříč všemi připojenými klienty.
Přehled
Stav v rámci Agenta je:
- Trvalý - Automaticky ukládá do SQLite, přežije restarty i hibernaci
- Synchronizováno - Změny se okamžitě odesílají všem připojeným klientům WebSocket
- Obousměrné - Stav mohou aktualizovat server i klienti
- Typově bezpečný - Plná podpora TypeScriptu s generiky
- Okamžitě konzistentní - Čtení vlastních zápisů
- Vláknově bezpečné - Bezpečné pro souběžné aktualizace
- Rychlé - Stav je uložen společně s místem, kde agent běží
Stav agenta se ukládá v databázi SQL vestavěné v každé jednotlivé instanci Agent. Se stavem můžete pracovat pomocí vysokoúrovňového this.setState API (doporučeno), které umožňuje synchronizovat stav a spouštět události při jeho změnách, nebo přímým dotazováním databáze pomocí 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],
});
}
}Definování počátečního stavu
Použijte initialState vlastnost pro definování výchozích hodnot nových instancí agenta:
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,
};
}Typová bezpečnost
Druhý generický parametr Agent definuje váš typ stavu:
// 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 });
}
}Když platí počáteční stav
Počáteční stav se aplikuje líně při prvním přístupu, ne při každém probuzení:
- Nový agent -
initialStatese použije a uloží - Stávající agent - Perzistentní stav se načítá z SQLite
- Ne
initialStatedefinováno -this.statejeundefined
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);
}
}Čtení stavu
Přístup k aktuálnímu stavu přes this.state getter:
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);
}
}Nedefinovaný stav
Pokud nedefinujete initialState, this.state vrací 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 });
}
}
}Aktualizace stavu
Použijte setState() k aktualizaci stavu. Tato akce:
- Ukládá do SQLite (trvalé)
- Vysílá všem připojeným klientům (kromě připojení, kde
shouldSendProtocolMessagesvrácenfalse) - Spouští
onStateChanged()(po broadcastu, s nejlepší možnou snahou)
// 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,
});Stav musí být serializovatelný
Stav se ukládá jako JSON, proto musí být serializovatelný:
// 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(),
});Reakce na změny stavu
Přepsat onStateChanged() k reakci na změny stavu (oznámení nebo vedlejší efekty):
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);
}
}Parametr source
source zobrazuje, kdo aktualizaci vyvolal:
| Hodnota | Význam |
|---|---|
"server" |
Agent s názvem setState() |
Connection |
Klient odeslal stav přes WebSocket |
Toto se hodí pro:
- Vyhýbání se nekonečným smyčkám (nereagujte na vlastní aktualizace)
- Ověřování vstupu od klienta
- Spouštění vedlejších účinků pouze na akce klienta
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);
}
}
}Běžný vzor: akce řízené klientem
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,
),
});
}
}
}Ověřování aktualizací stavu
Pokud chcete aktualizace stavu validovat nebo odmítat, přepište validateStateChange():
- Spuštění před perzistencí a vysíláním
- Musí být synchronní
- Vyhození výjimky aktualizaci přeruší
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");
}
}
}Synchronizace stavu na straně klienta
Stav se automaticky synchronizuje s připojenými klienty.
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 });Stavový tok
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
Stav z Workflows
Při použití Workflows, můžete aktualizovat stav agenta z kroků 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;
}
}Jde o trvalé operace, které přetrvávají, i když workflow provede nový pokus.
SQL API
Každá jednotlivá instance Agenta má vlastní databázi SQL (SQLite), která běží ve stejném kontextu jako samotný Agent. Díky tomu je vkládání i dotazování dat ve vašem Agentovi prakticky bez latence: Agent nemusí cestovat přes celý kontinent nebo svět, aby přistoupil ke svým vlastním datům.
K SQL API máte přístup v rámci kterékoli metody Agenta přes this.sql. SQL API přijímá template literály:
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);
}
}Dotazu můžete také předat argument typu TypeScript, který se použije k odvození typu výsledku:
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);
}
}Není nutné určovat typ pole (User[] nebo Array<User>), jako this.sql vždy vrátí pole zadaného typu.
SQL API zpřístupněné agentovi je podobné tomu, v rámci Durable Objects. Stejné dotazy SQL můžete použít i pro databázi Agenta. Vytvářejte tabulky a dotazujte se na data stejně jako u Durable Objects nebo D1.
Osvědčené postupy
Udržujte stav malý
Stav se vysílá všem klientům při každé změně. Pro velká data:
// 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}`;
}Optimistické aktualizace
Pro citlivě reagující uživatelská rozhraní aktualizujte stav klienta okamžitě:
// 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,
),
});
}
}
}Stav vs SQL
| Použití stavu pro | Použití SQL pro |
|---|---|
| Stav UI (načítání, vybrané položky) | Historická data |
| Počítadla v reálném čase | Velké kolekce |
| Data aktivní relace | Vztahy |
| Konfigurace | Dotazovatelná data |
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,
});
}
}Vyhněte se nekonečným smyčkám
Dávejte pozor, abyste jako reakci na vlastní aktualizace nevyvolali další aktualizace stavu:
// 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() });
}Použití stavu Agenta jako kontextu modelu
V Agentovi můžete kombinovat state a SQL API s jeho schopností volat modely AI k zahrnutí historického kontextu do promptů pro model. Moderní velké jazykové modely (LLM) mají často velmi rozsáhlá kontextová okna (až miliony tokenů), díky čemuž můžete relevantní kontext vložit přímo do promptu.
Můžete například použít vestavěnou databázi SQL Agenta k načtení historie, dotázat se pomocí ní modelu a před dalším voláním modelu do historie přidat výsledek:
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;
}
}
}Funguje to proto, že každá instance Agenta má vlastní databázi a stav uložený v této databázi patří výhradně danému Agentovi, ať už jedná jménem jednotlivého uživatele, místnosti nebo kanálu, nebo nástroje pro hloubkový výzkum. Ve výchozím stavu tak nemusíte řešit souběh přístupů ani se kvůli načtení a uložení stavu obracet přes síť na centralizovanou databázi.
Referenční dokumentace API
Vlastnosti
| Vlastnost | Typ | Popis |
|---|---|---|
state |
State |
Aktuální stav (getter) |
initialState |
State |
Výchozí stav pro nové agenty |
Metody
| Metoda | Podpis | Popis |
|---|---|---|
setState |
(state: State) => void |
Aktualizace stavu, perzistence a vysílání |
onStateChanged |
(state: State, source: Connection | "server") => void |
Volá se při změně stavu |
validateStateChange |
(nextState: State, source: Connection | "server") => void |
Ověřte před uložením (v případě zamítnutí vyvolejte výjimku) |
Metody kroků workflow
| Metoda | Popis |
|---|---|
step.updateAgentState(state) |
Nahrazení stavu agenta z workflow |
step.mergeAgentState(partial) |
Sloučit částečný stav z workflow |
step.resetAgentState() |
Resetovat na initialState z workflow |