Facts and procedures
Long-term extracted knowledge and operating memory.
facts() and procedures() are the long-term memory blocks. They both extract candidates from turns, run policy, and either propose or write memory. The difference is how the agent should use the result.
Facts are declarative knowledge: "User prefers concise answers", "Project Atlas uses Convex", "The launch date is May 20". Procedures are operating instructions: "Ask one clarifying question before planning", "Use direct answers before examples", "Do not suggest crawler ingestion as a built-in feature".
import { facts, procedures, memory } from "@use-crux/core/memory";
const profileFacts = facts({
id: "profile",
embed: dense,
extract: extractFactsFromTurn,
write: { mode: "propose" },
render: {
strategy: "semantic",
query: ({ input }) => String(input.message),
limit: 5,
},
});
const operatingMemory = procedures({
id: "procedures",
embed: dense,
extract: extractProceduresFromTurn,
write: { mode: "propose" },
render: { strategy: "recent", limit: 4 },
});
const agentMemory = memory({
id: "assistant",
records,
namespace: ({ input }) => `user:${input.userId}`,
blocks: [profileFacts, operatingMemory],
});By default these blocks create proposals instead of mutating long-term memory. That is deliberate. A model can infer useful things, but users should be able to review durable profile and behavior changes.
const proposals = await agentMemory.proposals.list({
namespace: "user:123",
status: "pending",
});
await agentMemory.proposals.approve(proposals[0].id);Only pending proposals can be approved, rejected, or edited. Once a proposal is decided, trying to change it again is an error, so a decision cannot accidentally write duplicate memory.
If your product has a safe write path, set write: { mode: 'auto' }. For example, a procedure extracted from an explicit settings screen can be auto-written; a preference inferred from a casual message should probably remain proposed. Use write: { mode: 'manual' } when capture should extract nothing automatically and writes should happen only through direct block method calls.
Policy
Policies are where you keep memory safe and product-specific:
const profileFacts = facts({
id: "profile",
extract: extractFactsFromTurn,
policy: {
redact: removeSecrets,
validate: factSchema,
shouldRemember: (fact) => fact.confidence >= 0.75,
},
});Use policy to prevent secrets, reject low-confidence guesses, or enforce a structured candidate shape before the candidate reaches proposals.
Retrieval
Both blocks support find(query, options) when a dense embedding is configured. memory() uses their render methods to include active memory in prompts, while direct calls let application code inspect or curate entries.
const activeProcedures = await operatingMemory.find("writing style", {
records,
namespace: "user:123",
memoryId: "assistant",
limit: 5,
});Semantic render strategies use the same path:
const projectFacts = facts({
id: "project",
embed: dense,
render: {
strategy: "semantic",
query: ({ input }) => `facts relevant to ${input.message}`,
limit: 6,
},
});Use strategy: 'list' or strategy: 'recent' when recency is more important than relevance, and use render: false when a block should store memory without entering the prompt.
Use facts for what the agent knows. Use procedures for how the agent should behave.