@rudderjs/ai 1.4.0 → 1.6.0

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 (179) hide show
  1. package/README.md +484 -7
  2. package/boost/guidelines.md +62 -2
  3. package/boost/skills/ai-tools/SKILL.md +14 -5
  4. package/dist/agent.d.ts +66 -15
  5. package/dist/agent.d.ts.map +1 -1
  6. package/dist/agent.js +529 -58
  7. package/dist/agent.js.map +1 -1
  8. package/dist/budget/pricing.d.ts +124 -0
  9. package/dist/budget/pricing.d.ts.map +1 -0
  10. package/dist/budget/pricing.js +175 -0
  11. package/dist/budget/pricing.js.map +1 -0
  12. package/dist/budget/storage.d.ts +104 -0
  13. package/dist/budget/storage.d.ts.map +1 -0
  14. package/dist/budget/storage.js +0 -0
  15. package/dist/budget/storage.js.map +1 -0
  16. package/dist/budget/with-budget.d.ts +119 -0
  17. package/dist/budget/with-budget.d.ts.map +1 -0
  18. package/dist/budget/with-budget.js +175 -0
  19. package/dist/budget/with-budget.js.map +1 -0
  20. package/dist/budget-orm/index.d.ts +96 -0
  21. package/dist/budget-orm/index.d.ts.map +1 -0
  22. package/dist/budget-orm/index.js +177 -0
  23. package/dist/budget-orm/index.js.map +1 -0
  24. package/dist/commands/ai-eval.d.ts +93 -0
  25. package/dist/commands/ai-eval.d.ts.map +1 -0
  26. package/dist/commands/ai-eval.js +378 -0
  27. package/dist/commands/ai-eval.js.map +1 -0
  28. package/dist/computer-use/actions.d.ts +214 -0
  29. package/dist/computer-use/actions.d.ts.map +1 -0
  30. package/dist/computer-use/actions.js +48 -0
  31. package/dist/computer-use/actions.js.map +1 -0
  32. package/dist/computer-use/errors.d.ts +57 -0
  33. package/dist/computer-use/errors.d.ts.map +1 -0
  34. package/dist/computer-use/errors.js +76 -0
  35. package/dist/computer-use/errors.js.map +1 -0
  36. package/dist/computer-use/index.d.ts +53 -0
  37. package/dist/computer-use/index.d.ts.map +1 -0
  38. package/dist/computer-use/index.js +51 -0
  39. package/dist/computer-use/index.js.map +1 -0
  40. package/dist/computer-use/playwright.d.ts +76 -0
  41. package/dist/computer-use/playwright.d.ts.map +1 -0
  42. package/dist/computer-use/playwright.js +270 -0
  43. package/dist/computer-use/playwright.js.map +1 -0
  44. package/dist/computer-use/tool.d.ts +154 -0
  45. package/dist/computer-use/tool.d.ts.map +1 -0
  46. package/dist/computer-use/tool.js +210 -0
  47. package/dist/computer-use/tool.js.map +1 -0
  48. package/dist/eval/fixtures.d.ts +65 -0
  49. package/dist/eval/fixtures.d.ts.map +1 -0
  50. package/dist/eval/fixtures.js +110 -0
  51. package/dist/eval/fixtures.js.map +1 -0
  52. package/dist/eval/html-reporter.d.ts +25 -0
  53. package/dist/eval/html-reporter.d.ts.map +1 -0
  54. package/dist/eval/html-reporter.js +209 -0
  55. package/dist/eval/html-reporter.js.map +1 -0
  56. package/dist/eval/index.d.ts +271 -0
  57. package/dist/eval/index.d.ts.map +1 -0
  58. package/dist/eval/index.js +510 -0
  59. package/dist/eval/index.js.map +1 -0
  60. package/dist/eval/json-reporter.d.ts +43 -0
  61. package/dist/eval/json-reporter.d.ts.map +1 -0
  62. package/dist/eval/json-reporter.js +40 -0
  63. package/dist/eval/json-reporter.js.map +1 -0
  64. package/dist/fake.d.ts +36 -1
  65. package/dist/fake.d.ts.map +1 -1
  66. package/dist/fake.js +49 -2
  67. package/dist/fake.js.map +1 -1
  68. package/dist/file-search.d.ts +168 -0
  69. package/dist/file-search.d.ts.map +1 -0
  70. package/dist/file-search.js +158 -0
  71. package/dist/file-search.js.map +1 -0
  72. package/dist/handoff.d.ts +95 -0
  73. package/dist/handoff.d.ts.map +1 -0
  74. package/dist/handoff.js +78 -0
  75. package/dist/handoff.js.map +1 -0
  76. package/dist/index.d.ts +29 -5
  77. package/dist/index.d.ts.map +1 -1
  78. package/dist/index.js +22 -2
  79. package/dist/index.js.map +1 -1
  80. package/dist/mcp/client-tools.d.ts +39 -0
  81. package/dist/mcp/client-tools.d.ts.map +1 -0
  82. package/dist/mcp/client-tools.js +147 -0
  83. package/dist/mcp/client-tools.js.map +1 -0
  84. package/dist/mcp/index.d.ts +16 -0
  85. package/dist/mcp/index.d.ts.map +1 -0
  86. package/dist/mcp/index.js +15 -0
  87. package/dist/mcp/index.js.map +1 -0
  88. package/dist/mcp/server-from-agent.d.ts +24 -0
  89. package/dist/mcp/server-from-agent.d.ts.map +1 -0
  90. package/dist/mcp/server-from-agent.js +113 -0
  91. package/dist/mcp/server-from-agent.js.map +1 -0
  92. package/dist/mcp/types.d.ts +64 -0
  93. package/dist/mcp/types.d.ts.map +1 -0
  94. package/dist/mcp/types.js +6 -0
  95. package/dist/mcp/types.js.map +1 -0
  96. package/dist/memory-embedding/index.d.ts +121 -0
  97. package/dist/memory-embedding/index.d.ts.map +1 -0
  98. package/dist/memory-embedding/index.js +229 -0
  99. package/dist/memory-embedding/index.js.map +1 -0
  100. package/dist/memory-extract.d.ts +60 -0
  101. package/dist/memory-extract.d.ts.map +1 -0
  102. package/dist/memory-extract.js +163 -0
  103. package/dist/memory-extract.js.map +1 -0
  104. package/dist/memory-inject.d.ts +39 -0
  105. package/dist/memory-inject.d.ts.map +1 -0
  106. package/dist/memory-inject.js +135 -0
  107. package/dist/memory-inject.js.map +1 -0
  108. package/dist/memory-orm/index.d.ts +118 -0
  109. package/dist/memory-orm/index.d.ts.map +1 -0
  110. package/dist/memory-orm/index.js +187 -0
  111. package/dist/memory-orm/index.js.map +1 -0
  112. package/dist/memory.d.ts +55 -0
  113. package/dist/memory.d.ts.map +1 -0
  114. package/dist/memory.js +132 -0
  115. package/dist/memory.js.map +1 -0
  116. package/dist/observers.d.ts +22 -0
  117. package/dist/observers.d.ts.map +1 -1
  118. package/dist/observers.js.map +1 -1
  119. package/dist/provider-tools.d.ts +15 -1
  120. package/dist/provider-tools.d.ts.map +1 -1
  121. package/dist/provider-tools.js +21 -1
  122. package/dist/provider-tools.js.map +1 -1
  123. package/dist/providers/anthropic.d.ts +9 -1
  124. package/dist/providers/anthropic.d.ts.map +1 -1
  125. package/dist/providers/anthropic.js +66 -11
  126. package/dist/providers/anthropic.js.map +1 -1
  127. package/dist/providers/bedrock.d.ts +60 -0
  128. package/dist/providers/bedrock.d.ts.map +1 -0
  129. package/dist/providers/bedrock.js +167 -0
  130. package/dist/providers/bedrock.js.map +1 -0
  131. package/dist/providers/elevenlabs.d.ts +98 -0
  132. package/dist/providers/elevenlabs.d.ts.map +1 -0
  133. package/dist/providers/elevenlabs.js +229 -0
  134. package/dist/providers/elevenlabs.js.map +1 -0
  135. package/dist/providers/google.d.ts +83 -1
  136. package/dist/providers/google.d.ts.map +1 -1
  137. package/dist/providers/google.js +491 -8
  138. package/dist/providers/google.js.map +1 -1
  139. package/dist/providers/openai.d.ts +8 -1
  140. package/dist/providers/openai.d.ts.map +1 -1
  141. package/dist/providers/openai.js +215 -5
  142. package/dist/providers/openai.js.map +1 -1
  143. package/dist/providers/openrouter.d.ts +43 -0
  144. package/dist/providers/openrouter.d.ts.map +1 -0
  145. package/dist/providers/openrouter.js +21 -0
  146. package/dist/providers/openrouter.js.map +1 -0
  147. package/dist/providers/voyage.d.ts +91 -0
  148. package/dist/providers/voyage.d.ts.map +1 -0
  149. package/dist/providers/voyage.js +166 -0
  150. package/dist/providers/voyage.js.map +1 -0
  151. package/dist/queue-job.d.ts +69 -4
  152. package/dist/queue-job.d.ts.map +1 -1
  153. package/dist/queue-job.js +114 -11
  154. package/dist/queue-job.js.map +1 -1
  155. package/dist/registry.d.ts +3 -1
  156. package/dist/registry.d.ts.map +1 -1
  157. package/dist/registry.js +10 -0
  158. package/dist/registry.js.map +1 -1
  159. package/dist/server/provider.d.ts.map +1 -1
  160. package/dist/server/provider.js +38 -1
  161. package/dist/server/provider.js.map +1 -1
  162. package/dist/similarity-search.d.ts +163 -0
  163. package/dist/similarity-search.d.ts.map +1 -0
  164. package/dist/similarity-search.js +147 -0
  165. package/dist/similarity-search.js.map +1 -0
  166. package/dist/sub-agent-run-store.d.ts +40 -3
  167. package/dist/sub-agent-run-store.d.ts.map +1 -1
  168. package/dist/sub-agent-run-store.js.map +1 -1
  169. package/dist/tool.d.ts +59 -0
  170. package/dist/tool.d.ts.map +1 -1
  171. package/dist/tool.js +45 -4
  172. package/dist/tool.js.map +1 -1
  173. package/dist/types.d.ts +285 -1
  174. package/dist/types.d.ts.map +1 -1
  175. package/dist/vector-stores/index.d.ts +96 -0
  176. package/dist/vector-stores/index.d.ts.map +1 -0
  177. package/dist/vector-stores/index.js +153 -0
  178. package/dist/vector-stores/index.js.map +1 -0
  179. package/package.json +43 -4
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Public types for `@rudderjs/ai/mcp`. Kept in a separate module so the
3
+ * client + server connectors can share them without circular imports.
4
+ */
5
+ /**
6
+ * Configuration for spawning a local MCP server as a stdio subprocess.
7
+ * Mirrors `StdioServerParameters` from `@modelcontextprotocol/sdk` but is
8
+ * re-exported here so consumers don't need a direct SDK import for the
9
+ * common case.
10
+ */
11
+ export interface StdioServerSpawn {
12
+ command: string;
13
+ args?: readonly string[];
14
+ /** Inherited from the parent process when omitted. */
15
+ env?: Readonly<Record<string, string>>;
16
+ /** Working directory for the spawned process. */
17
+ cwd?: string;
18
+ }
19
+ /**
20
+ * Anything `mcpClientTools()` accepts as the connection target.
21
+ *
22
+ * - `string` / `URL` — connects via the Streamable HTTP transport
23
+ * - `StdioServerSpawn` — spawns a subprocess and connects over stdio
24
+ * - existing `Client` instance — used as-is, lifecycle remains the caller's
25
+ *
26
+ * Implementation note: keeping `Client` as `unknown` here so the type union
27
+ * doesn't force a hard dep on `@modelcontextprotocol/sdk` at module load.
28
+ * The runtime code uses an instanceof check via dynamic import.
29
+ */
30
+ export type McpClientTransport = string | URL | StdioServerSpawn | object;
31
+ export interface McpClientToolsOptions {
32
+ /** Filter exposed tools — return `false` to drop a remote tool from the result. Defaults to all. */
33
+ filter?: (toolName: string) => boolean;
34
+ /** Prefix tool names to avoid collisions when wiring multiple remote servers. */
35
+ namePrefix?: string;
36
+ /**
37
+ * Forward MCP `notifications/progress` from the remote server as `tool-update`
38
+ * chunks during agent execution. Defaults to `true`.
39
+ */
40
+ streaming?: boolean;
41
+ }
42
+ export interface McpServerFromAgentOptions {
43
+ /** Server name advertised over MCP. Default: `${AgentClass.name}Server`. */
44
+ name?: string;
45
+ /** Server version. Default: `'1.0.0'`. */
46
+ version?: string;
47
+ /**
48
+ * Server instructions advertised over MCP — typically the agent's own
49
+ * system prompt. Default: tries `agent.instructions()`, falls back to undefined.
50
+ */
51
+ instructions?: string;
52
+ /**
53
+ * What to expose:
54
+ * - `'tools'` (default): one MCP tool per `agent.tools()` entry — external
55
+ * MCP clients call them as if they were the server's own.
56
+ * - `'agent'`: one MCP tool `prompt(text: string)` that runs the whole agent
57
+ * and returns the response text. Ship one agent, callable from any MCP client.
58
+ * - `'both'`: both of the above.
59
+ */
60
+ expose?: 'tools' | 'agent' | 'both';
61
+ /** Name of the synthetic prompt-tool when `expose: 'agent' | 'both'`. Default: agent class name. */
62
+ agentToolName?: string;
63
+ }
64
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/mcp/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,CAAC,EAAI,SAAS,MAAM,EAAE,CAAA;IAC1B,sDAAsD;IACtD,GAAG,CAAC,EAAK,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACzC,iDAAiD;IACjD,GAAG,CAAC,EAAK,MAAM,CAAA;CAChB;AAED;;;;;;;;;;GAUG;AACH,MAAM,MAAM,kBAAkB,GAC1B,MAAM,GACN,GAAG,GACH,gBAAgB,GAChB,MAAM,CAAA;AAEV,MAAM,WAAW,qBAAqB;IACpC,oGAAoG;IACpG,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAA;IACtC,iFAAiF;IACjF,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB;;;OAGG;IACH,SAAS,CAAC,EAAE,OAAO,CAAA;CACpB;AAED,MAAM,WAAW,yBAAyB;IACxC,4EAA4E;IAC5E,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,0CAA0C;IAC1C,OAAO,CAAC,EAAE,MAAM,CAAA;IAChB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,OAAO,GAAG,OAAO,GAAG,MAAM,CAAA;IACnC,oGAAoG;IACpG,aAAa,CAAC,EAAE,MAAM,CAAA;CACvB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Public types for `@rudderjs/ai/mcp`. Kept in a separate module so the
3
+ * client + server connectors can share them without circular imports.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/mcp/types.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `@rudderjs/ai/memory-embedding` — embedding-backed {@link UserMemory}
3
+ * for #A4 Phase 5.
4
+ *
5
+ * Composes Phase 4's {@link OrmUserMemory} with the embedding
6
+ * provider registered on {@link AiRegistry}: `remember()` embeds the
7
+ * fact and writes the Float32-packed vector into the row's
8
+ * `embedding` column; `recall()` embeds the query and ranks by
9
+ * cosine similarity. `forget()` / `forgetAll()` delegate to the
10
+ * inner store — the embedding lives in the same row, so deleting
11
+ * the row deletes the vector. GDPR right-to-be-forgotten cascades
12
+ * automatically.
13
+ *
14
+ * v1 is **pure-JS cosine over the user's full set** — fine up to
15
+ * a few thousand facts per user. For larger workloads, B7 lands a
16
+ * pgvector-backed `EmbeddingUserMemory` that pushes the dot-product
17
+ * into the database.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { OrmUserMemory } from '@rudderjs/ai/memory-orm'
22
+ * import { EmbeddingUserMemory } from '@rudderjs/ai/memory-embedding'
23
+ *
24
+ * const memory = new EmbeddingUserMemory({
25
+ * inner: new OrmUserMemory(),
26
+ * model: 'openai/text-embedding-3-small',
27
+ * threshold: 0.5, // cosine floor; matches below are dropped
28
+ * })
29
+ * ```
30
+ *
31
+ * **Pre-Phase-5 facts** (rows with `embedding === null`) fall back to
32
+ * token-overlap matching against the `fact` column — same shape as
33
+ * `MemoryUserMemory.recall()`. So upgrading from `OrmUserMemory` to
34
+ * `EmbeddingUserMemory` doesn't lose recall on existing rows; new
35
+ * `remember()` calls populate the embedding column going forward.
36
+ */
37
+ import { OrmUserMemory } from '../memory-orm/index.js';
38
+ import type { MemoryEntry, UserMemory } from '../types.js';
39
+ export interface EmbeddingUserMemoryOptions {
40
+ /**
41
+ * The composed inner store. Must be {@link OrmUserMemory} for v1
42
+ * — the composer reads/writes the `embedding Bytes?` column on
43
+ * the same row. Other backends (Pinecone, Weaviate) implement
44
+ * their own.
45
+ */
46
+ inner: OrmUserMemory;
47
+ /**
48
+ * Embedding model id (`'<provider>/<model>'`). Used for both
49
+ * fact embedding on `remember()` and query embedding on
50
+ * `recall()`. Default: whatever `AI.embed()` picks (`AiRegistry`
51
+ * default).
52
+ */
53
+ model?: string;
54
+ /**
55
+ * Cosine-similarity floor in `[-1, 1]`. Matches below the
56
+ * threshold are dropped before sorting. Default `0` — return
57
+ * everything ranked. Tighten for higher precision; loosen for
58
+ * higher recall.
59
+ */
60
+ threshold?: number;
61
+ /**
62
+ * Optional fallback for rows whose `embedding` column is `null`
63
+ * (rows persisted without the embedding composer wired in).
64
+ *
65
+ * - `'token-overlap'` (default) — score 0 if any ≥3-char token
66
+ * from the query appears in the row's `fact`. Lets you
67
+ * upgrade `OrmUserMemory` → `EmbeddingUserMemory` without
68
+ * losing recall on existing rows.
69
+ * - `'skip'` — drop null-embedding rows entirely.
70
+ */
71
+ nullEmbeddingFallback?: 'token-overlap' | 'skip';
72
+ }
73
+ export declare class EmbeddingUserMemory implements UserMemory {
74
+ private readonly inner;
75
+ private readonly model;
76
+ private readonly threshold;
77
+ private readonly fallback;
78
+ constructor(opts: EmbeddingUserMemoryOptions);
79
+ remember(userId: string, fact: string, opts?: {
80
+ tags?: string[];
81
+ score?: number;
82
+ }): Promise<MemoryEntry>;
83
+ recall(userId: string, query: string, opts?: {
84
+ limit?: number;
85
+ tags?: string[];
86
+ }): Promise<MemoryEntry[]>;
87
+ forget(userId: string, factId: string): Promise<void>;
88
+ list(userId: string, opts?: {
89
+ tags?: string[];
90
+ limit?: number;
91
+ }): Promise<MemoryEntry[]>;
92
+ forgetAll(userId: string): Promise<void>;
93
+ /**
94
+ * Single-string embedding via the {@link AI} facade. Returns the
95
+ * first (and only) embedding vector. Throws on provider/network
96
+ * failure; callers route through try/catch and degrade.
97
+ */
98
+ private embed;
99
+ }
100
+ /**
101
+ * Pack a `number[]` into a Float32 byte buffer. 4 bytes per dim;
102
+ * a 1536-dim OpenAI embedding compresses to 6144 bytes.
103
+ *
104
+ * Uses `ArrayBuffer` + `Float32Array` so the output is a portable
105
+ * `Uint8Array` (works in Node, browser, RN). Prisma's `Bytes`
106
+ * column accepts both `Uint8Array` and `Buffer`.
107
+ */
108
+ export declare function serializeVector(v: number[]): Uint8Array;
109
+ /**
110
+ * Reverse of {@link serializeVector}. Reads the underlying byte
111
+ * buffer as Float32 and returns a fresh `number[]` so callers can
112
+ * mutate without affecting the source row.
113
+ */
114
+ export declare function deserializeVector(bytes: Uint8Array): number[];
115
+ /**
116
+ * Cosine similarity in `[-1, 1]`. Returns `0` when either vector
117
+ * has zero magnitude, or when lengths don't match (defensive — should
118
+ * never happen if remember/recall use the same embedding model).
119
+ */
120
+ export declare function cosineSimilarity(a: number[], b: number[]): number;
121
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/memory-embedding/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAGH,OAAO,EAAE,aAAa,EAAoB,MAAM,wBAAwB,CAAA;AACxE,OAAO,KAAK,EACV,WAAW,EACX,UAAU,EACX,MAAM,aAAa,CAAA;AAEpB,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,KAAK,EAAE,aAAa,CAAA;IACpB;;;;;OAKG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;IACd;;;;;OAKG;IACH,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB;;;;;;;;;OASG;IACH,qBAAqB,CAAC,EAAE,eAAe,GAAG,MAAM,CAAA;CACjD;AAED,qBAAa,mBAAoB,YAAW,UAAU;IACpD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkB;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAuB;IAC7C,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAQ;IAClC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA0B;gBAEvC,IAAI,EAAE,0BAA0B;IAOtC,QAAQ,CACZ,MAAM,EAAE,MAAM,EACd,IAAI,EAAI,MAAM,EACd,IAAI,CAAC,EAAG;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1C,OAAO,CAAC,WAAW,CAAC;IAkBjB,MAAM,CACV,MAAM,EAAE,MAAM,EACd,KAAK,EAAG,MAAM,EACd,IAAI,CAAC,EAAG;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,GAC1C,OAAO,CAAC,WAAW,EAAE,CAAC;IA4CnB,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAMrD,IAAI,CACR,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAG;QAAE,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,GAC1C,OAAO,CAAC,WAAW,EAAE,CAAC;IAInB,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9C;;;;OAIG;YACW,KAAK;CAMpB;AAID;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAAC,CAAC,EAAE,MAAM,EAAE,GAAG,UAAU,CAKvD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,EAAE,CAK7D;AAED;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAcjE"}
@@ -0,0 +1,229 @@
1
+ /**
2
+ * `@rudderjs/ai/memory-embedding` — embedding-backed {@link UserMemory}
3
+ * for #A4 Phase 5.
4
+ *
5
+ * Composes Phase 4's {@link OrmUserMemory} with the embedding
6
+ * provider registered on {@link AiRegistry}: `remember()` embeds the
7
+ * fact and writes the Float32-packed vector into the row's
8
+ * `embedding` column; `recall()` embeds the query and ranks by
9
+ * cosine similarity. `forget()` / `forgetAll()` delegate to the
10
+ * inner store — the embedding lives in the same row, so deleting
11
+ * the row deletes the vector. GDPR right-to-be-forgotten cascades
12
+ * automatically.
13
+ *
14
+ * v1 is **pure-JS cosine over the user's full set** — fine up to
15
+ * a few thousand facts per user. For larger workloads, B7 lands a
16
+ * pgvector-backed `EmbeddingUserMemory` that pushes the dot-product
17
+ * into the database.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { OrmUserMemory } from '@rudderjs/ai/memory-orm'
22
+ * import { EmbeddingUserMemory } from '@rudderjs/ai/memory-embedding'
23
+ *
24
+ * const memory = new EmbeddingUserMemory({
25
+ * inner: new OrmUserMemory(),
26
+ * model: 'openai/text-embedding-3-small',
27
+ * threshold: 0.5, // cosine floor; matches below are dropped
28
+ * })
29
+ * ```
30
+ *
31
+ * **Pre-Phase-5 facts** (rows with `embedding === null`) fall back to
32
+ * token-overlap matching against the `fact` column — same shape as
33
+ * `MemoryUserMemory.recall()`. So upgrading from `OrmUserMemory` to
34
+ * `EmbeddingUserMemory` doesn't lose recall on existing rows; new
35
+ * `remember()` calls populate the embedding column going forward.
36
+ */
37
+ import { AI } from '../facade.js';
38
+ import { UserMemoryRecord } from '../memory-orm/index.js';
39
+ export class EmbeddingUserMemory {
40
+ inner;
41
+ model;
42
+ threshold;
43
+ fallback;
44
+ constructor(opts) {
45
+ this.inner = opts.inner;
46
+ if (opts.model !== undefined)
47
+ this.model = opts.model;
48
+ this.threshold = opts.threshold ?? 0;
49
+ this.fallback = opts.nullEmbeddingFallback ?? 'token-overlap';
50
+ }
51
+ async remember(userId, fact, opts) {
52
+ const entry = await this.inner.remember(userId, fact, opts);
53
+ // Best-effort embed + persist. Failures are logged via the inner
54
+ // store still having the entry; we don't break the caller.
55
+ try {
56
+ const vector = await this.embed(fact);
57
+ await UserMemoryRecord.update(entry.id, {
58
+ embedding: serializeVector(vector),
59
+ });
60
+ }
61
+ catch {
62
+ // Embedding failed (network, missing peer SDK). The row is
63
+ // already in the store; recall will fall back to
64
+ // token-overlap if the column stays null. No-op.
65
+ }
66
+ return entry;
67
+ }
68
+ async recall(userId, query, opts) {
69
+ let queryVector = null;
70
+ try {
71
+ queryVector = await this.embed(query);
72
+ }
73
+ catch {
74
+ // Embed failed — fall through to token-overlap on every row.
75
+ }
76
+ const rows = await UserMemoryRecord.where('userId', userId).get();
77
+ const wanted = opts?.tags;
78
+ const queryTokens = tokenize(query);
79
+ const scored = [];
80
+ for (const row of rows) {
81
+ const entry = rowToEntry(row);
82
+ if (!matchesTags(entry, wanted))
83
+ continue;
84
+ let score;
85
+ if (queryVector !== null && row.embedding !== null && row.embedding !== undefined) {
86
+ const factVector = deserializeVector(row.embedding);
87
+ score = cosineSimilarity(queryVector, factVector);
88
+ }
89
+ else if (this.fallback === 'skip') {
90
+ continue;
91
+ }
92
+ else {
93
+ // token-overlap fallback — score 0 (mid-range) if any
94
+ // token matches, otherwise drop.
95
+ if (factHasAnyToken(entry.fact, queryTokens)) {
96
+ score = 0;
97
+ }
98
+ else {
99
+ continue;
100
+ }
101
+ }
102
+ if (score >= this.threshold) {
103
+ scored.push({ entry, score });
104
+ }
105
+ }
106
+ scored.sort((a, b) => b.score - a.score);
107
+ const capped = capLimit(scored, opts?.limit);
108
+ return capped.map(s => ({ ...s.entry, score: s.score }));
109
+ }
110
+ async forget(userId, factId) {
111
+ // The embedding lives in the same row — deleting via the inner
112
+ // store deletes the vector too. GDPR cascade is automatic.
113
+ return this.inner.forget(userId, factId);
114
+ }
115
+ async list(userId, opts) {
116
+ return this.inner.list(userId, opts);
117
+ }
118
+ async forgetAll(userId) {
119
+ if (!this.inner.forgetAll)
120
+ return;
121
+ return this.inner.forgetAll(userId);
122
+ }
123
+ /**
124
+ * Single-string embedding via the {@link AI} facade. Returns the
125
+ * first (and only) embedding vector. Throws on provider/network
126
+ * failure; callers route through try/catch and degrade.
127
+ */
128
+ async embed(text) {
129
+ const result = await AI.embed(text, this.model ? { model: this.model } : undefined);
130
+ const vec = result.embeddings[0];
131
+ if (!vec)
132
+ throw new Error('[RudderJS AI] embed() returned no vectors');
133
+ return vec;
134
+ }
135
+ }
136
+ // ─── Vector + similarity helpers (exported for tests + B7) ─────
137
+ /**
138
+ * Pack a `number[]` into a Float32 byte buffer. 4 bytes per dim;
139
+ * a 1536-dim OpenAI embedding compresses to 6144 bytes.
140
+ *
141
+ * Uses `ArrayBuffer` + `Float32Array` so the output is a portable
142
+ * `Uint8Array` (works in Node, browser, RN). Prisma's `Bytes`
143
+ * column accepts both `Uint8Array` and `Buffer`.
144
+ */
145
+ export function serializeVector(v) {
146
+ const buf = new ArrayBuffer(v.length * 4);
147
+ const view = new Float32Array(buf);
148
+ for (let i = 0; i < v.length; i++)
149
+ view[i] = v[i];
150
+ return new Uint8Array(buf);
151
+ }
152
+ /**
153
+ * Reverse of {@link serializeVector}. Reads the underlying byte
154
+ * buffer as Float32 and returns a fresh `number[]` so callers can
155
+ * mutate without affecting the source row.
156
+ */
157
+ export function deserializeVector(bytes) {
158
+ // The `bytes.buffer` may be a slice; honor byteOffset + byteLength
159
+ // so we don't read into adjacent memory.
160
+ const view = new Float32Array(bytes.buffer, bytes.byteOffset, bytes.byteLength / 4);
161
+ return Array.from(view);
162
+ }
163
+ /**
164
+ * Cosine similarity in `[-1, 1]`. Returns `0` when either vector
165
+ * has zero magnitude, or when lengths don't match (defensive — should
166
+ * never happen if remember/recall use the same embedding model).
167
+ */
168
+ export function cosineSimilarity(a, b) {
169
+ if (a.length !== b.length)
170
+ return 0;
171
+ let dot = 0;
172
+ let magA = 0;
173
+ let magB = 0;
174
+ for (let i = 0; i < a.length; i++) {
175
+ const ai = a[i];
176
+ const bi = b[i];
177
+ dot += ai * bi;
178
+ magA += ai * ai;
179
+ magB += bi * bi;
180
+ }
181
+ if (magA === 0 || magB === 0)
182
+ return 0;
183
+ return dot / (Math.sqrt(magA) * Math.sqrt(magB));
184
+ }
185
+ // ─── Internal helpers ─────────────────────────────────────
186
+ function rowToEntry(row) {
187
+ const tags = row.getTags();
188
+ const out = {
189
+ id: row.id,
190
+ userId: row.userId,
191
+ fact: row.fact,
192
+ createdAt: row.createdAt,
193
+ };
194
+ if (tags.length > 0)
195
+ out.tags = tags;
196
+ if (row.score != null)
197
+ out.score = row.score;
198
+ if (row.updatedAt != null)
199
+ out.updatedAt = row.updatedAt;
200
+ return out;
201
+ }
202
+ function tokenize(s) {
203
+ const out = new Set();
204
+ for (const tok of s.toLowerCase().split(/[^a-z0-9]+/)) {
205
+ if (tok.length >= 3)
206
+ out.add(tok);
207
+ }
208
+ return out;
209
+ }
210
+ function factHasAnyToken(fact, queryTokens) {
211
+ if (queryTokens.size === 0)
212
+ return true;
213
+ const factTokens = tokenize(fact);
214
+ for (const t of factTokens)
215
+ if (queryTokens.has(t))
216
+ return true;
217
+ return false;
218
+ }
219
+ function matchesTags(entry, wanted) {
220
+ if (!wanted || wanted.length === 0)
221
+ return true;
222
+ if (!entry.tags || entry.tags.length === 0)
223
+ return false;
224
+ return wanted.every(t => entry.tags.includes(t));
225
+ }
226
+ function capLimit(items, limit) {
227
+ return limit !== undefined && limit > 0 ? items.slice(0, limit) : items;
228
+ }
229
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/memory-embedding/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,60 @@
1
+ import type { AiMiddleware, MemoryEntry, RemembersSpec } from './types.js';
2
+ import type { UserMemoryLookup } from './memory.js';
3
+ export interface MemoryExtractOptions {
4
+ /**
5
+ * Override the {@link UserMemory} lookup. Defaults to the module-level
6
+ * `resolveUserMemory()` registry that `AiProvider` writes to from
7
+ * `AiConfig.memory`. Tests pass a closure to inject a fake.
8
+ */
9
+ lookup?: UserMemoryLookup;
10
+ /**
11
+ * Confidence floor for the small model's self-rated `score`. Facts
12
+ * with a score < threshold are dropped before any `remember()` call.
13
+ * Default `0.7`.
14
+ *
15
+ * **Tuning note (poisoning mitigation):** the threshold is the v1
16
+ * defense against a malicious user planting adversarial "facts." A
17
+ * low threshold accepts more spam; a high threshold filters useful
18
+ * signal. Pair with `MemoryExtractOptions.onExtracted` for an audit
19
+ * log when you ship extract to production.
20
+ */
21
+ threshold?: number;
22
+ /**
23
+ * Called after a successful extract with the entries that survived
24
+ * the threshold filter and were written to the store. Use this to
25
+ * stream entries into telescope, an audit log, or test assertions.
26
+ */
27
+ onExtracted?: (entries: MemoryEntry[]) => void;
28
+ /**
29
+ * Called when extract fails — small-model network error, JSON parse
30
+ * failure, schema-validation rejection, or `mem.remember()` throw.
31
+ * Errors are otherwise swallowed; the parent run never breaks.
32
+ */
33
+ onError?: (err: unknown) => void;
34
+ }
35
+ /**
36
+ * Post-conversation {@link AiMiddleware} that asks a small model to
37
+ * distill the latest `[user, assistant]` turn into durable facts and
38
+ * writes the survivors (above `threshold`) to the registered
39
+ * {@link UserMemory}. Auto-installed by `Agent.prompt` /
40
+ * `Agent.stream` when `Agent.remembers().extract === 'auto'` and
41
+ * `extractWith` is set; can also be dropped into `Agent.middleware()`
42
+ * manually.
43
+ *
44
+ * Runs in `onFinish` — only fires on a successful loop, so failed
45
+ * runs don't pollute memory. Failures inside the extract path
46
+ * (network, JSON parse, zod validation, `remember()` throw) are
47
+ * routed through `MemoryExtractOptions.onError` and otherwise
48
+ * swallowed; the parent prompt never breaks because of memory work.
49
+ *
50
+ * **Auto-installed extracts skip continuation calls** (`options.messages`
51
+ * set) at the host level — the same way auto-inject does. Manually
52
+ * installed extracts always run.
53
+ *
54
+ * **Pitfall — memory poisoning:** auto-extraction lets a malicious
55
+ * user plant adversarial "facts." The threshold (default 0.7) is the
56
+ * baseline defense; pair with `onExtracted` for an audit log when you
57
+ * ship to production. A content-filter middleware is a follow-up.
58
+ */
59
+ export declare function withMemoryExtract(spec: RemembersSpec, opts?: MemoryExtractOptions): AiMiddleware;
60
+ //# sourceMappingURL=memory-extract.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memory-extract.d.ts","sourceRoot":"","sources":["../src/memory-extract.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAEV,YAAY,EAEZ,WAAW,EACX,aAAa,EACd,MAAM,YAAY,CAAA;AACnB,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAA;AAEnD,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,MAAM,CAAC,EAAO,gBAAgB,CAAA;IAC9B;;;;;;;;;;OAUG;IACH,SAAS,CAAC,EAAI,MAAM,CAAA;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,KAAK,IAAI,CAAA;IAC9C;;;;OAIG;IACH,OAAO,CAAC,EAAM,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACrC;AAuBD;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,aAAa,EACnB,IAAI,GAAE,oBAAyB,GAC9B,YAAY,CAwDd"}
@@ -0,0 +1,163 @@
1
+ import { z } from 'zod';
2
+ import { agent, resolveUserMemory } from './agent.js';
3
+ import { Output } from './output.js';
4
+ /**
5
+ * The shape we ask the small model to fill in when distilling facts.
6
+ * `score` is the model's self-rated confidence in [0, 1]; tags are
7
+ * additive — the spec's `tags` are unioned in regardless.
8
+ */
9
+ const ExtractedFactsSchema = z.object({
10
+ facts: z.array(z.object({
11
+ fact: z.string().min(1),
12
+ score: z.number().min(0).max(1),
13
+ tags: z.array(z.string()).optional(),
14
+ })),
15
+ });
16
+ const EXTRACT_INSTRUCTIONS = [
17
+ 'You distill durable facts about a USER from a single conversation turn.',
18
+ 'A "durable fact" is something true about the user that future conversations would benefit from knowing — preferences, identifying details, ongoing projects, persistent constraints.',
19
+ 'Skip anything specific to this conversation, ephemeral state, or already-obvious context.',
20
+ 'Self-rate each fact\'s confidence in [0, 1]; the host filters out anything below the threshold.',
21
+ 'If nothing is worth remembering, return {"facts": []}.',
22
+ ].join(' ');
23
+ /**
24
+ * Post-conversation {@link AiMiddleware} that asks a small model to
25
+ * distill the latest `[user, assistant]` turn into durable facts and
26
+ * writes the survivors (above `threshold`) to the registered
27
+ * {@link UserMemory}. Auto-installed by `Agent.prompt` /
28
+ * `Agent.stream` when `Agent.remembers().extract === 'auto'` and
29
+ * `extractWith` is set; can also be dropped into `Agent.middleware()`
30
+ * manually.
31
+ *
32
+ * Runs in `onFinish` — only fires on a successful loop, so failed
33
+ * runs don't pollute memory. Failures inside the extract path
34
+ * (network, JSON parse, zod validation, `remember()` throw) are
35
+ * routed through `MemoryExtractOptions.onError` and otherwise
36
+ * swallowed; the parent prompt never breaks because of memory work.
37
+ *
38
+ * **Auto-installed extracts skip continuation calls** (`options.messages`
39
+ * set) at the host level — the same way auto-inject does. Manually
40
+ * installed extracts always run.
41
+ *
42
+ * **Pitfall — memory poisoning:** auto-extraction lets a malicious
43
+ * user plant adversarial "facts." The threshold (default 0.7) is the
44
+ * baseline defense; pair with `onExtracted` for an audit log when you
45
+ * ship to production. A content-filter middleware is a follow-up.
46
+ */
47
+ export function withMemoryExtract(spec, opts = {}) {
48
+ const lookup = opts.lookup ?? resolveUserMemory;
49
+ const threshold = opts.threshold ?? 0.7;
50
+ const wrapper = Output.object({ schema: ExtractedFactsSchema });
51
+ return {
52
+ name: 'memory-extract',
53
+ async onFinish(ctx) {
54
+ try {
55
+ if (spec.extract !== 'auto')
56
+ return;
57
+ if (!spec.extractWith)
58
+ return;
59
+ const mem = lookup();
60
+ if (!mem)
61
+ return;
62
+ const turn = extractLatestTurn(ctx.messages);
63
+ if (!turn)
64
+ return;
65
+ const extractor = agent({
66
+ instructions: `${EXTRACT_INSTRUCTIONS}\n\n${wrapper.toSystemPrompt()}`,
67
+ model: spec.extractWith,
68
+ });
69
+ const prompt = [
70
+ `User said: ${JSON.stringify(turn.user)}`,
71
+ `Assistant replied: ${JSON.stringify(turn.assistant)}`,
72
+ '',
73
+ 'Extract durable facts about the USER from the above. Return strictly valid JSON.',
74
+ ].join('\n');
75
+ const response = await extractor.prompt(prompt);
76
+ const parsed = wrapper.parse(response.text);
77
+ const surviving = parsed.facts.filter(f => f.score >= threshold);
78
+ if (surviving.length === 0) {
79
+ opts.onExtracted?.([]);
80
+ return;
81
+ }
82
+ const tagsFromSpec = spec.tags ?? [];
83
+ const written = [];
84
+ for (const f of surviving) {
85
+ const merged = mergeTags(f.tags, tagsFromSpec);
86
+ const entry = await mem.remember(spec.user, f.fact, {
87
+ score: f.score,
88
+ ...(merged.length > 0 ? { tags: merged } : {}),
89
+ });
90
+ written.push(entry);
91
+ }
92
+ opts.onExtracted?.(written);
93
+ }
94
+ catch (err) {
95
+ opts.onError?.(err);
96
+ // Never break the parent run.
97
+ }
98
+ },
99
+ };
100
+ }
101
+ // ─── Helpers ──────────────────────────────────────────────
102
+ /**
103
+ * Walk `messages` from the end and return the most recent
104
+ * `(user, assistant)` pair where the assistant message follows the
105
+ * user message. Skips trailing `tool` messages so multi-step tool
106
+ * loops still surface the original user request and the model's
107
+ * final synthesis.
108
+ *
109
+ * Returns `null` when:
110
+ * - the loop didn't reach a final assistant message (stopped on a
111
+ * client-tool pause, approval gate, or handoff), or
112
+ * - the assistant message has no extractable text content.
113
+ */
114
+ function extractLatestTurn(messages) {
115
+ let assistantText = null;
116
+ let lastAssistantIdx = -1;
117
+ // Walk backwards looking for the LAST assistant message that's a
118
+ // text reply (not a tool-calls message).
119
+ for (let i = messages.length - 1; i >= 0; i--) {
120
+ const m = messages[i];
121
+ if (!m || m.role !== 'assistant')
122
+ continue;
123
+ if (m.toolCalls && m.toolCalls.length > 0)
124
+ continue; // tool-call step, not a final reply
125
+ const text = contentToString(m.content);
126
+ if (text.length === 0)
127
+ continue;
128
+ assistantText = text;
129
+ lastAssistantIdx = i;
130
+ break;
131
+ }
132
+ if (assistantText === null || lastAssistantIdx === -1)
133
+ return null;
134
+ // Find the most recent user message before that assistant message.
135
+ for (let i = lastAssistantIdx - 1; i >= 0; i--) {
136
+ const m = messages[i];
137
+ if (!m || m.role !== 'user')
138
+ continue;
139
+ const text = contentToString(m.content);
140
+ if (text.length === 0)
141
+ continue;
142
+ return { user: text, assistant: assistantText };
143
+ }
144
+ return null;
145
+ }
146
+ function contentToString(content) {
147
+ if (typeof content === 'string')
148
+ return content;
149
+ const out = [];
150
+ for (const p of content) {
151
+ if (p.type === 'text' && typeof p.text === 'string')
152
+ out.push(p.text);
153
+ }
154
+ return out.join('\n');
155
+ }
156
+ function mergeTags(modelTags, specTags) {
157
+ if (!modelTags || modelTags.length === 0)
158
+ return [...specTags];
159
+ if (specTags.length === 0)
160
+ return [...modelTags];
161
+ return Array.from(new Set([...modelTags, ...specTags]));
162
+ }
163
+ //# sourceMappingURL=memory-extract.js.map