agentfootprint
Version:
The explainable agent framework — backtrack a wrong answer to the exact context that caused it (evidence, not guesses). Built on footprintjs.
591 lines • 32.9 kB
JavaScript
/**
* defineMemory — the single factory the consumer uses to register a
* memory subsystem on an Agent.
*
* defineMemory({ id, type, strategy, store }) → MemoryDefinition
*
* The factory's job:
* 1. Switch on `type` (Episodic / Semantic / Narrative / Causal)
* to pick the right family of pipelines.
* 2. Switch on `strategy.kind` within that family to wire stage
* configs (loadCount / topK / threshold / extractor / ...).
* 3. Return an opaque `MemoryDefinition` that step-4's
* `Agent.memory()` builder method consumes.
*
* Pattern: Factory + Strategy (GoF). One factory, N strategies, four
* types — all reduce to two compiled FlowCharts (`read`,
* `write?`) that mount as subflows.
*
* Role: Layer-2 of the memory stack. Sits between the const-objects
* contract (Layer 1) and the Agent builder method (Layer 4).
*
* Emits: Indirectly — the compiled subflows emit
* `agentfootprint.context.injected` with `source: 'memory'`
* when their formatter writes to the system-prompt slot.
*
* @see ./define.types.ts for the const-objects + types
* @see ./asRoleRefusal.ts for why `asRole` is refused, not honoured
* @see ./redactRefusal.ts for why `redact` is refused, not implemented
* @see ./pipeline/*.ts for the existing pipeline factories this dispatches to
*/
import { refuseAsRole } from './asRoleRefusal.js';
import { refuseMemoryRedact } from './redactRefusal.js';
import { assertStrategyRequirements, assertStrategyShape } from './strategies.js';
import { resolveRankingMode } from './store/capability.js';
import { assertStoreHonours, bindWriteOptions, } from './store/writeOptions.js';
import { defaultPipeline } from './pipeline/default.js';
import { ephemeralPipeline } from './pipeline/ephemeral.js';
import { semanticPipeline } from './pipeline/semantic.js';
import { factPipeline } from './pipeline/fact.js';
import { narrativePipeline } from './pipeline/narrative.js';
import { autoPipeline } from './pipeline/auto.js';
import { snapshotPipeline } from './causal/index.js';
import { MEMORY_TYPES, MEMORY_STRATEGIES, MEMORY_TIMING, } from './define.types.js';
// ─── Public factory ────────────────────────────────────────────────
/**
* Build a `MemoryDefinition` from a high-level `{ type, strategy, store }`
* config. Internally dispatches to one of the existing pipeline factories
* (defaultPipeline / semanticPipeline / factPipeline / narrativePipeline /
* autoPipeline / ephemeralPipeline) and wires the compiled flowcharts
* into the opaque definition that `Agent.memory()` consumes.
*
* Supported combinations:
*
* | type | strategy.kind | underlying pipeline |
* | --------- | ------------- | ------------------------ |
* | EPISODIC | WINDOW | defaultPipeline |
* | EPISODIC | BUDGET | defaultPipeline |
* | EPISODIC | SUMMARIZE | defaultPipeline + summarize stage (one call per span, written back) |
* | EPISODIC | DECAY | defaultPipeline + filterByDecay stage |
* | SEMANTIC | TOP_K | semanticPipeline |
* | SEMANTIC | EXTRACT | factPipeline |
* | SEMANTIC | WINDOW | factPipeline (recency-load) |
* | NARRATIVE | EXTRACT | narrativePipeline |
* | NARRATIVE | WINDOW | narrativePipeline (recency-load) |
* | (any) | HYBRID | autoPipeline (when sub-strategies map cleanly) |
*
* Unsupported combinations throw with a remediation hint pointing to a
* working alternative or to the raw `mountMemoryRead`/`mountMemoryWrite`
* helpers for power users.
*/
export function defineMemory(options) {
validate(options);
// The write options and the scope ride every write AND search the PIPELINE
// makes, so the pipeline is built over the store with them bound. The
// definition still carries the caller's own store (below), unbound, so the
// turn anchor keeps deduplicating by reference — a direct call on
// `definition.store` carries no scope (a store built with
// `requireNamespaceScope` refuses it).
const pipeline = buildPipeline(withBoundWrites(options));
// The declared-requirements backstop (9.5.0), deliberately AFTER the
// dispatch: the arms above know things this check does not (the
// server-text exemption, that it does not apply to CAUSAL, that EXTRACT's
// `llm` matters only for `extractor: 'llm'`), so they speak first and this
// says something only when nothing better did. What it guarantees is that
// no requirement `listMemoryStrategies()` DECLARES can go unchecked —
// including the ones a composed pipeline would have quietly ignored.
assertStrategyRequirements(options, `defineMemory[${options.id}]`);
// `readOnly` drops the write half entirely (8.8.0). Not "writes are
// skipped at runtime" — the subflow is never compiled and never mounted,
// so a read-only memory has no write stage in its chart, no write commit
// in its log, and nothing to disable later by accident.
const write = options.readOnly === true ? undefined : pipeline.write;
const billing = summarizerBilling(options);
// The three names the compiled pipeline would otherwise swallow. Read
// structurally, the way `validate` reads the same fields: the union's arms
// declare each other's spellings as `never`, so intersecting them here would
// make the very fields this reads unreadable. See MemoryDefinition.strategy.
const declared = options.strategy;
// The embedder's OWN id first: it is the thing that ran. `embedderId` is the
// declared filter, and it is the only answer for a server-text store, where
// there is no embedder instance to ask.
const embedderId = declared?.embedder?.id ?? declared?.embedderId;
const definition = {
id: options.id,
...(options.description !== undefined && { description: options.description }),
type: options.type,
...(declared?.kind !== undefined && { strategy: declared.kind }),
...(typeof declared?.retrieval?.name === 'string' && { retrieval: declared.retrieval.name }),
...(embedderId !== undefined && { embedderId }),
read: brandPipeline(pipeline.read),
...(write !== undefined && { write: brandPipeline(write) }),
// Carried in the open so the host can resolve WHICH TURN this is against
// the same durable namespace the pipeline writes into — the turn number
// is a key, not a label (see MemoryDefinition.store).
store: options.store,
timing: options.timing ?? MEMORY_TIMING.TURN_START,
// Declared so `Agent.memory()` can refuse a summarizer that is the agent's
// own instance at the agent's own model — a check only the builder can
// make. See `MemoryDefinition.billing`.
...(billing !== undefined && { billing }),
...(options.corpus !== undefined && { corpus: options.corpus }),
...(options.flavor !== undefined && { flavor: options.flavor }),
...(options.type === MEMORY_TYPES.CAUSAL &&
options.projection !== undefined && {
projection: options.projection,
}),
};
return Object.freeze(definition);
}
// ─── Validation ────────────────────────────────────────────────────
function validate(options) {
if (!options.id || options.id.trim() === '') {
throw new Error('defineMemory: `id` is required and must be non-empty.');
}
// `asRole` no longer type-checks; this catches JavaScript callers and
// casts. Refused before anything else is validated so the message is
// about the option the caller actually wrote.
refuseAsRole(options, `defineMemory('${options.id}')`);
// `redact` no longer type-checks either; same reason, same placement — it
// was reserved and read by nothing (`./redactRefusal.ts`).
refuseMemoryRedact(options, `defineMemory('${options.id}')`);
if (!options.store) {
throw new Error(`defineMemory[id=${options.id}]: \`store\` is required. ` +
'Pass `new InMemoryStore()` for dev/tests, or a backed store for production.');
}
if (options.writeOptions !== undefined && options.readOnly === true) {
throw new Error(`defineMemory[id=${options.id}]: \`writeOptions\` cannot be combined with ` +
'`readOnly: true` — a read-only memory compiles no write half, so there is no ' +
'write to apply them to. Drop one of the two (`namespaceScope` scopes reads too).');
}
if (options.namespaceScope !== undefined && options.corpus !== undefined) {
throw new Error(`defineMemory[id=${options.id}]: \`namespaceScope\` cannot be combined with ` +
'`corpus` — a corpus memory runs under the corpus identity, not the run’s, so a ' +
'per-identity scope would be resolved for the corpus and never for the person. Scope ' +
'a corpus by its own identity, or drop one of the two.');
}
const bound = boundOptionsOf(options);
if (bound !== undefined) {
// Before the first turn: a store that does not DECLARE an option would
// refuse it on every write, so a definition that asks for one is refused
// now, by name, instead.
assertStoreHonours(options.store, bound, `defineMemory[${options.id}]`);
}
// SHAPE before anything reads a field (9.5.1). Both the dispatch below and
// the requirements walk read fields off the strategy — `h.strategies[0]`,
// `for (const sub of strategy.strategies)` — and a field read off a shape
// that never had it is a `TypeError` from inside this library, naming
// neither the option the caller got wrong nor the one they meant.
// Field-reported: `{ kind: 'hybrid', size: 5 }` produced `Cannot read
// properties of undefined (reading '0')`.
assertStrategyShape(options.strategy, `defineMemory[${options.id}]`);
// The shorthand and the spelled-out rule EXCLUDE. Accepting both would
// mean one of two numbers silently loses, and the recording would name
// a `k` the run did not use.
// Read through a structural shape, not the union: the union's arms
// declare the OTHER spelling as `never`, so intersecting them here
// would make the very field this check reads unreadable.
const strategy = options.strategy;
if (strategy?.retrieval !== undefined) {
const shorthand = [];
if (strategy.topK !== undefined)
shorthand.push('`topK`');
if (strategy.threshold !== undefined)
shorthand.push('`threshold`');
if (shorthand.length > 0) {
throw new Error(`defineMemory[id=${options.id}]: ${shorthand.join(' and ')} cannot be combined with ` +
'`retrieval` — they are two spellings of the same rule and could disagree. ' +
`Keep the strategy (\`retrieval: ${strategy.retrieval.name}({ ... })\`) and drop ` +
`${shorthand.join('/')}, or drop \`retrieval\`.`);
}
}
}
// ─── Pipeline dispatch ─────────────────────────────────────────────
/** The write options and scope to bind to the store, when either was given. */
function boundOptionsOf(options) {
if (options.writeOptions === undefined && options.namespaceScope === undefined)
return undefined;
return {
...options.writeOptions,
...(options.namespaceScope !== undefined && { namespaceScope: options.namespaceScope }),
};
}
/**
* The options with the store bound: writes carrying `writeOptions` and the
* scope, searches confined to the scope — when either was given.
*/
function withBoundWrites(options) {
const bound = boundOptionsOf(options);
if (bound === undefined)
return options;
return { ...options, store: bindWriteOptions(options.store, bound) };
}
function buildPipeline(options) {
switch (options.type) {
case MEMORY_TYPES.EPISODIC:
return buildEpisodicPipeline(options);
case MEMORY_TYPES.SEMANTIC:
return buildSemanticPipeline(options);
case MEMORY_TYPES.NARRATIVE:
return buildNarrativePipeline(options);
case MEMORY_TYPES.CAUSAL:
return buildCausalPipeline(options);
default: {
const _exhaustive = options;
void _exhaustive;
throw new Error(`defineMemory: unknown type — ${options.type}`);
}
}
}
// ─── EPISODIC type ─────────────────────────────────────────────────
function buildEpisodicPipeline(options) {
const s = options.strategy;
switch (s.kind) {
case MEMORY_STRATEGIES.WINDOW: {
const w = s;
const config = { store: options.store, loadCount: w.size };
return defaultPipeline(config);
}
case MEMORY_STRATEGIES.BUDGET: {
const b = s;
const config = {
store: options.store,
...(b.reserveTokens !== undefined && { reserveTokens: b.reserveTokens }),
...(b.minimumTokens !== undefined && { minimumTokens: b.minimumTokens }),
...(b.maxEntries !== undefined && { maxEntries: b.maxEntries }),
};
return defaultPipeline(config);
}
case MEMORY_STRATEGIES.SUMMARIZE: {
// Wired in 9.14.0. Through 9.13.0 this arm built `defaultPipeline({
// loadCount: recent })` — the last `recent` entries, verbatim, with the
// `summarize` stage composed into nothing and the required `llm` never
// called. Eight turns through it with a counting provider made zero
// `complete()` calls. What it needed before it could be wired was
// exactly what this arm now demands: a named model, and a summary that
// is written back so the call is paid once instead of once per recall.
const sum = s;
const size = sum.size ?? DEFAULT_SUMMARIZE_WINDOW;
requireSummarizeConfig(options, sum, size);
const config = {
store: options.store,
loadCount: size,
summarize: {
llm: sum.llm,
model: sum.model,
preserveRecent: sum.recent,
// The window filling IS the trigger: recall cannot grow past
// `size`, so "the pool is full and there is material older than the
// tail" is the moment compression starts being worth its call.
triggerMinEntries: size,
// Never spend a call to fold less than the tail kept verbatim. The
// stage's own floor is 2 (don't "compress" one entry); this is the
// cost policy on top of it, and it is what stops a saturated window
// from folding two entries every turn forever.
minFoldEntries: Math.max(2, sum.recent),
strategyId: options.id,
...(sum.systemPrompt !== undefined && { systemPrompt: sum.systemPrompt }),
},
};
return defaultPipeline(config);
}
case MEMORY_STRATEGIES.HYBRID: {
// Compose multiple sub-strategies onto one store. Currently
// delegates to the first sub-strategy that's valid for episodic
// data; richer selector-style merge of all sub-strategies'
// outputs is planned.
const h = s;
const inner = h.strategies[0];
if (!inner) {
// Unreachable since 9.5.1 — `assertStrategyShape` refuses a missing,
// non-array or empty `strategies` in `validate()`, with the field
// named and a working line in the message. Kept as the index-access
// backstop the type system asks for, not as the message a caller sees.
throw new Error(`defineMemory[${options.id}]: HYBRID strategy requires at least one sub-strategy.`);
}
return buildEpisodicPipeline({ ...options, strategy: inner });
}
case MEMORY_STRATEGIES.EXTRACT:
throw new Error(`defineMemory[${options.id}]: EXTRACT strategy on EPISODIC type is not idiomatic — ` +
'extraction produces structured outputs (facts/beats), so use type=SEMANTIC or NARRATIVE.');
case MEMORY_STRATEGIES.TOP_K:
throw new Error(`defineMemory[${options.id}]: TOP_K strategy on EPISODIC type requires a vector store. ` +
'Use type=SEMANTIC for vector retrieval, or type=EPISODIC with strategy=WINDOW for recency.');
case MEMORY_STRATEGIES.DECAY: {
// Wired in 9.5.0. Until then this arm threw "not yet wired" while
// `MEMORY_STRATEGIES` went on offering the choice — a const that
// advertised seven strategies and built six.
const d = s;
if (!Number.isFinite(d.halfLifeMs) || d.halfLifeMs < 0) {
throw new Error(`defineMemory[${options.id}]: DECAY needs a \`halfLifeMs\` that is a non-negative ` +
`number of milliseconds — how long before an untouched entry is worth half as ` +
`much. Got \`${String(d.halfLifeMs)}\`.\n` +
` A negative half-life inverts the curve (older scores HIGHER), which no config ` +
`means to say, so it is refused rather than obeyed.\n` +
` Fix: a day is \`86_400_000\`; an hour is \`3_600_000\`.`);
}
const config = {
store: options.store,
decay: {
halfLifeMs: d.halfLifeMs,
...(d.minScore !== undefined && { minScore: d.minScore }),
},
};
return defaultPipeline(config);
}
default: {
const _exhaustive = s;
void _exhaustive;
throw new Error(`defineMemory: unknown strategy kind`);
}
}
}
/**
* The `{ provider, model }` this memory's own model calls bill to, or
* `undefined` for the six strategies that make none.
*
* Only SUMMARIZE calls a model on the host's behalf ON EVERY RUN. EXTRACT with
* `extractor: 'llm'` also holds a provider, and is deliberately NOT declared
* here: it carries no `model` at all (its extractors compose their own
* request), so there is no second half to compare and the refusal this feeds
* would have nothing to say.
*/
function summarizerBilling(options) {
const written = options.strategy;
// A hybrid builds its FIRST sub-strategy (buildEpisodicPipeline), so that is
// the one whose summarizer would run and the one the refusal must see.
const strategy = written?.kind === MEMORY_STRATEGIES.HYBRID ? written.strategies?.[0] : written;
if (strategy?.kind !== MEMORY_STRATEGIES.SUMMARIZE)
return undefined;
if (strategy.llm === undefined || typeof strategy.model !== 'string')
return undefined;
return { provider: strategy.llm, model: strategy.model };
}
/**
* How much history a SUMMARIZE memory loads per turn when the strategy does
* not say. Not a new number: it is `loadRecent`'s own default, which every
* other EPISODIC pipeline has used since 2.x.
*/
const DEFAULT_SUMMARIZE_WINDOW = 20;
/**
* The four things a SUMMARIZE memory is refused for — an unnamed `model`, a
* `recent` that is not a positive integer, a verbatim tail as large as the
* window, and `readOnly` — each at BUILD and each naming the line that fixes
* it.
*
* The `llm` itself is not checked here — `assertStrategyRequirements` declares
* and enforces that one for every strategy, and saying it twice would mean two
* messages for one mistake.
*/
function requireSummarizeConfig(options, strategy, size) {
const id = options.id;
// No summarizer at all is not a `model` problem, and saying so here would
// pre-empt the backstop's better sentence with a worse one. Let
// `assertStrategyRequirements` name the `llm` — it is the thing missing.
if (strategy.llm === undefined)
return;
if (typeof strategy.model !== 'string' || strategy.model.trim() === '') {
throw new Error(`defineMemory[${id}]: SUMMARIZE needs a \`model\` — the id the summarizer is called ` +
`with — and none was passed.\n` +
` It used to default to whatever the agent ran, and that default had no correct case: ` +
`the same provider family quietly billed your MAIN model for every fold (the one thing ` +
`naming a cheap summarizer exists to prevent), and a different vendor was sent a model ` +
`id it has never heard of, mid-conversation, on a paid run.\n` +
` Fix: \`strategy: { kind: 'summarize', recent: ${strategy.recent}, llm, ` +
`model: 'claude-haiku-4-5' }\` — or whatever your summarizer's cheap model is called.`);
}
if (!Number.isInteger(strategy.recent) || strategy.recent < 1) {
throw new Error(`defineMemory[${id}]: SUMMARIZE needs \`recent\` to be an integer >= 1 — how many of the ` +
`newest entries stay verbatim — and got \`${String(strategy.recent)}\`.\n` +
` Keeping zero verbatim would hand the model a summary of a conversation and none of ` +
`the conversation, including the turn it is answering.`);
}
if (strategy.recent >= size) {
throw new Error(`defineMemory[${id}]: SUMMARIZE keeps \`recent: ${strategy.recent}\` entries verbatim out ` +
`of a \`size: ${size}\` window, so nothing older than the verbatim tail is ever loaded ` +
`and the summarizer would never have anything to fold — a paid dependency wired to a ` +
`stage that cannot fire.\n` +
` Fix: raise \`size\` (how much history to load per turn, default ` +
`${DEFAULT_SUMMARIZE_WINDOW}) above \`recent\`, e.g. \`size: ${strategy.recent * 4}\`.\n` +
` Or: this is \`{ kind: 'window', size: ${strategy.recent} }\`, which costs nothing.`);
}
if (options.readOnly === true) {
throw new Error(`defineMemory[${id}]: SUMMARIZE cannot be \`readOnly\`. The two options contradict each ` +
`other: \`readOnly\` says nothing this memory sees is ever stored back, and SUMMARIZE's ` +
`cost model IS the store — the summary is written under ` +
`\`msg-summary-{from}-{to}\` so a span is compressed once instead of once per turn.\n` +
` Fix: drop \`readOnly\`, or use \`{ kind: 'window', size: ${size} }\` for a memory ` +
`that only ever reads.`);
}
}
// ─── SEMANTIC type ─────────────────────────────────────────────────
function buildSemanticPipeline(options) {
const s = options.strategy;
switch (s.kind) {
case MEMORY_STRATEGIES.TOP_K: {
const t = s;
// Whether an embedder is required is a fact about the STORE, which the
// type cannot see — so the requirement lives here (9.3.0). Absence is
// legal for exactly one store shape and refused for every other.
const ranksBy = resolveRankingMode(options.store, `defineMemory[${options.id}]`);
if (t.embedder === undefined) {
if (ranksBy !== 'server-text') {
throw new Error(`defineMemory[${options.id}]: TOP_K needs an \`embedder\` — somebody has to turn ` +
`the query into a vector before this store can rank it.\n` +
` Omit it only for a store that declares \`ranksBy: 'server-text'\`, which ` +
`embeds and ranks the question on its own side.`);
}
if (options.readOnly !== true) {
throw new Error(`defineMemory[${options.id}]: a store that declares \`ranksBy: 'server-text'\` has ` +
`no write half here — this library cannot embed the turn (no embedder) and the ` +
`backend does its own ingestion.\n` +
` Fix: pass \`readOnly: true\` (which \`defineRAG\` sets for you), so the write ` +
`subflow is never compiled rather than compiled and silently doing nothing.`);
}
}
const config = {
store: options.store,
...(t.embedder !== undefined && { embedder: t.embedder }),
...(t.topK !== undefined && { k: t.topK }),
...(t.threshold !== undefined && { minScore: t.threshold }),
// Composes with either spelling — a size bound, not a second
// spelling of the count rule. See TopKRetrievalStrategy.maxChars.
...(t.maxChars !== undefined && { maxChars: t.maxChars }),
...(t.embedderId !== undefined && { embedderId: t.embedderId }),
...(t.retrieval !== undefined && { retrieval: t.retrieval }),
...(options.flavor !== undefined && { flavor: options.flavor }),
};
return semanticPipeline(config);
}
case MEMORY_STRATEGIES.EXTRACT: {
const e = s;
if (e.extractor === 'llm' && !e.llm) {
throw new Error(`defineMemory[${options.id}]: EXTRACT with extractor='llm' requires \`llm\` provider. ` +
"Pass `extractor: 'pattern'` to use the regex-heuristics extractor instead.");
}
const config = { store: options.store };
return factPipeline(config);
}
case MEMORY_STRATEGIES.WINDOW: {
// SEMANTIC × WINDOW: load top-N recent facts (no embedding query).
// factPipeline already loads by recency by default; size is interpreted
// as the load limit.
const w = s;
const config = { store: options.store, loadLimit: w.size };
return factPipeline(config);
}
case MEMORY_STRATEGIES.HYBRID: {
// SEMANTIC × HYBRID — compose facts + beats via autoPipeline.
const config = { store: options.store };
return autoPipeline(config);
}
case MEMORY_STRATEGIES.BUDGET:
case MEMORY_STRATEGIES.SUMMARIZE:
case MEMORY_STRATEGIES.DECAY:
throw new Error(`defineMemory[${options.id}]: ${String(s.kind)} strategy is not supported on SEMANTIC type. ` +
'Use TOP_K (vector retrieval), EXTRACT (LLM/pattern fact extraction), ' +
'WINDOW (recency-load), or HYBRID (auto compose).');
default: {
const _exhaustive = s;
void _exhaustive;
throw new Error(`defineMemory: unknown strategy kind`);
}
}
}
// ─── NARRATIVE type ────────────────────────────────────────────────
function buildNarrativePipeline(options) {
const s = options.strategy;
switch (s.kind) {
case MEMORY_STRATEGIES.EXTRACT: {
const e = s;
if (e.extractor === 'llm' && !e.llm) {
throw new Error(`defineMemory[${options.id}]: EXTRACT with extractor='llm' requires \`llm\` provider.`);
}
const config = { store: options.store };
return narrativePipeline(config);
}
case MEMORY_STRATEGIES.WINDOW: {
const w = s;
const config = { store: options.store, loadCount: w.size };
return narrativePipeline(config);
}
case MEMORY_STRATEGIES.HYBRID: {
const config = { store: options.store };
return autoPipeline(config);
}
case MEMORY_STRATEGIES.TOP_K:
case MEMORY_STRATEGIES.BUDGET:
case MEMORY_STRATEGIES.SUMMARIZE:
case MEMORY_STRATEGIES.DECAY:
throw new Error(`defineMemory[${options.id}]: ${String(s.kind)} strategy is not supported on NARRATIVE type. ` +
'Use EXTRACT (LLM/heuristic beat extraction), WINDOW (recency-load), or HYBRID.');
default: {
const _exhaustive = s;
void _exhaustive;
throw new Error(`defineMemory: unknown strategy kind`);
}
}
}
// ─── CAUSAL type ───────────────────────────────────────────────────
function buildCausalPipeline(options) {
const s = options.strategy;
// Causal memory writes (query, finalContent) snapshots tagged with
// the original user query. Retrieval embeds the new query and
// cosine-matches against past queries, returning the most relevant
// snapshot for replay. Strict threshold: no match → no injection.
if (s.kind !== MEMORY_STRATEGIES.TOP_K) {
throw new Error(`defineMemory[${options.id}]: CAUSAL type only supports TOP_K strategy. ` +
'Snapshots are matched semantically against the new user query; ' +
"WINDOW/BUDGET/SUMMARIZE/EXTRACT/DECAY/HYBRID don't apply.");
}
if (!options.store.search) {
throw new Error(`defineMemory[${options.id}]: CAUSAL type requires a vector-capable store. ` +
'Pass `new InMemoryStore({ embedder })` for dev/tests, or a vector adapter ' +
'(pgvector, Pinecone, Qdrant) for production.');
}
// CAUSAL predates the retrieval seam and reads the shorthand only. A
// strategy passed here would be accepted and ignored, so it is refused.
if (s.retrieval !== undefined) {
throw new Error(`defineMemory[${options.id}]: CAUSAL type does not read \`retrieval\` — snapshot recall ` +
'matches a stored query vector, not a document pool. Use `topK` + `threshold`.');
}
// Same law, same reason (8.19.0): the snapshot pipeline has no character
// budget, so accepting one here would be an option the run does not read.
if (s.maxChars !== undefined) {
throw new Error(`defineMemory[${options.id}]: CAUSAL type does not read \`maxChars\` — it recalls whole ` +
'past-run snapshots, not a pool of passages to spend a character budget across. ' +
'Bound it with `topK` (snapshots recalled) or `projection` (how much of each one).');
}
// CAUSAL matches a stored QUERY VECTOR against a new one — there is no
// server-side text path into a snapshot pool, so the embedder stays
// unconditionally required here even though the type made it optional for
// the semantic case (9.3.0).
if (s.embedder === undefined) {
throw new Error(`defineMemory[${options.id}]: CAUSAL type requires an \`embedder\` — snapshot recall ` +
'matches the new query against the query vector each past run was filed under, so ' +
"there is always a vector to produce. (`ranksBy: 'server-text'` relaxes this for " +
'SEMANTIC retrieval only.)');
}
const config = {
store: options.store,
embedder: s.embedder,
topK: s.topK,
...(s.threshold !== undefined && { minScore: s.threshold }),
...(options.projection !== undefined && { projection: options.projection }),
};
return snapshotPipeline(config);
}
// ─── Helpers ───────────────────────────────────────────────────────
/**
* The factory hands back an opaque `ReadonlyMemoryFlowChart<T>` brand
* to keep consumers from reaching into the FlowChart shape directly —
* step-4's `Agent.memory()` is the only place that unwraps it.
*/
function brandPipeline(fc) {
return fc;
}
/**
* Internal — unwrap the brand. Used by `Agent.memory()` (step 4)
* to mount the pipeline. NOT exported.
*
* @internal
*/
export function unwrapMemoryFlowChart(branded) {
return branded;
}
// Suppress ESLint unused-import warning for `ephemeralPipeline` —
// reserved for a future `readOnly: true` config flag.
void ephemeralPipeline;
// Suppress for `Strategy` — kept as exported type for consumers.
void null;
//# sourceMappingURL=define.js.map