Memory
Compose reusable memory blocks for recent context, working state, episodes, facts, procedures, and custom agent memory.
Memory is not one feature. A useful agent needs several kinds of state: the last few messages, a typed scratchpad, searchable events, durable facts, and learned operating procedures. Crux keeps those concerns separate as blocks, then composes them with memory().
import { memory, recentMessages, workingState, facts, procedures } from '@crux/core/memory'
import { z } from 'zod'
const userMemory = memory({
id: 'assistant',
store,
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([]),
}),
}),
facts({
id: 'facts',
embed: dense,
extract: extractFactsFromTurn,
write: { mode: 'propose' },
}),
procedures({
id: 'procedures',
embed: dense,
write: { mode: 'auto' },
}),
],
})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 captures the user and assistant turn, runs each block's capture logic, and flushes pending writes. Streaming works the same way after stream().completion().
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.
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".
memoryBlock() is the escape hatch. Use it when a product has its own memory domain, render format, tools, or approval workflow.
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',
})If a memory is safe to write immediately, set write: { mode: 'auto' }. If a block should only be written by application code, keep the default block capture empty and call its direct methods yourself.
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' },
{ store, namespace: 'user:123', memoryId: 'assistant' },
)
await episodesBlock.record(
{ content: 'User accepted the launch plan', metadata: { source: 'ui' } },
{ store, namespace: 'user:123', memoryId: 'assistant' },
)Pass memoryId when you want direct calls to read and write the same keys as a composed memory() instance.
Observability
Memory blocks emit memory:read and memory:write events with memoryType: 'block', blockId, blockKind, and a privacy-safe namespaceHash. Devtools groups them in the Memory Explorer, the CLI includes them in memory stats, and @crux/otel adds block attributes to memory spans.
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 '@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.