@dbx-tools/appkit-mastra 0.9.129 → 0.9.132

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 (51) hide show
  1. package/README.md +182 -117
  2. package/index.ts +10 -8
  3. package/lib/index.d.ts +10 -8
  4. package/lib/index.js +8 -7
  5. package/lib/src/agents.d.ts +4 -0
  6. package/lib/src/agents.js +12 -3
  7. package/lib/src/config.d.ts +7 -2
  8. package/lib/src/config.js +6 -3
  9. package/lib/src/defaults.d.ts +9 -0
  10. package/lib/src/defaults.js +29 -1
  11. package/lib/src/genie-chat.d.ts +71 -0
  12. package/lib/src/genie-chat.js +272 -0
  13. package/lib/src/genie.d.ts +2 -0
  14. package/lib/src/genie.js +11 -8
  15. package/lib/src/memory.d.ts +5 -5
  16. package/lib/src/memory.js +10 -3
  17. package/lib/src/observability.d.ts +3 -0
  18. package/lib/src/observability.js +2 -2
  19. package/lib/src/plugin.d.ts +36 -7
  20. package/lib/src/plugin.js +220 -33
  21. package/lib/src/request-context.d.ts +2 -0
  22. package/lib/src/request-context.js +3 -0
  23. package/lib/src/server.d.ts +4 -0
  24. package/lib/src/server.js +40 -2
  25. package/lib/src/statement.d.ts +16 -1
  26. package/lib/src/statement.js +50 -18
  27. package/lib/src/telemetry.d.ts +22 -1
  28. package/lib/src/telemetry.js +174 -18
  29. package/lib/src/workspaces.d.ts +2 -2
  30. package/lib/src/workspaces.js +3 -2
  31. package/lib/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +32 -25
  33. package/src/agents.ts +15 -1
  34. package/src/config.ts +10 -4
  35. package/src/defaults.ts +30 -0
  36. package/src/genie-chat.ts +341 -0
  37. package/src/genie.ts +11 -7
  38. package/src/memory.ts +9 -4
  39. package/src/observability.ts +6 -2
  40. package/src/plugin.ts +262 -31
  41. package/src/request-context.ts +2 -0
  42. package/src/server.ts +35 -0
  43. package/src/statement.ts +71 -19
  44. package/src/telemetry.ts +206 -16
  45. package/src/workspaces.ts +4 -3
  46. package/exports.ts +0 -7
  47. package/lib/exports.d.ts +0 -1
  48. package/lib/exports.js +0 -2
  49. package/lib/src/openapi-tool.d.ts +0 -29
  50. package/lib/src/openapi-tool.js +0 -175
  51. package/src/openapi-tool.ts +0 -245
package/src/plugin.ts CHANGED
@@ -56,13 +56,15 @@ import {
56
56
  type PluginManifest,
57
57
  type ResourceRequirement,
58
58
  } from "@databricks/appkit";
59
- import { lakebaseResolver, pluginRegistry } from "@dbx-tools/appkit";
59
+ import { lakebaseResolver, pluginRegistry, telemetry as appkitTelemetry } 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 {
63
63
  feedback,
64
64
  routes,
65
+ wire,
65
66
  type Chart,
67
+ type GenieChatRequest,
66
68
  type MastraClientConfig,
67
69
  type MastraFeedbackRequest,
68
70
  type StatementData,
@@ -71,10 +73,16 @@ import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
71
73
  import type { Agent } from "@mastra/core/agent";
72
74
  import { Mastra } from "@mastra/core/mastra";
73
75
  import type { RequestContext } from "@mastra/core/request-context";
76
+ import { pipeUIMessageStreamToResponse } from "ai";
74
77
  import express from "express";
75
78
  import type { Pool } from "pg";
76
79
 
77
- import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.ts";
80
+ import {
81
+ buildAgents,
82
+ FALLBACK_AGENT_ID,
83
+ type BuiltAgents,
84
+ type MastraOutputProcessorFactory,
85
+ } from "./agents.ts";
78
86
  import { fetchChart } from "./chart.ts";
79
87
  import { agentChatRoutes } from "./chat.ts";
80
88
  import {
@@ -86,11 +94,21 @@ import {
86
94
  import {
87
95
  chartFetchDefaults,
88
96
  feedbackWriteDefaults,
97
+ genieConversationReadDefaults,
98
+ genieConversationWriteDefaults,
89
99
  genieSuggestionDefaults,
90
100
  modelCatalogueDefaults,
91
101
  statementDataDefaults,
92
102
  } from "./defaults.ts";
93
- import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie.ts";
103
+ import {
104
+ deleteGenieThread,
105
+ deleteGenieThreadMessage,
106
+ genieChatStream,
107
+ listGenieThreadMessages,
108
+ listGenieThreads,
109
+ type GenieChatSpace,
110
+ } from "./genie-chat.ts";
111
+ import { collectSpaceSuggestions, resolveGenieSpaces, type GenieSpaceConfig } from "./genie.ts";
94
112
  import {
95
113
  requestUserId,
96
114
  resolveIdentityMode,
@@ -117,8 +135,8 @@ import {
117
135
  MastraServer,
118
136
  } from "./server.ts";
119
137
  import { resolveServingConfig } from "./serving.ts";
120
- import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement.ts";
121
- import { attachChatTurnTelemetry } from "./telemetry.ts";
138
+ import { fetchStatementEmbedData, STATEMENT_ROW_CAP } from "./statement.ts";
139
+ import { attachChatTurnTelemetry, registerAppMlflowTraceInfoProcessor } from "./telemetry.ts";
122
140
  import { invalidFields } from "./validation.ts";
123
141
  import { inlineSkillsFromLocalMounts } from "./workspaces.ts";
124
142
 
@@ -225,6 +243,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
225
243
  private mastra: Mastra | null = null;
226
244
  private mastraApp: express.Express | null = null;
227
245
  private mastraServer: MastraServer | null = null;
246
+ private readonly outputProcessorFactories: MastraOutputProcessorFactory[] = [];
228
247
  /**
229
248
  * The optional MCP server exposing this plugin's agents / tools, or
230
249
  * `null` when `config.mcp` is disabled (the default). Built in
@@ -242,6 +261,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
242
261
  // app boot with a clear message rather than 500-ing the first chat request.
243
262
  this.identityMode = resolveIdentityMode(this.config.genieIdentity);
244
263
  validateFeedbackConfig(this.config.feedback);
264
+ if (this.config.observability !== false) {
265
+ await registerAppMlflowTraceInfoProcessor();
266
+ }
245
267
  // Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
246
268
  // the lakebase pool is valid when storage/memory are enabled.
247
269
  this.context?.onLifecycle("setup:complete", async () => {
@@ -252,8 +274,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
252
274
  }
253
275
 
254
276
  /**
255
- * The plugin instance the chat path's Databricks calls should run through for
256
- * `req`, honoring {@link identityMode}.
277
+ * The plugin instance agent-backed Databricks calls should run through for
278
+ * `req`, honoring {@link identityMode}. Direct Genie conversation routes use
279
+ * AppKit OBO through `asUser(req)` instead.
257
280
  *
258
281
  * In `"user"` mode (the default) this is `this.asUser(req)` - AppKit's own
259
282
  * proxy that runs every call inside `runInUserContext`, so `getExecutionContext()`
@@ -261,9 +284,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
261
284
  * unwrapped, so the same methods run in the ambient service context and
262
285
  * `getExecutionContext()` returns the app service principal's client - the same
263
286
  * "just don't enter `asUser`" pattern AppKit's own service-context calls use. One
264
- * helper covers every workspace call the chat path makes (serving catalogue,
265
- * Genie suggestions, `ask_genie` via the dispatched agent turn, statement fetch,
266
- * feedback), so the identity decision lives in one place.
287
+ * helper covers the agent-backed workspace calls (serving catalogue, general
288
+ * suggestions, dispatched `ask_genie`, statement fetch, feedback), so the
289
+ * identity decision lives in one place.
267
290
  */
268
291
  private scopedSelf(req: express.Request): this {
269
292
  return useServicePrincipal(this.identityMode, req) ? this : this.asUser(req);
@@ -323,6 +346,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
323
346
  */
324
347
  getDefault: (): Agent | null =>
325
348
  (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
349
+ /**
350
+ * Register an output-processor factory before agents are built.
351
+ *
352
+ * Deferred sibling plugins use this generic extension point without
353
+ * adding provider-specific behavior to the Mastra plugin.
354
+ */
355
+ registerOutputProcessor: (factory: MastraOutputProcessorFactory): void => {
356
+ if (this.built) {
357
+ throw new Error("Mastra output processors must be registered before agents are built");
358
+ }
359
+ this.outputProcessorFactories.push(factory);
360
+ },
326
361
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
327
362
  getMastra: () => this.mastra,
328
363
  /**
@@ -441,6 +476,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
441
476
  this.registerModelRoutes(router);
442
477
  this.registerEmbedRoutes(router);
443
478
  this.registerSuggestionRoutes(router);
479
+ this.registerGenieChatRoutes(router);
444
480
  this.registerFeedbackRoutes(router);
445
481
  this.registerMlflowExperimentRoute(router);
446
482
  this.registerAgentRoutes(router);
@@ -545,10 +581,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
545
581
  // `GET /embed/:type/:id` is the single resolver for every embed
546
582
  // marker the agent emits in prose (`[chart:<id>]`,
547
583
  // `[data:<id>]`, ...). `:type` selects a resolver from the
548
- // registry below; `:id` is that resolver's lookup key. The
549
- // grammar (see `marker.ts`) is type-agnostic on purpose - new
550
- // embed kinds are added by registering a resolver here, with no
551
- // client or grammar change.
584
+ // registry below; `:id` is that resolver's lookup key. The marker grammar
585
+ // is type-agnostic; the client still chooses how each registered type
586
+ // renders.
552
587
  //
553
588
  // Status codes:
554
589
  // - 200 with the resolver's JSON body when the id resolves.
@@ -563,7 +598,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
563
598
  // settles (`result` / `error`) or the budget elapses (then
564
599
  // returns the still-processing entry to poll again).
565
600
  // `?timeoutMs=<n>` (default 60s, capped 5min) tunes it.
566
- // - `data`: one OBO-scoped Statement Execution fetch.
601
+ // - `data`: one Statement Execution fetch under `genieIdentity`.
602
+ // - `visualization`: the same payload under mandatory OBO identity for
603
+ // direct Genie chat, whose conversations belong to the signed-in user.
567
604
  // `?limit=<n>` caps rows (clamped to STATEMENT_ROW_CAP).
568
605
  //
569
606
  // Built once (this handler is registered once) and keyed by the
@@ -587,6 +624,13 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
587
624
  signal,
588
625
  });
589
626
  },
627
+ visualization: (req, id, signal) => {
628
+ const limit = parseStatementLimit(req.query.limit);
629
+ return this.asUser(req).fetchStatement(id, {
630
+ ...(limit !== undefined ? { limit } : {}),
631
+ signal,
632
+ });
633
+ },
590
634
  };
591
635
 
592
636
  this.route(router, {
@@ -619,13 +663,10 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
619
663
  res.status(400).json({ error: "id is required" });
620
664
  return;
621
665
  }
622
- // Express's `req` predates `AbortSignal`; bridge the `close`
623
- // event onto an `AbortController` so a closed connection
624
- // unblocks any long-poll immediately and frees the request
625
- // thread. The listener is GC'd with the request on normal
626
- // completion.
666
+ // Bridge response close onto an AbortSignal so a client disconnect
667
+ // releases long-poll and workspace calls immediately.
627
668
  const controller = new AbortController();
628
- req.on("close", () => controller.abort());
669
+ res.on("close", () => controller.abort());
629
670
  const result = await resolve(req, id, controller.signal);
630
671
  if (controller.signal.aborted) return;
631
672
  if (!result.ok) {
@@ -663,14 +704,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
663
704
  // empty state (no built-in example prompts). The `:agentId`
664
705
  // segment is accepted for URL symmetry with the chat / history
665
706
  // routes; Genie spaces are resolved per-plugin, not per-agent,
666
- // so it doesn't change the result. OBO-scoped like the other
667
- // data routes so the space lookup runs as the calling user.
707
+ // so it doesn't change the result. The space lookup uses the configured
708
+ // agent-backed identity.
668
709
  const handleSuggestions = async (
669
710
  req: express.Request,
670
711
  res: express.Response,
671
712
  ): Promise<void> => {
672
713
  const controller = new AbortController();
673
- req.on("close", () => controller.abort());
714
+ res.on("close", () => controller.abort());
674
715
  const result = await this.scopedSelf(req).fetchSuggestions(controller.signal);
675
716
  if (controller.signal.aborted) return;
676
717
  if (!result.ok) {
@@ -700,6 +741,189 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
700
741
  });
701
742
  }
702
743
 
744
+ /**
745
+ * Register the direct Genie chat routes. Each route is scoped to one Genie
746
+ * space alias and sits directly on the Genie APIs under AppKit's OBO user
747
+ * context: conversations are threads, Genie messages are history, and a turn
748
+ * streams the same AI SDK chunks as an agent's `ask_genie` call. Nothing is
749
+ * written to Mastra Memory.
750
+ */
751
+ private registerGenieChatRoutes(router: IAppRouter): void {
752
+ const path = (...parts: string[]) => routes.genieRoutePath(":alias", ...parts);
753
+ const { chat: chatSegment, messages, suggestions, threads } = routes.MASTRA_ROUTES;
754
+ /** Resolve the route's alias, answering 404 when no such space is configured. */
755
+ const spaceFor = (req: express.Request, res: express.Response): GenieChatSpace | undefined => {
756
+ const alias = stringUtils.firstNonEmpty(req.params.alias) ?? "";
757
+ const configured = resolveGenieSpaces(this.config, this.context)[alias];
758
+ if (configured) return { alias, spaceId: configured.spaceId };
759
+ res.status(404).json({ error: `Unknown Genie space alias "${alias}"` });
760
+ return undefined;
761
+ };
762
+ /** Bridge the connection `close` event onto an abort signal. */
763
+ const closeSignal = (res: express.Response): AbortSignal => {
764
+ const controller = new AbortController();
765
+ res.on("close", () => controller.abort());
766
+ return controller.signal;
767
+ };
768
+
769
+ this.route(router, {
770
+ name: "genieChat",
771
+ method: "post",
772
+ path: path(chatSegment),
773
+ handler: async (req, res) => {
774
+ const space = spaceFor(req, res);
775
+ if (!space) return;
776
+ const parsed = wire.GenieChatRequestSchema.safeParse(req.body);
777
+ if (!parsed.success) {
778
+ res
779
+ .status(400)
780
+ .json({ error: "question is required", fields: invalidFields(parsed.error) });
781
+ return;
782
+ }
783
+ await this.asUser(req).streamGenieChat(res, space, parsed.data, closeSignal(res));
784
+ },
785
+ });
786
+ this.route(router, {
787
+ name: "genieThreads",
788
+ method: "get",
789
+ path: path(threads),
790
+ handler: async (req, res) => {
791
+ const space = spaceFor(req, res);
792
+ if (!space) return;
793
+ const result = await this.asUser(req).genieConversationRead((client) =>
794
+ listGenieThreads(client, space),
795
+ );
796
+ if (!result.ok) {
797
+ this.sendFailure(res, result, "genie-threads", "Could not list Genie conversations");
798
+ return;
799
+ }
800
+ res.json({ threads: result.data });
801
+ },
802
+ });
803
+ this.route(router, {
804
+ name: "genieThreadMessages",
805
+ method: "get",
806
+ path: path(threads, ":threadId", messages),
807
+ handler: async (req, res) => {
808
+ const space = spaceFor(req, res);
809
+ if (!space) return;
810
+ const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
811
+ const result = await this.asUser(req).genieConversationRead((client) =>
812
+ listGenieThreadMessages({
813
+ client,
814
+ space,
815
+ conversationId: threadId,
816
+ }),
817
+ );
818
+ if (!result.ok) {
819
+ this.sendFailure(res, result, "genie-messages", "Could not load the Genie conversation");
820
+ return;
821
+ }
822
+ res.json({ messages: result.data });
823
+ },
824
+ });
825
+ this.route(router, {
826
+ name: "genieThreadDelete",
827
+ method: "delete",
828
+ path: path(threads, ":threadId"),
829
+ handler: async (req, res) => {
830
+ const space = spaceFor(req, res);
831
+ if (!space) return;
832
+ const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
833
+ const result = await this.asUser(req).genieConversationWrite((client) =>
834
+ deleteGenieThread(client, space, threadId),
835
+ );
836
+ if (!result.ok) {
837
+ this.sendFailure(
838
+ res,
839
+ result,
840
+ "genie-thread-delete",
841
+ "Could not delete the Genie conversation",
842
+ );
843
+ return;
844
+ }
845
+ res.status(204).end();
846
+ },
847
+ });
848
+ this.route(router, {
849
+ name: "genieThreadMessageDelete",
850
+ method: "delete",
851
+ path: path(threads, ":threadId", messages, ":messageId"),
852
+ handler: async (req, res) => {
853
+ const space = spaceFor(req, res);
854
+ if (!space) return;
855
+ const threadId = stringUtils.firstNonEmpty(req.params.threadId) ?? "";
856
+ const messageId = stringUtils.firstNonEmpty(req.params.messageId) ?? "";
857
+ const result = await this.asUser(req).genieConversationWrite((client) =>
858
+ deleteGenieThreadMessage(client, space, threadId, messageId),
859
+ );
860
+ if (!result.ok) {
861
+ this.sendFailure(
862
+ res,
863
+ result,
864
+ "genie-message-delete",
865
+ "Could not delete the Genie message",
866
+ );
867
+ return;
868
+ }
869
+ res.status(204).end();
870
+ },
871
+ });
872
+ this.route(router, {
873
+ name: "genieSuggestions",
874
+ method: "get",
875
+ path: path(suggestions),
876
+ handler: async (req, res) => {
877
+ const space = spaceFor(req, res);
878
+ if (!space) return;
879
+ const result = await this.asUser(req).fetchSuggestions(undefined, {
880
+ [space.alias]: { spaceId: space.spaceId },
881
+ });
882
+ res.json({ questions: result.ok ? result.data : [] });
883
+ },
884
+ });
885
+ }
886
+
887
+ /**
888
+ * Stream one direct Genie turn to `res` under AppKit's OBO user context.
889
+ * Direct conversations cannot use the shared service principal because Genie
890
+ * owns them under the Databricks caller.
891
+ */
892
+ private async streamGenieChat(
893
+ res: express.Response,
894
+ space: GenieChatSpace,
895
+ request: GenieChatRequest,
896
+ signal: AbortSignal,
897
+ ): Promise<void> {
898
+ const context = getExecutionContext();
899
+ const client = context.client;
900
+ await pipeUIMessageStreamToResponse({
901
+ response: res,
902
+ stream: genieChatStream({
903
+ client,
904
+ space,
905
+ request,
906
+ signal,
907
+ }),
908
+ });
909
+ }
910
+
911
+ /** Run one Genie conversation read under the active request identity. */
912
+ private genieConversationRead<T>(
913
+ read: (client: ReturnType<typeof getExecutionContext>["client"]) => Promise<T>,
914
+ ): Promise<ExecutionResult<T>> {
915
+ const client = getExecutionContext().client;
916
+ return this.execute(() => read(client), genieConversationReadDefaults);
917
+ }
918
+
919
+ /** Run one Genie conversation write under the active request identity. */
920
+ private genieConversationWrite(
921
+ write: (client: ReturnType<typeof getExecutionContext>["client"]) => Promise<void>,
922
+ ): Promise<ExecutionResult<void>> {
923
+ const client = getExecutionContext().client;
924
+ return this.execute(() => write(client), genieConversationWriteDefaults);
925
+ }
926
+
703
927
  /** Register MLflow feedback submission when configured. */
704
928
  private registerFeedbackRoutes(router: IAppRouter): void {
705
929
  // `POST /route/feedback` logs a thumbs / comment assessment against
@@ -866,11 +1090,15 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
866
1090
  * AppKit user-context proxy so `getExecutionContext()` returns the
867
1091
  * OBO-scoped client. Resolves the plugin's Genie spaces and merges
868
1092
  * their curated `sample_questions` (see {@link collectSpaceSuggestions}).
869
- * Returns `[]` when no Genie space is configured so the client
870
- * shows a bare empty state instead of built-in example prompts.
1093
+ * `only` narrows the lookup to the given spaces (the direct Genie chat
1094
+ * passes its own). Returns `[]` when no Genie space is configured so the
1095
+ * client shows a bare empty state instead of built-in example prompts.
871
1096
  */
872
- private async fetchSuggestions(signal?: AbortSignal): Promise<ExecutionResult<string[]>> {
873
- const spaces = resolveGenieSpaces(this.config, this.context);
1097
+ private async fetchSuggestions(
1098
+ signal?: AbortSignal,
1099
+ only?: Record<string, GenieSpaceConfig>,
1100
+ ): Promise<ExecutionResult<string[]>> {
1101
+ const spaces = only ?? resolveGenieSpaces(this.config, this.context);
874
1102
  if (Object.keys(spaces).length === 0) return { ok: true, data: [] };
875
1103
  const client = getExecutionContext().client;
876
1104
  return this.execute((executeSignal) => {
@@ -942,7 +1170,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
942
1170
  async (executeSignal) => {
943
1171
  const combined = asyncUtils.combineAbortSignals(options.signal, executeSignal);
944
1172
  try {
945
- const data = await fetchStatementData(client, statementId, {
1173
+ const { data, visualization } = await fetchStatementEmbedData(client, statementId, {
946
1174
  limit,
947
1175
  ...(combined ? { signal: combined } : {}),
948
1176
  });
@@ -951,6 +1179,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
951
1179
  rows: data.rows,
952
1180
  rowCount: data.rowCount,
953
1181
  truncated: data.rows.length < data.rowCount,
1182
+ visualization,
954
1183
  };
955
1184
  } catch (err) {
956
1185
  // The Databricks SDK throws on 404; surface as `undefined`
@@ -1045,7 +1274,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1045
1274
  pluginRegistry.require(this.context, lakebase, this.config).exports().pool,
1046
1275
  ) as Pool)
1047
1276
  : undefined;
1048
- const memoryBuilder = memoryPool ? createMemoryBuilder(this.config, memoryPool) : undefined;
1277
+ const memoryBuilder = createMemoryBuilder(this.config, memoryPool);
1049
1278
 
1050
1279
  // Build every agent declared in `config.agents` (or the built-in
1051
1280
  // fallback when none are declared). Each agent's `model` resolves
@@ -1080,6 +1309,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1080
1309
  config: this.config,
1081
1310
  context: this.context,
1082
1311
  memoryBuilder,
1312
+ outputProcessorFactories: this.outputProcessorFactories,
1083
1313
  log: this.logger,
1084
1314
  extraSkillMounts: localSkillMounts,
1085
1315
  standardSkills,
@@ -1106,6 +1336,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1106
1336
  const observability = await buildObservability({
1107
1337
  serviceName: this.name,
1108
1338
  enabled: this.config.observability,
1339
+ tracerProvider: await appkitTelemetry.deferredNativeTracerProvider(),
1109
1340
  });
1110
1341
  // Optional MCP exposure: build a Mastra MCP server from the
1111
1342
  // registered agents (and, opt-in, the ambient tools) and register
@@ -1151,7 +1382,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1151
1382
  backgroundTurns: this.config.backgroundTurns !== false,
1152
1383
  apiAccess: this.config.apiAccess ?? "scoped",
1153
1384
  mcp: this.mcp ? `${this.basePath}${this.mcp.httpPath}` : "off",
1154
- lakebase: memoryBuilder !== undefined,
1385
+ lakebase: memoryPool !== undefined,
1155
1386
  feedback: this.feedbackEnabled(),
1156
1387
  observability: observability !== undefined ? "mlflow" : "off",
1157
1388
  });
@@ -0,0 +1,2 @@
1
+ /** Request-context key containing the browser application's canonical origin. */
2
+ export const MASTRA_ORIGIN_KEY = "mastra__origin";
package/src/server.ts CHANGED
@@ -20,6 +20,7 @@ import type express from "express";
20
20
 
21
21
  import {
22
22
  attributedUserId,
23
+ MASTRA_AUTH_MODE_KEY,
23
24
  MASTRA_REQUEST_ID_KEY,
24
25
  MASTRA_SCOPES_KEY,
25
26
  MASTRA_USER_EMAIL_KEY,
@@ -30,6 +31,7 @@ import {
30
31
  } from "./config.ts";
31
32
  import { requestUserEmail, requestUserId } from "./identity.ts";
32
33
  import { resolveFeedbackEnabled } from "./mlflow.ts";
34
+ import { MASTRA_ORIGIN_KEY } from "./request-context.ts";
33
35
  import {
34
36
  extractModelOverride,
35
37
  MASTRA_MODEL_OVERRIDE_KEY,
@@ -51,7 +53,9 @@ const TRUSTED_REQUEST_CONTEXT_KEYS = [
51
53
  MASTRA_USER_KEY,
52
54
  MASTRA_USER_NAME_KEY,
53
55
  MASTRA_USER_EMAIL_KEY,
56
+ MASTRA_AUTH_MODE_KEY,
54
57
  MASTRA_REQUEST_ID_KEY,
58
+ MASTRA_ORIGIN_KEY,
55
59
  MASTRA_SCOPES_KEY,
56
60
  MASTRA_MODEL_OVERRIDE_KEY,
57
61
  MASTRA_RESOLVED_MODEL_KEY,
@@ -64,6 +68,29 @@ export function clearTrustedRequestContext(requestContext: RequestContext): void
64
68
  }
65
69
  }
66
70
 
71
+ /** Resolve the canonical browser origin from standard proxy and browser headers. */
72
+ export function requestOrigin(req: express.Request): string | undefined {
73
+ for (const value of [req.get("origin"), req.get("referer")]) {
74
+ if (!value) continue;
75
+ try {
76
+ const url = new URL(value);
77
+ if (url.protocol === "http:" || url.protocol === "https:") return url.origin;
78
+ } catch {
79
+ // Continue to proxy headers.
80
+ }
81
+ }
82
+ const host = req.get("x-forwarded-host")?.split(",", 1)[0]?.trim() || req.get("host")?.trim();
83
+ if (!host) return undefined;
84
+ const protocol =
85
+ req.get("x-forwarded-proto")?.split(",", 1)[0]?.trim() || req.protocol || "https";
86
+ if (protocol !== "http" && protocol !== "https") return undefined;
87
+ try {
88
+ return new URL(`${protocol}://${host}`).origin;
89
+ } catch {
90
+ return undefined;
91
+ }
92
+ }
93
+
67
94
  /**
68
95
  * Who a turn is ATTRIBUTED to, independent of which Databricks credential runs
69
96
  * its calls. Supplied by the HTTP middleware from the forwarded user headers so
@@ -116,6 +143,7 @@ export async function stampRequestContextUser(
116
143
  };
117
144
  requestContext.set(MASTRA_USER_KEY, user);
118
145
  requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
146
+ requestContext.set(MASTRA_AUTH_MODE_KEY, isUserContext ? "obo" : "service-principal");
119
147
  // AppKit's `UserContext` surfaces display name / email only on
120
148
  // OBO requests. Service-context calls (background tasks, server
121
149
  // start-up) leave these undefined and we skip the stamp so
@@ -197,6 +225,7 @@ export class MastraServer extends ExpressMastraServer {
197
225
  this.configureRequestContextThreadId(req, res, requestContext);
198
226
  this.configureRequestContextModelOverride(req, requestContext);
199
227
  this.configureRequestContextRequestId(req, res, requestContext);
228
+ this.configureRequestContextOrigin(req, requestContext);
200
229
  this.configureRequestContextScopes(req, requestContext);
201
230
 
202
231
  this.configureMlflowTraceId(res);
@@ -238,6 +267,12 @@ export class MastraServer extends ExpressMastraServer {
238
267
  recordActiveTraceUser(traceUser);
239
268
  }
240
269
 
270
+ /** Stamp the public browser origin so plugin tools can return canonical app links. */
271
+ configureRequestContextOrigin(req: express.Request, requestContext: RequestContext): void {
272
+ const origin = requestOrigin(req);
273
+ if (origin) requestContext.set(MASTRA_ORIGIN_KEY, origin);
274
+ }
275
+
241
276
  /**
242
277
  * Stamp a per-request id and echo it on the response so an upstream
243
278
  * proxy / curl client / browser-side log line can pair its view of
package/src/statement.ts CHANGED
@@ -22,7 +22,7 @@
22
22
  import type { WorkspaceClient } from "@databricks/appkit";
23
23
  import { databricks } from "@dbx-tools/appkit";
24
24
  import { object } from "@dbx-tools/shared-core";
25
- import type { GenieDatasetData } from "@dbx-tools/shared-mastra";
25
+ import { wire, type GenieDatasetData, type GenieVisualizationData } from "@dbx-tools/shared-mastra";
26
26
 
27
27
  /**
28
28
  * Hard server-side cap on rows returned by the
@@ -48,6 +48,60 @@ function coerceCell(cell: string | null): unknown {
48
48
  return object.toNumber(cell, { separators: false, percent: false }) ?? cell;
49
49
  }
50
50
 
51
+ type StatementResponse = Awaited<ReturnType<WorkspaceClient["statementExecution"]["getStatement"]>>;
52
+
53
+ async function fetchStatementResponse(
54
+ client: WorkspaceClient,
55
+ statementId: string,
56
+ signal?: AbortSignal,
57
+ ): Promise<StatementResponse> {
58
+ const context = signal ? databricks.toContext(signal) : undefined;
59
+ return client.statementExecution.getStatement({ statement_id: statementId }, context);
60
+ }
61
+
62
+ function normalizedStatementData(response: StatementResponse, limit?: number): GenieDatasetData {
63
+ const columns = (response.manifest?.schema?.columns ?? []).map((column) => column.name ?? "");
64
+ const dataArray = (response.result?.data_array ?? []) as Array<Array<string | null>>;
65
+ const sliced = limit !== undefined && limit >= 0 ? dataArray.slice(0, limit) : dataArray;
66
+ const rows = sliced.map((row) => {
67
+ const record: Record<string, unknown> = {};
68
+ columns.forEach((column, index) => {
69
+ record[column] = coerceCell(row[index] ?? null);
70
+ });
71
+ return record;
72
+ });
73
+ return {
74
+ columns,
75
+ rows,
76
+ rowCount: response.manifest?.total_row_count ?? dataArray.length,
77
+ };
78
+ }
79
+
80
+ function visualizationData(response: StatementResponse, limit?: number): GenieVisualizationData {
81
+ const dataArray = (response.result?.data_array ?? []) as Array<Array<string | null>>;
82
+ return wire.GenieVisualizationDataSchema.parse({
83
+ manifest: {
84
+ schema: {
85
+ columns: (response.manifest?.schema?.columns ?? []).map((column) => ({
86
+ name: column.name,
87
+ type_name: column.type_name,
88
+ })),
89
+ },
90
+ },
91
+ result: {
92
+ data_array: limit !== undefined && limit >= 0 ? dataArray.slice(0, limit) : dataArray,
93
+ },
94
+ });
95
+ }
96
+
97
+ /** Normalized and AppKit-native projections of one statement response. */
98
+ export interface StatementEmbedData {
99
+ /** Row records used by tools, sortable tables, and chart planners. */
100
+ data: GenieDatasetData;
101
+ /** Manifest and row arrays consumed by AppKit UI's Genie visualization. */
102
+ visualization: GenieVisualizationData;
103
+ }
104
+
51
105
  /**
52
106
  * Fetch a single statement's rows via the Statement Execution API
53
107
  * and reshape into the shared {@link GenieDatasetData} shape
@@ -70,24 +124,22 @@ export async function fetchStatementData(
70
124
  statementId: string,
71
125
  options?: { limit?: number; signal?: AbortSignal },
72
126
  ): Promise<GenieDatasetData> {
73
- const ctx = options?.signal ? databricks.toContext(options.signal) : undefined;
74
- const r = await client.statementExecution.getStatement({ statement_id: statementId }, ctx);
75
- const columns = (r.manifest?.schema?.columns ?? []).map((c) => c.name ?? "");
76
- const dataArray = (r.result?.data_array ?? []) as Array<Array<string | null>>;
77
- const sliced =
78
- options?.limit !== undefined && options.limit >= 0
79
- ? dataArray.slice(0, options.limit)
80
- : dataArray;
81
- const rows = sliced.map((row) => {
82
- const obj: Record<string, unknown> = {};
83
- columns.forEach((col, i) => {
84
- obj[col] = coerceCell(row[i] ?? null);
85
- });
86
- return obj;
87
- });
127
+ const response = await fetchStatementResponse(client, statementId, options?.signal);
128
+ return normalizedStatementData(response, options?.limit);
129
+ }
130
+
131
+ /**
132
+ * Fetch one statement once and project it for both the existing data table and
133
+ * AppKit UI's native Genie visualization.
134
+ */
135
+ export async function fetchStatementEmbedData(
136
+ client: WorkspaceClient,
137
+ statementId: string,
138
+ options?: { limit?: number; signal?: AbortSignal },
139
+ ): Promise<StatementEmbedData> {
140
+ const response = await fetchStatementResponse(client, statementId, options?.signal);
88
141
  return {
89
- columns,
90
- rows,
91
- rowCount: r.manifest?.total_row_count ?? dataArray.length,
142
+ data: normalizedStatementData(response, options?.limit),
143
+ visualization: visualizationData(response, options?.limit),
92
144
  };
93
145
  }