{"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/orchestrator/memory.ts"],"sourcesContent":["import type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type {\n  MemoryItem,\n  RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type {\n  OrchestratorMemoryConfig,\n  OrchestratorMemoryScope,\n} from \"../contracts/orchestrator/orchestrator-config.type\";\nimport type { TurnSnapshot } from \"../contracts/result/orchestrator-result.type\";\nimport type { SupervisorInput } from \"../contracts/supervisor/supervisor-input.type\";\n\n/** Default key the recalled memories are injected under in the context bag. */\nconst DEFAULT_INJECT_KEY = \"memories\";\n\n/**\n * Default isolation boundary: a turn recalls only what its own session\n * remembered. Cross-session pooling is opt-in (`scope: \"shared\"`) — the\n * default must not leak one user's remembered turns into another's\n * context, since one memory store backs every session of an\n * orchestrator instance.\n */\nconst DEFAULT_SCOPE = \"session\" as const;\n\n/**\n * Memory wiring resolved once per turn from `OrchestratorConfig.memory`\n * (memory core M2). Normalizes the two accepted config shapes — a bare\n * {@link MemoryContract} or the richer {@link OrchestratorMemoryConfig} —\n * into a single flat record the lifecycle phase reads, so `runTurn` never\n * branches on which form the dev supplied.\n */\nexport type ResolvedOrchestratorMemory = {\n  /** The store recalled-from before dispatch and remembered-into after. */\n  store: MemoryContract;\n  /** Recall count cap; `0` disables recall (write-only memory). */\n  k?: number;\n  /** Semantic-similarity floor for recall. */\n  threshold?: number;\n  /** Single-tier recall restriction. */\n  tier?: ResolvedTier;\n  /** Whether a clean turn writes its outcome back. Default `true`. */\n  remember: boolean;\n  /** Tier the remembered outcome lands in. Omit for the memory's `defaultTier`. */\n  rememberTier?: ResolvedTier;\n  /**\n   * Isolation boundary for recall + write-back. Default `\"session\"` —\n   * the turn's `sessionId` keys every read and write, so one session\n   * cannot recall another's memories out of the shared store.\n   */\n  scope: OrchestratorMemoryScope;\n  /** Context-bag key the recalled memories are injected under. */\n  injectKey: string;\n};\n\ntype ResolvedTier = NonNullable<OrchestratorMemoryConfig[\"recall\"]>[\"tier\"];\n\n/**\n * A `MemoryContract` is the bare-store form; anything carrying a `store`\n * is the {@link OrchestratorMemoryConfig} wrapper. Distinguished by the\n * presence of `recall` — a method on the contract, absent on the config\n * (whose own `recall` is a plain options object, never a function).\n */\nfunction isBareMemory(\n  value: MemoryContract | OrchestratorMemoryConfig,\n): value is MemoryContract {\n  return typeof (value as MemoryContract).recall === \"function\";\n}\n\n/**\n * Normalize `OrchestratorConfig.memory` into {@link ResolvedOrchestratorMemory},\n * or `undefined` when no memory is configured. Centralizes the\n * bare-store-vs-config distinction so the engine context carries one\n * shape and the lifecycle phase stays branch-free.\n */\nexport function resolveOrchestratorMemory(\n  memory: MemoryContract | OrchestratorMemoryConfig | undefined,\n): ResolvedOrchestratorMemory | undefined {\n  if (!memory) {\n    return undefined;\n  }\n\n  if (isBareMemory(memory)) {\n    return {\n      store: memory,\n      remember: true,\n      scope: DEFAULT_SCOPE,\n      injectKey: DEFAULT_INJECT_KEY,\n    };\n  }\n\n  return {\n    store: memory.store,\n    k: memory.recall?.k,\n    threshold: memory.recall?.threshold,\n    tier: memory.recall?.tier,\n    remember: memory.remember ?? true,\n    rememberTier: memory.rememberTier,\n    scope: memory.scope ?? DEFAULT_SCOPE,\n    injectKey: memory.injectKey ?? DEFAULT_INJECT_KEY,\n  };\n}\n\n/**\n * Resolve the isolation key a turn reads and writes memories under\n * (4.15.0 — security fix for cross-session recall).\n *\n * The memory store is resolved once per orchestrator instance and reused\n * by every session, so this — not the store — is what keeps one session's\n * remembered turns out of another's recall. It is derived from the\n * execute-time `sessionId` by the engine and handed to every tier as an\n * exact-match filter; the model, the tool payload, and the per-call\n * `context` bag have no say in it.\n *\n * `\"shared\"` resolves to `undefined`, i.e. the store's unscoped pool —\n * the explicit opt-in back to pre-4.15.0 cross-session behavior, which\n * also keeps memories written before this release readable.\n */\nexport function memoryScopeFor(\n  memory: ResolvedOrchestratorMemory,\n  sessionId: string,\n): string | undefined {\n  if (memory.scope === \"shared\") {\n    return undefined;\n  }\n\n  if (typeof memory.scope === \"function\") {\n    return memory.scope(sessionId);\n  }\n\n  return sessionMemoryScope(sessionId);\n}\n\n/**\n * The default `\"session\"` scope key: the session id under a reserved\n * prefix, so a custom `scope` callback returning a bare tenant id can\n * never accidentally collide with a session-scoped pool.\n */\nexport function sessionMemoryScope(sessionId: string): string {\n  return `session:${sessionId}`;\n}\n\n/**\n * Coerce a turn's {@link SupervisorInput} (string or structured object)\n * into the natural-language query the memory store recalls / embeds\n * against. Strings pass through; objects are JSON-serialized — the same\n * coercion the supervisor applies when forwarding an object input to a\n * child agent without an explicit `input(ctx)` override.\n */\nexport function memoryQueryFromInput(input: SupervisorInput): string {\n  return typeof input === \"string\" ? input : JSON.stringify(input);\n}\n\n/**\n * Recall the memories relevant to a turn's input (memory core M2 — the\n * pre-dispatch half). Returns the scored {@link RecalledMemory}[] the\n * lifecycle injects into the turn's `context` bag under\n * `memory.injectKey`. Returns an empty array — never throws on \"no hits\"\n * — and short-circuits when `k === 0` (recall disabled / write-only\n * memory) so a write-only config never round-trips the embedder.\n *\n * The recall is confined to the calling session's scope (see\n * {@link memoryScopeFor}) — `sessionId` is required, not optional, so a\n * new call site cannot silently recall across every session.\n */\nexport async function recallForTurn(\n  memory: ResolvedOrchestratorMemory,\n  input: SupervisorInput,\n  sessionId: string,\n): Promise<RecalledMemory[]> {\n  if (memory.k === 0) {\n    return [];\n  }\n\n  return memory.store.recall(memoryQueryFromInput(input), {\n    k: memory.k,\n    threshold: memory.threshold,\n    tier: memory.tier,\n    scope: memoryScopeFor(memory, sessionId),\n  });\n}\n\n/**\n * Merge the recalled memories into a fresh per-turn context bag under\n * `memory.injectKey` (memory core M2 — the injection half). Never\n * mutates the caller's `context` object — returns a new bag (or the\n * original when there is nothing to inject) so the request-scoped input\n * stays immutable, and the supervisor's intake (which freezes a\n * shallow copy) sees the recalled set on every `ctx.context[injectKey]`.\n *\n * A pre-existing value at `injectKey` is preserved when recall produced\n * nothing, and overwritten with the recalled set otherwise — the\n * orchestrator owns that key once memory is configured.\n */\nexport function injectMemories(\n  context: Record<string, unknown> | undefined,\n  memory: ResolvedOrchestratorMemory,\n  recalled: RecalledMemory[],\n): Record<string, unknown> | undefined {\n  if (recalled.length === 0) {\n    return context;\n  }\n\n  return { ...(context ?? {}), [memory.injectKey]: recalled };\n}\n\n/**\n * Remember a settled turn's outcome (memory core M2 — the post-dispatch\n * half). Called only after a clean turn (cancelled / failed turns revert\n * and never remember — §17). No-ops when `remember` is `false`\n * (read-only memory) or when the produced text is empty.\n *\n * The remembered text is the turn input followed by the model's textual\n * outcome when one is available, so a later `recall` keyed on a similar\n * input surfaces both the prior question and its answer.\n *\n * The write is tagged with the calling session's scope (see\n * {@link memoryScopeFor}) so only that session recalls it later —\n * turn text routinely contains one user's private content.\n */\nexport async function rememberTurnOutcome(\n  memory: ResolvedOrchestratorMemory,\n  input: SupervisorInput,\n  outcomeText: string | undefined,\n  sessionId: string,\n): Promise<void> {\n  if (!memory.remember) {\n    return;\n  }\n\n  const text = buildOutcomeText(input, outcomeText);\n\n  if (!text) {\n    return;\n  }\n\n  const item: MemoryItem = {\n    text,\n    tier: memory.rememberTier,\n    scope: memoryScopeFor(memory, sessionId),\n  };\n\n  await memory.store.remember(item);\n}\n\n/**\n * Compose the text written to memory for a turn: the input query, plus\n * the outcome text on a following line when the dispatch produced one.\n * Returns `undefined` when neither side carries content so an empty turn\n * never pollutes the store.\n */\nfunction buildOutcomeText(\n  input: SupervisorInput,\n  outcomeText: string | undefined,\n): string | undefined {\n  const query = memoryQueryFromInput(input).trim();\n  const outcome = outcomeText?.trim();\n\n  if (query && outcome) {\n    return `${query}\\n${outcome}`;\n  }\n\n  return query || outcome || undefined;\n}\n\n/**\n * Derive a turn's textual outcome for remembering (memory core M2).\n * Prefers the validated `result.data` (an `output` schema reshaped it);\n * otherwise stringifies the dispatched intents' branch outputs from the\n * turn snapshot, joined newline-wise so a multi-branch fan-out\n * contributes every output. Returns `undefined` when the turn produced\n * no usable text — the caller then remembers the input alone.\n */\nexport function outcomeTextFromTurn(\n  data: unknown,\n  turnSnapshot: TurnSnapshot,\n): string | undefined {\n  const fromData = stringifyOutcome(data);\n\n  if (fromData) {\n    return fromData;\n  }\n\n  const outputs = Object.values(turnSnapshot.result)\n    .map((branch) => stringifyOutcome(branch.output))\n    .filter((text): text is string => Boolean(text));\n\n  return outputs.length > 0 ? outputs.join(\"\\n\") : undefined;\n}\n\n/**\n * Coerce one outcome value to text: strings pass through; everything\n * else (objects, numbers) is JSON-serialized. `undefined` / `null` and\n * empty strings collapse to `undefined` so they don't masquerade as\n * content.\n */\nfunction stringifyOutcome(value: unknown): string | undefined {\n  if (value === undefined || value === null) {\n    return undefined;\n  }\n\n  const text = typeof value === \"string\" ? value : JSON.stringify(value);\n\n  return text.trim() ? text : undefined;\n}\n"],"mappings":";;AAaA,MAAM,qBAAqB;;;;;;;;AAS3B,MAAM,gBAAgB;;;;;;;AAwCtB,SAAS,aACP,OACyB;CACzB,OAAO,OAAQ,MAAyB,WAAW;AACrD;;;;;;;AAQA,SAAgB,0BACd,QACwC;CACxC,IAAI,CAAC,QACH;CAGF,IAAI,aAAa,MAAM,GACrB,OAAO;EACL,OAAO;EACP,UAAU;EACV,OAAO;EACP,WAAW;CACb;CAGF,OAAO;EACL,OAAO,OAAO;EACd,GAAG,OAAO,QAAQ;EAClB,WAAW,OAAO,QAAQ;EAC1B,MAAM,OAAO,QAAQ;EACrB,UAAU,OAAO,YAAY;EAC7B,cAAc,OAAO;EACrB,OAAO,OAAO,SAAS;EACvB,WAAW,OAAO,aAAa;CACjC;AACF;;;;;;;;;;;;;;;;AAiBA,SAAgB,eACd,QACA,WACoB;CACpB,IAAI,OAAO,UAAU,UACnB;CAGF,IAAI,OAAO,OAAO,UAAU,YAC1B,OAAO,OAAO,MAAM,SAAS;CAG/B,OAAO,mBAAmB,SAAS;AACrC;;;;;;AAOA,SAAgB,mBAAmB,WAA2B;CAC5D,OAAO,WAAW;AACpB;;;;;;;;AASA,SAAgB,qBAAqB,OAAgC;CACnE,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;AACjE;;;;;;;;;;;;;AAcA,eAAsB,cACpB,QACA,OACA,WAC2B;CAC3B,IAAI,OAAO,MAAM,GACf,OAAO,CAAC;CAGV,OAAO,OAAO,MAAM,OAAO,qBAAqB,KAAK,GAAG;EACtD,GAAG,OAAO;EACV,WAAW,OAAO;EAClB,MAAM,OAAO;EACb,OAAO,eAAe,QAAQ,SAAS;CACzC,CAAC;AACH;;;;;;;;;;;;;AAcA,SAAgB,eACd,SACA,QACA,UACqC;CACrC,IAAI,SAAS,WAAW,GACtB,OAAO;CAGT,OAAO;EAAE,GAAI,WAAW,CAAC;GAAK,OAAO,YAAY;CAAS;AAC5D;;;;;;;;;;;;;;;AAgBA,eAAsB,oBACpB,QACA,OACA,aACA,WACe;CACf,IAAI,CAAC,OAAO,UACV;CAGF,MAAM,OAAO,iBAAiB,OAAO,WAAW;CAEhD,IAAI,CAAC,MACH;CAGF,MAAM,OAAmB;EACvB;EACA,MAAM,OAAO;EACb,OAAO,eAAe,QAAQ,SAAS;CACzC;CAEA,MAAM,OAAO,MAAM,SAAS,IAAI;AAClC;;;;;;;AAQA,SAAS,iBACP,OACA,aACoB;CACpB,MAAM,QAAQ,qBAAqB,KAAK,CAAC,CAAC,KAAK;CAC/C,MAAM,UAAU,aAAa,KAAK;CAElC,IAAI,SAAS,SACX,OAAO,GAAG,MAAM,IAAI;CAGtB,OAAO,SAAS,WAAW;AAC7B;;;;;;;;;AAUA,SAAgB,oBACd,MACA,cACoB;CACpB,MAAM,WAAW,iBAAiB,IAAI;CAEtC,IAAI,UACF,OAAO;CAGT,MAAM,UAAU,OAAO,OAAO,aAAa,MAAM,CAAC,CAC/C,KAAK,WAAW,iBAAiB,OAAO,MAAM,CAAC,CAAC,CAChD,QAAQ,SAAyB,QAAQ,IAAI,CAAC;CAEjD,OAAO,QAAQ,SAAS,IAAI,QAAQ,KAAK,IAAI,IAAI;AACnD;;;;;;;AAQA,SAAS,iBAAiB,OAAoC;CAC5D,IAAI,UAAU,UAAa,UAAU,MACnC;CAGF,MAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,KAAK,UAAU,KAAK;CAErE,OAAO,KAAK,KAAK,IAAI,OAAO;AAC9B"}