INTEGRITY Документация

Хранение и синхронизация состояния

Agents предоставляют встроенное управление состоянием с автоматическим сохранением и синхронизацией в реальном времени по всем подключённым клиентам.

Обзор

Состояние в рамках 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 });
	}
}

Когда применяется начальное состояние

Начальное состояние применяется лениво при первом обращении, а не при каждом пробуждении:

  1. Новый агент - initialState используется и сохраняется
  2. Существующий агент - Сохранённое состояние загружается из SQLite
  3. Нет 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() чтобы обновить состояние. Это:

  1. Сохраняет в SQLite (постоянно)
  2. Рассылает всем подключённым клиентам (кроме подключений, где shouldSendProtocolMessages возвращено false)
  3. Запускает 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

Следующие шаги

Agents API

Полный справочник API для Agents SDK.

Создайте чат-агента

Создайте чат-агента ИИ и разверните его.

WebSockets

Создавайте интерактивных агентов с потоковой передачей данных в реальном времени.

Запуск Workflows

Оркеструйте асинхронные workflows из своего агента.