Skip to content

非公式本サイトは非公式の日本語ドキュメントであり、Cloudflare 公式サイトではありません。最新情報はdevelopers.cloudflare.comをご確認ください。

状態の保存と同期

最終更新 Markdown で表示Agent セットアップ

エージェントには組み込みの状態管理があり、自動永続化と、接続中の全クライアントへのリアルタイム同期が付きます。

概要

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 });
	}
}

初期状態が適用されるタイミング

初期状態は、起動のたびにではなく、最初のアクセス時に遅延適用されます。

  1. 新しいエージェントinitialState が使われ、永続化されます
  2. 既存のエージェント — 永続化された状態が SQLite から読み込まれます
  3. initialState 未定義this.stateundefined です
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);
	}
}

undefined の状態

initialState を定義しない場合、this.stateundefined を返します。

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() を使います。次が行われます。

  1. SQLite へ保存します(永続化)
  2. 接続中の全クライアントへブロードキャストします(shouldSendProtocolMessagesfalse を返した接続は除外)
  3. 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 パラメータ

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");
		}
	}
}

クライアント側の状態同期

状態は、接続中のクライアントと自動で同期されます。

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/>(SQLite に永続化)"]
    end
    subgraph Clients
        C1["クライアント 1"]
        C2["クライアント 2"]
        C3["クライアント 3"]
    end
    C1 & C2 & C3 -->|setState| S
    S -->|WebSocket 経由でブロードキャスト| C1 & C2 & C3

Workflows からの状態

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;
	}
}

これらは耐久性のある操作です。ワークフローがリトライしても残ります。

SQL API

個々の 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

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 の状態をモデルコンテキストとして使う

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 だけに属するからです。単一ユーザー、ルームやチャネル、深い調査ツールのいずれの代理でも同じです。デフォルトでは、競合の管理や、集中データベースへのネットワーク往復は不要です。

API リファレンス

プロパティ

プロパティ 説明
state State 現在の状態(ゲッター)
initialState State 新しいエージェントのデフォルト状態

メソッド

メソッド シグネチャ 説明
setState (state: State) => void 状態を更新し、永続化してブロードキャストします
onStateChanged (state: State, source: Connection | "server") => void 状態が変わったときに呼び出されます
validateStateChange (nextState: State, source: Connection | "server") => void 永続化前に検証します(reject するには throw)

Workflow ステップのメソッド

メソッド 説明
step.updateAgentState(state) ワークフローからエージェント状態を置き換えます
step.mergeAgentState(partial) ワークフローから部分状態をマージします
step.resetAgentState() ワークフローから initialState へリセットします

次のステップ

Agents API

Agents SDK の完全な API リファレンスです。

WebSockets

リアルタイムのデータストリーミングで、対話型エージェントを構築します。

Workflows の実行

エージェントから非同期ワークフローをオーケストレーションします。

役に立ちましたか?