@dbx-tools/appkit-mastra 0.1.39 → 0.1.41
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/dist/src/memory.d.ts +34 -25
- package/dist/src/memory.js +48 -35
- package/dist/src/plugin.d.ts +25 -0
- package/dist/src/plugin.js +71 -9
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +6 -5
- package/src/memory.ts +48 -45
- package/src/plugin.ts +82 -9
package/package.json
CHANGED
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
11
|
"name": "@dbx-tools/appkit-mastra",
|
|
12
|
-
"version": "0.1.
|
|
12
|
+
"version": "0.1.41",
|
|
13
13
|
"dependencies": {
|
|
14
14
|
"@databricks/sdk-experimental": "^0.17",
|
|
15
|
-
"@dbx-tools/appkit-mastra-shared": "0.1.
|
|
16
|
-
"@dbx-tools/genie": "0.1.
|
|
17
|
-
"@dbx-tools/genie-shared": "0.1.
|
|
18
|
-
"@dbx-tools/shared": "0.1.
|
|
15
|
+
"@dbx-tools/appkit-mastra-shared": "0.1.41",
|
|
16
|
+
"@dbx-tools/genie": "0.1.41",
|
|
17
|
+
"@dbx-tools/genie-shared": "0.1.41",
|
|
18
|
+
"@dbx-tools/shared": "0.1.41",
|
|
19
19
|
"@mastra/ai-sdk": "^1",
|
|
20
20
|
"@mastra/core": "^1",
|
|
21
21
|
"@mastra/express": "^1",
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
"@mastra/otel-bridge": "^1",
|
|
26
26
|
"@mastra/pg": "^1",
|
|
27
27
|
"fuse.js": "^7.0.0",
|
|
28
|
+
"pg": "^8.20.0",
|
|
28
29
|
"zod": "^4.3.6"
|
|
29
30
|
},
|
|
30
31
|
"devDependencies": {
|
package/src/memory.ts
CHANGED
|
@@ -27,23 +27,46 @@
|
|
|
27
27
|
* is registered); per-agent settings cascade on top of that.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
30
|
+
import { getUsernameWithApiLookup } from "@databricks/appkit";
|
|
31
|
+
import { logUtils } from "@dbx-tools/shared";
|
|
32
32
|
import { fastembed } from "@mastra/fastembed";
|
|
33
33
|
import { Memory } from "@mastra/memory";
|
|
34
34
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
35
35
|
import { randomUUID } from "node:crypto";
|
|
36
|
-
import
|
|
36
|
+
import { Pool, type PoolConfig } from "pg";
|
|
37
37
|
|
|
38
38
|
import type { MastraAgentDefinition, MastraMemoryConfigOverride } from "./agents.js";
|
|
39
39
|
import type { MastraPluginConfig } from "./config.js";
|
|
40
40
|
|
|
41
41
|
const log = logUtils.logger("mastra/memory");
|
|
42
42
|
|
|
43
|
-
/**
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Build a dedicated **service-principal** Lakebase pool for Mastra
|
|
45
|
+
* memory from the lakebase plugin's resolved SP pg config.
|
|
46
|
+
*
|
|
47
|
+
* The plugin's `exports().pool` is a `RoutingPool` that switches to
|
|
48
|
+
* the per-user (OBO) pool whenever a query runs inside an `asUser`
|
|
49
|
+
* scope - exactly the context the mastra plugin establishes around
|
|
50
|
+
* every chat turn. Memory (threads / messages + semantic recall) must
|
|
51
|
+
* instead always act as the app service principal: it owns the
|
|
52
|
+
* auto-created `mastra_*` schemas (a per-user role usually can't
|
|
53
|
+
* `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
|
|
54
|
+
* request's OBO identity.
|
|
55
|
+
*
|
|
56
|
+
* `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
|
|
57
|
+
* **outside** any `asUser` scope (i.e. during setup), so it carries
|
|
58
|
+
* the SP connection target, OAuth token-refresh `password` callback,
|
|
59
|
+
* and any `lakebase({ pool })` tuning overrides - all of which this
|
|
60
|
+
* pool inherits. See the call site in `plugin.ts`.
|
|
61
|
+
*/
|
|
62
|
+
export async function createServicePrincipalPool(pgConfig: PoolConfig): Promise<Pool> {
|
|
63
|
+
// `getPgConfig()` resolves the SP username synchronously from
|
|
64
|
+
// `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
|
|
65
|
+
// lookup (e.g. local dev authenticating via PAT) so the pool always
|
|
66
|
+
// has an identity to connect with.
|
|
67
|
+
const user = pgConfig.user ?? (await getUsernameWithApiLookup());
|
|
68
|
+
return new Pool({ ...pgConfig, user });
|
|
69
|
+
}
|
|
47
70
|
|
|
48
71
|
/** Effective per-knob setting after the plugin/agent cascade. */
|
|
49
72
|
type StorageSetting = MastraAgentDefinition["storage"];
|
|
@@ -51,9 +74,9 @@ type MemorySetting = MastraAgentDefinition["memory"];
|
|
|
51
74
|
|
|
52
75
|
/**
|
|
53
76
|
* True when any plugin-level or per-agent setting could need the
|
|
54
|
-
* Lakebase pool. Used by `plugin.ts` to gate
|
|
55
|
-
*
|
|
56
|
-
*
|
|
77
|
+
* Lakebase pool. Used by `plugin.ts` to gate creation of the
|
|
78
|
+
* service-principal pool and the {@link MemoryBuilder} that consumes
|
|
79
|
+
* it; when false neither is built.
|
|
57
80
|
*/
|
|
58
81
|
export function needsLakebase(config: MastraPluginConfig): boolean {
|
|
59
82
|
if (settingNeedsSharedPool(config.storage)) return true;
|
|
@@ -65,42 +88,29 @@ export function needsLakebase(config: MastraPluginConfig): boolean {
|
|
|
65
88
|
}
|
|
66
89
|
|
|
67
90
|
/**
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*/
|
|
73
|
-
export function resolveLakebasePool(
|
|
74
|
-
context: appkitUtils.PluginContextLike | undefined,
|
|
75
|
-
caller: MastraPluginConfig,
|
|
76
|
-
): LakebasePool {
|
|
77
|
-
return appkitUtils.require(context, lakebase, caller).exports().pool;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Construct a per-agent {@link Memory} factory. Caches the shared
|
|
82
|
-
* `PgVector` singleton (built on first need) and the lazily-resolved
|
|
83
|
-
* Lakebase pool so each agent build is O(1) after the first.
|
|
91
|
+
* Construct a per-agent {@link Memory} factory bound to the supplied
|
|
92
|
+
* service-principal pool (see {@link createServicePrincipalPool}).
|
|
93
|
+
* Caches the shared `PgVector` singleton (built on first need) so each
|
|
94
|
+
* agent build is O(1) after the first.
|
|
84
95
|
*/
|
|
85
96
|
export function createMemoryBuilder(
|
|
86
97
|
config: MastraPluginConfig,
|
|
87
|
-
|
|
98
|
+
servicePrincipalPool: Pool,
|
|
88
99
|
): MemoryBuilder {
|
|
89
|
-
return new MemoryBuilder(config,
|
|
100
|
+
return new MemoryBuilder(config, servicePrincipalPool);
|
|
90
101
|
}
|
|
91
102
|
|
|
92
103
|
/**
|
|
93
|
-
* Builds one `Memory` per agent
|
|
94
|
-
*
|
|
95
|
-
* registering N agents stays cheap.
|
|
104
|
+
* Builds one `Memory` per agent against a shared service-principal
|
|
105
|
+
* Lakebase pool. Per-instance state keeps the shared `PgVector` alive
|
|
106
|
+
* across calls so registering N agents stays cheap.
|
|
96
107
|
*/
|
|
97
108
|
export class MemoryBuilder {
|
|
98
109
|
private sharedVector: PgVector | undefined;
|
|
99
|
-
private pool: LakebasePool | undefined;
|
|
100
110
|
|
|
101
111
|
constructor(
|
|
102
112
|
private readonly config: MastraPluginConfig,
|
|
103
|
-
private readonly
|
|
113
|
+
private readonly servicePrincipalPool: Pool,
|
|
104
114
|
) {}
|
|
105
115
|
|
|
106
116
|
/**
|
|
@@ -133,7 +143,7 @@ export class MemoryBuilder {
|
|
|
133
143
|
return new PostgresStore({
|
|
134
144
|
id: "mastra-store__instance",
|
|
135
145
|
schemaName: "mastra_instance",
|
|
136
|
-
pool: this.
|
|
146
|
+
pool: this.servicePrincipalPool,
|
|
137
147
|
});
|
|
138
148
|
}
|
|
139
149
|
|
|
@@ -179,7 +189,7 @@ export class MemoryBuilder {
|
|
|
179
189
|
return new PostgresStore({
|
|
180
190
|
id: `mastra-store__${agentId}`,
|
|
181
191
|
schemaName: `mastra_${agentId}`,
|
|
182
|
-
pool: this.
|
|
192
|
+
pool: this.servicePrincipalPool,
|
|
183
193
|
});
|
|
184
194
|
}
|
|
185
195
|
// Cast: `withId` guarantees `id` is set, but the distributive
|
|
@@ -209,17 +219,10 @@ export class MemoryBuilder {
|
|
|
209
219
|
|
|
210
220
|
private getSharedVector(): PgVector {
|
|
211
221
|
if (!this.sharedVector) {
|
|
212
|
-
this.sharedVector = buildSharedPgVector(this.
|
|
222
|
+
this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
|
|
213
223
|
}
|
|
214
224
|
return this.sharedVector;
|
|
215
225
|
}
|
|
216
|
-
|
|
217
|
-
private requirePool(): LakebasePool {
|
|
218
|
-
if (!this.pool) {
|
|
219
|
-
this.pool = resolveLakebasePool(this.context, this.config);
|
|
220
|
-
}
|
|
221
|
-
return this.pool;
|
|
222
|
-
}
|
|
223
226
|
}
|
|
224
227
|
|
|
225
228
|
/**
|
|
@@ -242,7 +245,7 @@ export class MemoryBuilder {
|
|
|
242
245
|
* the lakebase pool from then on. The placeholder pool is `.end()`'d
|
|
243
246
|
* so its socket book-keeping is released.
|
|
244
247
|
*/
|
|
245
|
-
function buildSharedPgVector(pool:
|
|
248
|
+
function buildSharedPgVector(pool: Pool): PgVector {
|
|
246
249
|
const vector = new PgVector({
|
|
247
250
|
id: `pg${randomUUID()}`,
|
|
248
251
|
host: "-1",
|
|
@@ -252,7 +255,7 @@ function buildSharedPgVector(pool: LakebasePool): PgVector {
|
|
|
252
255
|
password: "_",
|
|
253
256
|
});
|
|
254
257
|
const placeholder = vector.pool;
|
|
255
|
-
vector.pool = pool
|
|
258
|
+
vector.pool = pool;
|
|
256
259
|
void placeholder.end().catch(() => undefined);
|
|
257
260
|
return vector;
|
|
258
261
|
}
|
package/src/plugin.ts
CHANGED
|
@@ -37,11 +37,12 @@ import {
|
|
|
37
37
|
type PluginManifest,
|
|
38
38
|
type ResourceRequirement,
|
|
39
39
|
} from "@databricks/appkit";
|
|
40
|
-
import { appkitUtils, logUtils } from "@dbx-tools/shared";
|
|
40
|
+
import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
|
|
41
41
|
import { chatRoute } from "@mastra/ai-sdk";
|
|
42
42
|
import type { Agent } from "@mastra/core/agent";
|
|
43
43
|
import { Mastra } from "@mastra/core/mastra";
|
|
44
44
|
import express from "express";
|
|
45
|
+
import type { Pool } from "pg";
|
|
45
46
|
|
|
46
47
|
import type {
|
|
47
48
|
MastraClientConfig,
|
|
@@ -52,7 +53,11 @@ import { fetchChart } from "./chart.js";
|
|
|
52
53
|
import type { MastraPluginConfig } from "./config.js";
|
|
53
54
|
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
|
|
54
55
|
import { historyRoute } from "./history.js";
|
|
55
|
-
import {
|
|
56
|
+
import {
|
|
57
|
+
createMemoryBuilder,
|
|
58
|
+
createServicePrincipalPool,
|
|
59
|
+
needsLakebase,
|
|
60
|
+
} from "./memory.js";
|
|
56
61
|
import { buildObservability } from "./observability.js";
|
|
57
62
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
58
63
|
import {
|
|
@@ -125,6 +130,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
125
130
|
private mastra: Mastra | null = null;
|
|
126
131
|
private mastraApp: express.Express | null = null;
|
|
127
132
|
private mastraServer: MastraServer | null = null;
|
|
133
|
+
/**
|
|
134
|
+
* Dedicated service-principal Lakebase pool backing Mastra memory /
|
|
135
|
+
* storage. Built once in {@link buildAgentAndServer} (outside any
|
|
136
|
+
* `asUser` scope, so it never inherits a request's OBO identity) and
|
|
137
|
+
* drained in {@link abortActiveOperations}. `null` until setup runs
|
|
138
|
+
* or when Lakebase isn't needed.
|
|
139
|
+
*/
|
|
140
|
+
private servicePrincipalPool: Pool | null = null;
|
|
128
141
|
|
|
129
142
|
override async setup(): Promise<void> {
|
|
130
143
|
// Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
|
|
@@ -150,6 +163,26 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
150
163
|
if (this.config.memory === undefined) this.config.memory = true;
|
|
151
164
|
}
|
|
152
165
|
|
|
166
|
+
/**
|
|
167
|
+
* Drain the memory service-principal pool on shutdown. AppKit calls
|
|
168
|
+
* this during teardown; the lakebase plugin closes its own SP / OBO
|
|
169
|
+
* pools the same way. Fire-and-forget so shutdown isn't blocked on a
|
|
170
|
+
* slow drain, and clear the handle so a re-`setup()` rebuilds it.
|
|
171
|
+
*/
|
|
172
|
+
override abortActiveOperations(): void {
|
|
173
|
+
super.abortActiveOperations();
|
|
174
|
+
if (this.servicePrincipalPool) {
|
|
175
|
+
this.log.info("closing memory SP pool");
|
|
176
|
+
const pool = this.servicePrincipalPool;
|
|
177
|
+
this.servicePrincipalPool = null;
|
|
178
|
+
pool.end().catch((err) => {
|
|
179
|
+
this.log.error("error closing memory SP pool", {
|
|
180
|
+
error: commonUtils.errorMessage(err),
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
153
186
|
override exports() {
|
|
154
187
|
return {
|
|
155
188
|
/**
|
|
@@ -339,10 +372,37 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
339
372
|
|
|
340
373
|
router.use((req, res, next) => {
|
|
341
374
|
if (!this.mastraApp) return res.status(503).end();
|
|
342
|
-
|
|
375
|
+
// Dispatch through a real method, NOT the `mastraApp` property. The
|
|
376
|
+
// AppKit `asUser(req)` proxy wraps function-valued props with
|
|
377
|
+
// `value.bind(target)`. `mastraApp` is an express app whose `.bind` is
|
|
378
|
+
// the HTTP BIND route registrar (express defines a method per HTTP verb,
|
|
379
|
+
// and BIND is one), not `Function.prototype.bind` - so binding it through
|
|
380
|
+
// the proxy registers a bogus route and crashes `pathToRegexp`
|
|
381
|
+
// ("path must be a string ..."). This only manifests in production where
|
|
382
|
+
// an OBO token makes `userScopedSelf` return the proxy. `dispatchMastra`
|
|
383
|
+
// is a plain method (its `.bind` is the normal one) and invokes
|
|
384
|
+
// `this.mastraApp` off the real target, keeping the OBO scope active.
|
|
385
|
+
return this.userScopedSelf(req).dispatchMastra(req, res, next);
|
|
343
386
|
});
|
|
344
387
|
}
|
|
345
388
|
|
|
389
|
+
/**
|
|
390
|
+
* Invoke the Mastra express sub-app. Exists as a method (instead of reading
|
|
391
|
+
* `this.mastraApp` through the `asUser(req)` proxy at the call site) so the
|
|
392
|
+
* proxy binds this plain method - whose `.bind` is `Function.prototype.bind`
|
|
393
|
+
* - rather than the express app, whose `.bind` is the HTTP BIND route
|
|
394
|
+
* registrar (see the note in `injectRoutes`). Runs inside the user scope so
|
|
395
|
+
* `getExecutionContext()` returns the OBO client for the agent/model
|
|
396
|
+
* resolvers.
|
|
397
|
+
*/
|
|
398
|
+
private dispatchMastra(
|
|
399
|
+
req: express.Request,
|
|
400
|
+
res: express.Response,
|
|
401
|
+
next: express.NextFunction,
|
|
402
|
+
): void {
|
|
403
|
+
this.mastraApp!(req, res, next);
|
|
404
|
+
}
|
|
405
|
+
|
|
346
406
|
/**
|
|
347
407
|
* Implementation backing the `/suggestions` route. Runs inside the
|
|
348
408
|
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
@@ -424,12 +484,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
424
484
|
}
|
|
425
485
|
|
|
426
486
|
private async buildAgentAndServer(): Promise<void> {
|
|
427
|
-
// Per-agent memory factory.
|
|
428
|
-
//
|
|
429
|
-
//
|
|
430
|
-
//
|
|
431
|
-
|
|
432
|
-
|
|
487
|
+
// Per-agent memory factory. When any storage / memory setting needs
|
|
488
|
+
// Postgres, stand up a dedicated service-principal pool first so
|
|
489
|
+
// memory acts as the app SP (owner of the `mastra_*` schemas),
|
|
490
|
+
// never the per-request OBO identity the chat turn runs under.
|
|
491
|
+
// `getPgConfig()` is read here, outside any `asUser` scope, so it
|
|
492
|
+
// returns the SP connection target + token refresh plus any
|
|
493
|
+
// `lakebase({ pool })` overrides; `require` turns a missing
|
|
494
|
+
// sibling into a clear wiring error. The builder caches the shared
|
|
495
|
+
// `PgVector` singleton so registering N agents stays cheap. See
|
|
496
|
+
// `./memory.js`.
|
|
497
|
+
if (needsLakebase(this.config)) {
|
|
498
|
+
const spPgConfig = appkitUtils
|
|
499
|
+
.require(this.context, lakebase, this.config)
|
|
500
|
+
.exports()
|
|
501
|
+
.getPgConfig();
|
|
502
|
+
this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
|
|
503
|
+
}
|
|
504
|
+
const memoryBuilder = this.servicePrincipalPool
|
|
505
|
+
? createMemoryBuilder(this.config, this.servicePrincipalPool)
|
|
433
506
|
: undefined;
|
|
434
507
|
|
|
435
508
|
this.log.debug("build:start", {
|