Agent State
Each agent has state that outlives its runs: a key-value store and typed document collections. State is scoped to the agent name and lives in the store.
Declaring collections
Declare collections with any Standard Schema, such as a zod schema:
const loop = z.object({
title: z.string(),
status: z.enum(['open', 'resolved']),
due: z.string().optional(),
});const digest = defineAgent({
name: 'digest',
model,
instructions: 'Track open loops across days.',
state: { loops: loop },
tools: ({ state }) => ({
countOpenLoops: tool({
description: 'Counts the open loops.',
inputSchema: z.object({}),
execute: async () => {
const open = await state.collection('loops').list({ where: { status: 'open' } });
return open.length;
},
}),
}),
});The tools factory receives state, typed by the declared schema.
The state API
| Method | Description |
|---|---|
state.get<T>(key) | Reads a value, undefined when absent |
state.set(key, value) | Writes a value |
state.collection(name).list({ where?, limit? }) | Lists documents as { id, ...doc }; where matches top-level fields by equality |
state.collection(name).upsert(id, doc) | Validates doc with the collection's schema, then creates or replaces it |
state.collection(name).remove(id) | Deletes a document |
upsert stores the schema's output. A document that fails validation throws ValidationError and nothing is stored; its issues hold the validation issues with their paths. Only declared collections are writable.
try {
await state.collection('loops').upsert('x', { title: 'Call Bob', status: 'later' as 'open' });
} catch (error) {
if (error instanceof ValidationError) {
for (const issue of error.issues) console.log(issue.path, issue.message);
}
}createAgentState(agent, schema, store) builds the same API outside a run, for scripts, admin tasks and tests:
const store = new MemoryStore();
const state = createAgentState('digest', { loops: loop }, store);
await state.set('lastUid', 1042);
await state.get<number>('lastUid'); // 1042
const loops = state.collection('loops');
await loops.upsert('reply-anna', { title: 'Reply to Anna', status: 'open' });
await loops.list({ where: { status: 'open' }, limit: 10 });
// [{ id: 'reply-anna', title: 'Reply to Anna', status: 'open' }]
await loops.remove('reply-anna');Generated tools
With a state schema, the model gets three tools per collection:
| Tool | Kind | Input |
|---|---|---|
state_<collection>_list | plain tool | { where?, limit? } |
state_<collection>_upsert | effect tool | { id, doc } (the doc's JSON Schema, when the schema can produce one) |
state_<collection>_remove | effect tool | { id } |
An invalid document becomes a tool error the model can correct. Each write gets its own effect key, so a replayed upsert A, upsert B, upsert A sequence still ends on the last write. Your own tools win on a name clash. createStateTools(state, schema) builds these tools directly.