Reading state
Turn stored values into focused views that your agents can use.
An agent doesn't automatically see everything your application stores. A reader turns a block's value into text, and the agent's read declaration selects which view enters its context. This lets you keep rich application data while giving each model only the information relevant to its work.
{
"tone":
"concise"
}assistant(state)
state → text"User prefers
concise answers."instructions
render.profile
history
user messageThis guide uses the profile block from Defining blocks. We'll follow its value from storage to the model, then explore different views of the same block.
Start with a stored value
Suppose the current thread's profile contains:
{
"tone": "concise"
}That object belongs to your application. The model does not automatically receive its JSON representation. You decide how to describe it through a reader on the block.
Define the view
Inside profileBlock, the readers field declares named functions:
// Inside src/shared/state/profile.block.ts
readers: {
assistant: (state) => `User prefers ${state.tone} answers.`,
},The function receives the block's typed value and returns text. For the stored value above, the result is:
User prefers concise answers.Use readers to select useful fields, summarize collections, and explain what a value means. Keep them focused on formatting state; fetch new information through tools and save it through updates.
The name assistant identifies a view. It is selected by the application's
read declaration, rather than chosen by the model at runtime.
Connect the reader to an agent
First, define a read budget in src/budgets.ts. Add this entry to your
existing budget configuration if it already exports readTokens:
export const readTokens = {
profile: 128,
};Then import the block and budget into your agent file:
import { profileBlock } from "@app/shared/state/profile.block.ts";
import { readTokens } from "@app/budgets.ts";The @app/* alias resolves to src/* (a tsconfig paths entry that the
scaffolded app, Studio, and keel dev all resolve), so shared declarations
never need ../../.. chains.
Add this entry to the reads array inside defineAgent:
reads: [
{
block: profileBlock,
reader: "assistant",
budgetTokens: readTokens.profile,
},
],| Field | What it controls |
|---|---|
block | Which block to read from the current thread. |
reader | Which named function renders that value. If omitted, it defaults to the agent's id. |
budgetTokens | How much context space this rendered view is allowed to use. |
Declaring a read does not grant write access. An agent that changes the
profile must separately declare it in writes and carry a tool that
submits the update.
Follow the value into context
When Keel assembles the turn's context, it:
- Reads the block's value for the current thread, applying the block's optional repair function and validating stored data.
- Calls the selected reader with that value and its token budget.
- Checks the size of the returned text and records the render.
- Adds the text to a context section named
render.profile.assistant.
The model receives the rendered sentence alongside its instructions, other
state views, and conversation context. The section id follows
render.<block>.<reader>, so two grants on the same block through different
readers stay distinguishable. render.profile.assistant identifies the
section for inspection; it is not an instruction you need to add to your
prompt.
See Context for how the complete model input is assembled.
One block, different audiences
Different agents often need different details from the same data. Suppose a notes block stores:
{
"notes": [
"Send the draft on Friday.",
"Include the latest figures."
]
}Its readers could expose the content to an assistant and a count to a
researcher. This example assumes a block schema with a notes: string[]
field:
readers: {
assistant: (state) =>
state.notes.map((note, index) => `${index + 1}. ${note}`).join("\n"),
researcher: (state) =>
`${state.notes.length} note(s) on file.`,
},| Selected reader | Text supplied to the model |
|---|---|
assistant | 1. Send the draft on Friday. followed by 2. Include the latest figures. |
researcher | 2 note(s) on file. |
Both read the same stored value. The assistant gets actionable details; the researcher gets a smaller view that avoids exposing unnecessary content. Readers control this state-to-context path. Keep tool outputs and other context sources equally focused if they can expose the same information.
Keep the view within its budget
A read budget limits the rendered text, not the size of the stored object. A block can hold many notes while its reader produces a short summary.
Readers receive the budget as their second argument:
readers: {
researcher: (state, budgetTokens) => {
const count = `${state.notes.length} note(s) on file.`;
if (budgetTokens < 128) return count;
const latest = state.notes.at(-1);
return latest ? `${count}\nLatest: ${latest}` : count;
},
},This lets a reader choose how much detail to include. The runtime still checks the result; choosing a shorter view does not guarantee it fits. Keel currently estimates tokens from text length, rather than using a provider-specific tokenizer.
When a view exceeds its grant, Keel clips the output and includes a notice:
…[clipped at the 128-token read budget]The render event records clipped: true, and the runtime emits a warning.
Shorten the reader or increase its grant when you see this. Prefer producing
a useful summary yourself, since clipping may cut through a sentence.
The complete context must also fit the model's window.
When changes become visible
Readers run during context assembly at the beginning of a turn. If a tool updates a block later in that turn, the already assembled state view does not automatically refresh for subsequent model steps. The model can still receive the tool's result through the tool-call transcript.
The next turn reads the current stored value and renders it again. For
example, after a successful preference update, the next turn in the same
thread can receive User prefers detailed answers.
When a read cannot be completed as declared
| Situation | Behavior |
|---|---|
| The named reader does not exist | defineAgent throws a configuration error. |
| No value is stored for the thread | The reader receives the block's initial value. |
| Stored data fails validation after optional repair | The reader receives initial. |
| Rendered text exceeds the read budget | Keel clips it with a notice and emits a warning. |
| Reader or repair code throws | Context assembly fails and the turn settles as a typed render-failure defect. No raw exception escapes runTurn; the turn is failed and refunded, not crashed. Keep these functions defensive so a bad value degrades to a smaller view instead of failing the turn. |
Read Repairing state for handling older or invalid stored values.
Inspect in Studio
Try this with the profile block and the tool from Updating state:
- Start a fresh thread in Studio and run a turn. Inspect
STATE, then find
render.profile.assistantin CONTEXT. Check that it contains the initial concise preference. - With a live model, ask the assistant to remember that you prefer detailed answers. Confirm that the tool actually updated the block in STATE. A scripted model needs a scripted tool call for this step.
- Send another message in the same thread. Inspect CONTEXT again: the new turn should contain the detailed preference.
This distinguishes the stored update from the text a particular turn received. If a preference looks stale, check both before changing the prompt.
Next: Updating state.
