@dbx-tools/appkit-mastra 0.3.28 → 0.3.30

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/README.md CHANGED
@@ -80,7 +80,7 @@ await createApp({
80
80
  plugin.mastra({
81
81
  agents: { analyst },
82
82
  defaultAgent: "analyst",
83
- genie: { spaces: { sales: "01ef..." } },
83
+ genieSpaces: { sales: "01ef..." },
84
84
  }),
85
85
  ],
86
86
  });
@@ -153,6 +153,31 @@ Tool calls dispatch back through the owning AppKit plugin, preserving OBO auth
153
153
  and AppKit telemetry behavior. Optional plugins should be guarded with `?.` when
154
154
  you spread their tools.
155
155
 
156
+ ### Tools Flow In, Not Out
157
+
158
+ The plugin is a tool _consumer_, not an AppKit `ToolProvider`: it deliberately
159
+ implements neither `getAgentTools()` nor `executeAgentTool()`, so its built-in
160
+ tools (`ask_genie`, `get_space_description`, `get_space_serialized`,
161
+ `get_statement`, `prepare_chart`, `render_data`, `summarize`) are reachable only
162
+ from a Mastra agent turn this plugin serves.
163
+
164
+ That is a property of the tools, not a gap. Each one reads the per-request
165
+ Mastra execution context - the AppKit user stamped on `RequestContext`, the
166
+ `writer` that streams Genie progress events to the chat, the per-call
167
+ `abortSignal` - and refuses to run without it. An AppKit `ToolProvider` call
168
+ carries none of that, so exposing these through one would advertise tools that
169
+ cannot work. Reach for
170
+ [native AppKit Agents](https://developers.databricks.com/docs/appkit/v0) when you
171
+ want your agent tools callable by other AppKit hosts.
172
+
173
+ Nothing here can be auto-inherited by another host as a side effect: with no
174
+ AppKit `ToolRegistry`, there is no `autoInheritable` surface to opt in or out
175
+ of. Every built-in tool is also read-only (Genie questions, statement reads,
176
+ chart planning, summarization), and the ambient tools stay off the MCP server
177
+ unless `mcp: { tools: true }` names them explicitly. Approval-gated tools you
178
+ register yourself are enforced separately: boot fails if one is registered
179
+ without Mastra storage to persist the suspended run.
180
+
156
181
  ## Memory And Storage
157
182
 
158
183
  The `memory` and `storage` config fields can be `false`, `true`, or a concrete
@@ -232,19 +257,24 @@ record, resolves the data in the background, and stores a terminal chart or
232
257
  error. `chart.fetchChart()` long-polls that cache for route handlers and custom
233
258
  clients.
234
259
 
260
+ Both take a `userKey`: the chart cache is namespaced by the caller's identity,
261
+ so a chart id lifted from another user's transcript resolves to nothing and the
262
+ embed route answers `404`. Use `config.resolveUserKey()`, which reads the AppKit
263
+ user off the Mastra request context and falls back to the ambient execution
264
+ context.
265
+
235
266
  ```ts
267
+ const userKey = config.resolveUserKey(requestContext);
268
+
236
269
  const { chartId } = await chart.prepareChart({
237
- config,
238
- request: {
239
- title: "Revenue by region",
240
- chartType: "bar",
241
- instructions: "Compare total revenue by region.",
242
- data: rows,
243
- },
244
- resolveData: async () => rows,
270
+ config: pluginConfig,
271
+ userKey,
272
+ title: "Revenue by region",
273
+ description: "Compare total revenue by region.",
274
+ resolveData: async () => ({ rows }),
245
275
  });
246
276
 
247
- const resolved = await chart.fetchChart(chartId);
277
+ const resolved = await chart.fetchChart(chartId, { userKey });
248
278
  ```
249
279
 
250
280
  Agents can return `[chart:<id>]` and `[data:<statement_id>]` markers in prose.
@@ -302,7 +332,9 @@ returning `{ agentId, model, displayName }` - the static serving-endpoint an
302
332
  agent falls back to when the client pins no model, plus its humanized label.
303
333
  `model` / `displayName` are `null` when the agent resolves its model
304
334
  dynamically at call time. This lets a model picker label its default option
305
- without waiting on the `/models` catalogue (so it never flashes a raw id).
335
+ without waiting on the `/models` catalogue (so it never flashes a raw id). A
336
+ `:agentId` that is not registered returns `404` with the registered ids, the
337
+ same as the history and threads routes.
306
338
 
307
339
  ## Threads, History, And Suggestions
308
340
 
@@ -366,6 +398,40 @@ client needs. Use `apiAccess: "full"` only for a trusted first-party console.
366
398
  `server.isMastraRequestAllowed()` is exported for tests and custom dispatch
367
399
  logic that need the same allowlist.
368
400
 
401
+ ## Routes
402
+
403
+ Mounted under the plugin base path, which is `/api/mastra` unless you override
404
+ `name`. Every route below is registered through AppKit's `route()` helper, so it
405
+ appears in the plugin's endpoint map and forwards handler errors to AppKit.
406
+
407
+ | Method | Path | Purpose |
408
+ | -------------------------- | --------------------------- | ------------------------------------------------------------------------------------------- |
409
+ | `GET` | `/models` | Serving-endpoint catalogue for a model picker. |
410
+ | `GET` | `/default-model[/:agentId]` | Static default model an agent falls back to, with its humanized label. `404` on unknown id. |
411
+ | `GET` | `/suggestions[/:agentId]` | Starter questions from the configured Genie spaces. Degrades to `[]`. |
412
+ | `GET` | `/embed/chart/:id` | Long-polls a `[chart:<id>]` marker's cached spec. `?timeoutMs=` up to 5 minutes. |
413
+ | `GET` | `/embed/data/:id` | Rows behind a `[data:<statement_id>]` marker. `?limit=` clamped server-side. |
414
+ | `GET` / `DELETE` | `/route/history[/:agentId]` | Load or clear the caller's thread messages. |
415
+ | `GET` / `DELETE` / `PATCH` | `/route/threads[/:agentId]` | List, delete, or rename the caller's conversations. |
416
+ | `POST` | `/route/feedback` | Log a thumbs / comment assessment to the turn's MLflow trace. `404` when feedback is off. |
417
+ | `POST` / `GET` | `/mcp`, `/sse`, `/messages` | MCP transports, when `mcp` is enabled. |
418
+
419
+ Agent inference itself rides the stock Mastra routes (`/agents/:id/stream`), so
420
+ `@mastra/client-js` and `@dbx-tools/ui-mastra` work without a bespoke protocol.
421
+
422
+ ## Environment Variables
423
+
424
+ Every value can also be set through plugin config, which wins. These are the
425
+ fallbacks, so a deployment that already follows AppKit's Databricks env naming
426
+ needs no extra wiring.
427
+
428
+ | Variable | Effect |
429
+ | ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
430
+ | `DATABRICKS_SERVING_ENDPOINT_NAME` | Model used when neither the agent nor `defaultModel` names one. |
431
+ | `DATABRICKS_GENIE_SPACE_ID` | Genie space registered under the `default` alias. |
432
+ | `OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT` | Presence of either turns Mastra tracing on when `observability` is unset. |
433
+ | `MLFLOW_EXPERIMENT_ID`, `MLFLOW_EXPERIMENT_NAME` | With an OTLP endpoint, turns MLflow feedback on when `feedback` is unset. |
434
+
369
435
  ## Configuration Reference
370
436
 
371
437
  The plugin config is intentionally centered on the AppKit lifecycle instead of
@@ -377,8 +443,11 @@ requiring callers to assemble a Mastra server by hand.
377
443
  name an agent explicitly.
378
444
  - `storage` and `memory` accept `true`, `false`, or concrete Mastra Postgres /
379
445
  PgVector options. `true` resolves from `lakebase()` when present.
380
- - `genie.spaces` maps aliases to Genie Space IDs. Those aliases flow into tools,
381
- suggestions, and chart/data workflows.
446
+ - `genieSpaces` maps aliases to Genie Space IDs (or to
447
+ `{ spaceId, hint }` objects). Those aliases flow into tool names,
448
+ suggestions, and chart/data workflows. An alias present with no space id is a
449
+ wiring contradiction and fails at construction rather than silently
450
+ registering no Genie tools.
382
451
  - `defaultModel`, `modelOverride`, and `modelFuzzyMatch` control how loose model
383
452
  names are resolved through Databricks Model Serving.
384
453
  - `feedback` controls whether MLflow feedback routes are exposed. The automatic
@@ -403,8 +472,10 @@ client that talks to these routes.
403
472
  - `genie` - Genie prompt, space normalization, Genie toolkits, and suggestions.
404
473
  - `chart` / `statement` / `writer` - chart cache, statement row fetches, and
405
474
  safe writer events.
406
- - `history` / `threads` / `pagination` - conversation persistence helpers and
407
- route handlers.
475
+ - `history` / `threads` / `pagination` / `validation` - conversation persistence
476
+ helpers, route handlers, and request-body validation.
477
+ - `defaults` - cache / retry / timeout settings for the plugin's own outbound
478
+ calls, one constant per call site with its reasoning.
408
479
  - `memory` / `storageSchema` - Lakebase-backed Mastra store/vector setup.
409
480
  - `workspaces` / `filesystems` - Mastra workspace creation and Databricks
410
481
  Workspace file adapters.
package/index.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  export * as agents from "./src/agents";
6
6
  export * as chart from "./src/chart";
7
7
  export * as config from "./src/config";
8
+ export * as defaults from "./src/defaults";
8
9
  export * as filesystems from "./src/filesystems";
9
10
  export * as genie from "./src/genie";
10
11
  export * as history from "./src/history";
@@ -24,6 +25,7 @@ export * as statement from "./src/statement";
24
25
  export * as storageSchema from "./src/storage-schema";
25
26
  export * as summarize from "./src/summarize";
26
27
  export * as threads from "./src/threads";
28
+ export * as validation from "./src/validation";
27
29
  export * as workspaces from "./src/workspaces";
28
30
  export * as writer from "./src/writer";
29
31
  export type { MastraTools, AppKitToolOptions, ToolkitOptions, MastraPluginToolkitProvider, MastraPlugins, MastraToolsFn, MastraAgentWorkspaceResolver, MastraAgentDefinition, MastraStorageConfigOverride, MastraMemoryConfigOverride, BuiltAgents } from "./src/agents";
@@ -43,4 +45,5 @@ export type { ModelOverrideRequest } from "./src/serving";
43
45
  export type { ServingChatMessage } from "./src/serving-sanitize";
44
46
  export type { SummarizeOptions } from "./src/summarize";
45
47
  export type { ListThreadsOptions, DeleteThreadOptions, RenameThreadOptions, ThreadsRouteOptions } from "./src/threads";
48
+ export type { SchemaIssues } from "./src/validation";
46
49
  export type { CreateWorkspaceOptions } from "./src/workspaces";
package/package.json CHANGED
@@ -27,23 +27,22 @@
27
27
  "@opentelemetry/api": "^1.9.1",
28
28
  "express": "^5.1.0",
29
29
  "pg": "^8.22.0",
30
- "zod": "^4.3.6",
31
- "@dbx-tools/appkit": "0.3.28",
32
- "@dbx-tools/core": "0.3.28",
33
- "@dbx-tools/model": "0.3.28",
34
- "@dbx-tools/shared-core": "0.3.28",
35
- "@dbx-tools/databricks": "0.3.28",
36
- "@dbx-tools/genie": "0.3.28",
37
- "@dbx-tools/shared-genie": "0.3.28",
38
- "@dbx-tools/shared-mastra": "0.3.28",
39
- "@dbx-tools/shared-model": "0.3.28"
30
+ "zod": "4.3.6",
31
+ "@dbx-tools/appkit": "0.3.30",
32
+ "@dbx-tools/genie": "0.3.30",
33
+ "@dbx-tools/core": "0.3.30",
34
+ "@dbx-tools/model": "0.3.30",
35
+ "@dbx-tools/shared-core": "0.3.30",
36
+ "@dbx-tools/shared-genie": "0.3.30",
37
+ "@dbx-tools/shared-mastra": "0.3.30",
38
+ "@dbx-tools/shared-model": "0.3.30"
40
39
  },
41
40
  "main": "index.ts",
42
41
  "license": "UNLICENSED",
43
42
  "publishConfig": {
44
43
  "access": "public"
45
44
  },
46
- "version": "0.3.28",
45
+ "version": "0.3.30",
47
46
  "types": "index.ts",
48
47
  "type": "module",
49
48
  "exports": {
package/src/agents.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  * @module
16
16
  */
17
17
 
18
+ import { ConfigurationError } from "@databricks/appkit";
18
19
  import { plugin } from "@dbx-tools/appkit";
19
20
  import { fallback } from "@dbx-tools/model";
20
21
  import { log, object, string } from "@dbx-tools/shared-core";
@@ -524,8 +525,9 @@ export async function buildAgents(opts: {
524
525
  }
525
526
 
526
527
  if (!agents[defaultAgentId]) {
527
- throw new Error(
528
- `mastra: defaultAgent "${defaultAgentId}" not found in registered agents (${ids.join(", ") || "none"})`,
528
+ throw ConfigurationError.resourceNotFound(
529
+ `mastra defaultAgent "${defaultAgentId}"`,
530
+ `Registered agents: ${ids.join(", ") || "none"}.`,
529
531
  );
530
532
  }
531
533
 
@@ -578,7 +580,7 @@ function assertApprovalGatedToolsHaveStorage(
578
580
  const detail = gated
579
581
  .map(({ agentId, toolIds }) => `${agentId}: ${toolIds.join(", ")}`)
580
582
  .join("; ");
581
- throw new Error(
583
+ throw new ConfigurationError(
582
584
  "mastra: approval-gated tools require plugin storage (PostgresStore) to persist suspended runs. " +
583
585
  `Affected agents/tools: ${detail}. ` +
584
586
  "Register lakebase() before mastra() so storage auto-enables, or pass storage: true explicitly.",
@@ -637,7 +639,7 @@ function resolveDefinitions(config: MastraPluginConfig): Record<string, MastraAg
637
639
  input.forEach((def, i) => {
638
640
  const key = deriveAgentKey(def, i);
639
641
  if (out[key]) {
640
- throw new Error(
642
+ throw new ConfigurationError(
641
643
  `mastra: duplicate agent id "${key}" derived from name "${def.name ?? ""}"; ` +
642
644
  `set unique \`name\`s on each definition`,
643
645
  );
@@ -749,7 +751,7 @@ function resolveAgentWorkspace(
749
751
  * `genie` is special-cased to swap the generic AppKit toolkit (which
750
752
  * runs `executeAgentTool` and only emits a single final `tool-result`
751
753
  * chunk per call) for the streaming-aware tools built by
752
- * {@link buildGenieProvider}. The streaming variant forwards each
754
+ * {@link buildGenieToolkitProvider}. The streaming variant forwards each
753
755
  * Genie wire event (status, SQL, row counts, errors) out through the
754
756
  * Mastra `ctx.writer`, so the UI gets `tool-output` chunks in real
755
757
  * time instead of staring at a spinner for the full Genie round-trip.
@@ -782,12 +784,11 @@ function buildPluginsMap(
782
784
  * Genie agent inherits the same model resolver / fallback
783
785
  * ladder the calling agents use.
784
786
  *
785
- * The Genie agent talks to Genie directly via `@dbx-tools/genie`
786
- * (`genieEventChat`) and the workspace
787
- * `statementExecution.getStatement` API. AppKit's stock `genie`
788
- * plugin is honored only for its resource manifest and `spaces`
789
- * config so existing `app.yaml` configs and `genie({ spaces })`
790
- * wiring keep working without change.
787
+ * The Genie tools talk to Genie directly through `@dbx-tools/genie`
788
+ * (`genieEventChat`) and the workspace `statementExecution.getStatement`
789
+ * API; AppKit's stock `genie` plugin contributes only its resource
790
+ * manifest and `spaces` config, so an `app.yaml` resource binding and a
791
+ * `genie({ spaces })` record have the same meaning here as they do there.
791
792
  */
792
793
  function resolveProvider(
793
794
  config: MastraPluginConfig,
package/src/chart.ts CHANGED
@@ -27,7 +27,7 @@
27
27
  * @module
28
28
  */
29
29
 
30
- import { CacheManager } from "@databricks/appkit";
30
+ import { AppKitError, CacheManager, ExecutionError } from "@databricks/appkit";
31
31
  import { async, error, hash, log, string, type BrandContext } from "@dbx-tools/shared-core";
32
32
  import { wire, type Chart, type ChartResult } from "@dbx-tools/shared-mastra";
33
33
  import { model } from "@dbx-tools/shared-model";
@@ -36,7 +36,7 @@ import type { RequestContext } from "@mastra/core/request-context";
36
36
  import { createTool } from "@mastra/core/tools";
37
37
  import { z } from "zod";
38
38
 
39
- import type { MastraPluginConfig } from "./config";
39
+ import { resolveUserKey, type MastraPluginConfig } from "./config";
40
40
  import { buildModel } from "./model";
41
41
 
42
42
  const logger = log.logger("mastra/chart");
@@ -55,20 +55,15 @@ const CHART_CACHE_TTL_SEC = 60 * 60;
55
55
  /** Cache namespace; keeps the chart keyspace tidy. */
56
56
  const CHART_CACHE_NAMESPACE = "mastra:chart";
57
57
 
58
- /**
59
- * `userKey` for `CacheManager.generateKey`. Chart ids are minted
60
- * via `hash.id()` (v4 UUID) and are unguessable, so a
61
- * constant user key is fine. The HTTP route can re-scope to the
62
- * requesting user when policy demands it.
63
- */
64
- const CHART_CACHE_USER_KEY = "mastra-chart";
65
-
66
58
  /** Default server-side long-poll budget for {@link fetchChart}. */
67
59
  const DEFAULT_FETCH_TIMEOUT_MS = 60_000;
68
60
 
69
61
  /** Default inter-poll sleep for {@link fetchChart}. */
70
62
  const DEFAULT_FETCH_INTERVAL_MS = 250;
71
63
 
64
+ /** Stable text stored on a chart entry whose planner run failed. */
65
+ const CHART_FAILED_MESSAGE = "Chart generation failed";
66
+
72
67
  /* ------------------------------- schemas ------------------------------- */
73
68
 
74
69
  /**
@@ -359,23 +354,27 @@ async function runChartPlanner(
359
354
 
360
355
  /* ------------------------------ cache helpers ------------------------------ */
361
356
 
362
- /** Build the canonical cache key for a `chartId`. */
363
- async function chartCacheKey(chartId: string): Promise<string> {
364
- return (await CacheManager.getInstance()).generateKey(
365
- [CHART_CACHE_NAMESPACE, chartId],
366
- CHART_CACHE_USER_KEY,
367
- );
357
+ /**
358
+ * Build the canonical cache key for a `chartId` owned by `userKey`.
359
+ *
360
+ * The identity is part of the key, not a filter applied after the read, so a
361
+ * chart id guessed or copied from another user's transcript resolves to a
362
+ * different key and simply misses. That miss is what the HTTP route turns into
363
+ * a 404, which is also the answer that leaks the least.
364
+ */
365
+ async function chartCacheKey(chartId: string, userKey: string): Promise<string> {
366
+ return (await CacheManager.getInstance()).generateKey([CHART_CACHE_NAMESPACE, chartId], userKey);
368
367
  }
369
368
 
370
369
  /**
371
- * Persist a {@link Chart} entry under its `chartId`. Refreshes
372
- * the TTL on every write. Cache-layer failures are logged and
370
+ * Persist a {@link Chart} entry under its `chartId`, owned by `userKey`.
371
+ * Refreshes the TTL on every write. Cache-layer failures are logged and
373
372
  * swallowed so background runners never throw into the
374
373
  * unhandled-rejection stream.
375
374
  */
376
- async function writeChart(entry: Chart): Promise<void> {
375
+ async function writeChart(entry: Chart, userKey: string): Promise<void> {
377
376
  try {
378
- const key = await chartCacheKey(entry.chartId);
377
+ const key = await chartCacheKey(entry.chartId, userKey);
379
378
  await CacheManager.getInstanceSync().set(key, entry, {
380
379
  ttl: CHART_CACHE_TTL_SEC,
381
380
  });
@@ -388,12 +387,13 @@ async function writeChart(entry: Chart): Promise<void> {
388
387
  }
389
388
 
390
389
  /**
391
- * Look up a chart by id. Returns `undefined` on miss, on
392
- * expiry, or when the cache layer is unhealthy - never throws.
390
+ * Look up a chart `userKey` owns. Returns `undefined` on miss, on
391
+ * expiry, when another identity owns the id, or when the cache layer is
392
+ * unhealthy - never throws.
393
393
  */
394
- async function readChart(chartId: string): Promise<Chart | undefined> {
394
+ async function readChart(chartId: string, userKey: string): Promise<Chart | undefined> {
395
395
  try {
396
- const key = await chartCacheKey(chartId);
396
+ const key = await chartCacheKey(chartId, userKey);
397
397
  const v = await CacheManager.getInstanceSync().get<Chart>(key);
398
398
  return v ?? undefined;
399
399
  } catch (err) {
@@ -411,6 +411,11 @@ async function readChart(chartId: string): Promise<Chart | undefined> {
411
411
  export interface PrepareChartOptions {
412
412
  /** Plugin config; resolves the planner agent's model. */
413
413
  config: MastraPluginConfig;
414
+ /**
415
+ * Identity that owns the minted chart. Only a {@link fetchChart} call
416
+ * carrying the same key resolves it. Use {@link resolveUserKey}.
417
+ */
418
+ userKey: string;
414
419
  /** Display title forwarded to the planner agent. */
415
420
  title?: string;
416
421
  /** Optional intent hint forwarded to the planner agent. */
@@ -455,7 +460,7 @@ export interface PrepareChartOptions {
455
460
  */
456
461
  export async function prepareChart(opts: PrepareChartOptions): Promise<{ chartId: string }> {
457
462
  const chartId = hash.id();
458
- await writeChart({ chartId });
463
+ await writeChart({ chartId }, opts.userKey);
459
464
  logger.debug("queued", { chartId });
460
465
  // Fire-and-forget. Failures land in the cache as `error` entries;
461
466
  // never escape into an unhandled rejection.
@@ -468,7 +473,7 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
468
473
  try {
469
474
  const data = await opts.resolveData(opts.signal);
470
475
  if (data.rows.length === 0) {
471
- throw new Error("dataset has no rows; nothing to chart");
476
+ throw new ExecutionError("Dataset has no rows; nothing to chart");
472
477
  }
473
478
  const result = await runChartPlanner(
474
479
  opts.config,
@@ -482,16 +487,18 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
482
487
  ...(opts.signal ? { abortSignal: opts.signal } : {}),
483
488
  },
484
489
  );
485
- await writeChart({ chartId, result });
490
+ await writeChart({ chartId, result }, opts.userKey);
486
491
  logger.info("done", {
487
492
  chartId,
488
493
  chartType: result.chartType,
489
494
  elapsedMs: Date.now() - startedAt,
490
495
  });
491
496
  } catch (err) {
492
- const errText = error.errorMessage(err);
493
- logger.warn("error", { chartId, error: errText });
494
- await writeChart({ chartId, error: errText });
497
+ logger.warn("error", { chartId, error: error.errorMessage(err) });
498
+ // The entry's `error` is rendered in the chat, so only an AppKitError's
499
+ // own message travels; anything else could carry upstream provider detail.
500
+ const errText = err instanceof AppKitError ? err.message : CHART_FAILED_MESSAGE;
501
+ await writeChart({ chartId, error: errText }, opts.userKey);
495
502
  }
496
503
  }
497
504
 
@@ -499,6 +506,11 @@ async function runPrepareChart(chartId: string, opts: PrepareChartOptions): Prom
499
506
 
500
507
  /** Inputs to {@link fetchChart}. */
501
508
  export interface FetchChartOptions {
509
+ /**
510
+ * Identity the chart must belong to. A chart minted under a different key
511
+ * is indistinguishable from an unknown id. Use {@link resolveUserKey}.
512
+ */
513
+ userKey: string;
502
514
  /**
503
515
  * Server-side polling budget in ms. When the entry stays in
504
516
  * the processing state past this window, the helper returns the
@@ -524,8 +536,8 @@ export interface FetchChartOptions {
524
536
  * - the resolved {@link Chart} when it settled, errored, or
525
537
  * stayed in processing past `timeoutMs` (so the client can
526
538
  * re-poll);
527
- * - `undefined` when the entry is missing or expired (the
528
- * consumer should treat as 404).
539
+ * - `undefined` when the entry is missing, expired, or owned by
540
+ * another identity (the consumer should treat as 404).
529
541
  *
530
542
  * `signal` lets the caller cancel ahead of timeout (e.g. the HTTP
531
543
  * request closed). Cancellation propagates to the inter-poll sleep
@@ -533,7 +545,7 @@ export interface FetchChartOptions {
533
545
  */
534
546
  export async function fetchChart(
535
547
  chartId: string,
536
- options: FetchChartOptions = {},
548
+ options: FetchChartOptions,
537
549
  ): Promise<Chart | undefined> {
538
550
  const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
539
551
  const intervalMs = options.intervalMs ?? DEFAULT_FETCH_INTERVAL_MS;
@@ -542,7 +554,7 @@ export async function fetchChart(
542
554
  let last: Chart | undefined;
543
555
  while (true) {
544
556
  options.signal?.throwIfAborted();
545
- last = await readChart(chartId);
557
+ last = await readChart(chartId, options.userKey);
546
558
  if (!last) return undefined;
547
559
  if (last.result !== undefined || last.error !== undefined) return last;
548
560
  const remaining = deadline - Date.now();
@@ -564,16 +576,34 @@ interface ChartTheme {
564
576
  textStyle: { fontFamily: string; color: string };
565
577
  }
566
578
 
579
+ /**
580
+ * AppKit's categorical chart palette (`--chart-cat-1` .. `--chart-cat-8` from
581
+ * `@databricks/appkit-ui`), as hex. Server-rendered chart specs cannot read the
582
+ * browser's CSS custom properties, so the values are mirrored here to keep a
583
+ * generated chart visually consistent with the AppKit-styled UI around it.
584
+ * Keep in sync with AppKit's stylesheet if it changes.
585
+ */
586
+ const APPKIT_CHART_PALETTE = [
587
+ "#2463EB", // blue
588
+ "#2EB88A", // teal
589
+ "#AB47BD", // purple
590
+ "#F69E23", // amber
591
+ "#DD2C4D", // rose
592
+ "#1BA3BB", // cyan
593
+ "#9B61D1", // lavender
594
+ "#34B262", // emerald
595
+ ] as const;
596
+
567
597
  /**
568
598
  * Expand two brand hex colors into a legible categorical cycle. Echarts
569
599
  * repeats the `color` array across series / categories, so a good default
570
600
  * needs several distinct hues, not two. We seed with the brand primary and
571
- * accent (the identity colors) and follow with a fixed, colorblind-friendly
572
- * spread so multi-series / many-slice charts stay readable while the first
573
- * one or two marks still land on-brand.
601
+ * accent (the identity colors) and follow with AppKit's own categorical chart
602
+ * palette, so multi-series / many-slice charts stay readable and match the
603
+ * surrounding AppKit UI while the first one or two marks land on-brand.
574
604
  */
575
605
  function brandColorCycle(primary: string, accent: string): string[] {
576
- const spread = ["#5B8FF9", "#F6BD16", "#E86452", "#6DC8EC", "#945FB9", "#FF9845", "#1E9493"];
606
+ const spread = APPKIT_CHART_PALETTE;
577
607
  const seen = new Set<string>();
578
608
  return [primary, accent, ...spread].filter((c) => {
579
609
  const key = c.toLowerCase();
@@ -742,6 +772,7 @@ export function buildRenderDataTool(config: MastraPluginConfig) {
742
772
  const ctx = ctxRaw as { requestContext?: RequestContext } | undefined;
743
773
  return prepareChart({
744
774
  config,
775
+ userKey: resolveUserKey(ctx?.requestContext),
745
776
  title,
746
777
  ...(description ? { description } : {}),
747
778
  resolveData: () => Promise.resolve({ rows: data }),