@dbx-tools/appkit-mastra 0.1.40 → 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.
@@ -26,45 +26,55 @@
26
26
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
- import { lakebase } from "@databricks/appkit";
30
- import { appkitUtils } from "@dbx-tools/shared";
31
29
  import { Memory } from "@mastra/memory";
32
30
  import { PostgresStore } from "@mastra/pg";
31
+ import { Pool, type PoolConfig } from "pg";
33
32
  import type { MastraAgentDefinition } from "./agents.js";
34
33
  import type { MastraPluginConfig } from "./config.js";
35
- /** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
36
- export type LakebasePool = ReturnType<InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]>["pool"];
37
34
  /**
38
- * True when any plugin-level or per-agent setting could need the
39
- * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
40
- * builder also acquires lazily so missed cases still fail with a
41
- * clear lakebase-not-registered error.
35
+ * Build a dedicated **service-principal** Lakebase pool for Mastra
36
+ * memory from the lakebase plugin's resolved SP pg config.
37
+ *
38
+ * The plugin's `exports().pool` is a `RoutingPool` that switches to
39
+ * the per-user (OBO) pool whenever a query runs inside an `asUser`
40
+ * scope - exactly the context the mastra plugin establishes around
41
+ * every chat turn. Memory (threads / messages + semantic recall) must
42
+ * instead always act as the app service principal: it owns the
43
+ * auto-created `mastra_*` schemas (a per-user role usually can't
44
+ * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
45
+ * request's OBO identity.
46
+ *
47
+ * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
48
+ * **outside** any `asUser` scope (i.e. during setup), so it carries
49
+ * the SP connection target, OAuth token-refresh `password` callback,
50
+ * and any `lakebase({ pool })` tuning overrides - all of which this
51
+ * pool inherits. See the call site in `plugin.ts`.
42
52
  */
43
- export declare function needsLakebase(config: MastraPluginConfig): boolean;
53
+ export declare function createServicePrincipalPool(pgConfig: PoolConfig): Promise<Pool>;
44
54
  /**
45
- * Look up the `lakebase` plugin and return its managed `pg.Pool`.
46
- * Throws when the sibling plugin is not registered; enabling
47
- * `storage` / `memory` without lakebase is a wiring bug, not a runtime
48
- * condition we can recover from.
55
+ * True when any plugin-level or per-agent setting could need the
56
+ * Lakebase pool. Used by `plugin.ts` to gate creation of the
57
+ * service-principal pool and the {@link MemoryBuilder} that consumes
58
+ * it; when false neither is built.
49
59
  */
50
- export declare function resolveLakebasePool(context: appkitUtils.PluginContextLike | undefined, caller: MastraPluginConfig): LakebasePool;
60
+ export declare function needsLakebase(config: MastraPluginConfig): boolean;
51
61
  /**
52
- * Construct a per-agent {@link Memory} factory. Caches the shared
53
- * `PgVector` singleton (built on first need) and the lazily-resolved
54
- * Lakebase pool so each agent build is O(1) after the first.
62
+ * Construct a per-agent {@link Memory} factory bound to the supplied
63
+ * service-principal pool (see {@link createServicePrincipalPool}).
64
+ * Caches the shared `PgVector` singleton (built on first need) so each
65
+ * agent build is O(1) after the first.
55
66
  */
56
- export declare function createMemoryBuilder(config: MastraPluginConfig, context: appkitUtils.PluginContextLike | undefined): MemoryBuilder;
67
+ export declare function createMemoryBuilder(config: MastraPluginConfig, servicePrincipalPool: Pool): MemoryBuilder;
57
68
  /**
58
- * Builds one `Memory` per agent. Per-instance state keeps the shared
59
- * `PgVector` and the resolved Lakebase pool alive across calls so
60
- * registering N agents stays cheap.
69
+ * Builds one `Memory` per agent against a shared service-principal
70
+ * Lakebase pool. Per-instance state keeps the shared `PgVector` alive
71
+ * across calls so registering N agents stays cheap.
61
72
  */
62
73
  export declare class MemoryBuilder {
63
74
  private readonly config;
64
- private readonly context;
75
+ private readonly servicePrincipalPool;
65
76
  private sharedVector;
66
- private pool;
67
- constructor(config: MastraPluginConfig, context: appkitUtils.PluginContextLike | undefined);
77
+ constructor(config: MastraPluginConfig, servicePrincipalPool: Pool);
68
78
  /**
69
79
  * Build a `Memory` for `agentId` after the plugin/agent cascade.
70
80
  * Returns `undefined` when the agent has neither storage nor a
@@ -96,5 +106,4 @@ export declare class MemoryBuilder {
96
106
  */
97
107
  private buildVector;
98
108
  private getSharedVector;
99
- private requirePool;
100
109
  }
@@ -26,18 +26,46 @@
26
26
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
- import { lakebase } from "@databricks/appkit";
30
- import { appkitUtils, logUtils } from "@dbx-tools/shared";
29
+ import { getUsernameWithApiLookup } from "@databricks/appkit";
30
+ import { logUtils } from "@dbx-tools/shared";
31
31
  import { fastembed } from "@mastra/fastembed";
32
32
  import { Memory } from "@mastra/memory";
33
33
  import { PgVector, PostgresStore } from "@mastra/pg";
34
34
  import { randomUUID } from "node:crypto";
35
+ import { Pool } from "pg";
35
36
  const log = logUtils.logger("mastra/memory");
37
+ /**
38
+ * Build a dedicated **service-principal** Lakebase pool for Mastra
39
+ * memory from the lakebase plugin's resolved SP pg config.
40
+ *
41
+ * The plugin's `exports().pool` is a `RoutingPool` that switches to
42
+ * the per-user (OBO) pool whenever a query runs inside an `asUser`
43
+ * scope - exactly the context the mastra plugin establishes around
44
+ * every chat turn. Memory (threads / messages + semantic recall) must
45
+ * instead always act as the app service principal: it owns the
46
+ * auto-created `mastra_*` schemas (a per-user role usually can't
47
+ * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
48
+ * request's OBO identity.
49
+ *
50
+ * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
51
+ * **outside** any `asUser` scope (i.e. during setup), so it carries
52
+ * the SP connection target, OAuth token-refresh `password` callback,
53
+ * and any `lakebase({ pool })` tuning overrides - all of which this
54
+ * pool inherits. See the call site in `plugin.ts`.
55
+ */
56
+ export async function createServicePrincipalPool(pgConfig) {
57
+ // `getPgConfig()` resolves the SP username synchronously from
58
+ // `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
59
+ // lookup (e.g. local dev authenticating via PAT) so the pool always
60
+ // has an identity to connect with.
61
+ const user = pgConfig.user ?? (await getUsernameWithApiLookup());
62
+ return new Pool({ ...pgConfig, user });
63
+ }
36
64
  /**
37
65
  * True when any plugin-level or per-agent setting could need the
38
- * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
39
- * builder also acquires lazily so missed cases still fail with a
40
- * clear lakebase-not-registered error.
66
+ * Lakebase pool. Used by `plugin.ts` to gate creation of the
67
+ * service-principal pool and the {@link MemoryBuilder} that consumes
68
+ * it; when false neither is built.
41
69
  */
42
70
  export function needsLakebase(config) {
43
71
  if (settingNeedsSharedPool(config.storage))
@@ -48,35 +76,26 @@ export function needsLakebase(config) {
48
76
  return defs.some((d) => settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory));
49
77
  }
50
78
  /**
51
- * Look up the `lakebase` plugin and return its managed `pg.Pool`.
52
- * Throws when the sibling plugin is not registered; enabling
53
- * `storage` / `memory` without lakebase is a wiring bug, not a runtime
54
- * condition we can recover from.
79
+ * Construct a per-agent {@link Memory} factory bound to the supplied
80
+ * service-principal pool (see {@link createServicePrincipalPool}).
81
+ * Caches the shared `PgVector` singleton (built on first need) so each
82
+ * agent build is O(1) after the first.
55
83
  */
56
- export function resolveLakebasePool(context, caller) {
57
- return appkitUtils.require(context, lakebase, caller).exports().pool;
84
+ export function createMemoryBuilder(config, servicePrincipalPool) {
85
+ return new MemoryBuilder(config, servicePrincipalPool);
58
86
  }
59
87
  /**
60
- * Construct a per-agent {@link Memory} factory. Caches the shared
61
- * `PgVector` singleton (built on first need) and the lazily-resolved
62
- * Lakebase pool so each agent build is O(1) after the first.
63
- */
64
- export function createMemoryBuilder(config, context) {
65
- return new MemoryBuilder(config, context);
66
- }
67
- /**
68
- * Builds one `Memory` per agent. Per-instance state keeps the shared
69
- * `PgVector` and the resolved Lakebase pool alive across calls so
70
- * registering N agents stays cheap.
88
+ * Builds one `Memory` per agent against a shared service-principal
89
+ * Lakebase pool. Per-instance state keeps the shared `PgVector` alive
90
+ * across calls so registering N agents stays cheap.
71
91
  */
72
92
  export class MemoryBuilder {
73
93
  config;
74
- context;
94
+ servicePrincipalPool;
75
95
  sharedVector;
76
- pool;
77
- constructor(config, context) {
96
+ constructor(config, servicePrincipalPool) {
78
97
  this.config = config;
79
- this.context = context;
98
+ this.servicePrincipalPool = servicePrincipalPool;
80
99
  }
81
100
  /**
82
101
  * Build a `Memory` for `agentId` after the plugin/agent cascade.
@@ -105,7 +124,7 @@ export class MemoryBuilder {
105
124
  return new PostgresStore({
106
125
  id: "mastra-store__instance",
107
126
  schemaName: "mastra_instance",
108
- pool: this.requirePool(),
127
+ pool: this.servicePrincipalPool,
109
128
  });
110
129
  }
111
130
  forAgent(agentId, def) {
@@ -143,7 +162,7 @@ export class MemoryBuilder {
143
162
  return new PostgresStore({
144
163
  id: `mastra-store__${agentId}`,
145
164
  schemaName: `mastra_${agentId}`,
146
- pool: this.requirePool(),
165
+ pool: this.servicePrincipalPool,
147
166
  });
148
167
  }
149
168
  // Cast: `withId` guarantees `id` is set, but the distributive
@@ -169,16 +188,10 @@ export class MemoryBuilder {
169
188
  }
170
189
  getSharedVector() {
171
190
  if (!this.sharedVector) {
172
- this.sharedVector = buildSharedPgVector(this.requirePool());
191
+ this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
173
192
  }
174
193
  return this.sharedVector;
175
194
  }
176
- requirePool() {
177
- if (!this.pool) {
178
- this.pool = resolveLakebasePool(this.context, this.config);
179
- }
180
- return this.pool;
181
- }
182
195
  }
183
196
  /**
184
197
  * Build the shared `PgVector` that backs the default
@@ -59,6 +59,14 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
59
59
  private mastra;
60
60
  private mastraApp;
61
61
  private mastraServer;
62
+ /**
63
+ * Dedicated service-principal Lakebase pool backing Mastra memory /
64
+ * storage. Built once in {@link buildAgentAndServer} (outside any
65
+ * `asUser` scope, so it never inherits a request's OBO identity) and
66
+ * drained in {@link abortActiveOperations}. `null` until setup runs
67
+ * or when Lakebase isn't needed.
68
+ */
69
+ private servicePrincipalPool;
62
70
  setup(): Promise<void>;
63
71
  /**
64
72
  * When the `lakebase` plugin is registered, auto-enable `storage`
@@ -68,6 +76,13 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
68
76
  * already in the registry by the time this fires.
69
77
  */
70
78
  private applyLakebaseAutoDefaults;
79
+ /**
80
+ * Drain the memory service-principal pool on shutdown. AppKit calls
81
+ * this during teardown; the lakebase plugin closes its own SP / OBO
82
+ * pools the same way. Fire-and-forget so shutdown isn't blocked on a
83
+ * slow drain, and clear the handle so a re-`setup()` rebuilds it.
84
+ */
85
+ abortActiveOperations(): void;
71
86
  exports(): {
72
87
  /**
73
88
  * Ids of every registered agent in registration order. Matches
@@ -27,7 +27,7 @@
27
27
  * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
28
28
  */
29
29
  import { genie, getExecutionContext, lakebase, Plugin, toPlugin, } from "@databricks/appkit";
30
- import { appkitUtils, logUtils } from "@dbx-tools/shared";
30
+ import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
31
31
  import { chatRoute } from "@mastra/ai-sdk";
32
32
  import { Mastra } from "@mastra/core/mastra";
33
33
  import express from "express";
@@ -35,7 +35,7 @@ import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
35
35
  import { fetchChart } from "./chart.js";
36
36
  import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.js";
37
37
  import { historyRoute } from "./history.js";
38
- import { createMemoryBuilder, needsLakebase } from "./memory.js";
38
+ import { createMemoryBuilder, createServicePrincipalPool, needsLakebase, } from "./memory.js";
39
39
  import { buildObservability } from "./observability.js";
40
40
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
41
41
  import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
@@ -93,6 +93,14 @@ export class MastraPlugin extends Plugin {
93
93
  mastra = null;
94
94
  mastraApp = null;
95
95
  mastraServer = null;
96
+ /**
97
+ * Dedicated service-principal Lakebase pool backing Mastra memory /
98
+ * storage. Built once in {@link buildAgentAndServer} (outside any
99
+ * `asUser` scope, so it never inherits a request's OBO identity) and
100
+ * drained in {@link abortActiveOperations}. `null` until setup runs
101
+ * or when Lakebase isn't needed.
102
+ */
103
+ servicePrincipalPool = null;
96
104
  async setup() {
97
105
  // Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
98
106
  // the lakebase pool is valid when storage/memory are enabled.
@@ -118,6 +126,25 @@ export class MastraPlugin extends Plugin {
118
126
  if (this.config.memory === undefined)
119
127
  this.config.memory = true;
120
128
  }
129
+ /**
130
+ * Drain the memory service-principal pool on shutdown. AppKit calls
131
+ * this during teardown; the lakebase plugin closes its own SP / OBO
132
+ * pools the same way. Fire-and-forget so shutdown isn't blocked on a
133
+ * slow drain, and clear the handle so a re-`setup()` rebuilds it.
134
+ */
135
+ abortActiveOperations() {
136
+ super.abortActiveOperations();
137
+ if (this.servicePrincipalPool) {
138
+ this.log.info("closing memory SP pool");
139
+ const pool = this.servicePrincipalPool;
140
+ this.servicePrincipalPool = null;
141
+ pool.end().catch((err) => {
142
+ this.log.error("error closing memory SP pool", {
143
+ error: commonUtils.errorMessage(err),
144
+ });
145
+ });
146
+ }
147
+ }
121
148
  exports() {
122
149
  return {
123
150
  /**
@@ -402,12 +429,25 @@ export class MastraPlugin extends Plugin {
402
429
  return listServingEndpoints(client, host, { ttlMs: serving.ttlMs });
403
430
  }
404
431
  async buildAgentAndServer() {
405
- // Per-agent memory factory. The builder resolves the Lakebase pool
406
- // lazily (on first agent that actually needs storage / vector) and
407
- // caches both the pool and the shared `PgVector` singleton so
408
- // registering N agents stays cheap. See `./memory.js`.
409
- const memoryBuilder = needsLakebase(this.config)
410
- ? createMemoryBuilder(this.config, this.context)
432
+ // Per-agent memory factory. When any storage / memory setting needs
433
+ // Postgres, stand up a dedicated service-principal pool first so
434
+ // memory acts as the app SP (owner of the `mastra_*` schemas),
435
+ // never the per-request OBO identity the chat turn runs under.
436
+ // `getPgConfig()` is read here, outside any `asUser` scope, so it
437
+ // returns the SP connection target + token refresh plus any
438
+ // `lakebase({ pool })` overrides; `require` turns a missing
439
+ // sibling into a clear wiring error. The builder caches the shared
440
+ // `PgVector` singleton so registering N agents stays cheap. See
441
+ // `./memory.js`.
442
+ if (needsLakebase(this.config)) {
443
+ const spPgConfig = appkitUtils
444
+ .require(this.context, lakebase, this.config)
445
+ .exports()
446
+ .getPgConfig();
447
+ this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
448
+ }
449
+ const memoryBuilder = this.servicePrincipalPool
450
+ ? createMemoryBuilder(this.config, this.servicePrincipalPool)
411
451
  : undefined;
412
452
  this.log.debug("build:start", {
413
453
  lakebase: memoryBuilder !== undefined,