Memory
Compose reusable memory blocks for recent context, working state, episodes, facts, procedures, and custom agent memory.
Every request to your agent starts from a blank slate: the model forgets the conversation, the task in progress, and everything it learned about the user. A useful agent needs several kinds of state, from the last few messages to a typed scratchpad, searchable events, durable facts, and learned operating procedures. Crux keeps those concerns separate as blocks, then composes them with memory(). Start with the two most common blocks:
import { memory, recentMessages, workingState } from "@use-crux/core/memory";
import { z } from "zod";
const userMemory = memory({
id: "assistant",
records,
namespace: ({ input }) => `user:${input.userId}`,
blocks: [
recentMessages({ id: "recent", maxMessages: 12 }),
workingState({
id: "state",
schema: z.object({
goal: z.string().optional(),
openQuestions: z.array(z.string()).default([]),
}),
}),
],
});Use the composed memory directly in a prompt:
const assistantPrompt = prompt({
id: "assistant",
use: [userMemory],
input: z.object({ userId: z.string(), message: z.string() }),
system: "Use memory when it is relevant. Do not invent preferences.",
prompt: ({ input }) => input.message,
});On every call, Crux renders each block into prompt context. After the model finishes, the adapter submits one completed turn to memory. Memory captures the turn across every block, then fans out each settled tool event in order, so episodes() records tool activity without duplicate events. Streaming schedules the same completed-turn capture after tokens have streamed.
capture.mode defaults to deferred:
deferreduses the active host binding to retain capture after the owning operation returns. If the environment cannot retain background work, Crux runs the same capture inline and waits, preserving memory at the cost of latency. Development builds warn once with a link to the host setup guide.inlinealways waits for capture. Use it for explicit read-after-write behavior and tests.
Configure background retention once through config({ host }); memory has no platform-specific lifecycle hook. flush() waits for capture accepted before the call, then runs each block's flush hook in declaration order. It also reports the first unobserved retained capture failure. Call it after generation returns, because retained work may start only when the owning scope closes.
const readAfterWriteMemory = memory({
// ...
capture: { mode: "inline" },
});In Evals, default deferred capture is recorded and suppressed so evaluation does not mutate memory. Use explicit inline with isolated storage when an Eval intentionally exercises real memory writes.
Inspect Capture
Catalog and Runs answer different questions about capture:
- In Catalog → Memory, select the memory and open Binding to see its effective configured mode. Catalog shows
deferredwhen the mode is omitted because it describes source configuration, not whether a particular host accepted background work. - In Runs, open the owning generation and select its
memory.capturechild to see what happened for that invocation. The card reports the requested mode, actual disposition (inline,inline-fallback,retained, oreval-captured), outcome, counts, and duration.
A configured deferred mode can therefore appear as inline-fallback in Runs. That means no retained host accepted the work, so Crux preserved the capture by running and awaiting it inline. eval-captured means Eval recorded the deferred intent without running memory hooks.
Capture lifecycle evidence is deliberately payload-free. It retains the authored memory id, lifecycle enums, counts, duration, and an optional stable failure code—not messages, tool payloads, namespaces, raw errors, or stacks. Storage work remains visible in nested memory.write spans under memory.capture.
How to Think About Blocks
recentMessages() is short-term continuity. It keeps the last N messages so the next response has enough local context without dragging in the full transcript.
workingState() is task state. It is one typed value, validated by Zod, that the application or agent can overwrite as the task progresses.
episodes() is an append-only event log. It is for things that happened: conversations, tool results, observations, decisions, and user actions. With a dense embedding it can recall related events. Use retention to document the real eviction window and evict() from your sweep job when entries age out.
facts() is extracted knowledge. It is for conclusions such as user preferences or stable project details. By default facts are proposed before becoming active memory.
procedures() is operating memory. It stores learned instructions and habits that should shape future behavior, such as "prefer direct answers before examples".
reflections() is generated higher-order memory. Its reflect() method turns supplied memory text into a concise reflection and stores the result for later recall.
memoryBlock() is the escape hatch. Use it when a product has its own memory domain, render format, tools, or approval workflow.
A full composition combines all of these, with per-block priorities, semantic recall, proposed writes, and a shared token budget:
import {
episodes,
facts,
memory,
procedures,
recentMessages,
workingState,
} from "@use-crux/core/memory";
import { z } from "zod";
const userMemory = memory({
id: "assistant",
records,
namespace: ({ input }) => `user:${input.userId}`,
capture: { mode: "deferred" },
budget: { maxTokens: 1200 },
blocks: [
recentMessages({ id: "recent", maxMessages: 12, priority: 80 }),
workingState({
id: "state",
priority: 90,
schema: z.object({
goal: z.string().optional(),
openQuestions: z.array(z.string()).default([]),
}),
}),
episodes({
id: "episodes",
embed: dense,
retention: "90d",
render: {
strategy: "semantic",
query: ({ input }) => String(input.message),
limit: 4,
},
}),
facts({
id: "facts",
embed: dense,
extract: extractFactsFromTurn,
write: { mode: "propose" },
render: {
strategy: "semantic",
query: ({ input }) => String(input.message),
limit: 5,
},
}),
procedures({
id: "procedures",
embed: dense,
write: { mode: "propose" },
render: { strategy: "recent", limit: 3 },
}),
],
});Migrating capture configuration
- Rename
afterResponsetodeferred. - Replace
detachedwithdeferred; retained work is tracked and observable throughflush(). - Remove
capture.waitUntil. Configure the supported host once withconfig({ host }). - Without a retained host, deferred capture remains correct and runs inline with a development warning.
Rendering and Budgets
Memory renders blocks in priority order. budget.maxTokens is enforced at both the block and composed-memory level, using the configured Crux tokenizer for counting. Block budgets trim a block's own rendered text first; the memory budget then keeps higher-priority sections before lower-priority sections.
Entry-style blocks support built-in render strategies:
facts({
id: "profile",
embed: dense,
render: {
strategy: "semantic",
query: ({ input }) => String(input.message),
limit: 5,
},
});
episodes({
id: "history",
render: { strategy: "recent", limit: 4 },
});Use render: false when a block should store memory but never appear in prompt context.
Proposals
Long-term memory should not silently mutate just because a model guessed something. For that reason facts() and procedures() default to proposed writes.
const pending = await userMemory.proposals.list({ namespace: "user:123" });
await userMemory.proposals.approve(pending[0].id);
await userMemory.proposals.reject(pending[1].id, {
reason: "Too speculative",
});Proposal transitions are strict. Only pending proposals can be approved, rejected, or edited; terminal proposals cannot be approved a second time and cannot create duplicate memory writes.
If a memory is safe to write immediately, set write: { mode: 'auto' }. Set write: { mode: 'manual' } when capture should extract nothing automatically and writes should happen only through direct block method calls.
Policies
Policies run before a fact or procedure is proposed or written. Use them to redact secrets, validate shape, or reject weak candidates.
const profile = facts({
id: "profile",
extract: extractFactsFromTurn,
policy: {
redact: removeSecrets,
validate: z.object({
content: z.string(),
confidence: z.number().min(0).max(1),
}),
shouldRemember: (fact) => fact.confidence >= 0.7,
},
});Direct Use
Blocks are useful outside prompt execution. You can store state from route handlers, cron jobs, tool callbacks, or product events.
await state.patch(
{ goal: "Prepare launch plan" },
{ records, namespace: "user:123", memoryId: "assistant" },
);
await episodesBlock.record(
{ content: "User accepted the launch plan", metadata: { source: "ui" } },
{ records, namespace: "user:123", memoryId: "assistant" },
);Pass memoryId when you want direct calls to read and write the same keys as a composed memory() instance.
Shared Read-Only Policy Memory
Policy memory can be a render-only block. Because it has no capture or write hooks, it is shared context rather than mutable user memory:
import { memory, memoryBlock } from "@use-crux/core/memory";
const supportPolicy = memory({
id: "support-policy",
namespace: "org:acme",
blocks: [
memoryBlock({
id: "tone",
kind: "procedures",
priority: 100,
render: () =>
"Support policy:\n- Confirm account-impacting actions before running tools.",
}),
],
});Observability
Every memory read, write, and budgeted render is observable: devtools groups activity in the Memory Explorer, including which blocks were included, trimmed, or dropped by the budget. See the memory reference for the span attributes, snapshot shape, and proposal review states.
Embeddings
Semantic memory uses dense embeddings for recall. Embeddings are documented with retrieval because the same primitive powers indexing, retrievers, semantic cache, and memory.
import { embedding } from "@use-crux/openai";
const dense = embedding({
name: "memory",
model: "text-embedding-3-small",
});
const profileFacts = facts({
id: "facts",
embed: dense,
});Read Embeddings when you need the dense, sparse, hybrid, caching, truncation, and retry model.
Where to Next
Working state
Typed task state with Zod validation.
Episodes
Append-only event memory with dense recall.
Facts and procedures
Long-term extracted knowledge and operating memory.
Storage
RecordStore persistence, vector recall compatibility, TTL, and adapters.
Embeddings
Dense, sparse, and hybrid vector setup.