@dbx-tools/appkit-mastra 0.1.18 → 0.1.19

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/package.json CHANGED
@@ -9,13 +9,13 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.18",
12
+ "version": "0.1.19",
13
13
  "dependencies": {
14
14
  "@databricks/sdk-experimental": "^0.17",
15
- "@dbx-tools/appkit-mastra-shared": "0.1.18",
16
- "@dbx-tools/genie": "0.1.18",
17
- "@dbx-tools/genie-shared": "0.1.18",
18
- "@dbx-tools/shared": "0.1.18",
15
+ "@dbx-tools/appkit-mastra-shared": "0.1.19",
16
+ "@dbx-tools/genie": "0.1.19",
17
+ "@dbx-tools/genie-shared": "0.1.19",
18
+ "@dbx-tools/shared": "0.1.19",
19
19
  "@mastra/ai-sdk": "^1",
20
20
  "@mastra/core": "^1",
21
21
  "@mastra/express": "^1",
package/src/agents.ts CHANGED
@@ -658,13 +658,12 @@ function buildPluginsMap(
658
658
  * Genie agent inherits the same model resolver / fallback
659
659
  * ladder the calling agents use.
660
660
  *
661
- * The legacy AppKit `genie` plugin's tools are no longer consulted
662
- * at runtime - the Genie agent talks to Genie directly via
663
- * `@dbx-tools/genie` (`genieEventChat`) and the workspace
664
- * `statementExecution.getStatement` API. But the plugin's
665
- * resource manifest and `spaces` config are still honored so
666
- * existing `app.yaml` configs and `genie({ spaces })` wiring
667
- * keep working without change.
661
+ * The Genie agent talks to Genie directly via `@dbx-tools/genie`
662
+ * (`genieEventChat`) and the workspace
663
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
664
+ * plugin is honored only for its resource manifest and `spaces`
665
+ * config so existing `app.yaml` configs and `genie({ spaces })`
666
+ * wiring keep working without change.
668
667
  */
669
668
  function resolveProvider(
670
669
  config: MastraPluginConfig,
package/src/chart.ts CHANGED
@@ -36,6 +36,7 @@ import { z } from "zod";
36
36
 
37
37
  import type { MastraPluginConfig } from "./config.js";
38
38
  import { ModelTier, modelForTier, buildModel } from "./model.js";
39
+ import { safeWrite } from "./writer.js";
39
40
 
40
41
  /**
41
42
  * Module-level logger tagged `[mastra/chart]`. Uses the shared
@@ -425,60 +426,43 @@ export function buildRenderDataTool(config: MastraPluginConfig) {
425
426
  // for the table-like fallback / hover, option for the
426
427
  // actual render. Best-effort write so a closed
427
428
  // downstream stream can't take the tool down.
428
- await safeWrite(writer, chartId, {
429
- type: "chart",
430
- chartId,
431
- title,
432
- ...(description ? { description } : {}),
433
- data,
434
- option,
435
- });
429
+ await safeWrite(
430
+ log,
431
+ writer,
432
+ {
433
+ type: "chart",
434
+ chartId,
435
+ title,
436
+ ...(description ? { description } : {}),
437
+ data,
438
+ option,
439
+ },
440
+ { chartId },
441
+ );
436
442
  } catch (err) {
437
443
  log.warn("render:error", {
438
444
  chartId,
439
445
  elapsedMs: Date.now() - startedAt,
440
- error: err instanceof Error ? err.message : String(err),
446
+ error: commonUtils.errorMessage(err),
441
447
  });
442
448
  // Surface as a writer-level error so the slot can
443
449
  // transition to "couldn't render chart" without the
444
450
  // parent agent surfacing a stack trace.
445
- await safeWrite(writer, chartId, {
446
- type: "error",
447
- error: err instanceof Error ? err.message : String(err),
448
- });
451
+ await safeWrite(
452
+ log,
453
+ writer,
454
+ {
455
+ type: "error",
456
+ error: commonUtils.errorMessage(err),
457
+ },
458
+ { chartId },
459
+ );
449
460
  }
450
461
  return { chartId };
451
462
  },
452
463
  });
453
464
  }
454
465
 
455
- /**
456
- * Best-effort writer.write. Failures are logged at `warn` (a
457
- * persistently-closed writer is the most likely culprit when
458
- * chart events go missing client-side) but swallowed so a closed
459
- * downstream stream (cancelled request, client navigated away)
460
- * can't take a tool down.
461
- */
462
- async function safeWrite(
463
- writer: MinimalWriter | undefined,
464
- chartId: string,
465
- chunk: unknown,
466
- ): Promise<void> {
467
- if (!writer) {
468
- log.debug("write:no-writer", { chartId });
469
- return;
470
- }
471
- try {
472
- await writer.write(chunk);
473
- log.debug("write:ok", { chartId });
474
- } catch (err) {
475
- log.warn("write:error", {
476
- chartId,
477
- error: err instanceof Error ? err.message : String(err),
478
- });
479
- }
480
- }
481
-
482
466
  /**
483
467
  * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
484
468
  * JSON. Centralized here so the planner agent only fills in the
package/src/genie.ts CHANGED
@@ -32,12 +32,11 @@
32
32
  * {@link GenieSummaryItem} `string` variant, and returns the
33
33
  * hydrated {@link GenieAgentResult}.
34
34
  *
35
- * The legacy AppKit `genie` plugin (`@databricks/appkit`'s `genie`)
36
- * is no longer used at runtime. The inner agent talks to Genie
37
- * directly via `@dbx-tools/genie` (`genieEventChat`) and the
38
- * workspace `statementExecution.getStatement` API. The plugin's
39
- * `spaces` config is still honored so existing AppKit-style wiring
40
- * keeps working without change.
35
+ * The inner agent talks to Genie directly via
36
+ * `@dbx-tools/genie` (`genieEventChat`) and the workspace
37
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
38
+ * plugin is honored only for its `spaces` config so existing
39
+ * AppKit-style wiring keeps working without change.
41
40
  */
42
41
 
43
42
  import { CacheManager, genie } from "@databricks/appkit";
@@ -73,6 +72,7 @@ import { runChartPlanner } from "./chart.js";
73
72
  import type { MastraPluginConfig } from "./config.js";
74
73
  import { MASTRA_USER_KEY, type User } from "./config.js";
75
74
  import { buildModel } from "./model.js";
75
+ import { safeWrite } from "./writer.js";
76
76
 
77
77
  const log = logUtils.logger("mastra/genie");
78
78
 
@@ -169,28 +169,6 @@ function extractStatementId(message: GenieMessage): string | undefined {
169
169
  return undefined;
170
170
  }
171
171
 
172
- /**
173
- * Best-effort `writer.write`. The writer carries the unified flat
174
- * event vocabulary directly - no translation layer - so
175
- * subscribers narrow on `event.type` and read fields inline.
176
- * Failures (downstream stream closed, cancelled request) are
177
- * swallowed with a `warn` log so an in-flight Genie turn isn't
178
- * taken down by a navigated-away client.
179
- */
180
- async function safeWrite(
181
- writer: MinimalWriter | undefined,
182
- chunk: unknown,
183
- ): Promise<void> {
184
- if (!writer) return;
185
- try {
186
- await writer.write(chunk);
187
- } catch (err) {
188
- log.warn("writer:error", {
189
- error: err instanceof Error ? err.message : String(err),
190
- });
191
- }
192
- }
193
-
194
172
  /**
195
173
  * Lowercased placeholder strings we reject at the `ask_genie`
196
174
  * boundary so the LLM doesn't spend a Genie round-trip on a
@@ -362,7 +340,7 @@ async function readCachedConversationId(
362
340
  return v ?? undefined;
363
341
  } catch (err) {
364
342
  log.warn("conversation-cache:read-error", {
365
- error: err instanceof Error ? err.message : String(err),
343
+ error: commonUtils.errorMessage(err),
366
344
  });
367
345
  return undefined;
368
346
  }
@@ -386,7 +364,7 @@ async function saveCachedConversationId(
386
364
  });
387
365
  } catch (err) {
388
366
  log.warn("conversation-cache:write-error", {
389
- error: err instanceof Error ? err.message : String(err),
367
+ error: commonUtils.errorMessage(err),
390
368
  });
391
369
  }
392
370
  }
@@ -398,7 +376,7 @@ async function evictCachedConversationId(cacheKey: string | undefined): Promise<
398
376
  await CacheManager.getInstanceSync().delete(cacheKey);
399
377
  } catch (err) {
400
378
  log.warn("conversation-cache:delete-error", {
401
- error: err instanceof Error ? err.message : String(err),
379
+ error: commonUtils.errorMessage(err),
402
380
  });
403
381
  }
404
382
  }
@@ -523,7 +501,7 @@ function buildAskGenieTool(deps: InnerToolDeps) {
523
501
  ...(seedConversationId ? { conversationId: seedConversationId } : {}),
524
502
  ...(signal ? { context: signal } : {}),
525
503
  })) {
526
- await safeWrite(writer, event);
504
+ await safeWrite(log, writer, event);
527
505
  // Wire events come in two flavors: the lifecycle `message`
528
506
  // event embeds the raw `GenieMessage` (read its
529
507
  // `conversation_id`), and the rest carry a flat
@@ -564,7 +542,7 @@ function buildAskGenieTool(deps: InnerToolDeps) {
564
542
  log.warn("conversation-cache:stale, resetting", {
565
543
  spaceId,
566
544
  conversationId: seeded,
567
- error: err instanceof Error ? err.message : String(err),
545
+ error: commonUtils.errorMessage(err),
568
546
  });
569
547
  await evictCachedConversationId(cacheKey);
570
548
  writeContextConversationId(requestContext, spaceId, undefined);
@@ -852,7 +830,7 @@ export function createGenieTool(opts: CreateGenieToolOptions) {
852
830
  spaceId,
853
831
  content: input.question,
854
832
  };
855
- await safeWrite(writer, startedEvent);
833
+ await safeWrite(log, writer, startedEvent);
856
834
 
857
835
  const resultSets = new Map<string, StatementEntry>();
858
836
 
@@ -999,7 +977,7 @@ export function createGenieTool(opts: CreateGenieToolOptions) {
999
977
  textItems: textItemCount,
1000
978
  dataItems: dataItemCount,
1001
979
  };
1002
- await safeWrite(writer, summaryEvent);
980
+ await safeWrite(log, writer, summaryEvent);
1003
981
 
1004
982
  // Chart every `data` item in parallel; map `text` items to
1005
983
  // the shared `string` summary variant verbatim. Missing
@@ -1011,87 +989,89 @@ export function createGenieTool(opts: CreateGenieToolOptions) {
1011
989
  // chart slot the moment its planner returns rather than
1012
990
  // waiting for the entire batch to finish.
1013
991
  const hydrated = await Promise.all(
1014
- submission.summary.map(async (item: AgentSummaryItem): Promise<GenieSummaryItem | undefined> => {
1015
- if (item.type === "text") {
1016
- return { type: "string", text: item.text };
1017
- }
1018
- const entry = resultSets.get(item.statementId);
1019
- if (!entry) {
1020
- log.warn("data:missing-statement", {
1021
- statementId: item.statementId,
1022
- });
1023
- return undefined;
1024
- }
1025
- const { data, messageId } = entry;
1026
- let dataset: GenieDataset = { data };
1027
- try {
1028
- const planned = await runChartPlanner({
1029
- config,
1030
- requestContext,
1031
- title: item.title ?? "Genie result",
1032
- ...(item.description ? { description: item.description } : {}),
1033
- data: data.rows,
1034
- ...(signal ? { signal } : {}),
1035
- });
1036
- const chartId = commonUtils.shortId();
1037
- // Slim chart reference for the LLM-bound result: just
1038
- // `chartId` + `chartType`. The full Echarts spec goes
1039
- // to the UI via the writer event AND into the
1040
- // request-scoped chart inventory below; the model
1041
- // only needs the id to place `[[chart:<id>]]`.
1042
- dataset = {
1043
- data,
1044
- chart: {
992
+ submission.summary.map(
993
+ async (item: AgentSummaryItem): Promise<GenieSummaryItem | undefined> => {
994
+ if (item.type === "text") {
995
+ return { type: "string", text: item.text };
996
+ }
997
+ const entry = resultSets.get(item.statementId);
998
+ if (!entry) {
999
+ log.warn("data:missing-statement", {
1000
+ statementId: item.statementId,
1001
+ });
1002
+ return undefined;
1003
+ }
1004
+ const { data, messageId } = entry;
1005
+ let dataset: GenieDataset = { data };
1006
+ try {
1007
+ const planned = await runChartPlanner({
1008
+ config,
1009
+ requestContext,
1010
+ title: item.title ?? "Genie result",
1011
+ ...(item.description ? { description: item.description } : {}),
1012
+ data: data.rows,
1013
+ ...(signal ? { signal } : {}),
1014
+ });
1015
+ const chartId = commonUtils.shortId();
1016
+ // Slim chart reference for the LLM-bound result: just
1017
+ // `chartId` + `chartType`. The full Echarts spec goes
1018
+ // to the UI via the writer event AND into the
1019
+ // request-scoped chart inventory below; the model
1020
+ // only needs the id to place `[[chart:<id>]]`.
1021
+ dataset = {
1022
+ data,
1023
+ chart: {
1024
+ chartId,
1025
+ chartType: planned.chartType,
1026
+ },
1027
+ };
1028
+ const chartEvent: ChartEvent = {
1029
+ type: "chart",
1045
1030
  chartId,
1046
- chartType: planned.chartType,
1047
- },
1048
- };
1049
- const chartEvent: ChartEvent = {
1050
- type: "chart",
1051
- chartId,
1031
+ statementId: item.statementId,
1032
+ messageId,
1033
+ ...(item.title ? { title: item.title } : {}),
1034
+ ...(item.description ? { description: item.description } : {}),
1035
+ data: data.rows,
1036
+ option: planned.option,
1037
+ };
1038
+ await safeWrite(log, writer, chartEvent);
1039
+ // Stash the resolved chart on the per-request
1040
+ // `RequestContext` so downstream code in the same
1041
+ // request (output processors, follow-up tool calls,
1042
+ // any post-run hook) can look up the full spec by
1043
+ // `chartId` without re-fetching or re-planning.
1044
+ recordChartInContext(requestContext, chartEvent);
1045
+ } catch (err) {
1046
+ const errorMessage = commonUtils.errorMessage(err);
1047
+ log.warn("chart:error", {
1048
+ statementId: item.statementId,
1049
+ messageId,
1050
+ error: errorMessage,
1051
+ });
1052
+ // Surface the chart-planner failure as a writer event
1053
+ // stamped with the same `messageId` the rest of this
1054
+ // ask's wire events carry, so the host UI groups the
1055
+ // failure into the same pill bucket and can surface
1056
+ // a "couldn't render chart" note next to the table
1057
+ // fallback instead of silently dropping the chart.
1058
+ const errorEvent: MastraGenieErrorEvent = {
1059
+ type: "error",
1060
+ spaceId,
1061
+ messageId,
1062
+ error: `chart-planner: ${errorMessage}`,
1063
+ };
1064
+ await safeWrite(log, writer, errorEvent);
1065
+ }
1066
+ return {
1067
+ type: "visualize",
1052
1068
  statementId: item.statementId,
1053
- messageId,
1054
1069
  ...(item.title ? { title: item.title } : {}),
1055
1070
  ...(item.description ? { description: item.description } : {}),
1056
- data: data.rows,
1057
- option: planned.option,
1071
+ dataset,
1058
1072
  };
1059
- await safeWrite(writer, chartEvent);
1060
- // Stash the resolved chart on the per-request
1061
- // `RequestContext` so downstream code in the same
1062
- // request (output processors, follow-up tool calls,
1063
- // any post-run hook) can look up the full spec by
1064
- // `chartId` without re-fetching or re-planning.
1065
- recordChartInContext(requestContext, chartEvent);
1066
- } catch (err) {
1067
- const errorMessage = err instanceof Error ? err.message : String(err);
1068
- log.warn("chart:error", {
1069
- statementId: item.statementId,
1070
- messageId,
1071
- error: errorMessage,
1072
- });
1073
- // Surface the chart-planner failure as a writer event
1074
- // stamped with the same `messageId` the rest of this
1075
- // ask's wire events carry, so the host UI groups the
1076
- // failure into the same pill bucket and can surface
1077
- // a "couldn't render chart" note next to the table
1078
- // fallback instead of silently dropping the chart.
1079
- const errorEvent: MastraGenieErrorEvent = {
1080
- type: "error",
1081
- spaceId,
1082
- messageId,
1083
- error: `chart-planner: ${errorMessage}`,
1084
- };
1085
- await safeWrite(writer, errorEvent);
1086
- }
1087
- return {
1088
- type: "visualize",
1089
- statementId: item.statementId,
1090
- ...(item.title ? { title: item.title } : {}),
1091
- ...(item.description ? { description: item.description } : {}),
1092
- dataset,
1093
- };
1094
- }),
1073
+ },
1074
+ ),
1095
1075
  );
1096
1076
  const summary = hydrated.filter((x): x is GenieSummaryItem => x !== undefined);
1097
1077
 
package/src/history.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
18
 
19
- import { logUtils } from "@dbx-tools/shared";
19
+ import { commonUtils, logUtils } from "@dbx-tools/shared";
20
20
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
21
21
  import type { Agent } from "@mastra/core/agent";
22
22
  import type { MastraDBMessage } from "@mastra/core/agent/message-list";
@@ -154,7 +154,7 @@ export async function clearHistory(
154
154
  log.debug("clear:probe-failed", {
155
155
  agentId: opts.agent.id,
156
156
  threadId: opts.threadId,
157
- error: err instanceof Error ? err.message : String(err),
157
+ error: commonUtils.errorMessage(err),
158
158
  });
159
159
  }
160
160
 
@@ -169,7 +169,7 @@ export async function clearHistory(
169
169
  log.warn("clear:delete-soft-failed", {
170
170
  agentId: opts.agent.id,
171
171
  threadId: opts.threadId,
172
- error: err instanceof Error ? err.message : String(err),
172
+ error: commonUtils.errorMessage(err),
173
173
  });
174
174
  }
175
175
  log.info("clear:done", {
package/src/writer.ts ADDED
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Shared helper for publishing events through Mastra's
3
+ * `ctx.writer`. Centralizes the "the downstream stream may already
4
+ * be closed, don't take the whole tool down" pattern that the
5
+ * Genie agent and chart tool both need.
6
+ *
7
+ * Failures are logged at `warn` (a persistently-closed writer is
8
+ * the most likely culprit when events go missing client-side) but
9
+ * swallowed so a cancelled request or a client that navigated
10
+ * away can't crash a tool mid-flight.
11
+ */
12
+
13
+ import type { MinimalWriter } from "@dbx-tools/appkit-mastra-shared";
14
+ import { commonUtils, type logUtils } from "@dbx-tools/shared";
15
+
16
+ /**
17
+ * Best-effort `writer.write`. No-op when `writer` is undefined;
18
+ * caught errors are logged via `log.warn("writer:error", ...)`
19
+ * along with any caller-supplied `context` fields (e.g. a
20
+ * `chartId` or `messageId`) so the warning is greppable per
21
+ * resource.
22
+ *
23
+ * Returns when the write resolves or rejects; never throws.
24
+ */
25
+ export async function safeWrite(
26
+ log: logUtils.Logger,
27
+ writer: MinimalWriter | undefined,
28
+ chunk: unknown,
29
+ context: Record<string, unknown> = {},
30
+ ): Promise<void> {
31
+ if (!writer) {
32
+ log.debug("writer:no-writer", context);
33
+ return;
34
+ }
35
+ try {
36
+ await writer.write(chunk);
37
+ log.debug("writer:ok", context);
38
+ } catch (err) {
39
+ log.warn("writer:error", {
40
+ ...context,
41
+ error: commonUtils.errorMessage(err),
42
+ });
43
+ }
44
+ }