エージェントには組み込みの状態管理があり、自動永続化と、接続中の全クライアントへのリアルタイム同期が付きます。
Agent 内の状態は次の性質を持ちます。
- 永続的 — SQLite に自動保存され、再起動とハイバネーションをまたいで残ります
- 同期される — 変更は接続中のすべての WebSocket クライアントへ即座にブロードキャストされます
- 双方向 — サーバーとクライアントの両方が状態を更新できます
- 型安全 — ジェネリクスによる TypeScript の完全なサポート
- 即時一貫性 — 自分の書き込みをすぐに読めます
- スレッドセーフ — 同時更新に対して安全です
- 高速 — 状態は Agent の実行場所と同じ場所に置かれます
Agent の状態は、各 Agent インスタンスに埋め込まれた SQL データベースに保存されます。推奨の高レベル API である this.setState で操作すると、状態を同期し、状態変更時にイベントを起こせます。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,
};
}Agent の 2 番目のジェネリックパラメータが、状態の型を定義します。
// 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()を呼び出します(ブロードキャスト後。ベストエフォート)
// 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 は、更新を起こした主体を示します。
| 値 | 意味 |
|---|---|
"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() をオーバーライドします。
- 永続化とブロードキャストの前に実行されます
- 同期である必要があります
- throw すると更新は中止されます
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");
}
}
}状態は、接続中のクライアントと自動で同期されます。
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>;
}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/>(SQLite に永続化)"]
end
subgraph Clients
C1["クライアント 1"]
C2["クライアント 2"]
C3["クライアント 3"]
end
C1 & C2 & C3 -->|setState| S
S -->|WebSocket 経由でブロードキャスト| C1 & C2 & C3
Workflows を使うとき、ワークフローのステップからエージェントの状態を更新できます。
// 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;
}
}これらは耐久性のある操作です。ワークフローがリトライしても残ります。
個々の Agent インスタンスには、Agent 自身と同じコンテキストで動く専用の SQL(SQLite)データベースがあります。そのため、Agent 内での挿入やクエリは実質ゼロレイテンシです。自分のデータへアクセスするために、大陸や世界をまたぐ往復は不要です。
Agent 上の任意のメソッドから、this.sql で SQL API にアクセスできます。SQL API はテンプレートリテラルを受け付けます。
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 は常に指定した型の配列を返します。
Agent に公開される SQL API は、Durable Objects 内の API と似ています。同じ 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}`;
}応答性の高い UI では、クライアント側の状態をすぐ更新します。
// 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 の用途 |
|---|---|
| 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 の状態と SQL API を、AI モデルの呼び出し と組み合わせ、プロンプトへ履歴コンテキストを含められます。最近の大規模言語モデル(LLM)はコンテキストウィンドウが非常に大きく(数百万トークンまで)、関連コンテキストをプロンプトへ直接入れられます。
たとえば、Agent 組み込みの SQL データベースから履歴を取得し、それを付けてモデルをクエリし、次の呼び出しの前に履歴へ追記できます。
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 だけに属するからです。単一ユーザー、ルームやチャネル、深い調査ツールのいずれの代理でも同じです。デフォルトでは、競合の管理や、集中データベースへのネットワーク往復は不要です。
| プロパティ | 型 | 説明 |
|---|---|---|
state |
State |
現在の状態(ゲッター) |
initialState |
State |
新しいエージェントのデフォルト状態 |
| メソッド | シグネチャ | 説明 |
|---|---|---|
setState |
(state: State) => void |
状態を更新し、永続化してブロードキャストします |
onStateChanged |
(state: State, source: Connection | "server") => void |
状態が変わったときに呼び出されます |
validateStateChange |
(nextState: State, source: Connection | "server") => void |
永続化前に検証します(reject するには throw) |
| メソッド | 説明 |
|---|---|
step.updateAgentState(state) |
ワークフローからエージェント状態を置き換えます |
step.mergeAgentState(partial) |
ワークフローから部分状態をマージします |
step.resetAgentState() |
ワークフローから initialState へリセットします |