Stateful Ladders
Add Persistent Memory with Slates
Add Persistent Memory with Slates
Give a ladder long-term memory across calls. A worked tutorial using real LADR slate grammar: declare, read (slateRead), write (slateWrite, gated writes), and retrieve (on-demand fetch).
Why slates (and not "just stuff context")
Most LLM apps that want "memory" do it by stuffing prior conversation into the prompt. This works until it doesn't:
- The prompt grows linearly with usage. After 50 calls, your prompt is 50k tokens.
- The model attends to old context unevenly. Relevant facts get lost in the noise.
- The author has no control over what's actually surfaced.
LADR slates are the explicit alternative. The author declares:
- The schema of memory (folders, files, metatags) at config time.
- What gets written;
slateWritestep directive, optionally gated. - What gets retrieved;
slateReadfield source, orretrieve:for on-demand fetch.
The runtime handles storage and retrieval. The author never loses control of what's in the prompt.
Step 1: Declare a slate
Add a top-level slates array. Each slate has folders; each folder has a schema, token budget, and access policy.
slates:
- title: "Lessons"
folders:
- name: wins
tokenLimit: 800
access: readWrite
evictionPolicy: FIFO
metatags:
- { name: summary, type: string }
- { name: tags, type: "string[]" }
files:
- name: index.md
init: blank
- name: failures
tokenLimit: 400
access: readWrite
metatags:
- { name: summary, type: string }
- { name: severity, type: number }
files:
- name: log.md
init: blankTwo folders: wins (high-quality past results) and failures (postmortem log). Each has user metatags (summary, tags, severity) for retrieval and ranking.
The engine also maintains system metatags on every folder/file: index (folder-level file listing), tree (recursive structure), created / modified / size (file-level).
Step 2: Read from a slate via slateRead
Add a field to your draft step that pulls the wins folder's index, so the LLM can decide what to retrieve:
- id: draft
fields:
- { name: Context, type: text, from: "input.context" }
- name: WinsIndex
type: slateRead
from: { slate: "Lessons", folder: wins, metatag: index }
systemPrompt: |
Answer the question.
The "Wins Index" field above lists prior successes you can reference.
If you want a specific file's contents, emit a retrieval request (see Step 5).slateRead is just "read these fields." The shape is:
from: { slate, folder, file?, metatag?, depth? }- Omit
metatag→ read the file contents (default). - Set
metatag: <user-metatag>→ read that metatag's value (one file's value, or all files' values iffileis omitted). - Set
metatag: index→ read the folder's auto-generated file listing. - Set
metatag: tree→ read the recursive folder+subfolder structure (depth: Ncontrols recursion).
On the first call, the slate is empty; the field renders blank or "[]". On later calls, prior wins surface automatically.
Note: slateRead does NOT support server-side filtering (op, value, orderBy, limit). It's "read these fields," period. For dynamic filtering at runtime, use retrieve: (Step 5).
Step 3: Write via schema-matched slateWrite
Add a slateWrite directive to your final step. The engine extracts JSON from the LLM output, validates against a schema, and writes if valid. No match = no-op (not error).
- id: refine
...
slateWrite:
to: { slate: "Lessons", folder: wins, file: latest.md }
on: overwrite # append | overwrite | mergeByKey
match: # JSON Schema; what valid output looks like
type: object
required: [summary, tags]
properties:
summary: { type: string, minLength: 1 }
tags: { type: array, items: { type: string } }
additionalProperties: false
systemPrompt: |
After producing the final answer, emit JSON summarizing what was learned:
{"summary": "<one line>", "tags": ["keyword1", "keyword2"]}What happens:
- The step runs and produces its normal output (the answer).
- The engine inspects the output, extracts the first JSON block, validates against
match. - On success: writes the JSON to
wins/latest.md(overwriting prior content). - On failure or no JSON: no-op. Silently skips the write (no error, no event) and moves on.
field: <name> optionally extracts one field from the validated JSON before writing. on: mergeByKey + key: <field> maintains a structured store (registry/knowledge-graph) keyed by a JSON field.
Step 4: Write via gated write (if/then)
The alternative write mechanism: gated writes under if/then. Use this when you want to write unconditionally on gate match (no schema extraction):
- id: record
systemPrompt: "Output RECORD if worth saving, else SKIP."
if:
equals: "RECORD"
then:
write:
to: { slate: "Lessons", folder: wins, file: log.md }
from: previous # writes all survivors of preceding step, formatted by target type
on: append # default
else: continuefrom: accepts the unified source vocabulary (also used by dynamic steps):
| Form | Meaning |
|---|---|
previous | All survivors of the immediately preceding step. Engine formats based on target: file contents get \n\n-joined; JSON-typed metatag gets a JSON array. |
output | The current step's own output. |
"literal text" | A literal string. |
{ stepId: "X" } | All survivors of step X. |
{ stepId: "X", nodeRef: "current" } | Per-node routing: writer node N ← source node N. |
{ stepId: "X", node: 3 } | One specific node by index. |
{ slate, folder, file?, metatag? } | Slate reference (cross-slate writes). |
Step 5: On-demand fetch via retrieve:
slateRead requires you to know at config time which file/metatag to read. retrieve: lets the LLM decide at runtime; it sees the index, requests specific files, and the engine fetches them within a bounded loop.
- id: research
type: normal
fields:
- { name: Question, type: text, from: "input.context" }
- { name: WinsIndex, type: slateRead, from: { slate: "Lessons", folder: wins, metatag: index } }
retrieve:
to: { slate: "Lessons", folder: wins }
as: RetrievedDocuments # injected field name on subsequent rounds
maxRounds: 5 # hard cap → guarantees termination
requestMatch: # JSON Schema; if output matches → retrieval request
type: object
required: [request]
properties:
request:
type: object
properties:
files: { type: array, items: { type: string } }
subfolders: { type: array, items: { type: string } }
metatag:
type: object
properties:
name: { type: string }
contains: { type: string }
glob: { type: string }
additionalProperties: false
additionalProperties: false
systemPrompt: |
You can see WinsIndex. To retrieve, emit exactly:
{"request": {"files": ["path.md"]}}
{"request": {"metatag": {"name": "tags", "contains": "calculus"}}}
When you have enough, output your final answer in prose.Execution model:
- Round 1: engine assembles fields (including
WinsIndex), LLM call #1 produces output. - If output matches
requestMatch→ engine fetches per the request, adds theRetrievedDocumentsfield, runs LLM call #2 with enriched context. Round increments. - If output does NOT match → step completes (the output is the final answer).
- Repeat until final answer OR
maxRounds: 5hit. On budget exhaustion, the engine injects: "Retrieval budget exhausted. Produce your final answer now." Next call must produce final answer.
Termination is guaranteed by maxRounds. A ladder author cannot cause infinite retrieval loops.
See Retrieve for the four retrieval modes (files / subfolders / metatag query / glob) and dedup behavior.
Slate lifecycle and scope
Slates are per-user by default. If user A calls the ladder, they read and write to their own slate. User B has a separate slate. Cross-user sharing is a planned feature.
Slates persist across calls; that's the whole point. A user who calls the ladder today and again next week sees the same memory.
Slates are scoped to the ladder. Two different ladders by the same author have separate slates. To share memory across ladders, use recursion or cross-ladder jumps (advanced; see Jumps).
Token cost and eviction
Every folder has a tokenLimit. When a write would push the folder over budget, the evictionPolicy kicks in:
FIFO(default); drop the oldest content. Cheap, predictable.LRU; drop the least-recently-read content. Better for hot/cold patterns.reject; fail the write, preserve existing content. Use for write-once folders.
For a wins folder with tokenLimit: 800 and FIFO eviction, the ladder always remembers roughly the 800 most recent tokens of wins; older wins fall off the front as new ones come in. Bump tokenLimit to remember more, but watch total prompt size: slate content gets rendered into the prompt.
Where to go next
- Slates Reference; full folder/file/metatag schema.
- Reads & Writes; complete
slateRead/slateWritereference with all options. - Retrieve; on-demand bounded fetch for pulling specific records by metatag at runtime.
- Crucible Reference Ladder; a complete ladder that uses slates for persistent memory across all its phases.