Overview
Define what your application remembers, how it changes, and what each agent can read.
Memory across turns
{
tone: "concise" |
"detailed"
}{ "tone": "concise" }
↓
{ "tone": "detailed" }{ "tone": "detailed" }
↓
"User prefers detailed answers."State is the information your application keeps across turns: a user's preferences, saved notes, or the progress of a task. Keel organizes this information into blocks. A block is a typed contract: it declares valid data through a schema, allowed changes through reducers, and views for agents through readers. The stored value is an instance of that contract.
TypeScript checks the shapes your code works with, while the Zod schema validates values at runtime as they flow through the block's paths: every render for a reader and every reducer update passes validation. The one exception is a TTL cache hit, which serves the stored value back to a tool call without re-validating it — see Storage and caching.
Why use a block?
With a plain shared object, your application decides where to validate updates and how to expose data to agents. A Keel block puts those decisions in one declaration and checks reducer results before storing them.
Caller-managed checks
profile.tone =
"brief";{
"tone": "brief"
}Contract-enforced checks
setTone({
tone: "brief"
}){
"tone": "concise"
}This comparison shows a plain object without runtime validation. Other systems can implement the same checks; in Keel, the block contract makes them a standard part of reading and updating state.
Rules by design
Every block follows the same contract:
One schema
Validate stored values on read and reducer results before writing. An invalid update leaves the previous value unchanged.
nextState → schema valid → store invalid → reject updateDeclared access
Agents declare which blocks they read and write. Every read names a reader and a token budget.
reads: [{ block, reader, budgetTokens }] writes: [block]Explicit views
Readers render a view for each audience. The model receives that text in its context.
state ├─ assistant → "Full notes…" └─ researcher → "3 notes"Updates through reducers
Tool code submits an event through feeds or commit; a reducer computes the next value. The model cannot mutate state directly. Keep reducers pure.
tool → event ↓ reducer(state, event) ↓ nextStateOne writer per block per turn
The first open obtains a writer. Later opens in that turn receive readers. This does not lock the block across independent turns.
Within one turn openBlock(block) → writer openBlock(block) → readerA fallback for invalid data
Reads apply the optional repair function, then validate. Failed validation returns initial. Repair code must handle arbitrary input without throwing.
stored → repair? → schema ├─ valid → value └─ invalid → initialValues scoped to a thread
Threads share a block definition, while each thread keeps its own values.
Same block, separate values thread A → { "tone": "concise" } thread B → { "tone": "detailed" }
Work with state
| Guide | What you'll learn |
|---|---|
| Defining blocks | Declare a schema, initial value, reducers, and readers. |
| Reading state | Give agents named views with explicit token budgets. |
| Updating state | Connect tools to reducers and validate updates. |
| Repairing state | Normalize older data and fall back when validation fails. |
| Storage and caching | Scope values to threads, choose storage, and cache tool results. |
For the model's correction loop, see Failure and recovery.
Next: Defining blocks.
