@warlock.js/ai 5.2.3 → 5.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/cjs/index.cjs +1 -1
  2. package/cjs/index.cjs.map +1 -1
  3. package/cjs/{magic-string.es-BQeqHJ-a.cjs → magic-string.es-G6Xl81ni.cjs} +2 -2
  4. package/cjs/{magic-string.es-BQeqHJ-a.cjs.map → magic-string.es-G6Xl81ni.cjs.map} +1 -1
  5. package/cjs/matcher-logic-07fFOz7r.cjs.map +1 -1
  6. package/cjs/{matchers-CINm4ojZ.cjs → matchers-D7PBk4ut.cjs} +7 -7
  7. package/cjs/{matchers-CINm4ojZ.cjs.map → matchers-D7PBk4ut.cjs.map} +1 -1
  8. package/esm/agent/agent-input-builder.mjs.map +1 -1
  9. package/esm/agent/agent-stream.d.mts.map +1 -1
  10. package/esm/agent/agent-stream.mjs.map +1 -1
  11. package/esm/agent/agent.d.mts.map +1 -1
  12. package/esm/agent/agent.mjs.map +1 -1
  13. package/esm/agent/json-stream-guard.mjs.map +1 -1
  14. package/esm/agent/signature.mjs.map +1 -1
  15. package/esm/agent/snapshot.mjs.map +1 -1
  16. package/esm/agent/spawn-sub-agent.d.mts.map +1 -1
  17. package/esm/batch/batch.d.mts.map +1 -1
  18. package/esm/batch/batch.mjs.map +1 -1
  19. package/esm/checkpoint/memory.d.mts.map +1 -1
  20. package/esm/checkpoint/pg.mjs.map +1 -1
  21. package/esm/checkpoint/redis.mjs.map +1 -1
  22. package/esm/config.d.mts.map +1 -1
  23. package/esm/eval/dataset.d.mts.map +1 -1
  24. package/esm/eval/dataset.mjs.map +1 -1
  25. package/esm/eval/eval-runner.d.mts.map +1 -1
  26. package/esm/eval/eval-runner.mjs.map +1 -1
  27. package/esm/eval/judge-scorer.d.mts.map +1 -1
  28. package/esm/eval/regression.d.mts.map +1 -1
  29. package/esm/eval/regression.mjs.map +1 -1
  30. package/esm/eval/report-json.d.mts.map +1 -1
  31. package/esm/eval/report-junit.mjs.map +1 -1
  32. package/esm/eval/scorers.d.mts.map +1 -1
  33. package/esm/eval/scorers.mjs.map +1 -1
  34. package/esm/guard/detectors/injection.mjs.map +1 -1
  35. package/esm/guard/detectors/moderation.mjs.map +1 -1
  36. package/esm/guard/detectors/pii.mjs.map +1 -1
  37. package/esm/guard/detectors/topic.mjs.map +1 -1
  38. package/esm/human/human-approval.mjs.map +1 -1
  39. package/esm/human/resume.d.mts.map +1 -1
  40. package/esm/human/stores/memory.d.mts.map +1 -1
  41. package/esm/human/stores/pg.mjs.map +1 -1
  42. package/esm/human/stores/redis.mjs.map +1 -1
  43. package/esm/image/image.mjs.map +1 -1
  44. package/esm/memory/derive-id.mjs.map +1 -1
  45. package/esm/memory/episodic-memory.mjs.map +1 -1
  46. package/esm/memory/memory.mjs.map +1 -1
  47. package/esm/memory/procedural-memory.mjs.map +1 -1
  48. package/esm/memory/semantic-memory.mjs.map +1 -1
  49. package/esm/memory/working-memory.mjs.map +1 -1
  50. package/esm/middleware/builtins/budget.mjs.map +1 -1
  51. package/esm/middleware/builtins/semantic-cache.mjs.map +1 -1
  52. package/esm/middleware/helpers/compose.d.mts.map +1 -1
  53. package/esm/middleware/helpers/for-tool.mjs.map +1 -1
  54. package/esm/middleware/pipeline.d.mts.map +1 -1
  55. package/esm/middleware/utils/extract-user-text.mjs.map +1 -1
  56. package/esm/middleware/utils/namespaced-state.d.mts.map +1 -1
  57. package/esm/mock/mock-agent.d.mts.map +1 -1
  58. package/esm/mock/mock-agent.mjs.map +1 -1
  59. package/esm/mock/mock-model.d.mts.map +1 -1
  60. package/esm/mock/mock-model.mjs.map +1 -1
  61. package/esm/mock/mock-router.d.mts.map +1 -1
  62. package/esm/model/fallback-model.mjs.map +1 -1
  63. package/esm/node_modules/.pnpm/@jridgewell_sourcemap-codec@1.6.0/node_modules/@jridgewell/sourcemap-codec/dist/sourcemap-codec.mjs.map +1 -1
  64. package/esm/node_modules/.pnpm/@vitest_expect@4.1.10/node_modules/@vitest/expect/dist/index.mjs.map +1 -1
  65. package/esm/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.mjs +2 -2
  66. package/esm/node_modules/.pnpm/@vitest_pretty-format@4.1.10/node_modules/@vitest/pretty-format/dist/index.mjs.map +1 -1
  67. package/esm/node_modules/.pnpm/@vitest_runner@4.1.10/node_modules/@vitest/runner/dist/chunk-artifact.mjs.map +1 -1
  68. package/esm/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.mjs +1 -1
  69. package/esm/node_modules/.pnpm/@vitest_snapshot@4.1.10/node_modules/@vitest/snapshot/dist/index.mjs.map +1 -1
  70. package/esm/node_modules/.pnpm/@vitest_spy@4.1.10/node_modules/@vitest/spy/dist/index.mjs.map +1 -1
  71. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/chunk-pathe.M-eThtNZ.mjs.map +1 -1
  72. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.mjs +1 -1
  73. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/diff.mjs.map +1 -1
  74. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.mjs +1 -1
  75. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/display.mjs.map +1 -1
  76. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/helpers.mjs.map +1 -1
  77. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/offset.mjs.map +1 -1
  78. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/serialize.mjs.map +1 -1
  79. package/esm/node_modules/.pnpm/@vitest_utils@4.1.10/node_modules/@vitest/utils/dist/source-map.mjs.map +1 -1
  80. package/esm/node_modules/.pnpm/chai@6.2.2/node_modules/chai/index.mjs.map +1 -1
  81. package/esm/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs +1 -1
  82. package/esm/node_modules/.pnpm/magic-string@0.30.21/node_modules/magic-string/dist/magic-string.es.mjs.map +1 -1
  83. package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/rpc.MzXet3jl.mjs.map +1 -1
  84. package/esm/node_modules/.pnpm/vitest@4.1.10_@opentelemetr_3b60e89b8b51a25e87011ae54ec49250/node_modules/vitest/dist/chunks/test.DNmyFkvJ.mjs.map +1 -1
  85. package/esm/object-stream/parse-partial-json.mjs.map +1 -1
  86. package/esm/object-stream/stream-object.d.mts.map +1 -1
  87. package/esm/object-stream/stream-object.mjs.map +1 -1
  88. package/esm/observe/observer-registry.d.mts.map +1 -1
  89. package/esm/orchestrator/as-tool.d.mts.map +1 -1
  90. package/esm/orchestrator/checkpoint.mjs.map +1 -1
  91. package/esm/orchestrator/compaction.mjs.map +1 -1
  92. package/esm/orchestrator/emitter.d.mts.map +1 -1
  93. package/esm/orchestrator/emitter.mjs.map +1 -1
  94. package/esm/orchestrator/execution.d.mts.map +1 -1
  95. package/esm/orchestrator/execution.mjs.map +1 -1
  96. package/esm/orchestrator/memory.mjs.map +1 -1
  97. package/esm/orchestrator/orchestrator-stream.d.mts.map +1 -1
  98. package/esm/orchestrator/orchestrator-stream.mjs.map +1 -1
  99. package/esm/orchestrator/orchestrator.d.mts.map +1 -1
  100. package/esm/orchestrator/orchestrator.mjs.map +1 -1
  101. package/esm/orchestrator/session-lock.d.mts.map +1 -1
  102. package/esm/orchestrator/signature.mjs.map +1 -1
  103. package/esm/planner/dag-scheduler.mjs.map +1 -1
  104. package/esm/planner/plan-prompt.mjs.map +1 -1
  105. package/esm/planner/planner-run.mjs.map +1 -1
  106. package/esm/planner/planner.d.mts.map +1 -1
  107. package/esm/planner/planner.mjs.map +1 -1
  108. package/esm/planner/signature.mjs.map +1 -1
  109. package/esm/planner/snapshot.mjs.map +1 -1
  110. package/esm/prompt/prompt-langfuse-sync.mjs.map +1 -1
  111. package/esm/prompt/prompt-validate.mjs.map +1 -1
  112. package/esm/prompt/prompt.mjs.map +1 -1
  113. package/esm/prompts/prompts-manager.d.mts.map +1 -1
  114. package/esm/prompts/prompts-manager.mjs.map +1 -1
  115. package/esm/prompts/prompts-validate.mjs.map +1 -1
  116. package/esm/rag/chunk/chunk.mjs.map +1 -1
  117. package/esm/rag/chunk/markdown.mjs.map +1 -1
  118. package/esm/rag/chunk/sentence.mjs.map +1 -1
  119. package/esm/rag/hybrid/bm25.mjs.map +1 -1
  120. package/esm/rag/hybrid/hybrid-rank.mjs.map +1 -1
  121. package/esm/rag/hybrid/rrf.mjs.map +1 -1
  122. package/esm/rag/loaders/load-html.mjs.map +1 -1
  123. package/esm/rag/loaders/load-pdf.mjs.map +1 -1
  124. package/esm/rag/loaders/load-text.mjs.map +1 -1
  125. package/esm/rag/rerank/keyword-reranker.mjs.map +1 -1
  126. package/esm/rag/rerank/llm-reranker.mjs.map +1 -1
  127. package/esm/rag/retrieve.mjs.map +1 -1
  128. package/esm/rag/store/cache-vector-store.mjs.map +1 -1
  129. package/esm/rag/store/pg-vector-store.mjs.map +1 -1
  130. package/esm/rag/transforms/multi-query.mjs.map +1 -1
  131. package/esm/security/outbound-policy.mjs.map +1 -1
  132. package/esm/security/private-ip.mjs.map +1 -1
  133. package/esm/security/redact.d.mts.map +1 -1
  134. package/esm/security/redact.mjs.map +1 -1
  135. package/esm/serve/serve.d.mts.map +1 -1
  136. package/esm/serve/serve.mjs.map +1 -1
  137. package/esm/serve/stream-to-sse.d.mts.map +1 -1
  138. package/esm/skills/catalog.mjs.map +1 -1
  139. package/esm/skills/skills.mjs.map +1 -1
  140. package/esm/skills/sources/directory-source.mjs.map +1 -1
  141. package/esm/skills/sources/parse-frontmatter.mjs.map +1 -1
  142. package/esm/skills/sources/url-source.mjs.map +1 -1
  143. package/esm/skills/store/mock-skills-store.mjs.map +1 -1
  144. package/esm/skills/store/procedural-skill-store.mjs.map +1 -1
  145. package/esm/snapshot/memory.d.mts.map +1 -1
  146. package/esm/snapshot/pg.mjs.map +1 -1
  147. package/esm/speech/speech.mjs.map +1 -1
  148. package/esm/supervisor/as-tool.d.mts.map +1 -1
  149. package/esm/supervisor/cancellation.mjs.map +1 -1
  150. package/esm/supervisor/emitter.d.mts.map +1 -1
  151. package/esm/supervisor/emitter.mjs.map +1 -1
  152. package/esm/supervisor/entries.mjs.map +1 -1
  153. package/esm/supervisor/execution.d.mts.map +1 -1
  154. package/esm/supervisor/execution.mjs.map +1 -1
  155. package/esm/supervisor/fan-out.mjs.map +1 -1
  156. package/esm/supervisor/router-factory.mjs.map +1 -1
  157. package/esm/supervisor/router-prompt.mjs.map +1 -1
  158. package/esm/supervisor/signature.mjs.map +1 -1
  159. package/esm/supervisor/snapshot.mjs.map +1 -1
  160. package/esm/supervisor/supervisor-stream.d.mts.map +1 -1
  161. package/esm/supervisor/supervisor-stream.mjs.map +1 -1
  162. package/esm/supervisor/supervisor.d.mts.map +1 -1
  163. package/esm/supervisor/supervisor.mjs.map +1 -1
  164. package/esm/system-prompt/refined-system-prompt.d.mts.map +1 -1
  165. package/esm/system-prompt/refined-system-prompt.mjs.map +1 -1
  166. package/esm/system-prompt/system-prompt.d.mts.map +1 -1
  167. package/esm/system-prompt/system-prompt.mjs.map +1 -1
  168. package/esm/team/team.d.mts.map +1 -1
  169. package/esm/testing/matcher-logic.mjs.map +1 -1
  170. package/esm/testing/register-lazy.d.mts.map +1 -1
  171. package/esm/tool/executable-as-tool.d.mts.map +1 -1
  172. package/esm/tool/tool.d.mts.map +1 -1
  173. package/esm/tool/tool.mjs.map +1 -1
  174. package/esm/transcribe/audio-input.mjs.map +1 -1
  175. package/esm/transcribe/transcribe.mjs.map +1 -1
  176. package/esm/utils/extract-json-payload.mjs.map +1 -1
  177. package/esm/utils/generate-run-id.mjs.map +1 -1
  178. package/esm/utils/prepare-attachment-part.mjs.map +1 -1
  179. package/esm/utils/run-context.d.mts.map +1 -1
  180. package/esm/utils/safe-json-parse.d.mts.map +1 -1
  181. package/esm/vcr/cassette-io.mjs.map +1 -1
  182. package/esm/vcr/hash-request.mjs.map +1 -1
  183. package/esm/vcr/vcr.mjs.map +1 -1
  184. package/esm/workflow/cancellation.mjs.map +1 -1
  185. package/esm/workflow/emitter.mjs.map +1 -1
  186. package/esm/workflow/engine.mjs.map +1 -1
  187. package/esm/workflow/retry.mjs.map +1 -1
  188. package/esm/workflow/router.mjs.map +1 -1
  189. package/esm/workflow/signature.d.mts.map +1 -1
  190. package/esm/workflow/signature.mjs.map +1 -1
  191. package/esm/workflow/snapshot.mjs.map +1 -1
  192. package/esm/workflow/step-runner.mjs.map +1 -1
  193. package/esm/workflow/step.d.mts.map +1 -1
  194. package/esm/workflow/step.mjs.map +1 -1
  195. package/esm/workflow/workflow.d.mts.map +1 -1
  196. package/esm/workflow/workflow.mjs.map +1 -1
  197. package/package.json +4 -4
@@ -1 +1 @@
1
- {"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"sourcesContent":["import { resolveDefaultStore } from \"../config\";\nimport type {\n MemoryConfig,\n WorkingMemoryConfig,\n} from \"../contracts/memory/memory-config.type\";\nimport type {\n MemoryItem,\n MemoryTier,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type { RecallOptions } from \"../contracts/memory/recall-options.type\";\nimport { EpisodicMemory } from \"./episodic-memory\";\nimport { ProceduralMemory } from \"./procedural-memory\";\nimport { SemanticMemory } from \"./semantic-memory\";\nimport { WorkingMemory } from \"./working-memory\";\n\nconst DEFAULT_NAME = \"memory\";\nconst DEFAULT_SEMANTIC_NAMESPACE = \"ai.memory.semantic\";\nconst DEFAULT_EPISODIC_NAMESPACE = \"ai.memory.episodic\";\nconst DEFAULT_PROCEDURAL_NAMESPACE = \"ai.memory.procedural\";\nconst DEFAULT_K = 5;\nconst DEFAULT_THRESHOLD = 0.7;\nconst DEFAULT_RECENCY_WEIGHT = 0.3;\nconst DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_REINFORCEMENT_WEIGHT = 0.3;\n\n/**\n * Entries the in-process working buffer holds before it starts evicting\n * its oldest (4.15.0 — security fix for unbounded growth). Sized to hold\n * a deep multi-session scratch history while capping the tier's worst\n * case at a few MB of resident text rather than \"everything this process\n * has ever been told.\"\n */\nconst DEFAULT_WORKING_MAX_ITEMS = 1000;\n\n/**\n * Create an agent memory store (memory core M2).\n *\n * Wires up to four tiers behind the {@link MemoryContract}: **working**\n * (in-run scratch, recency), **semantic** (durable facts by cosine\n * similarity), **episodic** (durable events, similarity blended with\n * recency), and **procedural** (durable how-tos, similarity blended with\n * reinforcement). The working tier is on by default; the other three each\n * activate only when their config is supplied. The three vector tiers\n * mirror how `semanticCache` delegates similarity to the cache driver's\n * `.similar()`.\n *\n * Resolution happens once here, at construction (loud), rather than per\n * call (silent until first use): a vector-tier config with no `store` and\n * no `ai.config({ defaultStore })` throws now; enabling no tier at all\n * throws now.\n *\n * TTL-based decay / forgetting remains deferred. The working tier is\n * size-bounded (`working: { maxItems }`, default `1000`, oldest-written\n * evicted first) because it is the one tier that holds everything it is\n * told in process memory for the life of the instance; the durable tiers\n * delegate retention to their `CacheDriver`.\n *\n * **Isolation (4.15.0).** `remember({ scope })` / `recall(query, { scope })`\n * carry an opaque tenant / session key that every tier enforces as an\n * exact-equality filter before scoring — one scope's memories never\n * surface in another's recall, and identical text under two scopes stays\n * two entries. Unscoped writes form a shared pool that only an unscoped\n * recall can read; there is no \"all scopes\" query. `ai.orchestrator()`\n * derives this from the turn's `sessionId` automatically.\n *\n * @example\n * import { ai } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const mem = ai.memory({\n * semantic: { embedder, store },\n * defaultTier: \"semantic\",\n * });\n *\n * await mem.remember({ text: \"User prefers concise answers.\" });\n * const hits = await mem.recall(\"how should I respond?\", { k: 3 });\n */\nexport function memory(config: MemoryConfig = {}): MemoryContract {\n const name = config.name ?? DEFAULT_NAME;\n const workingConfig = config.working ?? true;\n const defaultK = config.k ?? DEFAULT_K;\n const defaultThreshold = config.threshold ?? DEFAULT_THRESHOLD;\n\n const working =\n workingConfig === false\n ? undefined\n : new WorkingMemory(resolveWorkingMaxItems(workingConfig, name));\n\n const semantic = config.semantic\n ? buildSemanticTier(config.semantic, name)\n : undefined;\n\n const episodic = config.episodic\n ? buildEpisodicTier(config.episodic, name)\n : undefined;\n\n const procedural = config.procedural\n ? buildProceduralTier(config.procedural, name)\n : undefined;\n\n const tiers: Tiers = { working, semantic, episodic, procedural };\n\n if (!working && !semantic && !episodic && !procedural) {\n throw new Error(\n `memory(\"${name}\"): no tier enabled — enable \\`working\\` (default) or pass a \\`semantic\\` / \\`episodic\\` / \\`procedural\\` config; a memory with no tiers can neither store nor recall`,\n );\n }\n\n const defaultTier: MemoryTier = config.defaultTier ?? \"working\";\n\n assertTierEnabled(defaultTier, tiers, name);\n\n return {\n name,\n async remember(items: MemoryItem | MemoryItem[]): Promise<void> {\n const list = Array.isArray(items) ? items : [items];\n\n const writes: Promise<void>[] = [];\n\n for (const item of list) {\n const tier = item.tier ?? defaultTier;\n\n assertTierEnabled(tier, tiers, name);\n\n if (tier === \"working\") {\n working!.remember(item);\n\n continue;\n }\n\n if (tier === \"semantic\") {\n writes.push(semantic!.remember(item));\n\n continue;\n }\n\n if (tier === \"episodic\") {\n writes.push(episodic!.remember(item));\n\n continue;\n }\n\n writes.push(procedural!.remember(item));\n }\n\n await Promise.all(writes);\n },\n async recall(\n query: string,\n options: RecallOptions = {},\n ): Promise<RecalledMemory[]> {\n const k = options.k ?? defaultK;\n const threshold = options.threshold ?? defaultThreshold;\n\n if (options.tier) {\n assertTierEnabled(options.tier, tiers, name);\n }\n\n const wants = (tier: MemoryTier): boolean =>\n !options.tier || options.tier === tier;\n\n // `options.scope` is the isolation key — each tier applies it as an\n // exact-equality filter internally, BEFORE its own scoring and\n // slicing, so nothing outside the scope reaches this merge.\n const scope = options.scope;\n\n const [workingHits, semanticHits, episodicHits, proceduralHits] =\n await Promise.all([\n working && wants(\"working\")\n ? Promise.resolve(working.recall(k, scope))\n : Promise.resolve([] as RecalledMemory[]),\n semantic && wants(\"semantic\")\n ? semantic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n episodic && wants(\"episodic\")\n ? episodic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n procedural && wants(\"procedural\")\n ? procedural.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n ]);\n\n return [\n ...workingHits,\n ...semanticHits,\n ...episodicHits,\n ...proceduralHits,\n ]\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n },\n async clear(tier?: MemoryTier): Promise<void> {\n const clears: Promise<void>[] = [];\n\n if (working && (!tier || tier === \"working\")) {\n working.clear();\n }\n\n if (semantic && (!tier || tier === \"semantic\")) {\n clears.push(semantic.clear());\n }\n\n if (episodic && (!tier || tier === \"episodic\")) {\n clears.push(episodic.clear());\n }\n\n if (procedural && (!tier || tier === \"procedural\")) {\n clears.push(procedural.clear());\n }\n\n await Promise.all(clears);\n },\n };\n}\n\n/** The four tier instances a `memory()` composes; `undefined` when off. */\ntype Tiers = {\n working: WorkingMemory | undefined;\n semantic: SemanticMemory | undefined;\n episodic: EpisodicMemory | undefined;\n procedural: ProceduralMemory | undefined;\n};\n\n/**\n * Resolve the working tier's size bound from the `working` config\n * (`true` / a `{ maxItems }` object), validating it at construction the\n * same way every other tier's wiring fails loud-and-now rather than on\n * first use. There is deliberately no unbounded setting — the buffer is\n * process-resident for the life of the memory instance, so \"no cap\" is\n * a memory-exhaustion vector, not a configuration choice.\n */\nfunction resolveWorkingMaxItems(\n workingConfig: true | WorkingMemoryConfig,\n name: string,\n): number {\n const maxItems =\n workingConfig === true\n ? DEFAULT_WORKING_MAX_ITEMS\n : (workingConfig.maxItems ?? DEFAULT_WORKING_MAX_ITEMS);\n\n if (!Number.isInteger(maxItems) || maxItems < 1) {\n throw new Error(\n `memory(\"${name}\"): working tier \\`maxItems\\` must be an integer >= 1 — received ${String(maxItems)}`,\n );\n }\n\n return maxItems;\n}\n\n/**\n * Resolve the semantic tier's store (explicit `store` wins, else the\n * global `ai.config({ defaultStore })`) and build the tier. Throws at\n * construction when neither is available — the same loud-now contract\n * `semanticCache` follows.\n */\nfunction buildSemanticTier(\n semanticConfig: NonNullable<MemoryConfig[\"semantic\"]>,\n name: string,\n): SemanticMemory {\n const store = semanticConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): semantic tier has no store — pass \\`semantic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new SemanticMemory(\n semanticConfig.embedder,\n store,\n semanticConfig.namespace ?? DEFAULT_SEMANTIC_NAMESPACE,\n );\n}\n\n/**\n * Resolve the episodic tier's store (explicit `store` wins, else the\n * global default) and build the tier with its recency knobs. Throws at\n * construction when neither store is available — the same loud-now\n * contract the semantic tier follows.\n */\nfunction buildEpisodicTier(\n episodicConfig: NonNullable<MemoryConfig[\"episodic\"]>,\n name: string,\n): EpisodicMemory {\n const store = episodicConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): episodic tier has no store — pass \\`episodic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new EpisodicMemory(\n episodicConfig.embedder,\n store,\n episodicConfig.namespace ?? DEFAULT_EPISODIC_NAMESPACE,\n episodicConfig.recencyWeight ?? DEFAULT_RECENCY_WEIGHT,\n episodicConfig.halfLifeMs ?? DEFAULT_HALF_LIFE_MS,\n episodicConfig.now ?? (() => Date.now()),\n );\n}\n\n/**\n * Resolve the procedural tier's store and build the tier with its\n * reinforcement knob. Throws at construction when no store is available.\n */\nfunction buildProceduralTier(\n proceduralConfig: NonNullable<MemoryConfig[\"procedural\"]>,\n name: string,\n): ProceduralMemory {\n const store = proceduralConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): procedural tier has no store — pass \\`procedural.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new ProceduralMemory(\n proceduralConfig.embedder,\n store,\n proceduralConfig.namespace ?? DEFAULT_PROCEDURAL_NAMESPACE,\n proceduralConfig.reinforcementWeight ?? DEFAULT_REINFORCEMENT_WEIGHT,\n );\n}\n\n/**\n * Guard that a tier referenced by config / a call is actually enabled,\n * failing fast with an actionable message instead of a downstream\n * `undefined` dereference.\n */\nfunction assertTierEnabled(tier: MemoryTier, tiers: Tiers, name: string): void {\n if (tier === \"working\" && !tiers.working) {\n throw new Error(\n `memory(\"${name}\"): working tier is disabled — set \\`working: true\\` (the default) to use it`,\n );\n }\n\n if (tier === \"semantic\" && !tiers.semantic) {\n throw new Error(\n `memory(\"${name}\"): semantic tier is not configured — pass \\`semantic\\` config to use it`,\n );\n }\n\n if (tier === \"episodic\" && !tiers.episodic) {\n throw new Error(\n `memory(\"${name}\"): episodic tier is not configured — pass \\`episodic\\` config to use it`,\n );\n }\n\n if (tier === \"procedural\" && !tiers.procedural) {\n throw new Error(\n `memory(\"${name}\"): procedural tier is not configured — pass \\`procedural\\` config to use it`,\n );\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/memory.ts"],"sourcesContent":["import { resolveDefaultStore } from \"../config\";\nimport type {\n MemoryConfig,\n WorkingMemoryConfig,\n} from \"../contracts/memory/memory-config.type\";\nimport type {\n MemoryItem,\n MemoryTier,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport type { MemoryContract } from \"../contracts/memory/memory.contract\";\nimport type { RecallOptions } from \"../contracts/memory/recall-options.type\";\nimport { EpisodicMemory } from \"./episodic-memory\";\nimport { ProceduralMemory } from \"./procedural-memory\";\nimport { SemanticMemory } from \"./semantic-memory\";\nimport { WorkingMemory } from \"./working-memory\";\n\nconst DEFAULT_NAME = \"memory\";\nconst DEFAULT_SEMANTIC_NAMESPACE = \"ai.memory.semantic\";\nconst DEFAULT_EPISODIC_NAMESPACE = \"ai.memory.episodic\";\nconst DEFAULT_PROCEDURAL_NAMESPACE = \"ai.memory.procedural\";\nconst DEFAULT_K = 5;\nconst DEFAULT_THRESHOLD = 0.7;\nconst DEFAULT_RECENCY_WEIGHT = 0.3;\nconst DEFAULT_HALF_LIFE_MS = 7 * 24 * 60 * 60 * 1000;\nconst DEFAULT_REINFORCEMENT_WEIGHT = 0.3;\n\n/**\n * Entries the in-process working buffer holds before it starts evicting\n * its oldest (4.15.0 — security fix for unbounded growth). Sized to hold\n * a deep multi-session scratch history while capping the tier's worst\n * case at a few MB of resident text rather than \"everything this process\n * has ever been told.\"\n */\nconst DEFAULT_WORKING_MAX_ITEMS = 1000;\n\n/**\n * Create an agent memory store (memory core M2).\n *\n * Wires up to four tiers behind the {@link MemoryContract}: **working**\n * (in-run scratch, recency), **semantic** (durable facts by cosine\n * similarity), **episodic** (durable events, similarity blended with\n * recency), and **procedural** (durable how-tos, similarity blended with\n * reinforcement). The working tier is on by default; the other three each\n * activate only when their config is supplied. The three vector tiers\n * mirror how `semanticCache` delegates similarity to the cache driver's\n * `.similar()`.\n *\n * Resolution happens once here, at construction (loud), rather than per\n * call (silent until first use): a vector-tier config with no `store` and\n * no `ai.config({ defaultStore })` throws now; enabling no tier at all\n * throws now.\n *\n * TTL-based decay / forgetting remains deferred. The working tier is\n * size-bounded (`working: { maxItems }`, default `1000`, oldest-written\n * evicted first) because it is the one tier that holds everything it is\n * told in process memory for the life of the instance; the durable tiers\n * delegate retention to their `CacheDriver`.\n *\n * **Isolation (4.15.0).** `remember({ scope })` / `recall(query, { scope })`\n * carry an opaque tenant / session key that every tier enforces as an\n * exact-equality filter before scoring — one scope's memories never\n * surface in another's recall, and identical text under two scopes stays\n * two entries. Unscoped writes form a shared pool that only an unscoped\n * recall can read; there is no \"all scopes\" query. `ai.orchestrator()`\n * derives this from the turn's `sessionId` automatically.\n *\n * @example\n * import { ai } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const mem = ai.memory({\n * semantic: { embedder, store },\n * defaultTier: \"semantic\",\n * });\n *\n * await mem.remember({ text: \"User prefers concise answers.\" });\n * const hits = await mem.recall(\"how should I respond?\", { k: 3 });\n */\nexport function memory(config: MemoryConfig = {}): MemoryContract {\n const name = config.name ?? DEFAULT_NAME;\n const workingConfig = config.working ?? true;\n const defaultK = config.k ?? DEFAULT_K;\n const defaultThreshold = config.threshold ?? DEFAULT_THRESHOLD;\n\n const working =\n workingConfig === false\n ? undefined\n : new WorkingMemory(resolveWorkingMaxItems(workingConfig, name));\n\n const semantic = config.semantic\n ? buildSemanticTier(config.semantic, name)\n : undefined;\n\n const episodic = config.episodic\n ? buildEpisodicTier(config.episodic, name)\n : undefined;\n\n const procedural = config.procedural\n ? buildProceduralTier(config.procedural, name)\n : undefined;\n\n const tiers: Tiers = { working, semantic, episodic, procedural };\n\n if (!working && !semantic && !episodic && !procedural) {\n throw new Error(\n `memory(\"${name}\"): no tier enabled — enable \\`working\\` (default) or pass a \\`semantic\\` / \\`episodic\\` / \\`procedural\\` config; a memory with no tiers can neither store nor recall`,\n );\n }\n\n const defaultTier: MemoryTier = config.defaultTier ?? \"working\";\n\n assertTierEnabled(defaultTier, tiers, name);\n\n return {\n name,\n async remember(items: MemoryItem | MemoryItem[]): Promise<void> {\n const list = Array.isArray(items) ? items : [items];\n\n const writes: Promise<void>[] = [];\n\n for (const item of list) {\n const tier = item.tier ?? defaultTier;\n\n assertTierEnabled(tier, tiers, name);\n\n if (tier === \"working\") {\n working!.remember(item);\n\n continue;\n }\n\n if (tier === \"semantic\") {\n writes.push(semantic!.remember(item));\n\n continue;\n }\n\n if (tier === \"episodic\") {\n writes.push(episodic!.remember(item));\n\n continue;\n }\n\n writes.push(procedural!.remember(item));\n }\n\n await Promise.all(writes);\n },\n async recall(\n query: string,\n options: RecallOptions = {},\n ): Promise<RecalledMemory[]> {\n const k = options.k ?? defaultK;\n const threshold = options.threshold ?? defaultThreshold;\n\n if (options.tier) {\n assertTierEnabled(options.tier, tiers, name);\n }\n\n const wants = (tier: MemoryTier): boolean =>\n !options.tier || options.tier === tier;\n\n // `options.scope` is the isolation key — each tier applies it as an\n // exact-equality filter internally, BEFORE its own scoring and\n // slicing, so nothing outside the scope reaches this merge.\n const scope = options.scope;\n\n const [workingHits, semanticHits, episodicHits, proceduralHits] =\n await Promise.all([\n working && wants(\"working\")\n ? Promise.resolve(working.recall(k, scope))\n : Promise.resolve([] as RecalledMemory[]),\n semantic && wants(\"semantic\")\n ? semantic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n episodic && wants(\"episodic\")\n ? episodic.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n procedural && wants(\"procedural\")\n ? procedural.recall(query, k, threshold, scope)\n : Promise.resolve([] as RecalledMemory[]),\n ]);\n\n return [\n ...workingHits,\n ...semanticHits,\n ...episodicHits,\n ...proceduralHits,\n ]\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n },\n async clear(tier?: MemoryTier): Promise<void> {\n const clears: Promise<void>[] = [];\n\n if (working && (!tier || tier === \"working\")) {\n working.clear();\n }\n\n if (semantic && (!tier || tier === \"semantic\")) {\n clears.push(semantic.clear());\n }\n\n if (episodic && (!tier || tier === \"episodic\")) {\n clears.push(episodic.clear());\n }\n\n if (procedural && (!tier || tier === \"procedural\")) {\n clears.push(procedural.clear());\n }\n\n await Promise.all(clears);\n },\n };\n}\n\n/** The four tier instances a `memory()` composes; `undefined` when off. */\ntype Tiers = {\n working: WorkingMemory | undefined;\n semantic: SemanticMemory | undefined;\n episodic: EpisodicMemory | undefined;\n procedural: ProceduralMemory | undefined;\n};\n\n/**\n * Resolve the working tier's size bound from the `working` config\n * (`true` / a `{ maxItems }` object), validating it at construction the\n * same way every other tier's wiring fails loud-and-now rather than on\n * first use. There is deliberately no unbounded setting — the buffer is\n * process-resident for the life of the memory instance, so \"no cap\" is\n * a memory-exhaustion vector, not a configuration choice.\n */\nfunction resolveWorkingMaxItems(\n workingConfig: true | WorkingMemoryConfig,\n name: string,\n): number {\n const maxItems =\n workingConfig === true\n ? DEFAULT_WORKING_MAX_ITEMS\n : (workingConfig.maxItems ?? DEFAULT_WORKING_MAX_ITEMS);\n\n if (!Number.isInteger(maxItems) || maxItems < 1) {\n throw new Error(\n `memory(\"${name}\"): working tier \\`maxItems\\` must be an integer >= 1 — received ${String(maxItems)}`,\n );\n }\n\n return maxItems;\n}\n\n/**\n * Resolve the semantic tier's store (explicit `store` wins, else the\n * global `ai.config({ defaultStore })`) and build the tier. Throws at\n * construction when neither is available — the same loud-now contract\n * `semanticCache` follows.\n */\nfunction buildSemanticTier(\n semanticConfig: NonNullable<MemoryConfig[\"semantic\"]>,\n name: string,\n): SemanticMemory {\n const store = semanticConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): semantic tier has no store — pass \\`semantic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new SemanticMemory(\n semanticConfig.embedder,\n store,\n semanticConfig.namespace ?? DEFAULT_SEMANTIC_NAMESPACE,\n );\n}\n\n/**\n * Resolve the episodic tier's store (explicit `store` wins, else the\n * global default) and build the tier with its recency knobs. Throws at\n * construction when neither store is available — the same loud-now\n * contract the semantic tier follows.\n */\nfunction buildEpisodicTier(\n episodicConfig: NonNullable<MemoryConfig[\"episodic\"]>,\n name: string,\n): EpisodicMemory {\n const store = episodicConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): episodic tier has no store — pass \\`episodic.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new EpisodicMemory(\n episodicConfig.embedder,\n store,\n episodicConfig.namespace ?? DEFAULT_EPISODIC_NAMESPACE,\n episodicConfig.recencyWeight ?? DEFAULT_RECENCY_WEIGHT,\n episodicConfig.halfLifeMs ?? DEFAULT_HALF_LIFE_MS,\n episodicConfig.now ?? (() => Date.now()),\n );\n}\n\n/**\n * Resolve the procedural tier's store and build the tier with its\n * reinforcement knob. Throws at construction when no store is available.\n */\nfunction buildProceduralTier(\n proceduralConfig: NonNullable<MemoryConfig[\"procedural\"]>,\n name: string,\n): ProceduralMemory {\n const store = proceduralConfig.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `memory(\"${name}\"): procedural tier has no store — pass \\`procedural.store\\` or call \\`ai.config({ defaultStore })\\` at app boot before constructing the memory`,\n );\n }\n\n return new ProceduralMemory(\n proceduralConfig.embedder,\n store,\n proceduralConfig.namespace ?? DEFAULT_PROCEDURAL_NAMESPACE,\n proceduralConfig.reinforcementWeight ?? DEFAULT_REINFORCEMENT_WEIGHT,\n );\n}\n\n/**\n * Guard that a tier referenced by config / a call is actually enabled,\n * failing fast with an actionable message instead of a downstream\n * `undefined` dereference.\n */\nfunction assertTierEnabled(tier: MemoryTier, tiers: Tiers, name: string): void {\n if (tier === \"working\" && !tiers.working) {\n throw new Error(\n `memory(\"${name}\"): working tier is disabled — set \\`working: true\\` (the default) to use it`,\n );\n }\n\n if (tier === \"semantic\" && !tiers.semantic) {\n throw new Error(\n `memory(\"${name}\"): semantic tier is not configured — pass \\`semantic\\` config to use it`,\n );\n }\n\n if (tier === \"episodic\" && !tiers.episodic) {\n throw new Error(\n `memory(\"${name}\"): episodic tier is not configured — pass \\`episodic\\` config to use it`,\n );\n }\n\n if (tier === \"procedural\" && !tiers.procedural) {\n throw new Error(\n `memory(\"${name}\"): procedural tier is not configured — pass \\`procedural\\` config to use it`,\n );\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"procedural-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/procedural-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per procedure. `uses` is the reinforcement counter —\n * how many times the procedure has been remembered/re-affirmed — and\n * feeds the reinforcement half of the blended recall score. The vector\n * lives in the driver's index, so it is not duplicated here.\n */\ntype StoredProcedure = {\n id: string;\n text: string;\n uses: number;\n /** Isolation key the procedure was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Extra candidates pulled from `similar()` before re-ranking by the\n * reinforcement-blended score and slicing to `k` — reinforcement can\n * promote a well-worn procedure past a slightly-closer one-off, which the\n * raw top-`k` by similarity would miss.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Procedural recall tier (memory core M2).\n *\n * Holds durable *how-to* knowledge — learned procedures, policies, and\n * playbooks — and retrieves the ones relevant to a query, **blended with\n * reinforcement** so procedures that have proven themselves (remembered /\n * re-affirmed more often) outrank one-offs at equal similarity. That\n * reinforcement weighting is the difference from the semantic tier (which\n * treats every fact equally): procedural memory gets *stronger with use*.\n *\n * Reinforcement is explicit and side-effect-free on read: re-remembering\n * a procedure (same id, or same text → same derived id) increments its\n * `uses`, so a caller strengthens a procedure by remembering it again\n * after a successful application. Recall never mutates.\n *\n * Like the other vector tiers it delegates similarity to the\n * `@warlock.js/cache` driver's `similar()`. The blended `score` stays in\n * `[0, 1]` so procedural hits merge and sort alongside the other tiers.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class ProceduralMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly reinforcementWeight: number,\n ) {}\n\n /**\n * Embed the procedure text and index it, incrementing its `uses` when\n * it already exists (reinforcement) or seeding it at `1` when new.\n * Metadata on a reinforcing write wins; an omitted metadata keeps the\n * prior value rather than wiping it.\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const key = this.keyFor(id, item.scope);\n const existing = await this.store.get<StoredProcedure>(key);\n const uses = (existing?.uses ?? 0) + 1;\n\n const value: StoredProcedure = {\n id,\n text: item.text,\n uses,\n scope: item.scope,\n metadata: item.metadata ?? existing?.metadata,\n };\n\n await this.store.set(key, value, { vector });\n }\n\n /**\n * Embed `query`, pull the nearest procedures clearing the similarity\n * `threshold`, then re-rank each by a reinforcement-blended score and\n * return the top `k`. The similarity floor still gates relevance;\n * reinforcement only reorders procedures that already cleared it.\n *\n * Procedures written under a different `scope` (another tenant /\n * session) are dropped here, before scoring and slicing, so they can\n * neither leak nor consume a slot. An unscoped recall reads only\n * unscoped procedures.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n const hits = await this.store.similar<StoredProcedure>(vector, {\n topK: Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredProcedure>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredProcedure>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"procedural\" as const,\n score: this.blend(hit.score, hit.value.uses),\n metadata: hit.value.metadata,\n }))\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n }\n\n /** Drop every procedure written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Combine raw similarity with a saturating reinforcement proxy:\n * `(1 - w)·similarity + w·(uses / (uses + 1))`. A first-time procedure\n * contributes `0.5`; each reinforcement nudges it toward `1` with\n * diminishing returns. With `reinforcementWeight` 0 the score is pure\n * similarity.\n */\n private blend(similarity: number, uses: number): number {\n const reinforcement = uses / (uses + 1);\n\n return (\n (1 - this.reinforcementWeight) * similarity +\n this.reinforcementWeight * reinforcement\n );\n }\n\n /**\n * Namespaced key for an entry — dot separator, matching `similar()`\n * keys, plus a hashed scope segment so reinforcement counters never\n * cross a scope boundary (one tenant re-affirming a procedure must not\n * strengthen — or overwrite — another tenant's identical text).\n * Unscoped keys keep their pre-4.15.0 shape.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;AAwBxB,IAAa,mBAAb,MAA8B;CAC5B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB,AAAiB,qBACjB;EAJiB;EACA;EACA;EACA;CAChB;;;;;;;CAQH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK;EACtC,MAAM,WAAW,MAAM,KAAK,MAAM,IAAqB,GAAG;EAC1D,MAAM,QAAQ,UAAU,QAAQ,KAAK;EAErC,MAAM,QAAyB;GAC7B;GACA,MAAM,KAAK;GACX;GACA,OAAO,KAAK;GACZ,UAAU,KAAK,YAAY,UAAU;EACvC;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE,OAAO,CAAC;CAC7C;;;;;;;;;;;;CAaA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAElD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAyB,QAAQ;GAC7D,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC;GACrC;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,EACC,KAAK,SAA2C;GAC/C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,KAAK,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI;GAC3C,UAAU,IAAI,MAAM;EACtB,EAAE,EACD,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,EAClD,MAAM,GAAG,CAAC;CACf;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;CASA,AAAQ,MAAM,YAAoB,MAAsB;EACtD,MAAM,gBAAgB,QAAQ,OAAO;EAErC,QACG,IAAI,KAAK,uBAAuB,aACjC,KAAK,sBAAsB;CAE/B;;;;;;;;CASA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
1
+ {"version":3,"file":"procedural-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/procedural-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Shape persisted per procedure. `uses` is the reinforcement counter —\n * how many times the procedure has been remembered/re-affirmed — and\n * feeds the reinforcement half of the blended recall score. The vector\n * lives in the driver's index, so it is not duplicated here.\n */\ntype StoredProcedure = {\n id: string;\n text: string;\n uses: number;\n /** Isolation key the procedure was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Extra candidates pulled from `similar()` before re-ranking by the\n * reinforcement-blended score and slicing to `k` — reinforcement can\n * promote a well-worn procedure past a slightly-closer one-off, which the\n * raw top-`k` by similarity would miss.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Procedural recall tier (memory core M2).\n *\n * Holds durable *how-to* knowledge — learned procedures, policies, and\n * playbooks — and retrieves the ones relevant to a query, **blended with\n * reinforcement** so procedures that have proven themselves (remembered /\n * re-affirmed more often) outrank one-offs at equal similarity. That\n * reinforcement weighting is the difference from the semantic tier (which\n * treats every fact equally): procedural memory gets *stronger with use*.\n *\n * Reinforcement is explicit and side-effect-free on read: re-remembering\n * a procedure (same id, or same text → same derived id) increments its\n * `uses`, so a caller strengthens a procedure by remembering it again\n * after a successful application. Recall never mutates.\n *\n * Like the other vector tiers it delegates similarity to the\n * `@warlock.js/cache` driver's `similar()`. The blended `score` stays in\n * `[0, 1]` so procedural hits merge and sort alongside the other tiers.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class ProceduralMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n private readonly reinforcementWeight: number,\n ) {}\n\n /**\n * Embed the procedure text and index it, incrementing its `uses` when\n * it already exists (reinforcement) or seeding it at `1` when new.\n * Metadata on a reinforcing write wins; an omitted metadata keeps the\n * prior value rather than wiping it.\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const key = this.keyFor(id, item.scope);\n const existing = await this.store.get<StoredProcedure>(key);\n const uses = (existing?.uses ?? 0) + 1;\n\n const value: StoredProcedure = {\n id,\n text: item.text,\n uses,\n scope: item.scope,\n metadata: item.metadata ?? existing?.metadata,\n };\n\n await this.store.set(key, value, { vector });\n }\n\n /**\n * Embed `query`, pull the nearest procedures clearing the similarity\n * `threshold`, then re-rank each by a reinforcement-blended score and\n * return the top `k`. The similarity floor still gates relevance;\n * reinforcement only reorders procedures that already cleared it.\n *\n * Procedures written under a different `scope` (another tenant /\n * session) are dropped here, before scoring and slicing, so they can\n * neither leak nor consume a slot. An unscoped recall reads only\n * unscoped procedures.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n const hits = await this.store.similar<StoredProcedure>(vector, {\n topK: Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredProcedure>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredProcedure>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"procedural\" as const,\n score: this.blend(hit.score, hit.value.uses),\n metadata: hit.value.metadata,\n }))\n .sort((first, second) => second.score - first.score)\n .slice(0, k);\n }\n\n /** Drop every procedure written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Combine raw similarity with a saturating reinforcement proxy:\n * `(1 - w)·similarity + w·(uses / (uses + 1))`. A first-time procedure\n * contributes `0.5`; each reinforcement nudges it toward `1` with\n * diminishing returns. With `reinforcementWeight` 0 the score is pure\n * similarity.\n */\n private blend(similarity: number, uses: number): number {\n const reinforcement = uses / (uses + 1);\n\n return (\n (1 - this.reinforcementWeight) * similarity +\n this.reinforcementWeight * reinforcement\n );\n }\n\n /**\n * Namespaced key for an entry — dot separator, matching `similar()`\n * keys, plus a hashed scope segment so reinforcement counters never\n * cross a scope boundary (one tenant re-affirming a procedure must not\n * strengthen — or overwrite — another tenant's identical text).\n * Unscoped keys keep their pre-4.15.0 shape.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AA6BA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;AAwBxB,IAAa,mBAAb,MAA8B;CAC5B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB,AAAiB,qBACjB;EAJiB;EACA;EACA;EACA;CAChB;;;;;;;CAQH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,MAAM,KAAK,OAAO,IAAI,KAAK,KAAK;EACtC,MAAM,WAAW,MAAM,KAAK,MAAM,IAAqB,GAAG;EAC1D,MAAM,QAAQ,UAAU,QAAQ,KAAK;EAErC,MAAM,QAAyB;GAC7B;GACA,MAAM,KAAK;GACX;GACA,OAAO,KAAK;GACZ,UAAU,KAAK,YAAY,UAAU;EACvC;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,EAAE,OAAO,CAAC;CAC7C;;;;;;;;;;;;CAaA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAElD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAyB,QAAQ;GAC7D,MAAM,KAAK,IAAI,IAAI,iBAAiB,CAAC;GACrC;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAA2C;GAC/C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,KAAK,MAAM,IAAI,OAAO,IAAI,MAAM,IAAI;GAC3C,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,OAAO,WAAW,OAAO,QAAQ,MAAM,KAAK,CAAC,CACnD,MAAM,GAAG,CAAC;CACf;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;CASA,AAAQ,MAAM,YAAoB,MAAsB;EACtD,MAAM,gBAAgB,QAAQ,OAAO;EAErC,QACG,IAAI,KAAK,uBAAuB,aACjC,KAAK,sBAAsB;CAE/B;;;;;;;;CASA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/semantic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Extra candidates pulled from `similar()` on a SCOPED recall before the\n * scope filter runs — the driver's ranking spans every scope in the\n * index, so a bare top-`k` can come back entirely foreign. Mirrors the\n * episodic / procedural tiers' overscan constant.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Shape persisted per semantic memory in the cache driver. The vector\n * itself is stored by the driver's own index (passed via\n * `set({ vector })`), so it is not duplicated in the value.\n */\ntype StoredMemory = {\n id: string;\n text: string;\n /** Isolation key the entry was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Semantic recall tier (memory core M1).\n *\n * Owns: embedding remembered text, writing it to a `@warlock.js/cache`\n * driver with `set({ vector })`, and retrieving by cosine similarity via\n * the driver's `similar()`. Does NOT own: the similarity algorithm or\n * the ANN index — those belong to the cache driver. This mirrors the\n * delegation model of `middleware/builtins/semantic-cache.ts`: memory is\n * embedding-agnostic and store-agnostic, gluing an {@link EmbedderContract}\n * to a {@link CacheDriver}.\n *\n * The driver may be shared across memory instances, so every key carries\n * the configured `namespace` and recall filters hits to that prefix —\n * foreign entries indexed by another instance never leak into a query.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class SemanticMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n ) {}\n\n /**\n * Embed the item's text and index it under a namespaced, id-derived\n * key. Re-remembering the same id overwrites the prior vector +\n * value (the driver upserts by key).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredMemory = {\n id,\n text: item.text,\n scope: item.scope,\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id, item.scope), value, { vector });\n }\n\n /**\n * Embed `query`, ask the driver for the `k` nearest entries clearing\n * `threshold`, and return those within this instance's namespace AND\n * this call's `scope` as scored {@link RecalledMemory}. Hits indexed\n * under a different namespace (a shared driver) or a different scope\n * (another tenant / session) are filtered out here, before the caller\n * ever sees them — an unscoped recall reads only unscoped entries.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n // A scoped recall overscans: the driver ranks across every scope in\n // the index, so a plain top-`k` could be filled entirely by foreign\n // scopes and starve this one. Pull extra candidates, filter, then cap.\n const hits = await this.store.similar<StoredMemory>(vector, {\n topK: scope === undefined ? k : Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredMemory>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredMemory>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"semantic\" as const,\n score: hit.score,\n metadata: hit.value.metadata,\n }))\n .slice(0, Math.max(0, k));\n }\n\n /** Drop every semantic entry written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Namespaced key for an entry. The cache's `parseKey` normalizes `:`\n * to `.`, so a dot separator keeps the prefix used here aligned with\n * the `hit.key` the driver returns from `similar()`.\n *\n * A scoped entry gets an extra hashed segment so two scopes writing\n * identical text (same derived id) don't overwrite each other; the\n * unscoped key shape is unchanged, so entries written before 4.15.0\n * still resolve. The hash is a write-separation device only — recall\n * authorization is the exact `value.scope` equality check, so even a\n * hash collision cannot widen what a scope can read.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AAcA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAiCxB,IAAa,iBAAb,MAA4B;CAC1B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB;EAHiB;EACA;EACA;CAChB;;;;;;CAOH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,QAAsB;GAC1B;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,OAAO,CAAC;CACrE;;;;;;;;;CAUA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAKlD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAsB,QAAQ;GAC1D,MAAM,UAAU,SAAY,IAAI,KAAK,IAAI,IAAI,iBAAiB,CAAC;GAC/D;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,EACC,KAAK,SAAwC;GAC5C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,IAAI;GACX,UAAU,IAAI,MAAM;EACtB,EAAE,EACD,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;CAC5B;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;;;;;;CAcA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
1
+ {"version":3,"file":"semantic-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/semantic-memory.ts"],"sourcesContent":["import type { CacheDriver, CacheSimilarHit } from \"@warlock.js/cache\";\nimport type { EmbedderContract } from \"../contracts/embedder.contract\";\nimport type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * Extra candidates pulled from `similar()` on a SCOPED recall before the\n * scope filter runs — the driver's ranking spans every scope in the\n * index, so a bare top-`k` can come back entirely foreign. Mirrors the\n * episodic / procedural tiers' overscan constant.\n */\nconst RECALL_OVERSCAN = 5;\n\n/**\n * Shape persisted per semantic memory in the cache driver. The vector\n * itself is stored by the driver's own index (passed via\n * `set({ vector })`), so it is not duplicated in the value.\n */\ntype StoredMemory = {\n id: string;\n text: string;\n /** Isolation key the entry was written under; absent = the shared pool. */\n scope?: string;\n metadata?: Record<string, unknown>;\n};\n\n/**\n * Semantic recall tier (memory core M1).\n *\n * Owns: embedding remembered text, writing it to a `@warlock.js/cache`\n * driver with `set({ vector })`, and retrieving by cosine similarity via\n * the driver's `similar()`. Does NOT own: the similarity algorithm or\n * the ANN index — those belong to the cache driver. This mirrors the\n * delegation model of `middleware/builtins/semantic-cache.ts`: memory is\n * embedding-agnostic and store-agnostic, gluing an {@link EmbedderContract}\n * to a {@link CacheDriver}.\n *\n * The driver may be shared across memory instances, so every key carries\n * the configured `namespace` and recall filters hits to that prefix —\n * foreign entries indexed by another instance never leak into a query.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class SemanticMemory {\n public constructor(\n private readonly embedder: EmbedderContract,\n private readonly store: CacheDriver<any, any>,\n private readonly namespace: string,\n ) {}\n\n /**\n * Embed the item's text and index it under a namespaced, id-derived\n * key. Re-remembering the same id overwrites the prior vector +\n * value (the driver upserts by key).\n */\n public async remember(item: MemoryItem): Promise<void> {\n const id = item.id ?? deriveMemoryId(item.text);\n const { vector } = await this.embedder.embed(item.text);\n\n const value: StoredMemory = {\n id,\n text: item.text,\n scope: item.scope,\n metadata: item.metadata,\n };\n\n await this.store.set(this.keyFor(id, item.scope), value, { vector });\n }\n\n /**\n * Embed `query`, ask the driver for the `k` nearest entries clearing\n * `threshold`, and return those within this instance's namespace AND\n * this call's `scope` as scored {@link RecalledMemory}. Hits indexed\n * under a different namespace (a shared driver) or a different scope\n * (another tenant / session) are filtered out here, before the caller\n * ever sees them — an unscoped recall reads only unscoped entries.\n */\n public async recall(\n query: string,\n k: number,\n threshold: number,\n scope?: string,\n ): Promise<RecalledMemory[]> {\n const { vector } = await this.embedder.embed(query);\n\n // A scoped recall overscans: the driver ranks across every scope in\n // the index, so a plain top-`k` could be filled entirely by foreign\n // scopes and starve this one. Pull extra candidates, filter, then cap.\n const hits = await this.store.similar<StoredMemory>(vector, {\n topK: scope === undefined ? k : Math.max(k * RECALL_OVERSCAN, k),\n threshold,\n });\n\n const prefix = `${this.namespace}.`;\n\n return hits\n .filter(\n (hit: CacheSimilarHit<StoredMemory>) =>\n hit.key.startsWith(prefix) && hit.value?.scope === scope,\n )\n .map((hit: CacheSimilarHit<StoredMemory>) => ({\n id: hit.value.id,\n text: hit.value.text,\n tier: \"semantic\" as const,\n score: hit.score,\n metadata: hit.value.metadata,\n }))\n .slice(0, Math.max(0, k));\n }\n\n /** Drop every semantic entry written under this instance's namespace. */\n public async clear(): Promise<void> {\n await this.store.removeNamespace(this.namespace);\n }\n\n /**\n * Namespaced key for an entry. The cache's `parseKey` normalizes `:`\n * to `.`, so a dot separator keeps the prefix used here aligned with\n * the `hit.key` the driver returns from `similar()`.\n *\n * A scoped entry gets an extra hashed segment so two scopes writing\n * identical text (same derived id) don't overwrite each other; the\n * unscoped key shape is unchanged, so entries written before 4.15.0\n * still resolve. The hash is a write-separation device only — recall\n * authorization is the exact `value.scope` equality check, so even a\n * hash collision cannot widen what a scope can read.\n */\n private keyFor(id: string, scope?: string): string {\n return scope === undefined\n ? `${this.namespace}.${id}`\n : `${this.namespace}.${deriveMemoryId(scope)}.${id}`;\n }\n}\n"],"mappings":";;;;;;;;;AAcA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;AAiCxB,IAAa,iBAAb,MAA4B;CAC1B,AAAO,YACL,AAAiB,UACjB,AAAiB,OACjB,AAAiB,WACjB;EAHiB;EACA;EACA;CAChB;;;;;;CAOH,MAAa,SAAS,MAAiC;EACrD,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAC9C,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK,IAAI;EAEtD,MAAM,QAAsB;GAC1B;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB;EAEA,MAAM,KAAK,MAAM,IAAI,KAAK,OAAO,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,OAAO,CAAC;CACrE;;;;;;;;;CAUA,MAAa,OACX,OACA,GACA,WACA,OAC2B;EAC3B,MAAM,EAAE,WAAW,MAAM,KAAK,SAAS,MAAM,KAAK;EAKlD,MAAM,OAAO,MAAM,KAAK,MAAM,QAAsB,QAAQ;GAC1D,MAAM,UAAU,SAAY,IAAI,KAAK,IAAI,IAAI,iBAAiB,CAAC;GAC/D;EACF,CAAC;EAED,MAAM,SAAS,GAAG,KAAK,UAAU;EAEjC,OAAO,KACJ,QACE,QACC,IAAI,IAAI,WAAW,MAAM,KAAK,IAAI,OAAO,UAAU,KACvD,CAAC,CACA,KAAK,SAAwC;GAC5C,IAAI,IAAI,MAAM;GACd,MAAM,IAAI,MAAM;GAChB,MAAM;GACN,OAAO,IAAI;GACX,UAAU,IAAI,MAAM;EACtB,EAAE,CAAC,CACF,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;CAC5B;;CAGA,MAAa,QAAuB;EAClC,MAAM,KAAK,MAAM,gBAAgB,KAAK,SAAS;CACjD;;;;;;;;;;;;;CAcA,AAAQ,OAAO,IAAY,OAAwB;EACjD,OAAO,UAAU,SACb,GAAG,KAAK,UAAU,GAAG,OACrB,GAAG,KAAK,UAAU,GAAG,eAAe,KAAK,EAAE,GAAG;CACpD;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"working-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/working-memory.ts"],"sourcesContent":["import type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * In-run working memory — the volatile scratch tier (memory core M1).\n *\n * Owns: an insertion-ordered buffer of remembered items keyed by id,\n * with overwrite-in-place on a repeated id. Does NOT own: durability,\n * cross-process sharing, embeddings, or similarity — working memory is\n * a plain in-process buffer the orchestrator threads across the turns of\n * a single run.\n *\n * Recall here is not semantic: with no vector index, \"relevant\" reduces\n * to \"recent.\" `recall()` returns the most-recently-remembered items\n * first, each scored on a `[0, 1]` recency proxy so a caller can merge\n * working hits with semantic hits and sort on one `score` field.\n *\n * **Bounded (4.15.0).** The buffer holds at most `maxItems` entries\n * across every scope; the oldest-written entry is evicted on overflow\n * (FIFO). The tier lives in process memory for the lifetime of the\n * `memory()` instance — which the orchestrator resolves once and reuses\n * for every session — so an unbounded buffer was a memory-exhaustion\n * vector for any long-lived, internet-reachable deployment.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class WorkingMemory {\n /**\n * Hard ceiling on buffered entries, across all scopes. Enforced on\n * every `remember()`; see {@link evictOverflow} for the policy.\n */\n private readonly maxItems: number;\n\n public constructor(maxItems: number) {\n this.maxItems = maxItems;\n }\n\n /**\n * Scoped key → entry. A `Map` preserves insertion order, so iteration\n * yields oldest-first; recall reverses it for most-recent-first.\n *\n * The map key folds in the item's `scope` so two scopes remembering\n * identical text (same derived id) stay two independent entries\n * instead of clobbering one another; the entry keeps its logical `id`\n * and its `scope` so recall can filter and still report the id the\n * caller knows.\n */\n private readonly entries = new Map<\n string,\n {\n id: string;\n text: string;\n scope?: string;\n metadata?: Record<string, unknown>;\n }\n >();\n\n /**\n * Append an item to the buffer (or overwrite the entry sharing its\n * id *within the same scope*). Re-inserting an existing key keeps its\n * original position; delete + set would move it to the end and lie\n * about recency, so the value is updated in place.\n *\n * Overflowing `maxItems` evicts from the front — see\n * {@link evictOverflow}.\n */\n public remember(item: MemoryItem): void {\n const id = item.id ?? deriveMemoryId(item.text);\n\n this.entries.set(scopedKey(item.scope, id), {\n id,\n text: item.text,\n scope: item.scope,\n metadata: item.metadata,\n });\n\n this.evictOverflow();\n }\n\n /**\n * Enforce the size bound by dropping oldest-written entries first\n * (FIFO over the `Map`'s insertion order).\n *\n * **Why FIFO, not LRU.** Recall here is a pure recency proxy — it\n * reverses insertion order and slices the newest `k` — and never\n * reorders anything, so the front of the buffer is by construction the\n * region recall reaches last. FIFO therefore evicts exactly the\n * entries a bounded recall would never have returned. True LRU would\n * need read-time reordering, which would also rewrite the `score`\n * every recall reports (a re-read entry would masquerade as freshly\n * remembered), trading a real correctness property for no gain.\n *\n * **Known limitation (documented, not a regression).** The bound is\n * global, not per-scope: a session writing heavily can push another\n * session's older entries out of the buffer. That is a recall-quality\n * degradation on a volatile scratch tier, never a disclosure — the\n * scope filter in {@link recall} still applies — and a per-scope quota\n * would not help anyway, since an attacker holding many sessions\n * evicts through the global bound regardless. Durable recall belongs\n * in the semantic / episodic tiers.\n */\n private evictOverflow(): void {\n while (this.entries.size > this.maxItems) {\n const oldest = this.entries.keys().next();\n\n if (oldest.done) {\n return;\n }\n\n this.entries.delete(oldest.value);\n }\n }\n\n /**\n * Return up to `k` most-recently-remembered items *within `scope`*,\n * newest first. The scope match is exact equality (an unscoped recall\n * sees only unscoped entries) and is applied BEFORE the slice, so a\n * foreign scope's entries can never consume a slot or leak out.\n *\n * The `score` is a linear recency proxy: the newest item scores `1`,\n * the oldest of the returned slice trends toward `0`. Working memory\n * ignores any similarity threshold — it has no vector to compare.\n */\n public recall(k: number, scope?: string): RecalledMemory[] {\n const ordered = [...this.entries.values()]\n .reverse()\n .filter((entry) => entry.scope === scope);\n\n const slice = ordered.slice(0, Math.max(0, k));\n\n return slice.map((entry, index) => ({\n id: entry.id,\n text: entry.text,\n tier: \"working\" as const,\n score: slice.length <= 1 ? 1 : 1 - index / slice.length,\n metadata: entry.metadata,\n }));\n }\n\n /** Drop every working-tier entry, across every scope. */\n public clear(): void {\n this.entries.clear();\n }\n}\n\n/**\n * Map key for a buffer entry: the isolation `scope` (empty for the\n * unscoped pool) length-prefixed and joined to the logical id. The\n * length prefix makes the encoding injective — no crafted scope/id pair\n * can collide with a different scope's entry the way a plain `:` join\n * would allow.\n */\nfunction scopedKey(scope: string | undefined, id: string): string {\n return `${scope?.length ?? 0}:${scope ?? \"\"}:${id}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAA2B;CAOzB,AAAO,YAAY,UAAkB;iCAcV,IAAI,IAQ7B;EArBA,KAAK,WAAW;CAClB;;;;;;;;;;CA+BA,AAAO,SAAS,MAAwB;EACtC,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAE9C,KAAK,QAAQ,IAAI,UAAU,KAAK,OAAO,EAAE,GAAG;GAC1C;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB,CAAC;EAED,KAAK,cAAc;CACrB;;;;;;;;;;;;;;;;;;;;;;;CAwBA,AAAQ,gBAAsB;EAC5B,OAAO,KAAK,QAAQ,OAAO,KAAK,UAAU;GACxC,MAAM,SAAS,KAAK,QAAQ,KAAK,EAAE,KAAK;GAExC,IAAI,OAAO,MACT;GAGF,KAAK,QAAQ,OAAO,OAAO,KAAK;EAClC;CACF;;;;;;;;;;;CAYA,AAAO,OAAO,GAAW,OAAkC;EAKzD,MAAM,QAJU,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,EACtC,QAAQ,EACR,QAAQ,UAAU,MAAM,UAAU,KAEjB,EAAE,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;EAE7C,OAAO,MAAM,KAAK,OAAO,WAAW;GAClC,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,MAAM;GACN,OAAO,MAAM,UAAU,IAAI,IAAI,IAAI,QAAQ,MAAM;GACjD,UAAU,MAAM;EAClB,EAAE;CACJ;;CAGA,AAAO,QAAc;EACnB,KAAK,QAAQ,MAAM;CACrB;AACF;;;;;;;;AASA,SAAS,UAAU,OAA2B,IAAoB;CAChE,OAAO,GAAG,OAAO,UAAU,EAAE,GAAG,SAAS,GAAG,GAAG;AACjD"}
1
+ {"version":3,"file":"working-memory.mjs","names":[],"sources":["../../../../../../../ai/src/memory/working-memory.ts"],"sourcesContent":["import type {\n MemoryItem,\n RecalledMemory,\n} from \"../contracts/memory/memory-item.type\";\nimport { deriveMemoryId } from \"./derive-id\";\n\n/**\n * In-run working memory — the volatile scratch tier (memory core M1).\n *\n * Owns: an insertion-ordered buffer of remembered items keyed by id,\n * with overwrite-in-place on a repeated id. Does NOT own: durability,\n * cross-process sharing, embeddings, or similarity — working memory is\n * a plain in-process buffer the orchestrator threads across the turns of\n * a single run.\n *\n * Recall here is not semantic: with no vector index, \"relevant\" reduces\n * to \"recent.\" `recall()` returns the most-recently-remembered items\n * first, each scored on a `[0, 1]` recency proxy so a caller can merge\n * working hits with semantic hits and sort on one `score` field.\n *\n * **Bounded (4.15.0).** The buffer holds at most `maxItems` entries\n * across every scope; the oldest-written entry is evicted on overflow\n * (FIFO). The tier lives in process memory for the lifetime of the\n * `memory()` instance — which the orchestrator resolves once and reuses\n * for every session — so an unbounded buffer was a memory-exhaustion\n * vector for any long-lived, internet-reachable deployment.\n *\n * Internal to the `memory()` factory — never exported on the package\n * surface.\n */\nexport class WorkingMemory {\n /**\n * Hard ceiling on buffered entries, across all scopes. Enforced on\n * every `remember()`; see {@link evictOverflow} for the policy.\n */\n private readonly maxItems: number;\n\n public constructor(maxItems: number) {\n this.maxItems = maxItems;\n }\n\n /**\n * Scoped key → entry. A `Map` preserves insertion order, so iteration\n * yields oldest-first; recall reverses it for most-recent-first.\n *\n * The map key folds in the item's `scope` so two scopes remembering\n * identical text (same derived id) stay two independent entries\n * instead of clobbering one another; the entry keeps its logical `id`\n * and its `scope` so recall can filter and still report the id the\n * caller knows.\n */\n private readonly entries = new Map<\n string,\n {\n id: string;\n text: string;\n scope?: string;\n metadata?: Record<string, unknown>;\n }\n >();\n\n /**\n * Append an item to the buffer (or overwrite the entry sharing its\n * id *within the same scope*). Re-inserting an existing key keeps its\n * original position; delete + set would move it to the end and lie\n * about recency, so the value is updated in place.\n *\n * Overflowing `maxItems` evicts from the front — see\n * {@link evictOverflow}.\n */\n public remember(item: MemoryItem): void {\n const id = item.id ?? deriveMemoryId(item.text);\n\n this.entries.set(scopedKey(item.scope, id), {\n id,\n text: item.text,\n scope: item.scope,\n metadata: item.metadata,\n });\n\n this.evictOverflow();\n }\n\n /**\n * Enforce the size bound by dropping oldest-written entries first\n * (FIFO over the `Map`'s insertion order).\n *\n * **Why FIFO, not LRU.** Recall here is a pure recency proxy — it\n * reverses insertion order and slices the newest `k` — and never\n * reorders anything, so the front of the buffer is by construction the\n * region recall reaches last. FIFO therefore evicts exactly the\n * entries a bounded recall would never have returned. True LRU would\n * need read-time reordering, which would also rewrite the `score`\n * every recall reports (a re-read entry would masquerade as freshly\n * remembered), trading a real correctness property for no gain.\n *\n * **Known limitation (documented, not a regression).** The bound is\n * global, not per-scope: a session writing heavily can push another\n * session's older entries out of the buffer. That is a recall-quality\n * degradation on a volatile scratch tier, never a disclosure — the\n * scope filter in {@link recall} still applies — and a per-scope quota\n * would not help anyway, since an attacker holding many sessions\n * evicts through the global bound regardless. Durable recall belongs\n * in the semantic / episodic tiers.\n */\n private evictOverflow(): void {\n while (this.entries.size > this.maxItems) {\n const oldest = this.entries.keys().next();\n\n if (oldest.done) {\n return;\n }\n\n this.entries.delete(oldest.value);\n }\n }\n\n /**\n * Return up to `k` most-recently-remembered items *within `scope`*,\n * newest first. The scope match is exact equality (an unscoped recall\n * sees only unscoped entries) and is applied BEFORE the slice, so a\n * foreign scope's entries can never consume a slot or leak out.\n *\n * The `score` is a linear recency proxy: the newest item scores `1`,\n * the oldest of the returned slice trends toward `0`. Working memory\n * ignores any similarity threshold — it has no vector to compare.\n */\n public recall(k: number, scope?: string): RecalledMemory[] {\n const ordered = [...this.entries.values()]\n .reverse()\n .filter((entry) => entry.scope === scope);\n\n const slice = ordered.slice(0, Math.max(0, k));\n\n return slice.map((entry, index) => ({\n id: entry.id,\n text: entry.text,\n tier: \"working\" as const,\n score: slice.length <= 1 ? 1 : 1 - index / slice.length,\n metadata: entry.metadata,\n }));\n }\n\n /** Drop every working-tier entry, across every scope. */\n public clear(): void {\n this.entries.clear();\n }\n}\n\n/**\n * Map key for a buffer entry: the isolation `scope` (empty for the\n * unscoped pool) length-prefixed and joined to the logical id. The\n * length prefix makes the encoding injective — no crafted scope/id pair\n * can collide with a different scope's entry the way a plain `:` join\n * would allow.\n */\nfunction scopedKey(scope: string | undefined, id: string): string {\n return `${scope?.length ?? 0}:${scope ?? \"\"}:${id}`;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,IAAa,gBAAb,MAA2B;CAOzB,AAAO,YAAY,UAAkB;iCAcV,IAAI,IAQ7B;EArBA,KAAK,WAAW;CAClB;;;;;;;;;;CA+BA,AAAO,SAAS,MAAwB;EACtC,MAAM,KAAK,KAAK,MAAM,eAAe,KAAK,IAAI;EAE9C,KAAK,QAAQ,IAAI,UAAU,KAAK,OAAO,EAAE,GAAG;GAC1C;GACA,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,UAAU,KAAK;EACjB,CAAC;EAED,KAAK,cAAc;CACrB;;;;;;;;;;;;;;;;;;;;;;;CAwBA,AAAQ,gBAAsB;EAC5B,OAAO,KAAK,QAAQ,OAAO,KAAK,UAAU;GACxC,MAAM,SAAS,KAAK,QAAQ,KAAK,CAAC,CAAC,KAAK;GAExC,IAAI,OAAO,MACT;GAGF,KAAK,QAAQ,OAAO,OAAO,KAAK;EAClC;CACF;;;;;;;;;;;CAYA,AAAO,OAAO,GAAW,OAAkC;EAKzD,MAAM,QAJU,CAAC,GAAG,KAAK,QAAQ,OAAO,CAAC,CAAC,CACvC,QAAQ,CAAC,CACT,QAAQ,UAAU,MAAM,UAAU,KAEjB,CAAC,CAAC,MAAM,GAAG,KAAK,IAAI,GAAG,CAAC,CAAC;EAE7C,OAAO,MAAM,KAAK,OAAO,WAAW;GAClC,IAAI,MAAM;GACV,MAAM,MAAM;GACZ,MAAM;GACN,OAAO,MAAM,UAAU,IAAI,IAAI,IAAI,QAAQ,MAAM;GACjD,UAAU,MAAM;EAClB,EAAE;CACJ;;CAGA,AAAO,QAAc;EACnB,KAAK,QAAQ,MAAM;CACrB;AACF;;;;;;;;AASA,SAAS,UAAU,OAA2B,IAAoB;CAChE,OAAO,GAAG,OAAO,UAAU,EAAE,GAAG,SAAS,GAAG,GAAG;AACjD"}
@@ -1 +1 @@
1
- {"version":3,"file":"budget.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/builtins/budget.ts"],"sourcesContent":["import type {\n AgentMiddleware,\n MiddlewareExecuteContext,\n} from \"../../contracts/middleware\";\nimport { BudgetExceededError, type BudgetUnit } from \"../../errors\";\nimport { namespacedState } from \"../utils\";\nimport type {\n BudgetContract,\n BudgetContractDimension,\n BudgetContractViolation,\n} from \"./budget-contract.type\";\n\nexport type {\n BudgetContract,\n BudgetContractDimension,\n BudgetContractFallback,\n BudgetContractViolation,\n BudgetContractViolationMode,\n} from \"./budget-contract.type\";\n\n/**\n * Per-model pricing used to compute USD cost from token counts.\n * Caller-supplied — no bundled table. Keys are model names (the\n * `ModelContract.name` value); values are input / output token\n * prices expressed as **USD per 1K tokens** to match every major\n * provider's published pricing sheet.\n */\nexport type BudgetPricing = Record<\n string,\n {\n /** USD per 1,000 input tokens. */\n inputPer1K: number;\n /** USD per 1,000 output tokens. */\n outputPer1K: number;\n }\n>;\n\n/**\n * Configuration for `budget()`. At least one of `maxTokens` or\n * `maxCostUSD` must be supplied — a budget with no cap is a no-op.\n */\nexport type BudgetOptions = {\n /**\n * Hard cap on cumulative total tokens (input + output, summed\n * across every trip of the run). Inclusive — exceeding triggers\n * the configured `onExceeded`.\n */\n maxTokens?: number;\n /**\n * Hard cap on cumulative USD cost. Requires `pricing` for the\n * agent's configured model — without a pricing entry the USD check\n * silently skips (tokens-only enforcement still applies).\n */\n maxCostUSD?: number;\n /**\n * Per-model pricing table used to compute USD cost. Only consulted\n * when `maxCostUSD` is set. Model names must match the running\n * agent's `ModelContract.name` exactly.\n */\n pricing?: BudgetPricing;\n /**\n * Behavior when a cap is breached. `\"abort\"` throws\n * `BudgetExceededError` — surfaces on `result.error`, stops the\n * run at the next trip boundary. `\"warn\"` logs a warning and\n * lets the run continue (useful for observability-first rollouts\n * before flipping the switch to abort). Default `\"abort\"`.\n */\n onExceeded?: \"abort\" | \"warn\";\n /**\n * Override the middleware name. Useful when two budgets coexist\n * (e.g. a per-request cap plus a session-wide cap via different\n * instances). Default `\"budget\"`.\n */\n name?: string;\n /**\n * Declarative SLO / cost contract enforced on top of (and\n * independently of) the legacy `maxTokens` / `maxCostUSD` caps.\n * Adds a wall-clock `maxLatencyMs` dimension and a per-contract\n * `onViolation` reaction (`\"abort\"` hard-stops, `\"fallback\"` records\n * a signal + fires `fallback` and lets the run continue). Omit to\n * keep the classic budget behavior unchanged.\n *\n * Read a recorded fallback signal back with\n * {@link readBudgetFallbackSignal}.\n */\n contract?: BudgetContract;\n};\n\ntype BudgetCounters = {\n tokens: number;\n costUSD: number;\n warned: boolean;\n /**\n * Wall-clock epoch ms captured at `execute.before`. Used to derive\n * cumulative run latency for the contract's `maxLatencyMs` clause.\n */\n startedAt: number;\n /**\n * Set once a `\"fallback\"` contract clause has fired, so the signal +\n * callback are emitted at most once per run even if later trips keep\n * breaching.\n */\n fallbackFired: boolean;\n};\n\n/**\n * Recorded contract fallback signal, stashed under the `<name>.fallback`\n * state key when a `\"fallback\"` clause trips. A fallback orchestrator\n * reads it via {@link readBudgetFallbackSignal} to decide how to degrade.\n */\nexport type BudgetFallbackSignal = BudgetContractViolation;\n\n/**\n * The `BudgetUnit` to stamp on the thrown error per contract dimension.\n * Latency has no native unit — it borrows `\"requests\"` as the least-wrong\n * operational measure, while the authoritative detail rides on the\n * error's `context.dimension`.\n */\nconst DIMENSION_UNIT: Record<BudgetContractDimension, BudgetUnit> = {\n tokens: \"tokens\",\n cost: \"usd\",\n latency: \"requests\",\n};\n\nfunction breach(\n limit: number,\n actual: number,\n unit: BudgetUnit,\n name: string,\n): never {\n throw new BudgetExceededError(\n `budget \"${name}\" exceeded — ${actual} ${unit} (cap: ${limit})`,\n { limit, actual, unit },\n );\n}\n\nfunction breachContract(\n name: string,\n dimension: BudgetContractDimension,\n limit: number,\n actual: number,\n): never {\n throw new BudgetExceededError(\n `budget \"${name}\" contract exceeded — ${dimension} ${actual} (cap: ${limit})`,\n {\n limit,\n actual,\n unit: DIMENSION_UNIT[dimension],\n context: { dimension, limit, actual, source: \"contract\" },\n },\n );\n}\n\n/**\n * Read the contract fallback signal recorded by a `budget()` middleware\n * running under `contract.onViolation: \"fallback\"`. Returns `undefined`\n * when no clause was breached.\n *\n * **Role.** The middleware cannot itself switch models on a soft breach,\n * so it records a typed {@link BudgetFallbackSignal} in the shared state\n * bag and lets the run continue. A fallback orchestrator (or the\n * `execute.after` hook of an outer middleware) reads it back here and\n * decides how to degrade the next run — cheaper model, cached answer,\n * truncated context.\n *\n * @param state - The middleware state bag (`ctx.state`).\n * @param name - The budget middleware's name. Default `\"budget\"`,\n * matching `BudgetOptions.name`'s default.\n *\n * @example\n * const guard = budget({ contract: { maxCostUSD: 0.05, onViolation: \"fallback\" } });\n *\n * // In an outer middleware's execute.after, after the run:\n * const signal = readBudgetFallbackSignal(ctx.state);\n * if (signal?.dimension === \"cost\") {\n * await rerunOnCheaperModel();\n * }\n */\nexport function readBudgetFallbackSignal(\n state: MiddlewareExecuteContext[\"state\"],\n name = \"budget\",\n): BudgetFallbackSignal | undefined {\n return namespacedState<BudgetFallbackSignal>(\n { state },\n `${name}.fallback`,\n ).get();\n}\n\n/**\n * Enforced token and / or USD budget for an agent run.\n *\n * **Role.** Guards against runaway tool loops, misconfigured\n * prompts, and unexpected provider price swings by capping\n * cumulative usage across every LLM trip of a single execution.\n * Aborts the run with a typed `BudgetExceededError` the moment a cap\n * is breached, rather than letting the damage grow trip by trip.\n *\n * **Scope.** Per-execution. A fresh counter is created at\n * `execute.before` and lives in the middleware state bag until the\n * run ends. Two concurrent `agent.execute()` calls on the same\n * agent therefore enforce the cap independently.\n *\n * **Token accounting.** After each successful trip, the middleware\n * adds `response.usage.total` to its running total and checks\n * against `maxTokens`. Synthetic trips (cache hits) contribute\n * `usage.total` as returned by the cache — cache middleware is\n * expected to surface zero usage on a hit, which naturally excludes\n * those trips from the budget.\n *\n * **USD accounting.** When `maxCostUSD` + `pricing[modelName]` are\n * both present, the middleware converts per-trip input / output\n * tokens to USD and accumulates. Missing pricing silently degrades\n * to tokens-only — explicit rather than guessing.\n *\n * **Warn mode.** `onExceeded: \"warn\"` logs a single warning the first\n * time a cap is breached and lets the run continue. Useful for\n * measuring real-world traffic against a proposed cap before flipping\n * to `\"abort\"` in production.\n *\n * **Contract / SLO mode.** Pass `contract` to enforce a declarative\n * service-level objective — `maxCostUSD`, `maxLatencyMs`, `maxTokens` —\n * on top of the legacy caps, with a single `onViolation` reaction:\n * `\"abort\"` hard-stops with `BudgetExceededError`; `\"fallback\"` records\n * a typed signal (read it via {@link readBudgetFallbackSignal}), fires\n * the optional `fallback` callback, and lets the run continue so an\n * outer layer can degrade gracefully. The contract's clauses are\n * evaluated independently of — and after — the top-level caps; the\n * top-level caps stay fully functional with or without a contract.\n *\n * @example\n * const budgetMiddleware = budget({ maxTokens: 50_000 });\n *\n * const myAgent = agent({\n * model,\n * middleware: [budgetMiddleware],\n * });\n *\n * @example\n * // With USD cap and custom pricing\n * const guard = budget({\n * maxCostUSD: 0.5,\n * pricing: {\n * \"gpt-4o\": { inputPer1K: 0.005, outputPer1K: 0.015 },\n * },\n * });\n *\n * @example\n * // SLO contract — soft-fallback on any breach\n * const sloGuard = budget({\n * pricing: { \"gpt-4o\": { inputPer1K: 0.005, outputPer1K: 0.015 } },\n * contract: {\n * maxCostUSD: 0.05,\n * maxLatencyMs: 8_000,\n * maxTokens: 40_000,\n * onViolation: \"fallback\",\n * fallback: (violation) => routeToCheaperModel(violation.dimension),\n * },\n * });\n */\nexport function budget(options: BudgetOptions): AgentMiddleware {\n const name = options.name ?? \"budget\";\n const onExceeded = options.onExceeded ?? \"abort\";\n const hasTokenCap = typeof options.maxTokens === \"number\";\n const hasCostCap = typeof options.maxCostUSD === \"number\";\n\n const contract = options.contract;\n const contractMode = contract?.onViolation ?? \"abort\";\n const hasContractTokenCap = typeof contract?.maxTokens === \"number\";\n const hasContractCostCap = typeof contract?.maxCostUSD === \"number\";\n const hasContractLatencyCap = typeof contract?.maxLatencyMs === \"number\";\n const contractNeedsCost = hasCostCap || hasContractCostCap;\n // Warn once per model when a cost cap is configured but the running model\n // has no pricing entry — without this the USD cap silently never enforces\n // (costUSD stays 0), a fail-open the JSDoc on `maxCostUSD` documents.\n const warnedUnpricedModels = new Set<string>();\n\n return {\n name,\n execute: {\n before(context) {\n const counters = namespacedState<BudgetCounters>(context, name);\n counters.set({\n tokens: 0,\n costUSD: 0,\n warned: false,\n startedAt: Date.now(),\n fallbackFired: false,\n });\n },\n },\n trip: {\n async after(context, response) {\n const counters = namespacedState<BudgetCounters>(context, name).get();\n\n if (!counters) {\n return;\n }\n\n counters.tokens += response.usage.total;\n\n if (contractNeedsCost) {\n const pricing = options.pricing?.[context.model.name];\n\n if (pricing) {\n const tripCost =\n (response.usage.input / 1000) * pricing.inputPer1K +\n (response.usage.output / 1000) * pricing.outputPer1K;\n counters.costUSD += tripCost;\n } else if (!warnedUnpricedModels.has(context.model.name)) {\n // A cost cap is set but no pricing matched the running model, so\n // costUSD can never grow and the USD cap silently never fires.\n // Surface the fail-open once per model instead of swallowing it.\n warnedUnpricedModels.add(context.model.name);\n console.warn(\n `ai.middleware.budget(\"${name}\"): a USD cost cap is set but no pricing entry ` +\n `matches the running model \"${context.model.name}\" — the cap cannot be enforced ` +\n `for it. Add a pricing entry for \"${context.model.name}\" to options.pricing.`,\n );\n }\n }\n\n if (hasTokenCap && counters.tokens > options.maxTokens!) {\n if (onExceeded === \"abort\") {\n breach(options.maxTokens!, counters.tokens, \"tokens\", name);\n }\n\n if (!counters.warned) {\n counters.warned = true;\n }\n }\n\n if (hasCostCap && counters.costUSD > options.maxCostUSD!) {\n if (onExceeded === \"abort\") {\n breach(options.maxCostUSD!, counters.costUSD, \"usd\", name);\n }\n\n if (!counters.warned) {\n counters.warned = true;\n }\n }\n\n if (!contract) {\n return;\n }\n\n if (hasContractTokenCap && counters.tokens > contract.maxTokens!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"tokens\",\n contract.maxTokens!,\n counters.tokens,\n );\n }\n\n if (hasContractCostCap && counters.costUSD > contract.maxCostUSD!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"cost\",\n contract.maxCostUSD!,\n counters.costUSD,\n );\n }\n\n if (hasContractLatencyCap) {\n const elapsedMs = Date.now() - counters.startedAt;\n\n if (elapsedMs > contract.maxLatencyMs!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"latency\",\n contract.maxLatencyMs!,\n elapsedMs,\n );\n }\n }\n },\n },\n };\n}\n\n/**\n * Apply the contract's reaction to a single breached clause. `\"abort\"`\n * throws `BudgetExceededError` (stops the run); `\"fallback\"` records the\n * signal once, fires the callback, and returns so the run continues.\n *\n * The callback is invoked at most once per run (guarded by\n * `counters.fallbackFired`) and its rejections are swallowed — a buggy\n * fallback hook must never crash the agent.\n */\nasync function enforceContract(\n context: MiddlewareExecuteContext,\n counters: BudgetCounters,\n name: string,\n mode: NonNullable<BudgetContract[\"onViolation\"]>,\n contract: BudgetContract,\n dimension: BudgetContractDimension,\n limit: number,\n actual: number,\n): Promise<void> {\n if (mode === \"abort\") {\n breachContract(name, dimension, limit, actual);\n }\n\n if (counters.fallbackFired) {\n return;\n }\n\n counters.fallbackFired = true;\n\n const violation: BudgetContractViolation = {\n dimension,\n limit,\n actual,\n mode,\n };\n\n namespacedState<BudgetContractViolation>(context, `${name}.fallback`).set(\n violation,\n );\n\n if (!contract.fallback) {\n return;\n }\n\n try {\n await contract.fallback(violation, context);\n } catch {\n // A fallback callback is a notification hook — its failure must\n // never crash the run. Swallow deliberately.\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"budget.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/builtins/budget.ts"],"sourcesContent":["import type {\n AgentMiddleware,\n MiddlewareExecuteContext,\n} from \"../../contracts/middleware\";\nimport { BudgetExceededError, type BudgetUnit } from \"../../errors\";\nimport { namespacedState } from \"../utils\";\nimport type {\n BudgetContract,\n BudgetContractDimension,\n BudgetContractViolation,\n} from \"./budget-contract.type\";\n\nexport type {\n BudgetContract,\n BudgetContractDimension,\n BudgetContractFallback,\n BudgetContractViolation,\n BudgetContractViolationMode,\n} from \"./budget-contract.type\";\n\n/**\n * Per-model pricing used to compute USD cost from token counts.\n * Caller-supplied — no bundled table. Keys are model names (the\n * `ModelContract.name` value); values are input / output token\n * prices expressed as **USD per 1K tokens** to match every major\n * provider's published pricing sheet.\n */\nexport type BudgetPricing = Record<\n string,\n {\n /** USD per 1,000 input tokens. */\n inputPer1K: number;\n /** USD per 1,000 output tokens. */\n outputPer1K: number;\n }\n>;\n\n/**\n * Configuration for `budget()`. At least one of `maxTokens` or\n * `maxCostUSD` must be supplied — a budget with no cap is a no-op.\n */\nexport type BudgetOptions = {\n /**\n * Hard cap on cumulative total tokens (input + output, summed\n * across every trip of the run). Inclusive — exceeding triggers\n * the configured `onExceeded`.\n */\n maxTokens?: number;\n /**\n * Hard cap on cumulative USD cost. Requires `pricing` for the\n * agent's configured model — without a pricing entry the USD check\n * silently skips (tokens-only enforcement still applies).\n */\n maxCostUSD?: number;\n /**\n * Per-model pricing table used to compute USD cost. Only consulted\n * when `maxCostUSD` is set. Model names must match the running\n * agent's `ModelContract.name` exactly.\n */\n pricing?: BudgetPricing;\n /**\n * Behavior when a cap is breached. `\"abort\"` throws\n * `BudgetExceededError` — surfaces on `result.error`, stops the\n * run at the next trip boundary. `\"warn\"` logs a warning and\n * lets the run continue (useful for observability-first rollouts\n * before flipping the switch to abort). Default `\"abort\"`.\n */\n onExceeded?: \"abort\" | \"warn\";\n /**\n * Override the middleware name. Useful when two budgets coexist\n * (e.g. a per-request cap plus a session-wide cap via different\n * instances). Default `\"budget\"`.\n */\n name?: string;\n /**\n * Declarative SLO / cost contract enforced on top of (and\n * independently of) the legacy `maxTokens` / `maxCostUSD` caps.\n * Adds a wall-clock `maxLatencyMs` dimension and a per-contract\n * `onViolation` reaction (`\"abort\"` hard-stops, `\"fallback\"` records\n * a signal + fires `fallback` and lets the run continue). Omit to\n * keep the classic budget behavior unchanged.\n *\n * Read a recorded fallback signal back with\n * {@link readBudgetFallbackSignal}.\n */\n contract?: BudgetContract;\n};\n\ntype BudgetCounters = {\n tokens: number;\n costUSD: number;\n warned: boolean;\n /**\n * Wall-clock epoch ms captured at `execute.before`. Used to derive\n * cumulative run latency for the contract's `maxLatencyMs` clause.\n */\n startedAt: number;\n /**\n * Set once a `\"fallback\"` contract clause has fired, so the signal +\n * callback are emitted at most once per run even if later trips keep\n * breaching.\n */\n fallbackFired: boolean;\n};\n\n/**\n * Recorded contract fallback signal, stashed under the `<name>.fallback`\n * state key when a `\"fallback\"` clause trips. A fallback orchestrator\n * reads it via {@link readBudgetFallbackSignal} to decide how to degrade.\n */\nexport type BudgetFallbackSignal = BudgetContractViolation;\n\n/**\n * The `BudgetUnit` to stamp on the thrown error per contract dimension.\n * Latency has no native unit — it borrows `\"requests\"` as the least-wrong\n * operational measure, while the authoritative detail rides on the\n * error's `context.dimension`.\n */\nconst DIMENSION_UNIT: Record<BudgetContractDimension, BudgetUnit> = {\n tokens: \"tokens\",\n cost: \"usd\",\n latency: \"requests\",\n};\n\nfunction breach(\n limit: number,\n actual: number,\n unit: BudgetUnit,\n name: string,\n): never {\n throw new BudgetExceededError(\n `budget \"${name}\" exceeded — ${actual} ${unit} (cap: ${limit})`,\n { limit, actual, unit },\n );\n}\n\nfunction breachContract(\n name: string,\n dimension: BudgetContractDimension,\n limit: number,\n actual: number,\n): never {\n throw new BudgetExceededError(\n `budget \"${name}\" contract exceeded — ${dimension} ${actual} (cap: ${limit})`,\n {\n limit,\n actual,\n unit: DIMENSION_UNIT[dimension],\n context: { dimension, limit, actual, source: \"contract\" },\n },\n );\n}\n\n/**\n * Read the contract fallback signal recorded by a `budget()` middleware\n * running under `contract.onViolation: \"fallback\"`. Returns `undefined`\n * when no clause was breached.\n *\n * **Role.** The middleware cannot itself switch models on a soft breach,\n * so it records a typed {@link BudgetFallbackSignal} in the shared state\n * bag and lets the run continue. A fallback orchestrator (or the\n * `execute.after` hook of an outer middleware) reads it back here and\n * decides how to degrade the next run — cheaper model, cached answer,\n * truncated context.\n *\n * @param state - The middleware state bag (`ctx.state`).\n * @param name - The budget middleware's name. Default `\"budget\"`,\n * matching `BudgetOptions.name`'s default.\n *\n * @example\n * const guard = budget({ contract: { maxCostUSD: 0.05, onViolation: \"fallback\" } });\n *\n * // In an outer middleware's execute.after, after the run:\n * const signal = readBudgetFallbackSignal(ctx.state);\n * if (signal?.dimension === \"cost\") {\n * await rerunOnCheaperModel();\n * }\n */\nexport function readBudgetFallbackSignal(\n state: MiddlewareExecuteContext[\"state\"],\n name = \"budget\",\n): BudgetFallbackSignal | undefined {\n return namespacedState<BudgetFallbackSignal>(\n { state },\n `${name}.fallback`,\n ).get();\n}\n\n/**\n * Enforced token and / or USD budget for an agent run.\n *\n * **Role.** Guards against runaway tool loops, misconfigured\n * prompts, and unexpected provider price swings by capping\n * cumulative usage across every LLM trip of a single execution.\n * Aborts the run with a typed `BudgetExceededError` the moment a cap\n * is breached, rather than letting the damage grow trip by trip.\n *\n * **Scope.** Per-execution. A fresh counter is created at\n * `execute.before` and lives in the middleware state bag until the\n * run ends. Two concurrent `agent.execute()` calls on the same\n * agent therefore enforce the cap independently.\n *\n * **Token accounting.** After each successful trip, the middleware\n * adds `response.usage.total` to its running total and checks\n * against `maxTokens`. Synthetic trips (cache hits) contribute\n * `usage.total` as returned by the cache — cache middleware is\n * expected to surface zero usage on a hit, which naturally excludes\n * those trips from the budget.\n *\n * **USD accounting.** When `maxCostUSD` + `pricing[modelName]` are\n * both present, the middleware converts per-trip input / output\n * tokens to USD and accumulates. Missing pricing silently degrades\n * to tokens-only — explicit rather than guessing.\n *\n * **Warn mode.** `onExceeded: \"warn\"` logs a single warning the first\n * time a cap is breached and lets the run continue. Useful for\n * measuring real-world traffic against a proposed cap before flipping\n * to `\"abort\"` in production.\n *\n * **Contract / SLO mode.** Pass `contract` to enforce a declarative\n * service-level objective — `maxCostUSD`, `maxLatencyMs`, `maxTokens` —\n * on top of the legacy caps, with a single `onViolation` reaction:\n * `\"abort\"` hard-stops with `BudgetExceededError`; `\"fallback\"` records\n * a typed signal (read it via {@link readBudgetFallbackSignal}), fires\n * the optional `fallback` callback, and lets the run continue so an\n * outer layer can degrade gracefully. The contract's clauses are\n * evaluated independently of — and after — the top-level caps; the\n * top-level caps stay fully functional with or without a contract.\n *\n * @example\n * const budgetMiddleware = budget({ maxTokens: 50_000 });\n *\n * const myAgent = agent({\n * model,\n * middleware: [budgetMiddleware],\n * });\n *\n * @example\n * // With USD cap and custom pricing\n * const guard = budget({\n * maxCostUSD: 0.5,\n * pricing: {\n * \"gpt-4o\": { inputPer1K: 0.005, outputPer1K: 0.015 },\n * },\n * });\n *\n * @example\n * // SLO contract — soft-fallback on any breach\n * const sloGuard = budget({\n * pricing: { \"gpt-4o\": { inputPer1K: 0.005, outputPer1K: 0.015 } },\n * contract: {\n * maxCostUSD: 0.05,\n * maxLatencyMs: 8_000,\n * maxTokens: 40_000,\n * onViolation: \"fallback\",\n * fallback: (violation) => routeToCheaperModel(violation.dimension),\n * },\n * });\n */\nexport function budget(options: BudgetOptions): AgentMiddleware {\n const name = options.name ?? \"budget\";\n const onExceeded = options.onExceeded ?? \"abort\";\n const hasTokenCap = typeof options.maxTokens === \"number\";\n const hasCostCap = typeof options.maxCostUSD === \"number\";\n\n const contract = options.contract;\n const contractMode = contract?.onViolation ?? \"abort\";\n const hasContractTokenCap = typeof contract?.maxTokens === \"number\";\n const hasContractCostCap = typeof contract?.maxCostUSD === \"number\";\n const hasContractLatencyCap = typeof contract?.maxLatencyMs === \"number\";\n const contractNeedsCost = hasCostCap || hasContractCostCap;\n // Warn once per model when a cost cap is configured but the running model\n // has no pricing entry — without this the USD cap silently never enforces\n // (costUSD stays 0), a fail-open the JSDoc on `maxCostUSD` documents.\n const warnedUnpricedModels = new Set<string>();\n\n return {\n name,\n execute: {\n before(context) {\n const counters = namespacedState<BudgetCounters>(context, name);\n counters.set({\n tokens: 0,\n costUSD: 0,\n warned: false,\n startedAt: Date.now(),\n fallbackFired: false,\n });\n },\n },\n trip: {\n async after(context, response) {\n const counters = namespacedState<BudgetCounters>(context, name).get();\n\n if (!counters) {\n return;\n }\n\n counters.tokens += response.usage.total;\n\n if (contractNeedsCost) {\n const pricing = options.pricing?.[context.model.name];\n\n if (pricing) {\n const tripCost =\n (response.usage.input / 1000) * pricing.inputPer1K +\n (response.usage.output / 1000) * pricing.outputPer1K;\n counters.costUSD += tripCost;\n } else if (!warnedUnpricedModels.has(context.model.name)) {\n // A cost cap is set but no pricing matched the running model, so\n // costUSD can never grow and the USD cap silently never fires.\n // Surface the fail-open once per model instead of swallowing it.\n warnedUnpricedModels.add(context.model.name);\n console.warn(\n `ai.middleware.budget(\"${name}\"): a USD cost cap is set but no pricing entry ` +\n `matches the running model \"${context.model.name}\" — the cap cannot be enforced ` +\n `for it. Add a pricing entry for \"${context.model.name}\" to options.pricing.`,\n );\n }\n }\n\n if (hasTokenCap && counters.tokens > options.maxTokens!) {\n if (onExceeded === \"abort\") {\n breach(options.maxTokens!, counters.tokens, \"tokens\", name);\n }\n\n if (!counters.warned) {\n counters.warned = true;\n }\n }\n\n if (hasCostCap && counters.costUSD > options.maxCostUSD!) {\n if (onExceeded === \"abort\") {\n breach(options.maxCostUSD!, counters.costUSD, \"usd\", name);\n }\n\n if (!counters.warned) {\n counters.warned = true;\n }\n }\n\n if (!contract) {\n return;\n }\n\n if (hasContractTokenCap && counters.tokens > contract.maxTokens!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"tokens\",\n contract.maxTokens!,\n counters.tokens,\n );\n }\n\n if (hasContractCostCap && counters.costUSD > contract.maxCostUSD!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"cost\",\n contract.maxCostUSD!,\n counters.costUSD,\n );\n }\n\n if (hasContractLatencyCap) {\n const elapsedMs = Date.now() - counters.startedAt;\n\n if (elapsedMs > contract.maxLatencyMs!) {\n await enforceContract(\n context,\n counters,\n name,\n contractMode,\n contract,\n \"latency\",\n contract.maxLatencyMs!,\n elapsedMs,\n );\n }\n }\n },\n },\n };\n}\n\n/**\n * Apply the contract's reaction to a single breached clause. `\"abort\"`\n * throws `BudgetExceededError` (stops the run); `\"fallback\"` records the\n * signal once, fires the callback, and returns so the run continues.\n *\n * The callback is invoked at most once per run (guarded by\n * `counters.fallbackFired`) and its rejections are swallowed — a buggy\n * fallback hook must never crash the agent.\n */\nasync function enforceContract(\n context: MiddlewareExecuteContext,\n counters: BudgetCounters,\n name: string,\n mode: NonNullable<BudgetContract[\"onViolation\"]>,\n contract: BudgetContract,\n dimension: BudgetContractDimension,\n limit: number,\n actual: number,\n): Promise<void> {\n if (mode === \"abort\") {\n breachContract(name, dimension, limit, actual);\n }\n\n if (counters.fallbackFired) {\n return;\n }\n\n counters.fallbackFired = true;\n\n const violation: BudgetContractViolation = {\n dimension,\n limit,\n actual,\n mode,\n };\n\n namespacedState<BudgetContractViolation>(context, `${name}.fallback`).set(\n violation,\n );\n\n if (!contract.fallback) {\n return;\n }\n\n try {\n await contract.fallback(violation, context);\n } catch {\n // A fallback callback is a notification hook — its failure must\n // never crash the run. Swallow deliberately.\n }\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"semantic-cache.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/builtins/semantic-cache.ts"],"sourcesContent":["import type { CacheDriver } from \"@warlock.js/cache\";\nimport { resolveDefaultStore } from \"../../config\";\nimport type { Message } from \"../../contracts/conversation-message.type\";\nimport type { EmbedderContract } from \"../../contracts/embedder.contract\";\nimport type { AgentMiddleware } from \"../../contracts/middleware\";\nimport type { MiddlewareTripContext } from \"../../contracts/middleware/middleware-context.type\";\nimport type { ModelResponse } from \"../../contracts/model.contract\";\nimport { extractUserText } from \"../utils\";\n\n/**\n * Isolation boundary for cache reads and writes.\n *\n * - `\"session\"` (default) — key every entry off the run's\n * `AgentExecuteOptions.sessionId`, so one session never receives a\n * response cached for another. Calls made WITHOUT a `sessionId` share\n * one unscoped pool (the pre-4.15.0 behavior); an unscoped read never\n * sees a session-scoped entry and vice versa.\n * - `\"shared\"` — one pool for every caller, regardless of session. The\n * explicit opt-in for genuinely public Q&A (docs bots, FAQ) where the\n * cross-user hit rate is the point and no response can carry one\n * caller's private context.\n * - a resolver — derive the key yourself, e.g. per tenant\n * (`ctx => ctx.options?.toolCtx?.tenantId`). Returning `undefined`\n * falls back to the unscoped pool, so return a constant sentinel (or\n * throw) if you need the call to fail closed instead.\n */\nexport type SemanticCacheScope =\n | \"session\"\n | \"shared\"\n | ((context: MiddlewareTripContext) => string | undefined);\n\n/**\n * Configuration for `semanticCache()`.\n */\nexport type SemanticCacheOptions = {\n /** Embedder used to produce the query vector from the prompt text. */\n embedder: EmbedderContract;\n /**\n * Vector-capable cache driver from `@warlock.js/cache`. Production\n * deployments pick a driver with a real ANN index (`pg` with\n * pgvector, `redis` with RediSearch). Dev / test environments use\n * `new MemoryCacheDriver()` — zero config, correct, but O(N) per\n * query. Drivers without similarity support throw\n * `CacheUnsupportedError` from `set({ vector })` / `similar()`.\n *\n * Falls back to `ai.config({ defaultStore })` when omitted. When\n * neither is set, the factory throws at construction time —\n * semantic cache cannot operate without a store.\n */\n store?: CacheDriver<any, any>;\n /**\n * Minimum cosine similarity for a vector hit. Between 0 and 1 —\n * 0.95 is a solid default for question-answering caches.\n */\n threshold: number;\n /**\n * Optional TTL in milliseconds. Entries whose `storedAt` is older\n * than this are treated as misses on read and overwritten on the\n * next write. Default: no expiry — entries live until the store\n * evicts them (per its own TTL/eviction policy).\n */\n ttlMs?: number;\n /**\n * Namespace prefix applied to every key the cache writes. Lets\n * multiple agents share one driver without collision. Default\n * `\"ai.cache\"`.\n */\n namespace?: string;\n /**\n * Per-caller isolation boundary. Default `\"session\"` — a cached\n * response is served back only to the session that produced it.\n *\n * A `semanticCache` is normally built once at app boot and shared by\n * every end user, and a hit is returned as the model's answer with no\n * LLM call in between; without a scope that pools every caller's Q&A\n * pairs into one namespace, which is both a disclosure path (user B's\n * near-enough prompt gets served user A's answer, personal context\n * included) and a poisoning path (an attacker seeds an entry near a\n * predictable future query). Set `\"shared\"` to opt back into pooling\n * where that is actually desirable. See {@link SemanticCacheScope}.\n */\n scope?: SemanticCacheScope;\n /**\n * Middleware name — also the state-bag key prefix inside a single\n * execution. Default `\"semantic-cache\"`.\n */\n name?: string;\n};\n\ntype CachedEntry = {\n response: ModelResponse;\n storedAt: number;\n /**\n * Isolation key the entry was written under; absent = the unscoped\n * pool (also the shape of every entry written before 4.15.0).\n */\n scope?: string;\n};\n\ntype PendingWrite = {\n promptKey: string;\n vector: number[];\n scope?: string;\n};\n\nconst DEFAULT_NAMESPACE = \"ai.cache\";\n\n/**\n * Extra candidates pulled from `similar()` on a SCOPED lookup before the\n * scope filter runs. The driver ranks across every scope in the index,\n * so a bare `topK: 1` can come back as a foreign entry and mask this\n * scope's own legitimate hit. Mirrors the memory tiers' overscan.\n */\nconst SIMILAR_OVERSCAN = 5;\n\n/**\n * Build a stable fingerprint for a prompt covering the full message\n * list (system + history + user turn). Ensures two prompts sharing\n * the user text but differing in prior context do not collide on\n * the exact-match fast path.\n *\n * FNV-1a variant — cheap, collision-resistant enough for a cache,\n * dependency-free. NOT a cryptographic hash: collisions would\n * surface as wrong cache hits, not a security issue in the current\n * trust model.\n */\nfunction hashPrompt(messages: ReadonlyArray<Message>): string {\n return fnv1a(\n messages\n .map((message) => {\n const role = message.role;\n const content = Array.isArray(message.content)\n ? message.content\n .filter((part) => part.type === \"text\")\n .map((part) => (part as { text: string }).text)\n .join(\"|\")\n : message.content;\n\n return `${role}:${content}`;\n })\n .join(\"||\"),\n );\n}\n\n/** FNV-1a over a string — see {@link hashPrompt} for the caveats. */\nfunction fnv1a(serialized: string): string {\n let hash = 0x811c9dc5;\n\n for (let index = 0; index < serialized.length; index++) {\n hash ^= serialized.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n\n return (hash >>> 0).toString(16);\n}\n\n/**\n * Resolve the isolation key this trip reads and writes under.\n *\n * Derived from the run's own `sessionId` (or the developer's resolver) —\n * never from the prompt, the model's output, or anything the LLM can\n * write to. `\"shared\"` and an unidentified run both resolve to\n * `undefined`, i.e. the unscoped pool, which a scoped lookup can never\n * read.\n */\nfunction resolveScope(\n scope: SemanticCacheScope,\n context: MiddlewareTripContext,\n): string | undefined {\n if (scope === \"shared\") {\n return undefined;\n }\n\n const key =\n typeof scope === \"function\"\n ? scope(context)\n : sessionScope(context.options?.sessionId);\n\n return key ? key : undefined;\n}\n\n/**\n * The default `\"session\"` key: the session id under a reserved prefix so\n * a custom resolver returning a bare tenant id can't collide with a\n * session pool. Mirrors the orchestrator's `sessionMemoryScope`.\n */\nfunction sessionScope(sessionId: string | undefined): string | undefined {\n return sessionId ? `session:${sessionId}` : undefined;\n}\n\nfunction isFresh(entry: CachedEntry, ttlMs: number | undefined): boolean {\n if (ttlMs === undefined) {\n return true;\n }\n\n return Date.now() - entry.storedAt <= ttlMs;\n}\n\n/**\n * Semantic-similarity response cache for an agent run.\n *\n * **Role.** Skips LLM round-trips when the current prompt is\n * semantically close to one the agent has already answered. For\n * FAQ / support-style traffic this often eliminates 60–80% of\n * model calls — the production win is massive for cost and\n * latency.\n *\n * **Delegation to `@warlock.js/cache`.** This middleware does NOT\n * implement similarity search itself. It delegates to the supplied\n * `CacheDriver`. Production deployments pick a driver with an ANN\n * index (`pg` + pgvector, `redis` + RediSearch). Dev / test\n * environments pass `new MemoryCacheDriver()` — zero config, correct,\n * but O(N) per query. Drivers without similarity support throw\n * `CacheUnsupportedError` from `set({ vector })` / `similar()`.\n *\n * **Two-tier lookup.**\n * 1. *Exact-match key* — a cheap FNV hash over the entire message\n * list. `store.get(hash)` returns the entry without an embedding\n * round trip when the prompt hasn't changed at all.\n * 2. *Vector-match* — on exact-match miss, embed the prompt and\n * call `store.similar(vector, { topK: 1, threshold })`. The\n * driver uses its native similarity index; anything clearing\n * `threshold` is returned as a hit.\n *\n * **Write-on-miss.** When both tiers miss, `trip.before` stashes\n * the prompt hash + vector in `ctx.state`; `trip.after` reads back\n * the pending entry and calls\n * `store.set(hash, entry, { vector })`. If an outer middleware\n * (guardrail) throws in `trip.after` before the cache's `trip.after`\n * runs, the pending entry is never written — bad responses stay out\n * of the cache **as long as the canonical install order is followed**\n * (cache outermost).\n *\n * **Synthetic-response on hit.** Returns a `ModelResponse` with\n * `usage: { input: 0, output: 0, total: 0 }` so budget /\n * observability correctly exclude the saved trip.\n *\n * **Per-session scoping (4.15.0).** One `semanticCache` instance\n * normally serves every end user, and a hit is returned as the answer\n * with no model call in between — so entries are keyed by the run's\n * `sessionId` (`scope`, default `\"session\"`) and a lookup only ever\n * sees entries written under the same key. Runs made without a\n * `sessionId` share one unscoped pool; pass `sessionId` on\n * `agent.execute()` (composites thread their own through automatically)\n * to get the isolation, or set `scope: \"shared\"` to pool deliberately.\n * Note the cost/benefit shift: scoping trades cross-user hit rate for\n * isolation, so public-FAQ deployments where no response can carry a\n * caller's private context should opt into `\"shared\"` explicitly.\n *\n * @example\n * import { semanticCache } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const cache = semanticCache({\n * embedder: openai.embedder({ name: \"text-embedding-3-small\" }),\n * store,\n * threshold: 0.95,\n * ttlMs: 60 * 60 * 1000,\n * });\n *\n * const myAgent = agent({ model, middleware: [cache] });\n */\nexport function semanticCache(options: SemanticCacheOptions): AgentMiddleware {\n const name = options.name ?? \"semantic-cache\";\n const namespace = options.namespace ?? DEFAULT_NAMESPACE;\n const scopeMode: SemanticCacheScope = options.scope ?? \"session\";\n const pendingKey = `${name}.pending`;\n\n // Resolve the effective store at factory time, not per-call. Every\n // subsequent hook closes over `store` so the resolution happens once.\n // Throws now (loud, at construction) instead of later during the\n // first trip (silent until the agent actually runs).\n const store = options.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `semanticCache: no store supplied — pass \\`store\\` in options or call \\`ai.config({ defaultStore })\\` at app boot before constructing the middleware`,\n );\n }\n\n // Cache's parseKey replaces \":\" with \".\" so the namespace boundary\n // matches what `similar()` actually returns in `hit.key`. Using a\n // dot here keeps prefix checks aligned with stored keys.\n //\n // A scoped entry gets an extra hashed segment, so two sessions asking\n // the identical question stay two entries instead of overwriting each\n // other; the scope is hashed because a `sessionId` is caller-supplied\n // and may contain the key delimiter. The unscoped key shape is\n // unchanged, so pre-4.15.0 entries still resolve. The hash is a\n // write-separation device only — a read is authorized by the exact\n // `entry.scope` equality check below, so even a hash collision cannot\n // widen what a session can read.\n const keyFor = (hash: string, scope: string | undefined): string =>\n scope === undefined\n ? `${namespace}.${hash}`\n : `${namespace}.${fnv1a(scope)}.${hash}`;\n\n return {\n name,\n log: true,\n trip: {\n async before(context) {\n // Only cache the first trip's response. Subsequent trips\n // happen because the previous trip requested tool calls — the\n // message list now carries tool results the original prompt\n // never saw, so a semantic match on the unchanged user text\n // would serve back the prior `tool_calls` response and loop\n // the agent forever. The first turn is also the only one\n // where a \"same question → same final answer\" caching story\n // is sound.\n if (context.tripIndex !== 0) {\n return;\n }\n\n const promptText = extractUserText(context.messages);\n\n if (!promptText) {\n return;\n }\n\n const scope = resolveScope(scopeMode, context);\n const promptKey = hashPrompt(context.messages);\n\n const exact = await store.get<CachedEntry>(keyFor(promptKey, scope));\n\n // The key already carries the scope; re-checking the stored\n // `scope` is the actual authorization step, so a key collision\n // or a hand-written entry can't serve across the boundary.\n if (exact && exact.scope === scope && isFresh(exact, options.ttlMs)) {\n return toSyntheticResponse(exact.response);\n }\n\n const query = await options.embedder.embed(promptText);\n\n const hits = await store.similar<CachedEntry>(query.vector, {\n topK: scope === undefined ? 1 : SIMILAR_OVERSCAN,\n threshold: options.threshold,\n });\n\n // Only entries written inside this cache's namespace AND this\n // caller's scope are eligible. A shared driver would otherwise\n // leak a foreign namespace's entries; a shared namespace would\n // leak another session's answer to this one.\n const hit = hits.find(\n (candidate) =>\n candidate.key.startsWith(`${namespace}.`) &&\n candidate.value?.scope === scope &&\n isFresh(candidate.value, options.ttlMs),\n );\n\n if (hit) {\n return toSyntheticResponse(hit.value.response);\n }\n\n const pending: PendingWrite = {\n promptKey,\n vector: query.vector,\n scope,\n };\n context.state.set(pendingKey, pending);\n\n return;\n },\n async after(context, response) {\n const pending = context.state.get(pendingKey) as PendingWrite | undefined;\n\n if (!pending) {\n return;\n }\n\n // Mid-stream tool-call responses must not be cached — the\n // useful answer comes from the trip *after* the tool returns.\n // Crucially, leave the pending entry in place so a later trip\n // (the one that actually finishes with `stop`) can read it\n // and write the final response under the *original* trip-0\n // prompt key. Deleting here would orphan the pending and the\n // post-tool answer would never make it into the store.\n if (response.finishReason === \"tool_calls\") {\n return;\n }\n\n context.state.delete(pendingKey);\n\n const entry: CachedEntry = {\n response,\n storedAt: Date.now(),\n scope: pending.scope,\n };\n\n await store.set(keyFor(pending.promptKey, pending.scope), entry, {\n vector: pending.vector,\n });\n\n return;\n },\n },\n };\n}\n\nfunction toSyntheticResponse(response: ModelResponse): ModelResponse {\n return {\n content: response.content,\n finishReason: response.finishReason,\n usage: { input: 0, output: 0, total: 0 },\n toolCalls: response.toolCalls,\n };\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"semantic-cache.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/builtins/semantic-cache.ts"],"sourcesContent":["import type { CacheDriver } from \"@warlock.js/cache\";\nimport { resolveDefaultStore } from \"../../config\";\nimport type { Message } from \"../../contracts/conversation-message.type\";\nimport type { EmbedderContract } from \"../../contracts/embedder.contract\";\nimport type { AgentMiddleware } from \"../../contracts/middleware\";\nimport type { MiddlewareTripContext } from \"../../contracts/middleware/middleware-context.type\";\nimport type { ModelResponse } from \"../../contracts/model.contract\";\nimport { extractUserText } from \"../utils\";\n\n/**\n * Isolation boundary for cache reads and writes.\n *\n * - `\"session\"` (default) — key every entry off the run's\n * `AgentExecuteOptions.sessionId`, so one session never receives a\n * response cached for another. Calls made WITHOUT a `sessionId` share\n * one unscoped pool (the pre-4.15.0 behavior); an unscoped read never\n * sees a session-scoped entry and vice versa.\n * - `\"shared\"` — one pool for every caller, regardless of session. The\n * explicit opt-in for genuinely public Q&A (docs bots, FAQ) where the\n * cross-user hit rate is the point and no response can carry one\n * caller's private context.\n * - a resolver — derive the key yourself, e.g. per tenant\n * (`ctx => ctx.options?.toolCtx?.tenantId`). Returning `undefined`\n * falls back to the unscoped pool, so return a constant sentinel (or\n * throw) if you need the call to fail closed instead.\n */\nexport type SemanticCacheScope =\n | \"session\"\n | \"shared\"\n | ((context: MiddlewareTripContext) => string | undefined);\n\n/**\n * Configuration for `semanticCache()`.\n */\nexport type SemanticCacheOptions = {\n /** Embedder used to produce the query vector from the prompt text. */\n embedder: EmbedderContract;\n /**\n * Vector-capable cache driver from `@warlock.js/cache`. Production\n * deployments pick a driver with a real ANN index (`pg` with\n * pgvector, `redis` with RediSearch). Dev / test environments use\n * `new MemoryCacheDriver()` — zero config, correct, but O(N) per\n * query. Drivers without similarity support throw\n * `CacheUnsupportedError` from `set({ vector })` / `similar()`.\n *\n * Falls back to `ai.config({ defaultStore })` when omitted. When\n * neither is set, the factory throws at construction time —\n * semantic cache cannot operate without a store.\n */\n store?: CacheDriver<any, any>;\n /**\n * Minimum cosine similarity for a vector hit. Between 0 and 1 —\n * 0.95 is a solid default for question-answering caches.\n */\n threshold: number;\n /**\n * Optional TTL in milliseconds. Entries whose `storedAt` is older\n * than this are treated as misses on read and overwritten on the\n * next write. Default: no expiry — entries live until the store\n * evicts them (per its own TTL/eviction policy).\n */\n ttlMs?: number;\n /**\n * Namespace prefix applied to every key the cache writes. Lets\n * multiple agents share one driver without collision. Default\n * `\"ai.cache\"`.\n */\n namespace?: string;\n /**\n * Per-caller isolation boundary. Default `\"session\"` — a cached\n * response is served back only to the session that produced it.\n *\n * A `semanticCache` is normally built once at app boot and shared by\n * every end user, and a hit is returned as the model's answer with no\n * LLM call in between; without a scope that pools every caller's Q&A\n * pairs into one namespace, which is both a disclosure path (user B's\n * near-enough prompt gets served user A's answer, personal context\n * included) and a poisoning path (an attacker seeds an entry near a\n * predictable future query). Set `\"shared\"` to opt back into pooling\n * where that is actually desirable. See {@link SemanticCacheScope}.\n */\n scope?: SemanticCacheScope;\n /**\n * Middleware name — also the state-bag key prefix inside a single\n * execution. Default `\"semantic-cache\"`.\n */\n name?: string;\n};\n\ntype CachedEntry = {\n response: ModelResponse;\n storedAt: number;\n /**\n * Isolation key the entry was written under; absent = the unscoped\n * pool (also the shape of every entry written before 4.15.0).\n */\n scope?: string;\n};\n\ntype PendingWrite = {\n promptKey: string;\n vector: number[];\n scope?: string;\n};\n\nconst DEFAULT_NAMESPACE = \"ai.cache\";\n\n/**\n * Extra candidates pulled from `similar()` on a SCOPED lookup before the\n * scope filter runs. The driver ranks across every scope in the index,\n * so a bare `topK: 1` can come back as a foreign entry and mask this\n * scope's own legitimate hit. Mirrors the memory tiers' overscan.\n */\nconst SIMILAR_OVERSCAN = 5;\n\n/**\n * Build a stable fingerprint for a prompt covering the full message\n * list (system + history + user turn). Ensures two prompts sharing\n * the user text but differing in prior context do not collide on\n * the exact-match fast path.\n *\n * FNV-1a variant — cheap, collision-resistant enough for a cache,\n * dependency-free. NOT a cryptographic hash: collisions would\n * surface as wrong cache hits, not a security issue in the current\n * trust model.\n */\nfunction hashPrompt(messages: ReadonlyArray<Message>): string {\n return fnv1a(\n messages\n .map((message) => {\n const role = message.role;\n const content = Array.isArray(message.content)\n ? message.content\n .filter((part) => part.type === \"text\")\n .map((part) => (part as { text: string }).text)\n .join(\"|\")\n : message.content;\n\n return `${role}:${content}`;\n })\n .join(\"||\"),\n );\n}\n\n/** FNV-1a over a string — see {@link hashPrompt} for the caveats. */\nfunction fnv1a(serialized: string): string {\n let hash = 0x811c9dc5;\n\n for (let index = 0; index < serialized.length; index++) {\n hash ^= serialized.charCodeAt(index);\n hash = Math.imul(hash, 0x01000193);\n }\n\n return (hash >>> 0).toString(16);\n}\n\n/**\n * Resolve the isolation key this trip reads and writes under.\n *\n * Derived from the run's own `sessionId` (or the developer's resolver) —\n * never from the prompt, the model's output, or anything the LLM can\n * write to. `\"shared\"` and an unidentified run both resolve to\n * `undefined`, i.e. the unscoped pool, which a scoped lookup can never\n * read.\n */\nfunction resolveScope(\n scope: SemanticCacheScope,\n context: MiddlewareTripContext,\n): string | undefined {\n if (scope === \"shared\") {\n return undefined;\n }\n\n const key =\n typeof scope === \"function\"\n ? scope(context)\n : sessionScope(context.options?.sessionId);\n\n return key ? key : undefined;\n}\n\n/**\n * The default `\"session\"` key: the session id under a reserved prefix so\n * a custom resolver returning a bare tenant id can't collide with a\n * session pool. Mirrors the orchestrator's `sessionMemoryScope`.\n */\nfunction sessionScope(sessionId: string | undefined): string | undefined {\n return sessionId ? `session:${sessionId}` : undefined;\n}\n\nfunction isFresh(entry: CachedEntry, ttlMs: number | undefined): boolean {\n if (ttlMs === undefined) {\n return true;\n }\n\n return Date.now() - entry.storedAt <= ttlMs;\n}\n\n/**\n * Semantic-similarity response cache for an agent run.\n *\n * **Role.** Skips LLM round-trips when the current prompt is\n * semantically close to one the agent has already answered. For\n * FAQ / support-style traffic this often eliminates 60–80% of\n * model calls — the production win is massive for cost and\n * latency.\n *\n * **Delegation to `@warlock.js/cache`.** This middleware does NOT\n * implement similarity search itself. It delegates to the supplied\n * `CacheDriver`. Production deployments pick a driver with an ANN\n * index (`pg` + pgvector, `redis` + RediSearch). Dev / test\n * environments pass `new MemoryCacheDriver()` — zero config, correct,\n * but O(N) per query. Drivers without similarity support throw\n * `CacheUnsupportedError` from `set({ vector })` / `similar()`.\n *\n * **Two-tier lookup.**\n * 1. *Exact-match key* — a cheap FNV hash over the entire message\n * list. `store.get(hash)` returns the entry without an embedding\n * round trip when the prompt hasn't changed at all.\n * 2. *Vector-match* — on exact-match miss, embed the prompt and\n * call `store.similar(vector, { topK: 1, threshold })`. The\n * driver uses its native similarity index; anything clearing\n * `threshold` is returned as a hit.\n *\n * **Write-on-miss.** When both tiers miss, `trip.before` stashes\n * the prompt hash + vector in `ctx.state`; `trip.after` reads back\n * the pending entry and calls\n * `store.set(hash, entry, { vector })`. If an outer middleware\n * (guardrail) throws in `trip.after` before the cache's `trip.after`\n * runs, the pending entry is never written — bad responses stay out\n * of the cache **as long as the canonical install order is followed**\n * (cache outermost).\n *\n * **Synthetic-response on hit.** Returns a `ModelResponse` with\n * `usage: { input: 0, output: 0, total: 0 }` so budget /\n * observability correctly exclude the saved trip.\n *\n * **Per-session scoping (4.15.0).** One `semanticCache` instance\n * normally serves every end user, and a hit is returned as the answer\n * with no model call in between — so entries are keyed by the run's\n * `sessionId` (`scope`, default `\"session\"`) and a lookup only ever\n * sees entries written under the same key. Runs made without a\n * `sessionId` share one unscoped pool; pass `sessionId` on\n * `agent.execute()` (composites thread their own through automatically)\n * to get the isolation, or set `scope: \"shared\"` to pool deliberately.\n * Note the cost/benefit shift: scoping trades cross-user hit rate for\n * isolation, so public-FAQ deployments where no response can carry a\n * caller's private context should opt into `\"shared\"` explicitly.\n *\n * @example\n * import { semanticCache } from \"@warlock.js/ai\";\n * import { MemoryCacheDriver } from \"@warlock.js/cache\";\n *\n * const store = new MemoryCacheDriver();\n * store.setOptions({});\n *\n * const cache = semanticCache({\n * embedder: openai.embedder({ name: \"text-embedding-3-small\" }),\n * store,\n * threshold: 0.95,\n * ttlMs: 60 * 60 * 1000,\n * });\n *\n * const myAgent = agent({ model, middleware: [cache] });\n */\nexport function semanticCache(options: SemanticCacheOptions): AgentMiddleware {\n const name = options.name ?? \"semantic-cache\";\n const namespace = options.namespace ?? DEFAULT_NAMESPACE;\n const scopeMode: SemanticCacheScope = options.scope ?? \"session\";\n const pendingKey = `${name}.pending`;\n\n // Resolve the effective store at factory time, not per-call. Every\n // subsequent hook closes over `store` so the resolution happens once.\n // Throws now (loud, at construction) instead of later during the\n // first trip (silent until the agent actually runs).\n const store = options.store ?? resolveDefaultStore();\n\n if (!store) {\n throw new Error(\n `semanticCache: no store supplied — pass \\`store\\` in options or call \\`ai.config({ defaultStore })\\` at app boot before constructing the middleware`,\n );\n }\n\n // Cache's parseKey replaces \":\" with \".\" so the namespace boundary\n // matches what `similar()` actually returns in `hit.key`. Using a\n // dot here keeps prefix checks aligned with stored keys.\n //\n // A scoped entry gets an extra hashed segment, so two sessions asking\n // the identical question stay two entries instead of overwriting each\n // other; the scope is hashed because a `sessionId` is caller-supplied\n // and may contain the key delimiter. The unscoped key shape is\n // unchanged, so pre-4.15.0 entries still resolve. The hash is a\n // write-separation device only — a read is authorized by the exact\n // `entry.scope` equality check below, so even a hash collision cannot\n // widen what a session can read.\n const keyFor = (hash: string, scope: string | undefined): string =>\n scope === undefined\n ? `${namespace}.${hash}`\n : `${namespace}.${fnv1a(scope)}.${hash}`;\n\n return {\n name,\n log: true,\n trip: {\n async before(context) {\n // Only cache the first trip's response. Subsequent trips\n // happen because the previous trip requested tool calls — the\n // message list now carries tool results the original prompt\n // never saw, so a semantic match on the unchanged user text\n // would serve back the prior `tool_calls` response and loop\n // the agent forever. The first turn is also the only one\n // where a \"same question → same final answer\" caching story\n // is sound.\n if (context.tripIndex !== 0) {\n return;\n }\n\n const promptText = extractUserText(context.messages);\n\n if (!promptText) {\n return;\n }\n\n const scope = resolveScope(scopeMode, context);\n const promptKey = hashPrompt(context.messages);\n\n const exact = await store.get<CachedEntry>(keyFor(promptKey, scope));\n\n // The key already carries the scope; re-checking the stored\n // `scope` is the actual authorization step, so a key collision\n // or a hand-written entry can't serve across the boundary.\n if (exact && exact.scope === scope && isFresh(exact, options.ttlMs)) {\n return toSyntheticResponse(exact.response);\n }\n\n const query = await options.embedder.embed(promptText);\n\n const hits = await store.similar<CachedEntry>(query.vector, {\n topK: scope === undefined ? 1 : SIMILAR_OVERSCAN,\n threshold: options.threshold,\n });\n\n // Only entries written inside this cache's namespace AND this\n // caller's scope are eligible. A shared driver would otherwise\n // leak a foreign namespace's entries; a shared namespace would\n // leak another session's answer to this one.\n const hit = hits.find(\n (candidate) =>\n candidate.key.startsWith(`${namespace}.`) &&\n candidate.value?.scope === scope &&\n isFresh(candidate.value, options.ttlMs),\n );\n\n if (hit) {\n return toSyntheticResponse(hit.value.response);\n }\n\n const pending: PendingWrite = {\n promptKey,\n vector: query.vector,\n scope,\n };\n context.state.set(pendingKey, pending);\n\n return;\n },\n async after(context, response) {\n const pending = context.state.get(pendingKey) as PendingWrite | undefined;\n\n if (!pending) {\n return;\n }\n\n // Mid-stream tool-call responses must not be cached — the\n // useful answer comes from the trip *after* the tool returns.\n // Crucially, leave the pending entry in place so a later trip\n // (the one that actually finishes with `stop`) can read it\n // and write the final response under the *original* trip-0\n // prompt key. Deleting here would orphan the pending and the\n // post-tool answer would never make it into the store.\n if (response.finishReason === \"tool_calls\") {\n return;\n }\n\n context.state.delete(pendingKey);\n\n const entry: CachedEntry = {\n response,\n storedAt: Date.now(),\n scope: pending.scope,\n };\n\n await store.set(keyFor(pending.promptKey, pending.scope), entry, {\n vector: pending.vector,\n });\n\n return;\n },\n },\n };\n}\n\nfunction toSyntheticResponse(response: ModelResponse): ModelResponse {\n return {\n content: response.content,\n finishReason: response.finishReason,\n usage: { input: 0, output: 0, total: 0 },\n toolCalls: response.toolCalls,\n };\n}\n"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"compose.d.mts","names":[],"sources":["../../../../../../../../ai/src/middleware/helpers/compose.ts"],"mappings":";;;;;AAsCA;;;;;;;;;;;;;;;;;AAEkB;;;;;;;;;;;;;;;;iBAFF,iBAAA,CAAA,GACX,OAAA,EAAS,aAAA,CAAc,eAAA,GAAkB,aAAA,CAAc,eAAA,KACzD,eAAA"}
1
+ {"version":3,"file":"compose.d.mts","names":[],"sources":["../../../../../../../../ai/src/middleware/helpers/compose.ts"],"mappings":";;;;;AAsCA;;;;;;;;;;;;;;;;;AAEkB;;;;;;;;;;;;;;;;iBAFF,iBAAA,IACX,OAAA,EAAS,aAAA,CAAc,eAAA,GAAkB,aAAA,CAAc,eAAA,KACzD,eAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"for-tool.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/helpers/for-tool.ts"],"sourcesContent":["import type { AgentMiddleware } from \"../../contracts/middleware\";\n\n/**\n * Scope a middleware's `tool`-level hooks to only fire for a\n * specific tool name (or a set of names). `execute` and `trip`\n * hooks pass through unchanged.\n *\n * **Role.** Tool-specific concerns — \"rate-limit `search_web`\",\n * \"cache results for `fetch_faq`\" — are common. Rather than adding\n * a `middleware` field to `ai.tool()` (see decisions §27), the\n * framework keeps one contract (`AgentMiddleware`) and offers this\n * helper for the locality problem. The middleware body stays agnostic\n * of the tool name; `forTool` handles the filtering.\n *\n * **What gets filtered.** Only `tool.before` / `tool.after` /\n * `tool.onError`. Each hook is wrapped so that `ctx.tool.name`\n * must be in the allowed set or the wrapped hook is a no-op.\n * `execute` and `trip` hooks are NOT touched — they run normally.\n *\n * **Why not filter execute/trip too?** Because a middleware that\n * reaches across levels (a tool-specific budget that initializes a\n * counter in `execute.before` and checks it in `tool.before`) still\n * needs `execute.before` to fire unconditionally. Scoping all hooks\n * would break cross-level middleware; scoping only `tool` hooks\n * matches the mental model of \"this middleware cares about these\n * tools.\"\n *\n * **Single-name vs multi-name.** A string matches one tool; a string\n * array matches any of the listed tools. No wildcards, no regex —\n * keep it boring.\n *\n * @example\n * // Single tool\n * const scoped = ai.middleware.forTool(\n * \"search_web\",\n * toolRateLimit({ maxCalls: 3 }),\n * );\n *\n * @example\n * // Multiple tools sharing a rule\n * const scoped = ai.middleware.forTool(\n * [\"paid_api\", \"expensive_db\"],\n * toolRateLimit({ maxCalls: 5 }),\n * );\n *\n * ai.agent({\n * model,\n * tools: [webTool, paidApiTool, expensiveDbTool],\n * middleware: [scoped],\n * });\n */\nexport function forTool(\n toolNames: string | ReadonlyArray<string>,\n middleware: AgentMiddleware,\n): AgentMiddleware {\n const allowed = new Set(\n typeof toolNames === \"string\" ? [toolNames] : toolNames,\n );\n const scope =\n allowed.size === 1 ? Array.from(allowed)[0] : Array.from(allowed).join(\"+\");\n\n if (!middleware.tool) {\n return middleware;\n }\n\n const innerBefore = middleware.tool.before;\n const innerAfter = middleware.tool.after;\n const innerOnError = middleware.tool.onError;\n\n return {\n ...middleware,\n name: `${middleware.name}[for:${scope}]`,\n tool: {\n before: innerBefore\n ? async ctx => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerBefore(ctx);\n }\n : undefined,\n after: innerAfter\n ? async (ctx, result) => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerAfter(ctx, result);\n }\n : undefined,\n onError: innerOnError\n ? async (ctx, error) => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerOnError(ctx, error);\n }\n : undefined,\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,QACd,WACA,YACiB;CACjB,MAAM,UAAU,IAAI,IAClB,OAAO,cAAc,WAAW,CAAC,SAAS,IAAI,SAChD;CACA,MAAM,QACJ,QAAQ,SAAS,IAAI,MAAM,KAAK,OAAO,EAAE,KAAK,MAAM,KAAK,OAAO,EAAE,KAAK,GAAG;CAE5E,IAAI,CAAC,WAAW,MACd,OAAO;CAGT,MAAM,cAAc,WAAW,KAAK;CACpC,MAAM,aAAa,WAAW,KAAK;CACnC,MAAM,eAAe,WAAW,KAAK;CAErC,OAAO;EACL,GAAG;EACH,MAAM,GAAG,WAAW,KAAK,OAAO,MAAM;EACtC,MAAM;GACJ,QAAQ,cACJ,OAAM,QAAO;IACX,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,YAAY,GAAG;GACxB,IACA;GACJ,OAAO,aACH,OAAO,KAAK,WAAW;IACrB,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,WAAW,KAAK,MAAM;GAC/B,IACA;GACJ,SAAS,eACL,OAAO,KAAK,UAAU;IACpB,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,aAAa,KAAK,KAAK;GAChC,IACA;EACN;CACF;AACF"}
1
+ {"version":3,"file":"for-tool.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/helpers/for-tool.ts"],"sourcesContent":["import type { AgentMiddleware } from \"../../contracts/middleware\";\n\n/**\n * Scope a middleware's `tool`-level hooks to only fire for a\n * specific tool name (or a set of names). `execute` and `trip`\n * hooks pass through unchanged.\n *\n * **Role.** Tool-specific concerns — \"rate-limit `search_web`\",\n * \"cache results for `fetch_faq`\" — are common. Rather than adding\n * a `middleware` field to `ai.tool()` (see decisions §27), the\n * framework keeps one contract (`AgentMiddleware`) and offers this\n * helper for the locality problem. The middleware body stays agnostic\n * of the tool name; `forTool` handles the filtering.\n *\n * **What gets filtered.** Only `tool.before` / `tool.after` /\n * `tool.onError`. Each hook is wrapped so that `ctx.tool.name`\n * must be in the allowed set or the wrapped hook is a no-op.\n * `execute` and `trip` hooks are NOT touched — they run normally.\n *\n * **Why not filter execute/trip too?** Because a middleware that\n * reaches across levels (a tool-specific budget that initializes a\n * counter in `execute.before` and checks it in `tool.before`) still\n * needs `execute.before` to fire unconditionally. Scoping all hooks\n * would break cross-level middleware; scoping only `tool` hooks\n * matches the mental model of \"this middleware cares about these\n * tools.\"\n *\n * **Single-name vs multi-name.** A string matches one tool; a string\n * array matches any of the listed tools. No wildcards, no regex —\n * keep it boring.\n *\n * @example\n * // Single tool\n * const scoped = ai.middleware.forTool(\n * \"search_web\",\n * toolRateLimit({ maxCalls: 3 }),\n * );\n *\n * @example\n * // Multiple tools sharing a rule\n * const scoped = ai.middleware.forTool(\n * [\"paid_api\", \"expensive_db\"],\n * toolRateLimit({ maxCalls: 5 }),\n * );\n *\n * ai.agent({\n * model,\n * tools: [webTool, paidApiTool, expensiveDbTool],\n * middleware: [scoped],\n * });\n */\nexport function forTool(\n toolNames: string | ReadonlyArray<string>,\n middleware: AgentMiddleware,\n): AgentMiddleware {\n const allowed = new Set(\n typeof toolNames === \"string\" ? [toolNames] : toolNames,\n );\n const scope =\n allowed.size === 1 ? Array.from(allowed)[0] : Array.from(allowed).join(\"+\");\n\n if (!middleware.tool) {\n return middleware;\n }\n\n const innerBefore = middleware.tool.before;\n const innerAfter = middleware.tool.after;\n const innerOnError = middleware.tool.onError;\n\n return {\n ...middleware,\n name: `${middleware.name}[for:${scope}]`,\n tool: {\n before: innerBefore\n ? async ctx => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerBefore(ctx);\n }\n : undefined,\n after: innerAfter\n ? async (ctx, result) => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerAfter(ctx, result);\n }\n : undefined,\n onError: innerOnError\n ? async (ctx, error) => {\n if (!allowed.has(ctx.tool.name)) {\n return;\n }\n\n return innerOnError(ctx, error);\n }\n : undefined,\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,QACd,WACA,YACiB;CACjB,MAAM,UAAU,IAAI,IAClB,OAAO,cAAc,WAAW,CAAC,SAAS,IAAI,SAChD;CACA,MAAM,QACJ,QAAQ,SAAS,IAAI,MAAM,KAAK,OAAO,CAAC,CAAC,KAAK,MAAM,KAAK,OAAO,CAAC,CAAC,KAAK,GAAG;CAE5E,IAAI,CAAC,WAAW,MACd,OAAO;CAGT,MAAM,cAAc,WAAW,KAAK;CACpC,MAAM,aAAa,WAAW,KAAK;CACnC,MAAM,eAAe,WAAW,KAAK;CAErC,OAAO;EACL,GAAG;EACH,MAAM,GAAG,WAAW,KAAK,OAAO,MAAM;EACtC,MAAM;GACJ,QAAQ,cACJ,OAAM,QAAO;IACX,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,YAAY,GAAG;GACxB,IACA;GACJ,OAAO,aACH,OAAO,KAAK,WAAW;IACrB,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,WAAW,KAAK,MAAM;GAC/B,IACA;GACJ,SAAS,eACL,OAAO,KAAK,UAAU;IACpB,IAAI,CAAC,QAAQ,IAAI,IAAI,KAAK,IAAI,GAC5B;IAGF,OAAO,aAAa,KAAK,KAAK;GAChC,IACA;EACN;CACF;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"pipeline.d.mts","names":[],"sources":["../../../../../../../ai/src/middleware/pipeline.ts"],"mappings":";;;;;;;;;AAmBA;;;;KAAY,eAAA;AAOZ;;;;;AAAA,KAAY,wBAAA;EACV,OAAA,EAAS,wBAAA;EACT,IAAA,EAAM,qBAAA;EACN,IAAA,EAAM,qBAAA;EACN,UAAA,EAAY,2BAAA;AAAA;;;;;;;;AAA2B;AA+CzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMkB;iBANI,WAAA,eAA0B,eAAA,UAAA,CAC9C,WAAA,EAAa,aAAA,CAAc,eAAA,GAC3B,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,wBAAA,CAAyB,KAAA,GAClC,KAAA,QAAa,OAAA,CAAQ,OAAA,GACrB,MAAA,GAAS,MAAA,GACR,OAAA,CAAQ,OAAA"}
1
+ {"version":3,"file":"pipeline.d.mts","names":[],"sources":["../../../../../../../ai/src/middleware/pipeline.ts"],"mappings":";;;;;;;;;AAmBA;;;;KAAY,eAAA;AAOZ;;;;;AAAA,KAAY,wBAAA;EACV,OAAA,EAAS,wBAAA;EACT,IAAA,EAAM,qBAAA;EACN,IAAA,EAAM,qBAAA;EACN,UAAA,EAAY,2BAAA;AAAA;;;;;;;;AAA2B;AA+CzC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAMkB;iBANI,WAAA,eAA0B,eAAA,WAC9C,WAAA,EAAa,aAAA,CAAc,eAAA,GAC3B,KAAA,EAAO,KAAA,EACP,OAAA,EAAS,wBAAA,CAAyB,KAAA,GAClC,KAAA,QAAa,OAAA,CAAQ,OAAA,GACrB,MAAA,GAAS,MAAA,GACR,OAAA,CAAQ,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"extract-user-text.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/utils/extract-user-text.ts"],"sourcesContent":["import type { Message } from \"../../contracts/conversation-message.type\";\n\n/**\n * Pull the text a content-inspection middleware should care about\n * from the outbound message list.\n *\n * **Role.** Built-ins that inspect \"what the user just said\" — the\n * guardrail on `trip.before`, the semantic cache on `trip.before`,\n * future consumers like PII redactors — all need the same string:\n * the most recent `user`-role message's text content. This helper\n * is the single authority on how that string is resolved.\n *\n * **Behavior.**\n * - Walks `messages` from the end backwards so the LAST user turn\n * wins (correct when the agent has history + a fresh prompt).\n * - Returns a plain string directly when `content` is a string.\n * - Joins `text` parts with `\"\\n\"` when `content` is a multipart\n * `ContentPart[]`. Non-text parts (images, audio, pdf) are skipped —\n * callers concerned with multimodal content inspect `request`\n * / attachments separately.\n * - Returns `\"\"` when there is no user message at all (e.g. a trip\n * composed entirely of tool results).\n *\n * **Coverage limit (D3).** Because only `text` parts are extracted, any\n * guardrail / PII detector built on this helper inspects **text only** —\n * image / PDF / audio attachment content is NOT scanned. A guardrail is\n * therefore not a multimodal safety control: for non-text inputs add an\n * attachment-level policy (e.g. an OCR / moderation pass before the call)\n * rather than relying on input detectors.\n *\n * @example\n * const prompt = extractUserText(context.messages);\n * if (!prompt) return;\n * const verdict = await inputCheck(prompt);\n */\nexport function extractUserText(messages: ReadonlyArray<Message>): string {\n for (let index = messages.length - 1; index >= 0; index--) {\n const message = messages[index];\n\n if (message.role !== \"user\") {\n continue;\n }\n\n if (typeof message.content === \"string\") {\n return message.content;\n }\n\n if (Array.isArray(message.content)) {\n return message.content\n .filter((part) => part.type === \"text\")\n .map((part) => (part as { text: string }).text)\n .join(\"\\n\");\n }\n }\n\n return \"\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,gBAAgB,UAA0C;CACxE,KAAK,IAAI,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG,SAAS;EACzD,MAAM,UAAU,SAAS;EAEzB,IAAI,QAAQ,SAAS,QACnB;EAGF,IAAI,OAAO,QAAQ,YAAY,UAC7B,OAAO,QAAQ;EAGjB,IAAI,MAAM,QAAQ,QAAQ,OAAO,GAC/B,OAAO,QAAQ,QACZ,QAAQ,SAAS,KAAK,SAAS,MAAM,EACrC,KAAK,SAAU,KAA0B,IAAI,EAC7C,KAAK,IAAI;CAEhB;CAEA,OAAO;AACT"}
1
+ {"version":3,"file":"extract-user-text.mjs","names":[],"sources":["../../../../../../../../ai/src/middleware/utils/extract-user-text.ts"],"sourcesContent":["import type { Message } from \"../../contracts/conversation-message.type\";\n\n/**\n * Pull the text a content-inspection middleware should care about\n * from the outbound message list.\n *\n * **Role.** Built-ins that inspect \"what the user just said\" — the\n * guardrail on `trip.before`, the semantic cache on `trip.before`,\n * future consumers like PII redactors — all need the same string:\n * the most recent `user`-role message's text content. This helper\n * is the single authority on how that string is resolved.\n *\n * **Behavior.**\n * - Walks `messages` from the end backwards so the LAST user turn\n * wins (correct when the agent has history + a fresh prompt).\n * - Returns a plain string directly when `content` is a string.\n * - Joins `text` parts with `\"\\n\"` when `content` is a multipart\n * `ContentPart[]`. Non-text parts (images, audio, pdf) are skipped —\n * callers concerned with multimodal content inspect `request`\n * / attachments separately.\n * - Returns `\"\"` when there is no user message at all (e.g. a trip\n * composed entirely of tool results).\n *\n * **Coverage limit (D3).** Because only `text` parts are extracted, any\n * guardrail / PII detector built on this helper inspects **text only** —\n * image / PDF / audio attachment content is NOT scanned. A guardrail is\n * therefore not a multimodal safety control: for non-text inputs add an\n * attachment-level policy (e.g. an OCR / moderation pass before the call)\n * rather than relying on input detectors.\n *\n * @example\n * const prompt = extractUserText(context.messages);\n * if (!prompt) return;\n * const verdict = await inputCheck(prompt);\n */\nexport function extractUserText(messages: ReadonlyArray<Message>): string {\n for (let index = messages.length - 1; index >= 0; index--) {\n const message = messages[index];\n\n if (message.role !== \"user\") {\n continue;\n }\n\n if (typeof message.content === \"string\") {\n return message.content;\n }\n\n if (Array.isArray(message.content)) {\n return message.content\n .filter((part) => part.type === \"text\")\n .map((part) => (part as { text: string }).text)\n .join(\"\\n\");\n }\n }\n\n return \"\";\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmCA,SAAgB,gBAAgB,UAA0C;CACxE,KAAK,IAAI,QAAQ,SAAS,SAAS,GAAG,SAAS,GAAG,SAAS;EACzD,MAAM,UAAU,SAAS;EAEzB,IAAI,QAAQ,SAAS,QACnB;EAGF,IAAI,OAAO,QAAQ,YAAY,UAC7B,OAAO,QAAQ;EAGjB,IAAI,MAAM,QAAQ,QAAQ,OAAO,GAC/B,OAAO,QAAQ,QACZ,QAAQ,SAAS,KAAK,SAAS,MAAM,CAAC,CACtC,KAAK,SAAU,KAA0B,IAAI,CAAC,CAC9C,KAAK,IAAI;CAEhB;CAEA,OAAO;AACT"}
@@ -1 +1 @@
1
- {"version":3,"file":"namespaced-state.d.mts","names":[],"sources":["../../../../../../../../ai/src/middleware/utils/namespaced-state.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;;;AAG4B;AAwB5B;;;;;;;;;;;;;AAIK;;;;;;iBA/BW,eAAA,GAAA,CACd,GAAA;EAAA,SAAgB,KAAA,EAAO,eAAA;AAAA,GACvB,SAAA,WACC,uBAAA,CAAwB,CAAA;;;;;;;;KAwBf,uBAAA;EACV,GAAA,IAAO,CAAA;EACP,GAAA,CAAI,KAAA,EAAO,CAAC;EACZ,MAAA;EACA,GAAA;AAAA"}
1
+ {"version":3,"file":"namespaced-state.d.mts","names":[],"sources":["../../../../../../../../ai/src/middleware/utils/namespaced-state.ts"],"mappings":";;;;;AAyCA;;;;;;;;;;;;;;;;AAG4B;AAwB5B;;;;;;;;;;;;;AAIK;;;;;;iBA/BW,eAAA,IACd,GAAA;EAAA,SAAgB,KAAA,EAAO,eAAA;AAAA,GACvB,SAAA,WACC,uBAAA,CAAwB,CAAA;;;;;;;;KAwBf,uBAAA;EACV,GAAA,IAAO,CAAA;EACP,GAAA,CAAI,KAAA,EAAO,CAAC;EACZ,MAAA;EACA,GAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock-agent.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-agent.ts"],"mappings":";;;;;;;AA6BA;;;;;;;;;;;;;;;;;;;;iBAAgB,SAAA,mBAAA,CACd,OAAA;EACE,IAAA;EACA,SAAA,GAAY,iBAAA;EACZ,KAAA,GAAQ,YAAA;;;;;;EAMR,SAAA;AAAA,IAED,aAAA,CAAc,OAAA"}
1
+ {"version":3,"file":"mock-agent.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-agent.ts"],"mappings":";;;;;;;AA6BA;;;;;;;;;;;;;;;;;;;;iBAAgB,SAAA,oBACd,OAAA;EACE,IAAA;EACA,SAAA,GAAY,iBAAA;EACZ,KAAA,GAAQ,YAAA;;;;;;EAMR,SAAA;AAAA,IAED,aAAA,CAAc,OAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock-agent.mjs","names":[],"sources":["../../../../../../../ai/src/mock/mock-agent.ts"],"sourcesContent":["import { agent } from \"../agent/agent\";\nimport type { AgentConfig } from \"../agent/agent-config.type\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { ToolContract } from \"../tool/tool\";\nimport type { MockModelResponse } from \"./mock-config.type\";\nimport { MockSDK } from \"./mock-sdk\";\n\n/**\n * Test helper that wires `MockSDK` → mock model → `agent()` in one call.\n *\n * Replaces the 4-line ritual specs typically write:\n * ```ts\n * const mock = MockSDK({ responses: [...] });\n * const myAgent = agent({ name: \"x\", model: mock.model({ name: \"m\" }) });\n * ```\n *\n * Defaults to a single empty `\"stop\"` response — enough to exercise an\n * agent that doesn't need a scripted reply (composition wiring,\n * anonymous-name tests, etc.). Pass `responses` to script outputs.\n *\n * `name` is optional — when omitted the resulting agent runs through\n * the normal anonymous-name fingerprint\n * (`anon_<provider>_<model>[_<tool1>+<tool2>...]`), which is the\n * default for `agent({ model })`.\n *\n * @example\n * const a = mockAgent({ name: \"writer\", responses: [{ content: \"hi\", finishReason: \"stop\" }] });\n * const result = await a.execute(\"anything\");\n */\nexport function mockAgent<TOutput = unknown>(\n options: {\n name?: string;\n responses?: MockModelResponse[];\n tools?: ToolContract<unknown, unknown>[];\n /**\n * Override the model name reported by the mock model. Defaults to\n * `\"mock-model\"` (the MockSDK default). Useful when a test needs\n * deterministic provider/model fingerprinting.\n */\n modelName?: string;\n } = {},\n): AgentContract<TOutput> {\n const responses = options.responses ?? [{ content: \"\", finishReason: \"stop\" as const }];\n const sdk = MockSDK({ responses });\n const model = sdk.model({ name: options.modelName ?? \"mock-model\" });\n\n const config: AgentConfig<TOutput> = { model };\n\n if (options.name !== undefined) {\n config.name = options.name;\n }\n\n if (options.tools !== undefined) {\n config.tools = options.tools;\n }\n\n return agent<TOutput>(config);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,UACd,UAUI,CAAC,GACmB;CAKxB,MAAM,SAA+B,EAAE,OAH3B,QAAQ,EAAE,WADJ,QAAQ,aAAa,CAAC;EAAE,SAAS;EAAI,cAAc;CAAgB,CAAC,EACtD,CAChB,EAAE,MAAM,EAAE,MAAM,QAAQ,aAAa,aAAa,CAEvB,EAAE;CAE7C,IAAI,QAAQ,SAAS,QACnB,OAAO,OAAO,QAAQ;CAGxB,IAAI,QAAQ,UAAU,QACpB,OAAO,QAAQ,QAAQ;CAGzB,OAAO,MAAe,MAAM;AAC9B"}
1
+ {"version":3,"file":"mock-agent.mjs","names":[],"sources":["../../../../../../../ai/src/mock/mock-agent.ts"],"sourcesContent":["import { agent } from \"../agent/agent\";\nimport type { AgentConfig } from \"../agent/agent-config.type\";\nimport type { AgentContract } from \"../contracts/agent/agent.contract\";\nimport type { ToolContract } from \"../tool/tool\";\nimport type { MockModelResponse } from \"./mock-config.type\";\nimport { MockSDK } from \"./mock-sdk\";\n\n/**\n * Test helper that wires `MockSDK` → mock model → `agent()` in one call.\n *\n * Replaces the 4-line ritual specs typically write:\n * ```ts\n * const mock = MockSDK({ responses: [...] });\n * const myAgent = agent({ name: \"x\", model: mock.model({ name: \"m\" }) });\n * ```\n *\n * Defaults to a single empty `\"stop\"` response — enough to exercise an\n * agent that doesn't need a scripted reply (composition wiring,\n * anonymous-name tests, etc.). Pass `responses` to script outputs.\n *\n * `name` is optional — when omitted the resulting agent runs through\n * the normal anonymous-name fingerprint\n * (`anon_<provider>_<model>[_<tool1>+<tool2>...]`), which is the\n * default for `agent({ model })`.\n *\n * @example\n * const a = mockAgent({ name: \"writer\", responses: [{ content: \"hi\", finishReason: \"stop\" }] });\n * const result = await a.execute(\"anything\");\n */\nexport function mockAgent<TOutput = unknown>(\n options: {\n name?: string;\n responses?: MockModelResponse[];\n tools?: ToolContract<unknown, unknown>[];\n /**\n * Override the model name reported by the mock model. Defaults to\n * `\"mock-model\"` (the MockSDK default). Useful when a test needs\n * deterministic provider/model fingerprinting.\n */\n modelName?: string;\n } = {},\n): AgentContract<TOutput> {\n const responses = options.responses ?? [{ content: \"\", finishReason: \"stop\" as const }];\n const sdk = MockSDK({ responses });\n const model = sdk.model({ name: options.modelName ?? \"mock-model\" });\n\n const config: AgentConfig<TOutput> = { model };\n\n if (options.name !== undefined) {\n config.name = options.name;\n }\n\n if (options.tools !== undefined) {\n config.tools = options.tools;\n }\n\n return agent<TOutput>(config);\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,UACd,UAUI,CAAC,GACmB;CAKxB,MAAM,SAA+B,EAAE,OAH3B,QAAQ,EAAE,WADJ,QAAQ,aAAa,CAAC;EAAE,SAAS;EAAI,cAAc;CAAgB,CAAC,EACtD,CAChB,CAAC,CAAC,MAAM,EAAE,MAAM,QAAQ,aAAa,aAAa,CAEvB,EAAE;CAE7C,IAAI,QAAQ,SAAS,QACnB,OAAO,OAAO,QAAQ;CAGxB,IAAI,QAAQ,UAAU,QACpB,OAAO,QAAQ,QAAQ;CAGzB,OAAO,MAAe,MAAM;AAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock-model.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-model.ts"],"mappings":";;;;;KAUK,YAAA;EACH,QAAA,EAAU,OAAA;EACV,OAAA,GAAU,gBAAgB;AAAA;;;;;;;;AAAA;AA+B5B;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,aAAA;EAAA,SAQd,IAAA;EAAA,iBACC,SAAA;EAAA,SARH,QAAA;EAAA,SACA,YAAA,GAAe,iBAAA;EAAA,QAEvB,aAAA;EAAA,QACA,KAAA;cAGU,IAAA,UACC,SAAA,EAAW,iBAAA,IAC5B,YAAA,GAAe,iBAAA;EADa;;;;;EAAA,IAWnB,WAAA,CAAA,GAAe,YAAA;EAQf;;;;EAAA,IAAA,SAAA,CAAA;EA8CW;;;;;;EAAA,QApCd,YAAA;EA6DN;;;;;EAAA,QAhDM,aAAA;EA8FI;AAAA;;;;;EAvEC,QAAA,CAAS,QAAA,EAAU,OAAA,IAAW,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,aAAA;;;;;;;;;EAwBlE,MAAA,CACZ,QAAA,EAAU,OAAA,IACV,OAAA,GAAU,gBAAA,GACT,aAAA,CAAc,gBAAA;;;;;;EA4CV,KAAA,CAAA;AAAA"}
1
+ {"version":3,"file":"mock-model.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-model.ts"],"mappings":";;;;;KAUK,YAAA;EACH,QAAA,EAAU,OAAA;EACV,OAAA,GAAU,gBAAgB;AAAA;;;;;;;;AAAA;AA+B5B;;;;;;;;;;;;;;;;;;;;cAAa,SAAA,YAAqB,aAAA;EAAA,SAQd,IAAA;EAAA,iBACC,SAAA;EAAA,SARH,QAAA;EAAA,SACA,YAAA,GAAe,iBAAA;EAAA,QAEvB,aAAA;EAAA,QACA,KAAA;cAGU,IAAA,UACC,SAAA,EAAW,iBAAA,IAC5B,YAAA,GAAe,iBAAA;EADa;;;;;EAAA,IAWnB,WAAA,IAAe,YAAA;EAQf;;;;EAAA,IAAA,SAAA;EA8CW;;;;;;EAAA,QApCd,YAAA;EA6DN;;;;;EAAA,QAhDM,aAAA;EA8FI;AAAA;;;;;EAvEC,QAAA,CAAS,QAAA,EAAU,OAAA,IAAW,OAAA,GAAU,gBAAA,GAAmB,OAAA,CAAQ,aAAA;;;;;;;;;EAwBlE,MAAA,CACZ,QAAA,EAAU,OAAA,IACV,OAAA,GAAU,gBAAA,GACT,aAAA,CAAc,gBAAA;;;;;;EA4CV,KAAA;AAAA"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock-model.mjs","names":[],"sources":["../../../../../../../ai/src/mock/mock-model.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n ModelCallOptions,\n ModelCapabilities,\n ModelContract,\n ModelResponse,\n ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { MockModelResponse } from \"./mock-config.type\";\n\ntype RecordedCall = {\n messages: Message[];\n options?: ModelCallOptions;\n};\n\n/**\n * Deterministic in-memory `ModelContract` implementation for tests.\n *\n * **Role.** Stands in for a real provider model so agent/workflow/supervisor\n * tests can assert behavior without hitting the network, spending tokens, or\n * depending on non-deterministic LLM output.\n *\n * **Responsibility.**\n * - Owns: a scripted queue of `MockModelResponse` entries, a call-history\n * log for assertions, and the index pointer that advances through the\n * queue on each `complete()` / `stream()` call.\n * - Does NOT own: any real inference, tokenization, or network I/O — when\n * the queue is exhausted, the final entry is reused so tests never crash\n * on accidental over-consumption.\n *\n * Every AI-related test in this repo uses `MockSDK` / `MockModel` — real\n * provider APIs are never hit from the test suite (see §6 of code-style.md).\n *\n * @example\n * const model = new MockModel(\"mock-gpt\", [\n * { content: \"Hello!\", finishReason: \"stop\" },\n * { content: \"Second turn.\", finishReason: \"stop\" },\n * ]);\n *\n * const first = await model.complete([{ role: \"user\", content: \"hi\" }]);\n * expect(first.content).toBe(\"Hello!\");\n * expect(model.callCount).toBe(1);\n */\nexport class MockModel implements ModelContract {\n public readonly provider = \"mock\";\n public readonly capabilities?: ModelCapabilities;\n\n private responseIndex = 0;\n private calls: RecordedCall[] = [];\n\n public constructor(\n public readonly name: string,\n private readonly responses: MockModelResponse[],\n capabilities?: ModelCapabilities,\n ) {\n this.capabilities = capabilities;\n }\n\n /**\n * Full history of calls made to this model. Each entry is the exact\n * `{ messages, options }` pair that was passed — useful for asserting\n * that an agent built the right prompt or forwarded the right tool list.\n */\n public get callHistory(): RecordedCall[] {\n return this.calls;\n }\n\n /**\n * Number of times `complete()` or `stream()` has been invoked. Convenient\n * shorthand for `callHistory.length` in assertions.\n */\n public get callCount(): number {\n return this.calls.length;\n }\n\n /**\n * Advance the scripted response queue by one and return the entry at the\n * current pointer. If the queue is exhausted, the final scripted entry is\n * returned repeatedly so over-consumption in tests produces predictable\n * output instead of `undefined`.\n */\n private nextResponse(): MockModelResponse {\n const response = this.responses[Math.min(this.responseIndex, this.responses.length - 1)];\n\n this.responseIndex++;\n\n return response ?? { content: \"Mock response\", finishReason: \"stop\" };\n }\n\n /**\n * Convert a scripted `MockModelResponse` into a full `ModelResponse` with\n * synthesized usage numbers when the script didn't supply them. Input\n * usage is a fixed estimate; output usage is derived from content length.\n */\n private buildResponse(mock: MockModelResponse): ModelResponse {\n const estimatedInput = 10;\n const estimatedOutput = Math.ceil(mock.content.length / 4);\n\n return {\n content: mock.content,\n finishReason: mock.finishReason ?? \"stop\",\n usage: {\n input: mock.usage?.input ?? estimatedInput,\n output: mock.usage?.output ?? estimatedOutput,\n total: (mock.usage?.input ?? estimatedInput) + (mock.usage?.output ?? estimatedOutput),\n ...(mock.usage?.cachedTokens !== undefined ? { cachedTokens: mock.usage.cachedTokens } : {}),\n },\n toolCalls: mock.toolCalls,\n };\n }\n\n /**\n * Record the call, optionally delay (to simulate latency), and either\n * throw the scripted error or return the scripted response. Mirrors the\n * real provider's `complete()` contract so agents cannot tell the\n * difference at runtime.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.calls.push({ messages, options });\n\n const mock = this.nextResponse();\n\n if (mock.delay) {\n await new Promise((resolve) => setTimeout(resolve, mock.delay));\n }\n\n if (mock.error) {\n throw mock.error;\n }\n\n return this.buildResponse(mock);\n }\n\n /**\n * Record the call, optionally delay, then emit the scripted response as a\n * sequence of stream chunks: the scripted `deltas` when the entry\n * supplies them, otherwise content split word-by-word, as `delta`\n * chunks, each scripted tool call as a `tool-call` chunk, and finally a\n * `done` chunk with finish reason + usage. Throws eagerly if the scripted\n * entry carries an `error`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.calls.push({ messages, options });\n\n const mock = this.nextResponse();\n\n if (mock.delay) {\n await new Promise((resolve) => setTimeout(resolve, mock.delay));\n }\n\n if (mock.error) {\n throw mock.error;\n }\n\n const chunks = mock.deltas ?? mock.content.split(\" \").map((word) => word + \" \");\n\n for (const chunk of chunks) {\n yield { type: \"delta\", content: chunk };\n }\n\n if (mock.toolCalls) {\n for (const toolCall of mock.toolCalls) {\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n };\n }\n }\n\n const response = this.buildResponse(mock);\n\n yield {\n type: \"done\",\n finishReason: response.finishReason,\n usage: response.usage,\n };\n }\n\n /**\n * Reset call history and response pointer back to their initial state.\n * Intended for test-suite `beforeEach` hooks so a single `MockModel`\n * instance can be reused across cases without cross-test leakage.\n */\n public reset(): void {\n this.calls = [];\n this.responseIndex = 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,YAAb,MAAgD;CAO9C,AAAO,YACL,AAAgB,MAChB,AAAiB,WACjB,cACA;EAHgB;EACC;kBARQ;uBAGH;eACQ,CAAC;EAO/B,KAAK,eAAe;CACtB;;;;;;CAOA,IAAW,cAA8B;EACvC,OAAO,KAAK;CACd;;;;;CAMA,IAAW,YAAoB;EAC7B,OAAO,KAAK,MAAM;CACpB;;;;;;;CAQA,AAAQ,eAAkC;EACxC,MAAM,WAAW,KAAK,UAAU,KAAK,IAAI,KAAK,eAAe,KAAK,UAAU,SAAS,CAAC;EAEtF,KAAK;EAEL,OAAO,YAAY;GAAE,SAAS;GAAiB,cAAc;EAAO;CACtE;;;;;;CAOA,AAAQ,cAAc,MAAwC;EAC5D,MAAM,iBAAiB;EACvB,MAAM,kBAAkB,KAAK,KAAK,KAAK,QAAQ,SAAS,CAAC;EAEzD,OAAO;GACL,SAAS,KAAK;GACd,cAAc,KAAK,gBAAgB;GACnC,OAAO;IACL,OAAO,KAAK,OAAO,SAAS;IAC5B,QAAQ,KAAK,OAAO,UAAU;IAC9B,QAAQ,KAAK,OAAO,SAAS,mBAAmB,KAAK,OAAO,UAAU;IACtE,GAAI,KAAK,OAAO,iBAAiB,SAAY,EAAE,cAAc,KAAK,MAAM,aAAa,IAAI,CAAC;GAC5F;GACA,WAAW,KAAK;EAClB;CACF;;;;;;;CAQA,MAAa,SAAS,UAAqB,SAAoD;EAC7F,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,OAAO,KAAK,cAAc,IAAI;CAChC;;;;;;;;;CAUA,OAAc,OACZ,UACA,SACiC;EACjC,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,MAAM,SAAS,KAAK,UAAU,KAAK,QAAQ,MAAM,GAAG,EAAE,KAAK,SAAS,OAAO,GAAG;EAE9E,KAAK,MAAM,SAAS,QAClB,MAAM;GAAE,MAAM;GAAS,SAAS;EAAM;EAGxC,IAAI,KAAK,WACP,KAAK,MAAM,YAAY,KAAK,WAC1B,MAAM;GACJ,MAAM;GACN,IAAI,SAAS;GACb,MAAM,SAAS;GACf,OAAO,SAAS;EAClB;EAIJ,MAAM,WAAW,KAAK,cAAc,IAAI;EAExC,MAAM;GACJ,MAAM;GACN,cAAc,SAAS;GACvB,OAAO,SAAS;EAClB;CACF;;;;;;CAOA,AAAO,QAAc;EACnB,KAAK,QAAQ,CAAC;EACd,KAAK,gBAAgB;CACvB;AACF"}
1
+ {"version":3,"file":"mock-model.mjs","names":[],"sources":["../../../../../../../ai/src/mock/mock-model.ts"],"sourcesContent":["import type { Message } from \"../contracts/conversation-message.type\";\nimport type {\n ModelCallOptions,\n ModelCapabilities,\n ModelContract,\n ModelResponse,\n ModelStreamChunk,\n} from \"../contracts/model.contract\";\nimport type { MockModelResponse } from \"./mock-config.type\";\n\ntype RecordedCall = {\n messages: Message[];\n options?: ModelCallOptions;\n};\n\n/**\n * Deterministic in-memory `ModelContract` implementation for tests.\n *\n * **Role.** Stands in for a real provider model so agent/workflow/supervisor\n * tests can assert behavior without hitting the network, spending tokens, or\n * depending on non-deterministic LLM output.\n *\n * **Responsibility.**\n * - Owns: a scripted queue of `MockModelResponse` entries, a call-history\n * log for assertions, and the index pointer that advances through the\n * queue on each `complete()` / `stream()` call.\n * - Does NOT own: any real inference, tokenization, or network I/O — when\n * the queue is exhausted, the final entry is reused so tests never crash\n * on accidental over-consumption.\n *\n * Every AI-related test in this repo uses `MockSDK` / `MockModel` — real\n * provider APIs are never hit from the test suite (see §6 of code-style.md).\n *\n * @example\n * const model = new MockModel(\"mock-gpt\", [\n * { content: \"Hello!\", finishReason: \"stop\" },\n * { content: \"Second turn.\", finishReason: \"stop\" },\n * ]);\n *\n * const first = await model.complete([{ role: \"user\", content: \"hi\" }]);\n * expect(first.content).toBe(\"Hello!\");\n * expect(model.callCount).toBe(1);\n */\nexport class MockModel implements ModelContract {\n public readonly provider = \"mock\";\n public readonly capabilities?: ModelCapabilities;\n\n private responseIndex = 0;\n private calls: RecordedCall[] = [];\n\n public constructor(\n public readonly name: string,\n private readonly responses: MockModelResponse[],\n capabilities?: ModelCapabilities,\n ) {\n this.capabilities = capabilities;\n }\n\n /**\n * Full history of calls made to this model. Each entry is the exact\n * `{ messages, options }` pair that was passed — useful for asserting\n * that an agent built the right prompt or forwarded the right tool list.\n */\n public get callHistory(): RecordedCall[] {\n return this.calls;\n }\n\n /**\n * Number of times `complete()` or `stream()` has been invoked. Convenient\n * shorthand for `callHistory.length` in assertions.\n */\n public get callCount(): number {\n return this.calls.length;\n }\n\n /**\n * Advance the scripted response queue by one and return the entry at the\n * current pointer. If the queue is exhausted, the final scripted entry is\n * returned repeatedly so over-consumption in tests produces predictable\n * output instead of `undefined`.\n */\n private nextResponse(): MockModelResponse {\n const response = this.responses[Math.min(this.responseIndex, this.responses.length - 1)];\n\n this.responseIndex++;\n\n return response ?? { content: \"Mock response\", finishReason: \"stop\" };\n }\n\n /**\n * Convert a scripted `MockModelResponse` into a full `ModelResponse` with\n * synthesized usage numbers when the script didn't supply them. Input\n * usage is a fixed estimate; output usage is derived from content length.\n */\n private buildResponse(mock: MockModelResponse): ModelResponse {\n const estimatedInput = 10;\n const estimatedOutput = Math.ceil(mock.content.length / 4);\n\n return {\n content: mock.content,\n finishReason: mock.finishReason ?? \"stop\",\n usage: {\n input: mock.usage?.input ?? estimatedInput,\n output: mock.usage?.output ?? estimatedOutput,\n total: (mock.usage?.input ?? estimatedInput) + (mock.usage?.output ?? estimatedOutput),\n ...(mock.usage?.cachedTokens !== undefined ? { cachedTokens: mock.usage.cachedTokens } : {}),\n },\n toolCalls: mock.toolCalls,\n };\n }\n\n /**\n * Record the call, optionally delay (to simulate latency), and either\n * throw the scripted error or return the scripted response. Mirrors the\n * real provider's `complete()` contract so agents cannot tell the\n * difference at runtime.\n */\n public async complete(messages: Message[], options?: ModelCallOptions): Promise<ModelResponse> {\n this.calls.push({ messages, options });\n\n const mock = this.nextResponse();\n\n if (mock.delay) {\n await new Promise((resolve) => setTimeout(resolve, mock.delay));\n }\n\n if (mock.error) {\n throw mock.error;\n }\n\n return this.buildResponse(mock);\n }\n\n /**\n * Record the call, optionally delay, then emit the scripted response as a\n * sequence of stream chunks: the scripted `deltas` when the entry\n * supplies them, otherwise content split word-by-word, as `delta`\n * chunks, each scripted tool call as a `tool-call` chunk, and finally a\n * `done` chunk with finish reason + usage. Throws eagerly if the scripted\n * entry carries an `error`.\n */\n public async *stream(\n messages: Message[],\n options?: ModelCallOptions,\n ): AsyncIterable<ModelStreamChunk> {\n this.calls.push({ messages, options });\n\n const mock = this.nextResponse();\n\n if (mock.delay) {\n await new Promise((resolve) => setTimeout(resolve, mock.delay));\n }\n\n if (mock.error) {\n throw mock.error;\n }\n\n const chunks = mock.deltas ?? mock.content.split(\" \").map((word) => word + \" \");\n\n for (const chunk of chunks) {\n yield { type: \"delta\", content: chunk };\n }\n\n if (mock.toolCalls) {\n for (const toolCall of mock.toolCalls) {\n yield {\n type: \"tool-call\",\n id: toolCall.id,\n name: toolCall.name,\n input: toolCall.input,\n };\n }\n }\n\n const response = this.buildResponse(mock);\n\n yield {\n type: \"done\",\n finishReason: response.finishReason,\n usage: response.usage,\n };\n }\n\n /**\n * Reset call history and response pointer back to their initial state.\n * Intended for test-suite `beforeEach` hooks so a single `MockModel`\n * instance can be reused across cases without cross-test leakage.\n */\n public reset(): void {\n this.calls = [];\n this.responseIndex = 0;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2CA,IAAa,YAAb,MAAgD;CAO9C,AAAO,YACL,AAAgB,MAChB,AAAiB,WACjB,cACA;EAHgB;EACC;kBARQ;uBAGH;eACQ,CAAC;EAO/B,KAAK,eAAe;CACtB;;;;;;CAOA,IAAW,cAA8B;EACvC,OAAO,KAAK;CACd;;;;;CAMA,IAAW,YAAoB;EAC7B,OAAO,KAAK,MAAM;CACpB;;;;;;;CAQA,AAAQ,eAAkC;EACxC,MAAM,WAAW,KAAK,UAAU,KAAK,IAAI,KAAK,eAAe,KAAK,UAAU,SAAS,CAAC;EAEtF,KAAK;EAEL,OAAO,YAAY;GAAE,SAAS;GAAiB,cAAc;EAAO;CACtE;;;;;;CAOA,AAAQ,cAAc,MAAwC;EAC5D,MAAM,iBAAiB;EACvB,MAAM,kBAAkB,KAAK,KAAK,KAAK,QAAQ,SAAS,CAAC;EAEzD,OAAO;GACL,SAAS,KAAK;GACd,cAAc,KAAK,gBAAgB;GACnC,OAAO;IACL,OAAO,KAAK,OAAO,SAAS;IAC5B,QAAQ,KAAK,OAAO,UAAU;IAC9B,QAAQ,KAAK,OAAO,SAAS,mBAAmB,KAAK,OAAO,UAAU;IACtE,GAAI,KAAK,OAAO,iBAAiB,SAAY,EAAE,cAAc,KAAK,MAAM,aAAa,IAAI,CAAC;GAC5F;GACA,WAAW,KAAK;EAClB;CACF;;;;;;;CAQA,MAAa,SAAS,UAAqB,SAAoD;EAC7F,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,OAAO,KAAK,cAAc,IAAI;CAChC;;;;;;;;;CAUA,OAAc,OACZ,UACA,SACiC;EACjC,KAAK,MAAM,KAAK;GAAE;GAAU;EAAQ,CAAC;EAErC,MAAM,OAAO,KAAK,aAAa;EAE/B,IAAI,KAAK,OACP,MAAM,IAAI,SAAS,YAAY,WAAW,SAAS,KAAK,KAAK,CAAC;EAGhE,IAAI,KAAK,OACP,MAAM,KAAK;EAGb,MAAM,SAAS,KAAK,UAAU,KAAK,QAAQ,MAAM,GAAG,CAAC,CAAC,KAAK,SAAS,OAAO,GAAG;EAE9E,KAAK,MAAM,SAAS,QAClB,MAAM;GAAE,MAAM;GAAS,SAAS;EAAM;EAGxC,IAAI,KAAK,WACP,KAAK,MAAM,YAAY,KAAK,WAC1B,MAAM;GACJ,MAAM;GACN,IAAI,SAAS;GACb,MAAM,SAAS;GACf,OAAO,SAAS;EAClB;EAIJ,MAAM,WAAW,KAAK,cAAc,IAAI;EAExC,MAAM;GACJ,MAAM;GACN,cAAc,SAAS;GACvB,OAAO,SAAS;EAClB;CACF;;;;;;CAOA,AAAO,QAAc;EACnB,KAAK,QAAQ,CAAC;EACd,KAAK,gBAAgB;CACvB;AACF"}
@@ -1 +1 @@
1
- {"version":3,"file":"mock-router.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-router.ts"],"mappings":";;;;;;AAWA;;;;;KAAY,kBAAA,UAA4B,MAAA,qBACpC,IAAA,KACE,OAAA,EAAS,YAAA,CAAa,MAAA,MAAY,IAAA;;;;;;;;;;;;;KAc5B,oBAAA;AAAZ;;;AAAA,KAKY,iBAAA;EALoB,wEAO9B,WAAA,GAAc,oBAAoB;AAAA;;;AAAA;AA+BpC;;;;;;;;;;;;;;;;;;;;;;;;AAG0C;iBAH1B,UAAA,UAAoB,MAAA,kBAAA,CAClC,SAAA,EAAW,kBAAA,CAAmB,MAAA,KAC9B,OAAA,GAAS,iBAAA,IACP,OAAA,EAAS,YAAA,CAAa,MAAA,MAAY,IAAA"}
1
+ {"version":3,"file":"mock-router.d.mts","names":[],"sources":["../../../../../../../ai/src/mock/mock-router.ts"],"mappings":";;;;;;AAWA;;;;;KAAY,kBAAA,UAA4B,MAAA,qBACpC,IAAA,KACE,OAAA,EAAS,YAAA,CAAa,MAAA,MAAY,IAAA;;;;;;;;;;;;;KAc5B,oBAAA;AAAZ;;;AAAA,KAKY,iBAAA;EALoB,wEAO9B,WAAA,GAAc,oBAAoB;AAAA;;;AAAA;AA+BpC;;;;;;;;;;;;;;;;;;;;;;;;AAG0C;iBAH1B,UAAA,UAAoB,MAAA,mBAClC,SAAA,EAAW,kBAAA,CAAmB,MAAA,KAC9B,OAAA,GAAS,iBAAA,IACP,OAAA,EAAS,YAAA,CAAa,MAAA,MAAY,IAAA"}