UNPKG

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
/** * 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