@dbx-tools/appkit-mastra 0.9.119 → 0.9.120

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/genie.ts CHANGED
@@ -248,7 +248,7 @@ async function readCachedConversationId(cacheKey: string | undefined): Promise<s
248
248
  return v ?? undefined;
249
249
  } catch (err) {
250
250
  logger.warn("conversation-cache:read-error", {
251
- error: errorUtils.errorMessage(err),
251
+ error: errorUtils.toError(err),
252
252
  });
253
253
  return undefined;
254
254
  }
@@ -272,7 +272,7 @@ async function saveCachedConversationId(
272
272
  });
273
273
  } catch (err) {
274
274
  logger.warn("conversation-cache:write-error", {
275
- error: errorUtils.errorMessage(err),
275
+ error: errorUtils.toError(err),
276
276
  });
277
277
  }
278
278
  }
@@ -284,7 +284,7 @@ async function evictCachedConversationId(cacheKey: string | undefined): Promise<
284
284
  await CacheManager.getInstanceSync().delete(cacheKey);
285
285
  } catch (err) {
286
286
  logger.warn("conversation-cache:delete-error", {
287
- error: errorUtils.errorMessage(err),
287
+ error: errorUtils.toError(err),
288
288
  });
289
289
  }
290
290
  }
@@ -515,7 +515,7 @@ function buildAskGenieTool(opts: {
515
515
  logger.warn("conversation-cache:stale, resetting", {
516
516
  spaceId,
517
517
  conversationId: rejectedConversationId,
518
- error: errorUtils.errorMessage(err),
518
+ error: errorUtils.toError(err),
519
519
  });
520
520
  if (ownsReusableConversation) {
521
521
  await evictCachedConversationId(cacheKey);
@@ -1115,7 +1115,9 @@ async function writeSuggestionCache(spaceId: string, questions: string[]): Promi
1115
1115
  * questions and a multi-space app round-trips breadth-first up to the
1116
1116
  * cap. A per-space fetch failure degrades to "no questions for that
1117
1117
  * space" (logged, not thrown) so one unreachable space never blanks
1118
- * the whole list. Returns `[]` when no spaces are configured.
1118
+ * the whole list. When `signal` aborts (the HTTP caller disconnected),
1119
+ * collection stops silently and returns what was merged so far.
1120
+ * Returns `[]` when no spaces are configured.
1119
1121
  */
1120
1122
  export async function collectSpaceSuggestions(opts: {
1121
1123
  spaces: Record<string, GenieSpaceConfig>;
@@ -1138,9 +1140,10 @@ export async function collectSpaceSuggestions(opts: {
1138
1140
  questions = genieSpace.genieSampleQuestions(space);
1139
1141
  await writeSuggestionCache(spaceId, questions);
1140
1142
  } catch (err) {
1143
+ if (opts.signal?.aborted) return merged;
1141
1144
  logger.warn("suggestions:fetch-error", {
1142
1145
  spaceId,
1143
- error: errorUtils.errorMessage(err),
1146
+ error: errorUtils.toError(err),
1144
1147
  });
1145
1148
  questions = [];
1146
1149
  }
package/src/memory.ts CHANGED
@@ -32,9 +32,7 @@
32
32
  * @module
33
33
  */
34
34
 
35
- import { getUsernameWithApiLookup } from "@databricks/appkit";
36
35
  import { migration as appkitMigration } from "@dbx-tools/appkit";
37
- import { postgresConnectionOptions } from "@dbx-tools/postgres";
38
36
  import { hash, log } from "@dbx-tools/shared-core";
39
37
  import type { AgentMemoryOption } from "@mastra/core/agent";
40
38
  import {
@@ -45,7 +43,7 @@ import {
45
43
  import { fastembed } from "@mastra/fastembed";
46
44
  import { Memory } from "@mastra/memory";
47
45
  import { PgVector, PostgresStore } from "@mastra/pg";
48
- import { Pool, type PoolConfig } from "pg";
46
+ import type { Pool } from "pg";
49
47
 
50
48
  import type { AnyMastraAgentDefinition, MastraMemoryConfigOverride } from "./agents.ts";
51
49
  import type { MastraPluginConfig } from "./config.ts";
@@ -132,34 +130,6 @@ export function withSoftStorageInit(store: PostgresStore): PostgresStore {
132
130
  return store;
133
131
  }
134
132
 
135
- /**
136
- * Build a dedicated **service-principal** Lakebase pool for Mastra
137
- * memory from the lakebase plugin's resolved SP pg config.
138
- *
139
- * The plugin's `exports().pool` is a `RoutingPool` that switches to
140
- * the per-user (OBO) pool whenever a query runs inside an `asUser`
141
- * scope - exactly the context the mastra plugin establishes around
142
- * every chat turn. Memory (threads / messages + semantic recall) must
143
- * instead always act as the app service principal: it owns the
144
- * auto-created `mastra_*` schemas (a per-user role usually can't
145
- * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
146
- * request's OBO identity.
147
- *
148
- * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
149
- * **outside** any `asUser` scope (i.e. during setup), so it carries
150
- * the SP connection target, OAuth token-refresh `password` callback,
151
- * and any `lakebase({ pool })` tuning overrides - all of which this
152
- * pool inherits. See the call site in `plugin.ts`.
153
- */
154
- export async function createServicePrincipalPool(pgConfig: PoolConfig): Promise<Pool> {
155
- // `getPgConfig()` resolves the SP username synchronously from
156
- // `PGUSER` / `DATABRICKS_CLIENT_ID`; fall back to the async API
157
- // lookup (e.g. local dev authenticating via PAT) so the pool always
158
- // has an identity to connect with.
159
- const user = pgConfig.user ?? (await getUsernameWithApiLookup());
160
- return new Pool(postgresConnectionOptions({ ...pgConfig, user }));
161
- }
162
-
163
133
  /** Effective per-knob setting after the plugin/agent cascade. */
164
134
  type StorageSetting = AnyMastraAgentDefinition["storage"];
165
135
  type MemorySetting = AnyMastraAgentDefinition["memory"];
@@ -179,7 +149,7 @@ export function needsLakebase(config: MastraPluginConfig): boolean {
179
149
 
180
150
  /**
181
151
  * Construct a per-agent {@link Memory} factory bound to the supplied
182
- * service-principal pool (see {@link createServicePrincipalPool}).
152
+ * service-principal pool.
183
153
  * Caches the shared `PgVector` singleton (built on first need) so each
184
154
  * agent build is O(1) after the first.
185
155
  */
package/src/mlflow.ts CHANGED
@@ -366,7 +366,7 @@ export async function initializeAppMlflowTraceInfo(client?: WorkspaceClient): Pr
366
366
  return true;
367
367
  } catch (err) {
368
368
  logger.warn("Databricks Apps MLflow trace-info promotion disabled", {
369
- error: errorUtils.errorMessage(err),
369
+ error: errorUtils.toError(err),
370
370
  });
371
371
  return false;
372
372
  }
@@ -424,7 +424,7 @@ export function persistAppMlflowTraceInfo(params: AppMlflowTraceInfo): void {
424
424
  .catch((err) => {
425
425
  logger.warn("Databricks Apps MLflow trace-info promotion failed", {
426
426
  traceId: params.traceId,
427
- error: errorUtils.errorMessage(err),
427
+ error: errorUtils.toError(err),
428
428
  });
429
429
  })
430
430
  .finally(() => pendingAppMlflowTraceInfo.delete(pending));
@@ -466,7 +466,7 @@ async function detectedDirectMlflowTraceLocation(
466
466
  } catch (err) {
467
467
  logger.warn("direct MLflow UC trace-location detection failed", {
468
468
  experimentId,
469
- error: errorUtils.errorMessage(err),
469
+ error: errorUtils.toError(err),
470
470
  });
471
471
  return undefined;
472
472
  }
@@ -530,7 +530,7 @@ export async function initializeDirectMlflowTracing(): Promise<boolean> {
530
530
  return true;
531
531
  } catch (err) {
532
532
  logger.warn("direct MLflow tracing disabled", {
533
- error: errorUtils.errorMessage(err),
533
+ error: errorUtils.toError(err),
534
534
  });
535
535
  return false;
536
536
  }
@@ -693,18 +693,10 @@ async function resolveExperimentId(client: WorkspaceClient): Promise<string | un
693
693
  }
694
694
 
695
695
  /** Parameters for {@link logFeedback}. */
696
- export interface LogFeedbackParams {
697
- /** MLflow trace id the assessment attaches to (`tr-<hex>`). */
698
- traceId: string;
699
- /** Assessment name; defaults per whether a value or a comment is sent. */
700
- name?: string;
701
- /** Thumbs / rating / label value. Omit for a comment-only submission. */
702
- value?: boolean | number | string;
703
- /** Freeform comment: the rationale alongside a value, or the value itself when none. */
704
- comment?: string;
696
+ export type LogFeedbackParams = feedback.MastraFeedbackRequest & {
705
697
  /** Identity the feedback is attributed to (user email / id). */
706
698
  sourceId?: string;
707
- }
699
+ };
708
700
 
709
701
  /**
710
702
  * Log a HUMAN feedback assessment to a trace. Returns the created
@@ -751,7 +743,7 @@ export async function logFeedback(
751
743
  logger.warn("feedback request failed", {
752
744
  traceId: params.traceId,
753
745
  assessmentTraceId,
754
- error: errorUtils.errorMessage(err),
746
+ error: errorUtils.toError(err),
755
747
  });
756
748
  return undefined;
757
749
  }
@@ -68,7 +68,7 @@ const montyContext = functionUtils.memoize(async (): Promise<MontyContext> => {
68
68
  } catch (error) {
69
69
  errors.push(errorUtils.toError(error));
70
70
  logger.debug(`${target.name} unavailable`, {
71
- error: errorUtils.errorMessage(error),
71
+ error: errorUtils.toError(error),
72
72
  });
73
73
  }
74
74
  }
package/src/plugin.ts CHANGED
@@ -56,7 +56,7 @@ import {
56
56
  type PluginManifest,
57
57
  type ResourceRequirement,
58
58
  } from "@databricks/appkit";
59
- import { pluginRegistry } from "@dbx-tools/appkit";
59
+ import { lakebaseResolver, pluginRegistry } from "@dbx-tools/appkit";
60
60
  import { modelCatalog } from "@dbx-tools/model";
61
61
  import { asyncUtils, errorUtils, log, object, stringUtils } from "@dbx-tools/shared-core";
62
62
  import {
@@ -98,7 +98,7 @@ import {
98
98
  type MastraIdentityMode,
99
99
  } from "./identity.ts";
100
100
  import { buildMcpServer, type ResolvedMcp } from "./mcp.ts";
101
- import { createMemoryBuilder, createServicePrincipalPool, needsLakebase } from "./memory.ts";
101
+ import { createMemoryBuilder, needsLakebase } from "./memory.ts";
102
102
  import {
103
103
  flushDirectMlflowTracing,
104
104
  logFeedback,
@@ -124,13 +124,6 @@ import { invalidFields } from "./validation.ts";
124
124
  const GENIE_MANIFEST = pluginRegistry.data(genie).plugin.manifest;
125
125
  const LAKEBASE_MANIFEST = pluginRegistry.data(lakebase).plugin.manifest;
126
126
 
127
- /**
128
- * Budget for draining the memory service-principal pool on shutdown. Well
129
- * inside AppKit's 15s graceful-shutdown window, so a stuck connection can
130
- * never be what holds the process open.
131
- */
132
- const POOL_DRAIN_TIMEOUT_MS = 5_000;
133
-
134
127
  /** Ceiling on the `?timeoutMs=` long-poll budget a client may request. */
135
128
  const MAX_EMBED_POLL_TIMEOUT_MS = 5 * 60_000;
136
129
 
@@ -243,15 +236,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
243
236
  * boot rather than the first request, and so every request agrees.
244
237
  */
245
238
  private identityMode: MastraIdentityMode = "user";
246
- /**
247
- * Dedicated service-principal Lakebase pool backing Mastra memory /
248
- * storage. Built once in {@link buildAgentAndServer} (outside any
249
- * `asUser` scope, so it never inherits a request's OBO identity) and
250
- * drained in {@link shutdown}. `null` until setup runs or when
251
- * Lakebase isn't needed.
252
- */
253
- private servicePrincipalPool: Pool | null = null;
254
-
255
239
  override async setup(): Promise<void> {
256
240
  // Resolve the identity mode up front so an invalid `genieIdentity` fails the
257
241
  // app boot with a clear message rather than 500-ing the first chat request.
@@ -298,44 +282,19 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
298
282
  if (this.config.memory === undefined) this.config.memory = true;
299
283
  }
300
284
 
301
- /**
302
- * Drain the memory service-principal pool. Idempotent: the handle is
303
- * cleared before the drain starts, so a second call is a no-op and a
304
- * later `setup()` rebuilds the pool. Bounded by
305
- * {@link POOL_DRAIN_TIMEOUT_MS} to stay well inside the 15s graceful
306
- * shutdown budget.
307
- */
285
+ /** Flush pending direct MLflow traces during graceful shutdown. */
308
286
  async shutdown(): Promise<void> {
309
287
  try {
310
288
  await flushDirectMlflowTracing();
311
289
  } catch (err) {
312
290
  this.logger.warn("error flushing direct MLflow traces", {
313
- error: errorUtils.errorMessage(err),
314
- });
315
- }
316
- const pool = this.servicePrincipalPool;
317
- if (!pool) return;
318
- this.servicePrincipalPool = null;
319
- this.logger.info("closing memory SP pool");
320
- // The budget timer is cancelled on the way out so a drain that finished
321
- // early cannot hold the event loop open for the rest of the window.
322
- const budget = new AbortController();
323
- try {
324
- await Promise.race([pool.end(), asyncUtils.sleep(POOL_DRAIN_TIMEOUT_MS, budget.signal)]);
325
- } catch (err) {
326
- this.logger.error("error closing memory SP pool", {
327
- error: errorUtils.errorMessage(err),
291
+ error: errorUtils.toError(err),
328
292
  });
329
- } finally {
330
- budget.abort();
331
293
  }
332
294
  }
333
295
 
334
296
  /**
335
- * Abort in-flight work. AppKit's graceful shutdown calls this hook
336
- * synchronously and never awaits {@link shutdown}, so the pool drain is
337
- * started here too; it cannot be awaited from a `void` hook, and
338
- * {@link shutdown} is idempotent so the duplicate call is free.
297
+ * Abort in-flight work and start the asynchronous trace flush.
339
298
  */
340
299
  override abortActiveOperations(): void {
341
300
  super.abortActiveOperations();
@@ -719,7 +678,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
719
678
  // the upstream status. Log and degrade.
720
679
  this.logger.warn("suggestions:error", {
721
680
  status: result.status,
722
- error: result.message,
681
+ error: errorUtils.toError(result.message),
723
682
  });
724
683
  res.json({ questions: [] });
725
684
  return;
@@ -765,7 +724,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
765
724
  if (!parsed.success) {
766
725
  // A Zod issue string quotes the received body back, so only the
767
726
  // offending field names travel; the full issue list is logged.
768
- this.logger.warn("feedback:invalid", { error: parsed.error.message });
727
+ this.logger.warn("feedback:invalid", { error: parsed.error });
769
728
  res.status(400).json({
770
729
  ok: false,
771
730
  error: INVALID_FEEDBACK_MESSAGE,
@@ -856,7 +815,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
856
815
  ): void {
857
816
  this.logger.warn(`${operation}:error`, {
858
817
  status: result.status,
859
- error: result.message,
818
+ error: errorUtils.toError(result.message),
860
819
  });
861
820
  if (res.headersSent) return;
862
821
  res.status(result.status).json({ error: clientMessage });
@@ -1074,32 +1033,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1074
1033
  if (result.ok) return result.data;
1075
1034
  this.logger.warn("listModels:error", {
1076
1035
  status: result.status,
1077
- error: result.message,
1036
+ error: errorUtils.toError(result.message),
1078
1037
  });
1079
1038
  throw new ExecutionError(MODEL_CATALOGUE_FAILED_MESSAGE);
1080
1039
  }
1081
1040
 
1082
1041
  private async buildAgentAndServer(): Promise<void> {
1083
- // Per-agent memory factory. When any storage / memory setting needs
1084
- // Postgres, stand up a dedicated service-principal pool first so
1085
- // memory acts as the app SP (owner of the `mastra_*` schemas),
1086
- // never the per-request OBO identity the chat turn runs under.
1087
- // `getPgConfig()` is read here, outside any `asUser` scope, so it
1088
- // returns the SP connection target + token refresh plus any
1089
- // `lakebase({ pool })` overrides; `require` turns a missing
1090
- // sibling into a clear wiring error. The builder caches the shared
1091
- // `PgVector` singleton so registering N agents stays cheap. See
1092
- // `./memory.js`.
1093
- if (needsLakebase(this.config)) {
1094
- const spPgConfig = pluginRegistry
1095
- .require(this.context, lakebase, this.config)
1096
- .exports()
1097
- .getPgConfig();
1098
- this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
1099
- }
1100
- const memoryBuilder = this.servicePrincipalPool
1101
- ? createMemoryBuilder(this.config, this.servicePrincipalPool)
1042
+ const memoryPool = needsLakebase(this.config)
1043
+ ? (lakebaseResolver.captureServicePrincipalPool(
1044
+ pluginRegistry.require(this.context, lakebase, this.config).exports().pool,
1045
+ ) as Pool)
1102
1046
  : undefined;
1047
+ const memoryBuilder = memoryPool ? createMemoryBuilder(this.config, memoryPool) : undefined;
1103
1048
 
1104
1049
  // Build every agent declared in `config.agents` (or the built-in
1105
1050
  // fallback when none are declared). Each agent's `model` resolves
@@ -436,7 +436,7 @@ async function readMetadata(fs: FileSystem): Promise<RemoteSkillsMetadata | unde
436
436
  if (!object.isRecord(parsed) || parsed.version !== METADATA_VERSION) return undefined;
437
437
  return object.isRecord(parsed.sources) ? parsed : undefined;
438
438
  } catch (err) {
439
- logger.debug("cache:unreadable", { error: errorUtils.errorMessage(err) });
439
+ logger.debug("cache:unreadable", { error: errorUtils.toError(err) });
440
440
  return undefined;
441
441
  }
442
442
  }
@@ -599,7 +599,7 @@ export async function provisionRemoteSkills(
599
599
  }
600
600
  logger.warn("source:skipped", {
601
601
  source: sourceOptions.source,
602
- error: errorUtils.errorMessage(err),
602
+ error: errorUtils.toError(err),
603
603
  });
604
604
  }
605
605
  }
@@ -658,7 +658,10 @@ async function cachedAiToolsTree(
658
658
  async function serializeSkillDirs(dirs: readonly StagedSkillDir[]): Promise<CachedSkillFile[]> {
659
659
  const files: CachedSkillFile[] = [];
660
660
  for (const dir of dirs) {
661
- for (const relative of find.findFiles("**/*", { cwd: dir.absolutePath, nodir: true })) {
661
+ for await (const relative of find.findFiles("**/*", {
662
+ cwd: dir.absolutePath,
663
+ nodir: true,
664
+ })) {
662
665
  files.push({
663
666
  path: posix.join(dir.name, relative.split(/[\\/]/).join("/")),
664
667
  content: (await readFile(join(dir.absolutePath, relative))).toString("base64"),
@@ -752,7 +755,7 @@ async function openWritableWorkspace(
752
755
  } catch (err) {
753
756
  logger.warn("destination:unwritable", {
754
757
  destination: basePath,
755
- error: errorUtils.errorMessage(err),
758
+ error: errorUtils.toError(err),
756
759
  });
757
760
  return undefined;
758
761
  }
@@ -996,7 +999,10 @@ async function collectSkillDirs(root: string): Promise<StagedSkillDir[]> {
996
999
  async function copySkillDirs(destination: FileSystem, dirs: StagedSkillDir[]): Promise<void> {
997
1000
  await destination.init();
998
1001
  for (const dir of dirs) {
999
- for (const relative of find.findFiles("**/*", { cwd: dir.absolutePath, nodir: true })) {
1002
+ for await (const relative of find.findFiles("**/*", {
1003
+ cwd: dir.absolutePath,
1004
+ nodir: true,
1005
+ })) {
1000
1006
  const buffer = await readFile(join(dir.absolutePath, relative));
1001
1007
  const skillPath = posix.join(dir.name, relative.split(/[\\/]/).join("/"));
1002
1008
  await destination.writeFile(skillPath, buffer, { overwrite: true });
package/src/server.ts CHANGED
@@ -484,7 +484,7 @@ export function attachStaleResumeRecovery(app: express.Express): void {
484
484
  }
485
485
  logger.warn("ignored stale mastra resume", {
486
486
  path: req.path,
487
- error: errorUtils.errorMessage(error),
487
+ error: errorUtils.toError(error),
488
488
  });
489
489
  if (res.headersSent) {
490
490
  res.end();
package/src/workspaces.ts CHANGED
@@ -840,7 +840,7 @@ async function loadInMemorySkillFilesystem(mount: LocalSkillMount): Promise<File
840
840
  root: "/",
841
841
  });
842
842
  await memory.init();
843
- for (const relative of find.findFiles("**/*", { cwd: mount.root, nodir: true })) {
843
+ for await (const relative of find.findFiles("**/*", { cwd: mount.root, nodir: true })) {
844
844
  await memory.writeFile(relative, await source.readFile(relative), { overwrite: true });
845
845
  }
846
846
  await source.close();
@@ -896,7 +896,7 @@ async function databricksFilesystem(
896
896
  logger.debug("databricks-mount:skipped", {
897
897
  root,
898
898
  readOnly,
899
- error: errorUtils.errorMessage(err),
899
+ error: errorUtils.toError(err),
900
900
  });
901
901
  }
902
902
  return undefined;
@@ -945,7 +945,7 @@ function verifyFirstFileSystemAccess<TFileSystem extends FileSystem>(
945
945
  readCheck = undefined;
946
946
  logger.debug("databricks-mount:read-check-failed", {
947
947
  root,
948
- error: errorUtils.errorMessage(error),
948
+ error: errorUtils.toError(error),
949
949
  });
950
950
  throw error;
951
951
  }
@@ -976,7 +976,7 @@ function verifyFirstFileSystemAccess<TFileSystem extends FileSystem>(
976
976
  if (!writeVerified) {
977
977
  logger.debug("databricks-mount:write-check-failed", {
978
978
  root,
979
- error: errorUtils.errorMessage(error),
979
+ error: errorUtils.toError(error),
980
980
  });
981
981
  }
982
982
  throw error;
@@ -1079,7 +1079,7 @@ async function resolveDistinctSkillPaths(
1079
1079
  } catch (error) {
1080
1080
  logger.debug("skill-path:skipped", {
1081
1081
  path,
1082
- error: errorUtils.errorMessage(error),
1082
+ error: errorUtils.toError(error),
1083
1083
  });
1084
1084
  }
1085
1085
  }
package/src/writer.ts CHANGED
@@ -44,7 +44,7 @@ export async function safeWriteProgress(
44
44
  } catch (err) {
45
45
  log.warn("writer:error", {
46
46
  ...context,
47
- error: errorUtils.errorMessage(err),
47
+ error: errorUtils.toError(err),
48
48
  });
49
49
  }
50
50
  }