@dbx-tools/appkit-mastra 0.1.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.
package/src/memory.ts ADDED
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Lakebase-backed Mastra memory wiring.
3
+ *
4
+ * Provides a {@link MemoryBuilder} that mints one `Memory` per agent
5
+ * with two independent knobs:
6
+ *
7
+ * - **Storage** (threads / messages via `PostgresStore`): defaults to
8
+ * **per-agent** namespacing via `schemaName: "mastra_<agentId>"` so
9
+ * conversation history stays isolated between agents in the same
10
+ * database. `PostgresStore` auto-creates the schema with
11
+ * `CREATE SCHEMA IF NOT EXISTS` on init.
12
+ * - **Memory** (semantic recall via `PgVector`): defaults to a single
13
+ * **shared** instance across every agent. Cross-agent recall on one
14
+ * index is almost always what users want; opt into per-agent recall
15
+ * by passing a {@link MastraMemoryConfigOverride} on the agent.
16
+ *
17
+ * Plugin-level `config.storage` / `config.memory` act as the baseline
18
+ * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
19
+ * is registered); per-agent settings cascade on top of that.
20
+ */
21
+
22
+ import { lakebase } from "@databricks/appkit";
23
+ import { pluginUtils } from "@dbx-tools/appkit-shared";
24
+ import { fastembed } from "@mastra/fastembed";
25
+ import { Memory } from "@mastra/memory";
26
+ import { PgVector, PostgresStore } from "@mastra/pg";
27
+ import { randomUUID } from "node:crypto";
28
+ import type { Pool } from "pg";
29
+
30
+ import type {
31
+ MastraAgentDefinition,
32
+ MastraMemoryConfigOverride,
33
+ } from "./agents.js";
34
+ import type { MastraPluginConfig } from "./config.js";
35
+
36
+ /** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
37
+ export type LakebasePool = ReturnType<
38
+ InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]
39
+ >["pool"];
40
+
41
+ /** Effective per-knob setting after the plugin/agent cascade. */
42
+ type StorageSetting = MastraAgentDefinition["storage"];
43
+ type MemorySetting = MastraAgentDefinition["memory"];
44
+
45
+ /**
46
+ * True when any plugin-level or per-agent setting could need the
47
+ * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
48
+ * builder also acquires lazily so missed cases still fail with a
49
+ * clear lakebase-not-registered error.
50
+ */
51
+ export function needsLakebase(config: MastraPluginConfig): boolean {
52
+ if (settingNeedsSharedPool(config.storage)) return true;
53
+ if (settingNeedsSharedPool(config.memory)) return true;
54
+ const defs = collectAgentDefinitions(config);
55
+ return defs.some(
56
+ (d) =>
57
+ settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory),
58
+ );
59
+ }
60
+
61
+ /**
62
+ * Look up the `lakebase` plugin and return its managed `pg.Pool`.
63
+ * Throws when the sibling plugin is not registered; enabling
64
+ * `storage` / `memory` without lakebase is a wiring bug, not a runtime
65
+ * condition we can recover from.
66
+ */
67
+ export function resolveLakebasePool(
68
+ context: pluginUtils.PluginContextLike | undefined,
69
+ caller: MastraPluginConfig,
70
+ ): LakebasePool {
71
+ return pluginUtils.require(context, lakebase, caller).exports().pool;
72
+ }
73
+
74
+ /**
75
+ * Construct a per-agent {@link Memory} factory. Caches the shared
76
+ * `PgVector` singleton (built on first need) and the lazily-resolved
77
+ * Lakebase pool so each agent build is O(1) after the first.
78
+ */
79
+ export function createMemoryBuilder(
80
+ config: MastraPluginConfig,
81
+ context: pluginUtils.PluginContextLike | undefined,
82
+ ): MemoryBuilder {
83
+ return new MemoryBuilder(config, context);
84
+ }
85
+
86
+ /**
87
+ * Builds one `Memory` per agent. Per-instance state keeps the shared
88
+ * `PgVector` and the resolved Lakebase pool alive across calls so
89
+ * registering N agents stays cheap.
90
+ */
91
+ export class MemoryBuilder {
92
+ private sharedVector: PgVector | undefined;
93
+ private pool: LakebasePool | undefined;
94
+
95
+ constructor(
96
+ private readonly config: MastraPluginConfig,
97
+ private readonly context: pluginUtils.PluginContextLike | undefined,
98
+ ) {}
99
+
100
+ /**
101
+ * Build a `Memory` for `agentId` after the plugin/agent cascade.
102
+ * Returns `undefined` when the agent has neither storage nor a
103
+ * vector store enabled - Mastra accepts a missing `memory` field
104
+ * and treats the agent as stateless.
105
+ */
106
+ forAgent(agentId: string, def: MastraAgentDefinition): Memory | undefined {
107
+ const storageSetting = def.storage ?? this.config.storage;
108
+ const memorySetting = def.memory ?? this.config.memory;
109
+
110
+ const storage = this.buildStorage(agentId, storageSetting);
111
+ const vector = this.buildVector(memorySetting);
112
+ if (!storage && !vector) return undefined;
113
+
114
+ return new Memory({
115
+ ...(storage ? { storage } : {}),
116
+ ...(vector ? { vector, embedder: fastembed } : {}),
117
+ options: {
118
+ lastMessages: 10,
119
+ ...(vector
120
+ ? { semanticRecall: { topK: 3, messageRange: 2 } }
121
+ : {}),
122
+ },
123
+ });
124
+ }
125
+
126
+ private buildStorage(
127
+ agentId: string,
128
+ setting: StorageSetting,
129
+ ): PostgresStore | undefined {
130
+ if (!setting) return undefined;
131
+ if (typeof setting === "boolean") {
132
+ return new PostgresStore({
133
+ id: `mastra-store__${agentId}`,
134
+ schemaName: `mastra_${agentId}`,
135
+ pool: this.requirePool() as Pool,
136
+ });
137
+ }
138
+ // Cast: `withId` guarantees `id` is set, but the distributive
139
+ // Omit + `id?: string` shape doesn't structurally narrow to the
140
+ // discriminated union members. Runtime shape is identical.
141
+ return new PostgresStore(
142
+ withId(setting, `mastra-store__${agentId}`) as ConstructorParameters<
143
+ typeof PostgresStore
144
+ >[0],
145
+ );
146
+ }
147
+
148
+ /**
149
+ * Resolve the agent's vector store. Cascade:
150
+ *
151
+ * - falsy: no vector.
152
+ * - `boolean` / `undefined-inheriting-true`: return the shared
153
+ * singleton (built lazily on first call). All agents that
154
+ * default-enable memory write into and recall from one index.
155
+ * - object: build a dedicated `PgVector` for this agent.
156
+ */
157
+ private buildVector(setting: MemorySetting): PgVector | undefined {
158
+ if (!setting) return undefined;
159
+ if (typeof setting === "boolean") return this.getSharedVector();
160
+ return buildPgVector(setting);
161
+ }
162
+
163
+ private getSharedVector(): PgVector {
164
+ if (!this.sharedVector) {
165
+ this.sharedVector = buildSharedPgVector(this.requirePool());
166
+ }
167
+ return this.sharedVector;
168
+ }
169
+
170
+ private requirePool(): LakebasePool {
171
+ if (!this.pool) {
172
+ this.pool = resolveLakebasePool(this.context, this.config);
173
+ }
174
+ return this.pool;
175
+ }
176
+ }
177
+
178
+ /**
179
+ * Build the shared `PgVector` that backs the default
180
+ * `def.memory === true` case across every agent.
181
+ *
182
+ * `PgVector`'s constructor accepts only connection-style configs
183
+ * (`HostConfig` / `ConnectionStringConfig` / `ClientConfig`); there is
184
+ * no `{ pool }` shorthand the way `PostgresStore` has one. Worse, the
185
+ * constructor synchronously kicks off a `cacheWarmupPromise` IIFE that
186
+ * calls `this.pool.connect()` before returning, so we can't cleanly
187
+ * hand it an inert config and patch the pool afterwards.
188
+ *
189
+ * The trick: pass illegal-but-validation-passing placeholders so the
190
+ * warmup's `net.connect()` rejects synchronously with `RangeError`
191
+ * (Node validates `0 <= port < 65536`). The IIFE's `catch {}` swallows
192
+ * it, no DNS lookup or TCP attempt happens, and we then swap
193
+ * `pgVector.pool` to the lakebase pool. Every subsequent `PgVector`
194
+ * method reads `this.pool` at call time, so all real I/O goes through
195
+ * the lakebase pool from then on. The placeholder pool is `.end()`'d
196
+ * so its socket book-keeping is released.
197
+ */
198
+ function buildSharedPgVector(pool: LakebasePool): PgVector {
199
+ const vector = new PgVector({
200
+ id: `pg${randomUUID()}`,
201
+ host: "-1",
202
+ port: -1,
203
+ database: "_",
204
+ user: "_",
205
+ password: "_",
206
+ });
207
+ const placeholder = vector.pool;
208
+ vector.pool = pool as Pool;
209
+ void placeholder.end().catch(() => undefined);
210
+ return vector;
211
+ }
212
+
213
+ /** Per-agent dedicated `PgVector` (rare; opt-in via object override). */
214
+ function buildPgVector(setting: MastraMemoryConfigOverride): PgVector {
215
+ return new PgVector(
216
+ withId(setting, `pg-vector__${randomUUID()}`) as ConstructorParameters<
217
+ typeof PgVector
218
+ >[0],
219
+ );
220
+ }
221
+
222
+ /** True when this setting requires the shared Lakebase pool. */
223
+ function settingNeedsSharedPool(
224
+ setting: StorageSetting | MemorySetting | undefined,
225
+ ): boolean {
226
+ return setting === true;
227
+ }
228
+
229
+ /** Walk the three shapes of `config.agents` into a flat list. */
230
+ function collectAgentDefinitions(
231
+ config: MastraPluginConfig,
232
+ ): MastraAgentDefinition[] {
233
+ const agents = config.agents;
234
+ if (!agents) return [];
235
+ if (Array.isArray(agents)) return agents;
236
+ if (typeof (agents as MastraAgentDefinition).instructions === "string") {
237
+ return [agents as MastraAgentDefinition];
238
+ }
239
+ return Object.values(agents as Record<string, MastraAgentDefinition>);
240
+ }
241
+
242
+ /** Fill in a default `id` when the caller didn't supply one. */
243
+ function withId<T extends { id?: string }>(value: T, fallback: string): T {
244
+ return value.id ? value : { ...value, id: fallback };
245
+ }