Skip to content

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:

ts
const loop = z.object({
    title: z.string(),
    status: z.enum(['open', 'resolved']),
    due: z.string().optional(),
});
ts
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 ​

MethodDescription
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.

ts
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:

ts
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:

ToolKindInput
state_<collection>_listplain tool{ where?, limit? }
state_<collection>_upserteffect tool{ id, doc } (the doc's JSON Schema, when the schema can produce one)
state_<collection>_removeeffect 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.