@dbx-tools/appkit-mastra 0.1.13 → 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/dist/src/chart.js CHANGED
@@ -1,44 +1,37 @@
1
1
  /**
2
2
  * Chart-rendering primitives.
3
3
  *
4
- * Three surfaces, one shared brain:
5
- *
6
- * - {@link buildRenderDataTool}: a Mastra tool the model calls
7
- * ("here is a dataset, render it as a chart"). The tool's
8
- * `execute` emits a `kind: "chart"` event with the raw rows to
9
- * `ctx.writer` synchronously, kicks off the chart-planner agent,
10
- * and `await`s the planner promise before returning so the
11
- * planner's latency is attributed to this tool's trace span.
12
- * The LLM-bound output is just `{ chartId }`, so its context
13
- * stays flat regardless of dataset size.
14
- *
15
- * - {@link emitChartWithPlanning}: the underlying helper that both
16
- * `render_data` and Genie's `drainGenieStream` call. Mints the
17
- * `chartId`, fires the dataset event immediately, runs the
18
- * planner in the background, and returns `{ chartId,
19
- * plannerPromise }` so callers can choose to await for trace
20
- * shape or fire-and-forget.
4
+ * Two surfaces, one shared brain:
21
5
  *
22
6
  * - {@link runChartPlanner}: the chart-planner Agent + ECOption
23
- * expansion as a plain async function. Used internally by
24
- * {@link emitChartWithPlanning}; producers shouldn't reach for
25
- * it directly so chart events keep a single wire-format
26
- * contract.
7
+ * expansion as a plain async function. Takes a dataset and
8
+ * returns a promise that resolves to a full `EChartsOption`
9
+ * JSON plus the chosen `chartType`. No background work, no
10
+ * writer side-effects, no id allocation - callers stitch the
11
+ * result into whatever shape their producer needs.
12
+ *
13
+ * - {@link buildRenderDataTool}: a Mastra tool the model calls
14
+ * ("here is a dataset, render it as a chart"). Mints a short
15
+ * `chartId`, `await`s {@link runChartPlanner} so the planner
16
+ * latency is attributed to this tool's trace span, emits one
17
+ * `type: "chart"` writer event carrying the dataset + resolved
18
+ * `option`, and returns `{ chartId }` to the model. The
19
+ * LLM-bound output stays flat regardless of dataset size.
27
20
  *
28
21
  * The model wires the chart into its reply by emitting the marker
29
22
  * `[[chart:<chartId>]]` on its own line in markdown. The chat
30
23
  * client splits the assistant text on these markers and drops a
31
- * `<ChartSlot>` in at the position the model placed it; the slot
32
- * shows a skeleton until the second `kind: "chart"` event (with
33
- * the resolved `EChartsOption`) arrives, then swaps in the
34
- * rendered Echarts visualisation.
24
+ * `<ChartSlot>` in at the position the model placed it. The slot
25
+ * resolves directly to the rendered Echarts visualisation - no
26
+ * skeleton state, because the option is in the same event as the
27
+ * dataset.
35
28
  */
36
- import { randomUUID } from "node:crypto";
37
- import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
29
+ import { commonUtils, logUtils, stringUtils } from "@dbx-tools/shared";
38
30
  import { Agent } from "@mastra/core/agent";
39
31
  import { createTool } from "@mastra/core/tools";
40
32
  import { z } from "zod";
41
33
  import { ModelTier, modelForTier, buildModel } from "./model.js";
34
+ import { safeWrite } from "./writer.js";
42
35
  /**
43
36
  * Module-level logger tagged `[mastra/chart]`. Uses the shared
44
37
  * {@link logUtils.logger} so calls below `LOG_LEVEL` are
@@ -47,6 +40,75 @@ import { ModelTier, modelForTier, buildModel } from "./model.js";
47
40
  * `write:ok(data)` → `planner:done` → `write:ok(option)`).
48
41
  */
49
42
  const log = logUtils.logger("mastra/chart");
43
+ /**
44
+ * One series data point. Wide variant set so the planner agent can
45
+ * faithfully pass through whatever the SQL row set contained
46
+ * (numbers, stringified numbers, nulls for missing measurements,
47
+ * `[x, y]` tuples for scatter, `{name, value}` slices for pie)
48
+ * without the structured-output guard rejecting the whole plan.
49
+ *
50
+ * Three layers of tolerance:
51
+ *
52
+ * 1. {@link z.preprocess} normalizes wire shapes BEFORE union
53
+ * dispatch: stringified numbers parse to numbers, finite
54
+ * checks reject `NaN` / `Infinity`, 2-element arrays coerce
55
+ * tuple components, and `{value}` objects with missing /
56
+ * stringified `value` get coerced or rejected uniformly.
57
+ * Anything not handleable becomes `null`.
58
+ * 2. The union accepts `null` as a first-class variant. Echarts
59
+ * renders null as a gap on bar / line / area (which is the
60
+ * right visual signal for "missing reading"). Scatter and
61
+ * pie filter nulls in {@link planToEchartsOption} because
62
+ * Echarts crashes on null tuples / slices.
63
+ * 3. {@link z.union#catch} backstops the whole thing: if
64
+ * preprocess somehow produces a shape that still doesn't
65
+ * match any variant, the bad item becomes `null` instead of
66
+ * taking down the entire chart with a
67
+ * `Structured output validation failed` error.
68
+ *
69
+ * Net effect: a 200-row dataset with a few sparse/null/string
70
+ * values still produces a chart; only a totally-malformed planner
71
+ * response (no items at all) falls through to the table fallback.
72
+ */
73
+ const chartDataPointSchema = z
74
+ .preprocess((v) => {
75
+ if (v === null || v === undefined)
76
+ return null;
77
+ if (typeof v === "number")
78
+ return Number.isFinite(v) ? v : null;
79
+ if (typeof v === "string") {
80
+ const n = Number(v);
81
+ return Number.isFinite(n) ? n : null;
82
+ }
83
+ if (Array.isArray(v) && v.length === 2) {
84
+ const x = typeof v[0] === "number" ? v[0] : Number(v[0]);
85
+ const y = typeof v[1] === "number" ? v[1] : Number(v[1]);
86
+ return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
87
+ }
88
+ if (typeof v === "object" && v !== null && "value" in v) {
89
+ const obj = v;
90
+ const val = typeof obj.value === "number" ? obj.value : Number(obj.value);
91
+ if (!Number.isFinite(val))
92
+ return null;
93
+ // Coerce numeric / boolean / nullish names to strings so a
94
+ // pie slice keyed on a year (`2024`) or category id is
95
+ // accepted without round-tripping through the catch arm.
96
+ const rawName = obj.name;
97
+ const name = typeof rawName === "string"
98
+ ? rawName
99
+ : rawName == null
100
+ ? ""
101
+ : String(rawName);
102
+ return { name, value: val };
103
+ }
104
+ return null;
105
+ }, z.union([
106
+ z.number(),
107
+ z.null(),
108
+ z.tuple([z.number(), z.number()]),
109
+ z.object({ name: z.string(), value: z.number() }),
110
+ ]))
111
+ .catch(null);
50
112
  /**
51
113
  * Compact, model-friendly representation of an Echarts spec. The
52
114
  * planner agent emits this; {@link planToEchartsOption} expands it
@@ -58,8 +120,7 @@ const log = logUtils.logger("mastra/chart");
58
120
  * across charts.
59
121
  */
60
122
  const chartPlanSchema = z.object({
61
- chartType: z
62
- .enum(["bar", "line", "area", "scatter", "pie"])
123
+ chartType: z.enum(["bar", "line", "area", "scatter", "pie"])
63
124
  .describe(stringUtils.toDescription `
64
125
  The chart shape that best matches the data and intent. Use
65
126
  \`bar\` for category-vs-value comparisons, \`line\` for
@@ -79,10 +140,7 @@ const chartPlanSchema = z.object({
79
140
  Axis label to the left of the chart. Used for bar / line /
80
141
  area / scatter; ignored for pie.
81
142
  `),
82
- categories: z
83
- .array(z.string())
84
- .optional()
85
- .describe(stringUtils.toDescription `
143
+ categories: z.array(z.string()).optional().describe(stringUtils.toDescription `
86
144
  X-axis category labels for \`bar\` / \`line\` / \`area\`
87
145
  charts (one per data point in each series). Omit for
88
146
  \`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
@@ -93,24 +151,14 @@ const chartPlanSchema = z.object({
93
151
  name: z.string().describe(stringUtils.toDescription `
94
152
  Legend name for this series.
95
153
  `),
96
- data: z
97
- .array(z.union([
98
- z.number(),
99
- z.tuple([z.number(), z.number()]),
100
- z.object({
101
- name: z.string(),
102
- value: z.number(),
103
- }),
104
- ]))
105
- .describe(stringUtils.toDescription `
154
+ data: z.array(chartDataPointSchema).describe(stringUtils.toDescription `
106
155
  Data points. For \`bar\` / \`line\` / \`area\`, an
107
156
  array of numbers aligned to \`categories\`. For
108
157
  \`scatter\`, an array of \`[x, y]\` numeric tuples.
109
158
  For \`pie\`, an array of \`{name, value}\` objects.
110
159
  `),
111
160
  }))
112
- .min(1)
113
- .describe(stringUtils.toDescription `
161
+ .min(1).describe(stringUtils.toDescription `
114
162
  One or more series to plot. Pie charts use exactly one
115
163
  series; bar/line/area can stack multiple series sharing
116
164
  the same \`categories\` axis.
@@ -193,13 +241,14 @@ function getPlannerAgent(config) {
193
241
  }
194
242
  /**
195
243
  * Run the chart planner against the given dataset and return a
196
- * full Echarts `EChartsOption` JSON. Used by
197
- * {@link emitChartWithPlanning}; tools and producers shouldn't
198
- * call this directly (use the helper instead so chart events
199
- * follow the same wire-format contract everywhere).
244
+ * full Echarts `EChartsOption` JSON. Pure async function: no
245
+ * writer side-effects, no id minting, no background work.
246
+ * Producers (the `render_data` tool, the Genie agent,
247
+ * anything else that needs a chart) await this and stitch the
248
+ * result into whatever shape their wire contract needs.
200
249
  */
201
250
  export async function runChartPlanner(opts) {
202
- const { config, requestContext, title, description, data } = opts;
251
+ const { config, requestContext, title, description, data, signal } = opts;
203
252
  const planner = getPlannerAgent(config);
204
253
  const prompt = [
205
254
  `Title: ${title}`,
@@ -215,122 +264,12 @@ export async function runChartPlanner(opts) {
215
264
  const result = await planner.generate(prompt, {
216
265
  structuredOutput: { schema: chartPlanSchema },
217
266
  ...(requestContext ? { requestContext } : {}),
267
+ ...(signal ? { abortSignal: signal } : {}),
218
268
  });
219
269
  const plan = result.object;
220
270
  const option = planToEchartsOption(plan, title);
221
271
  return { option, chartType: plan.chartType };
222
272
  }
223
- /**
224
- * Shared chart-emission primitive used by both the `render_data`
225
- * tool and Genie's `drainGenieStream`. Keeps both producers on
226
- * one wire-format contract so the chat client only ever has to
227
- * understand a single chart event shape.
228
- *
229
- * Behaviour:
230
- *
231
- * 1. Generates a short `chartId` (8 hex chars).
232
- * 2. Immediately emits `{ kind: "chart", chartId, title,
233
- * description?, data }` via the writer so the chat client can
234
- * mount its `<ChartSlot>` with the rows in hand.
235
- * 3. Kicks off the chart-planner agent in the background. On
236
- * success, emits a second `{ kind: "chart", chartId, option }`
237
- * event - same `chartId`, just the spec - so the client merges
238
- * the two into one rendered chart. On failure, no follow-up
239
- * event fires; the client falls back to whatever it can do
240
- * with the dataset alone (typically a "render failed" frame
241
- * after the parent tool finishes).
242
- *
243
- * Returns `chartId` synchronously so the caller can include it in
244
- * the tool result (model uses it in `[[chart:<chartId>]]`
245
- * markers), and `plannerPromise` so the caller can choose
246
- * trace-spanning vs. snappy-return semantics.
247
- */
248
- export async function emitChartWithPlanning(opts) {
249
- const { writer, config, requestContext, title, description, data } = opts;
250
- // Short, marker-friendly id. The LLM types this verbatim into
251
- // `[[chart:<id>]]`; an 8-hex-char prefix is unique within a
252
- // single assistant turn (collision odds ~1 in 4 billion) and
253
- // much less error-prone for the model to reproduce.
254
- const chartId = randomUUID().replace(/-/g, "").slice(0, 8);
255
- log.debug("emit:start", {
256
- chartId,
257
- title,
258
- rows: data.length,
259
- columns: data[0] ? Object.keys(data[0]) : [],
260
- hasWriter: writer !== undefined,
261
- });
262
- // Initial event: rows + metadata, no option yet. The client
263
- // mounts a chart slot that shows a skeleton until the option
264
- // event arrives (or until the parent tool finishes without
265
- // one, in which case it falls back).
266
- await safeWrite(writer, chartId, "data", {
267
- kind: "chart",
268
- chartId,
269
- title,
270
- ...(description ? { description } : {}),
271
- data,
272
- });
273
- // Background planner. Awaitable for trace observability via the
274
- // returned `plannerPromise`; safe to ignore for pure
275
- // fire-and-forget. Failures are intentionally swallowed (only
276
- // logged): the dataset event already landed, so the client has
277
- // enough to surface a fallback.
278
- const plannerPromise = (async () => {
279
- const startedAt = Date.now();
280
- try {
281
- const { option, chartType } = await runChartPlanner({
282
- config,
283
- ...(requestContext ? { requestContext } : {}),
284
- title,
285
- ...(description ? { description } : {}),
286
- data,
287
- });
288
- log.debug("planner:done", {
289
- chartId,
290
- chartType,
291
- elapsedMs: Date.now() - startedAt,
292
- });
293
- await safeWrite(writer, chartId, "option", { kind: "chart", chartId, option });
294
- }
295
- catch (err) {
296
- // No follow-up event on failure. The client treats a
297
- // dataset-only chart slot as "render failed" once the
298
- // parent tool's status flips to done. Surface as a `warn`
299
- // so the failure is visible at the default log level
300
- // without being mistaken for a fatal error.
301
- log.warn("planner:error", {
302
- chartId,
303
- elapsedMs: Date.now() - startedAt,
304
- error: err instanceof Error ? err.message : String(err),
305
- });
306
- }
307
- })();
308
- return { chartId, plannerPromise };
309
- }
310
- /**
311
- * Best-effort writer.write. Failures are logged at `warn` (a
312
- * persistently-closed writer is the most likely culprit when
313
- * chart events go missing client-side) but swallowed so a closed
314
- * downstream stream (cancelled request, client navigated away)
315
- * can't take a tool down.
316
- */
317
- async function safeWrite(writer, chartId, phase, chunk) {
318
- if (!writer) {
319
- log.debug("write:no-writer", { chartId, phase });
320
- return;
321
- }
322
- try {
323
- await writer.write(chunk);
324
- log.debug("write:ok", { chartId, phase });
325
- }
326
- catch (err) {
327
- log.warn("write:error", {
328
- chartId,
329
- phase,
330
- error: err instanceof Error ? err.message : String(err),
331
- });
332
- }
333
- }
334
273
  const renderDataInputSchema = z.object({
335
274
  title: z.string().describe(stringUtils.toDescription `
336
275
  Title shown above the rendered chart. Use a concise
@@ -343,9 +282,7 @@ const renderDataInputSchema = z.object({
343
282
  The chart-planner reads this when picking the chart type and
344
283
  axis encodings; the user does not see it directly.
345
284
  `),
346
- data: z
347
- .array(z.record(z.string(), z.unknown()))
348
- .min(1)
285
+ data: z.array(z.record(z.string(), z.unknown())).min(1)
349
286
  .describe(stringUtils.toDescription `
350
287
  Tabular dataset to chart. One object per row, keyed by
351
288
  column name. Values may be strings, numbers, booleans, or
@@ -365,16 +302,14 @@ const renderDataOutputSchema = z.object({
365
302
  /**
366
303
  * Build the `render_data` tool bound to the given plugin config.
367
304
  *
368
- * The tool is a thin wrapper around {@link emitChartWithPlanning}:
369
- * a single `kind: "chart"` writer event ships the raw rows to
370
- * the client immediately, the chart-planner agent runs alongside
371
- * (so the calling LLM stays unblocked while the planner thinks),
372
- * and a follow-up `kind: "chart"` event with the resolved
373
- * `EChartsOption` lands when it's ready. The tool's `execute`
374
- * awaits the planner promise so the planner work shows up under
375
- * the tool's trace span; the LLM still gets back just
376
- * `{ chartId }`, so its context stays small regardless of dataset
377
- * size.
305
+ * The tool awaits {@link runChartPlanner} so the planner's
306
+ * latency is attributed to this tool's trace span, then emits
307
+ * one `type: "chart"` writer event carrying the dataset and the
308
+ * resolved `EChartsOption`. The LLM-bound output is just
309
+ * `{ chartId }` so the model's context stays flat regardless of
310
+ * dataset size. Planner failures are caught and surfaced as a
311
+ * `type: "error"` writer event so the slot can fall back to
312
+ * "couldn't render chart" without taking the parent agent down.
378
313
  */
379
314
  export function buildRenderDataTool(config) {
380
315
  return createTool({
@@ -389,14 +324,14 @@ export function buildRenderDataTool(config) {
389
324
 
390
325
  Placement contract: embed \`[[chart:<chartId>]]\` on its own
391
326
  line (blank lines above and below) wherever you want the
392
- chart to appear in your reply. The client shows a skeleton
393
- at that spot until the chart is ready, then swaps in the
394
- rendered Echarts visualization. You can call
395
- \`render_data\` multiple times in the same turn (the tool
396
- is parallel-safe) and interleave the markers with prose so
397
- each chart sits next to its commentary. A chart whose
398
- marker is omitted falls through to the end of your reply
399
- as a fallback - safe but less polished.
327
+ chart to appear in your reply. The chart is fully resolved
328
+ by the time the tool returns, so it renders immediately at
329
+ that spot. You can call \`render_data\` multiple times in
330
+ the same turn (the tool is parallel-safe) and interleave
331
+ the markers with prose so each chart sits next to its
332
+ commentary. A chart whose marker is omitted falls through
333
+ to the end of your reply as a fallback - safe but less
334
+ polished.
400
335
 
401
336
  Use whenever a SQL row set, API response, or hand-built
402
337
  dataset would land better as a picture than as a list or
@@ -410,18 +345,58 @@ export function buildRenderDataTool(config) {
410
345
  const writer = ctx?.writer;
411
346
  const requestContext = ctx
412
347
  ?.requestContext;
413
- const { chartId, plannerPromise } = await emitChartWithPlanning({
414
- ...(writer ? { writer } : {}),
415
- config,
416
- ...(requestContext ? { requestContext } : {}),
348
+ // Marker-friendly short id. The LLM types this verbatim
349
+ // into `[[chart:<id>]]`; 8 hex chars is unique within a
350
+ // single assistant turn and easy for the model to copy.
351
+ const chartId = commonUtils.shortId();
352
+ const startedAt = Date.now();
353
+ log.debug("render:start", {
354
+ chartId,
417
355
  title,
418
- ...(description ? { description } : {}),
419
- data,
356
+ rows: data.length,
357
+ columns: data[0] ? Object.keys(data[0]) : [],
358
+ hasWriter: writer !== undefined,
420
359
  });
421
- // Await the planner so its latency is attributed to this
422
- // tool's trace span. The promise itself swallows planner
423
- // failures, so this never throws.
424
- await plannerPromise;
360
+ try {
361
+ const { option, chartType } = await runChartPlanner({
362
+ config,
363
+ ...(requestContext ? { requestContext } : {}),
364
+ title,
365
+ ...(description ? { description } : {}),
366
+ data,
367
+ });
368
+ log.debug("render:done", {
369
+ chartId,
370
+ chartType,
371
+ elapsedMs: Date.now() - startedAt,
372
+ });
373
+ // Single chart event with everything resolved: dataset
374
+ // for the table-like fallback / hover, option for the
375
+ // actual render. Best-effort write so a closed
376
+ // downstream stream can't take the tool down.
377
+ await safeWrite(log, writer, {
378
+ type: "chart",
379
+ chartId,
380
+ title,
381
+ ...(description ? { description } : {}),
382
+ data,
383
+ option,
384
+ }, { chartId });
385
+ }
386
+ catch (err) {
387
+ log.warn("render:error", {
388
+ chartId,
389
+ elapsedMs: Date.now() - startedAt,
390
+ error: commonUtils.errorMessage(err),
391
+ });
392
+ // Surface as a writer-level error so the slot can
393
+ // transition to "couldn't render chart" without the
394
+ // parent agent surfacing a stack trace.
395
+ await safeWrite(log, writer, {
396
+ type: "error",
397
+ error: commonUtils.errorMessage(err),
398
+ }, { chartId });
399
+ }
425
400
  return { chartId };
426
401
  },
427
402
  });
@@ -437,6 +412,11 @@ function planToEchartsOption(plan, fallbackTitle) {
437
412
  const baseTitle = plan.title ?? fallbackTitle;
438
413
  const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
439
414
  if (plan.chartType === "pie") {
415
+ // Echarts crashes on null pie slices - filter them out.
416
+ // `{name, value}` slices are the only valid pie data shape,
417
+ // so drop bare numbers / tuples / nulls the planner may
418
+ // have leaked into a pie series.
419
+ const slices = (plan.series[0]?.data ?? []).filter((d) => d !== null && typeof d === "object" && !Array.isArray(d));
440
420
  return {
441
421
  title: { text: baseTitle, left: "center" },
442
422
  tooltip: { trigger: "item" },
@@ -446,12 +426,15 @@ function planToEchartsOption(plan, fallbackTitle) {
446
426
  name: plan.series[0]?.name ?? baseTitle,
447
427
  type: "pie",
448
428
  radius: ["35%", "65%"],
449
- data: plan.series[0]?.data ?? [],
429
+ data: slices,
450
430
  },
451
431
  ],
452
432
  };
453
433
  }
454
434
  if (plan.chartType === "scatter") {
435
+ // Echarts crashes on null scatter points - keep only valid
436
+ // `[x, y]` tuples. Bare numbers / objects / nulls from a
437
+ // mismatched plan get dropped silently.
455
438
  return {
456
439
  title: { text: baseTitle, left: "center" },
457
440
  tooltip: { trigger: "item" },
@@ -462,7 +445,7 @@ function planToEchartsOption(plan, fallbackTitle) {
462
445
  series: plan.series.map((s) => ({
463
446
  name: s.name,
464
447
  type: "scatter",
465
- data: s.data,
448
+ data: s.data.filter((d) => Array.isArray(d) && d.length === 2),
466
449
  })),
467
450
  };
468
451
  }
@@ -8,12 +8,46 @@ import type { BasePluginConfig, getExecutionContext } from "@databricks/appkit";
8
8
  import type { AgentConfig } from "@mastra/core/agent";
9
9
  import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
10
10
  import type { MastraAgentDefinition, MastraTools } from "./agents.js";
11
+ import type { GenieSpacesConfig } from "./genie.js";
11
12
  /**
12
13
  * `RequestContext` key under which {@link MastraServer} stores the
13
14
  * resolved AppKit user. `model.ts` reads it to mint user-scoped
14
15
  * Databricks tokens.
15
16
  */
16
17
  export declare const MASTRA_USER_KEY = "mastra__user";
18
+ /**
19
+ * `RequestContext` keys for AppKit user metadata stamped by
20
+ * {@link MastraServer}. Surfaced as trace metadata via
21
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so traces are filterable by who
22
+ * issued the request without leaking the full user object.
23
+ */
24
+ export declare const MASTRA_USER_NAME_KEY = "mastra__userName";
25
+ export declare const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
26
+ /**
27
+ * `RequestContext` key for the per-HTTP-request id stamped by
28
+ * {@link MastraServer}. Reads `X-Request-Id` from the incoming
29
+ * headers when present (so an upstream load balancer / API gateway
30
+ * can keep its trace correlation), falls back to a freshly minted
31
+ * UUID. Echoed back on the response and surfaced on every span via
32
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so logs and traces share a
33
+ * join key.
34
+ */
35
+ export declare const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
36
+ /**
37
+ * Canonical list of `RequestContext` keys we want Mastra to extract
38
+ * as metadata on every observability span (agent runs, model calls,
39
+ * tool invocations, workflow steps).
40
+ *
41
+ * Mirrors {@link https://mastra.ai/docs/observability/tracing/overview#automatic-metadata-from-requestcontext}:
42
+ * passed verbatim into `Observability.configs[*].requestContextKeys`,
43
+ * so any key listed here is read from `RequestContext` at trace
44
+ * start and attached as scalar span metadata. Keep the set to plain
45
+ * scalars - never include {@link MASTRA_USER_KEY} (it carries the
46
+ * full AppKit execution context with a `WorkspaceClient` reference).
47
+ *
48
+ * Order is purely cosmetic; Mastra de-dupes internally.
49
+ */
50
+ export declare const TRACE_REQUEST_CONTEXT_KEYS: readonly string[];
17
51
  /** AppKit execution context plus the canonical user id. */
18
52
  export interface User {
19
53
  id: string;
@@ -180,4 +214,74 @@ export interface MastraPluginConfig extends BasePluginConfig {
180
214
  * and the style block leans on the model's recency bias.
181
215
  */
182
216
  styleInstructions?: string | false;
217
+ /**
218
+ * Genie spaces this plugin's agents can delegate to. One Mastra
219
+ * tool is registered per alias (`genie` for the well-known
220
+ * `default` alias, `genie_<alias>` otherwise). Each tool spins
221
+ * up a per-question Genie sub-agent that runs Databricks
222
+ * "agent mode" against the space, broadcasts wire events to the
223
+ * UI, fetches statement rows for non-empty results, and returns
224
+ * a `(string | data | chart)[]` summary the host UI renders
225
+ * inline.
226
+ *
227
+ * Entries accept either a full {@link GenieSpaceConfig} object
228
+ * or a bare `space_id` string when no extras are needed:
229
+ *
230
+ * ```ts
231
+ * mastra({
232
+ * genieSpaces: {
233
+ * default: "01ef0d3c0e1b1f4a8d2c3e4f5a6b7c8d",
234
+ * forecasts: { spaceId: "01ef...", hint: "weekly demand forecasts" },
235
+ * },
236
+ * });
237
+ * ```
238
+ *
239
+ * Reach the spaces from an agent's `tools(plugins)` callback via
240
+ * `plugins.genie?.toolkit()`; the resulting tools accept
241
+ * `{ content, conversationId? }` and return a hydrated summary.
242
+ *
243
+ * **Fallback discovery** (highest precedence first): if this
244
+ * field is omitted, the Genie agent also picks up spaces from
245
+ * (1) the AppKit `genie({ spaces: { ... } })` plugin instance
246
+ * when registered, and (2) the `DATABRICKS_GENIE_SPACE_ID`
247
+ * env var (registered under the `default` alias). This keeps
248
+ * existing AppKit deployments working without restating the
249
+ * spaces config in two places.
250
+ */
251
+ genieSpaces?: GenieSpacesConfig;
252
+ /**
253
+ * TTL for the in-memory Genie space metadata cache, in
254
+ * milliseconds. Defaults to 5 minutes. The Genie agent calls
255
+ * `client.genie.getSpace(...)` on every cold-start to get the
256
+ * title / description / warehouse id; cached responses skip the
257
+ * round-trip and concurrent callers coalesce on a single
258
+ * in-flight fetch. Drop to a smaller value when analysts are
259
+ * actively editing space metadata and you want changes visible
260
+ * within seconds; raise it to amortise the round-trip when
261
+ * space metadata is effectively frozen.
262
+ *
263
+ * Backed by AppKit's `CacheManager`, so the cache participates
264
+ * in telemetry spans (`cache.getOrExecute`) and benefits from
265
+ * Lakebase persistence when the `lakebase` plugin is wired up.
266
+ */
267
+ genieSpaceCacheTtlMs?: number;
268
+ /**
269
+ * Maximum LLM steps the Genie agent gets per turn.
270
+ * Each step is one round-trip to the underlying model
271
+ * (`get_space_description`, each `ask_genie` call, and the
272
+ * mandatory `submit_summary` closer all consume one step).
273
+ *
274
+ * Defaults to 16, which fits ~10 `ask_genie` calls plus
275
+ * grounding plus the summary closer - matches the
276
+ * decomposition pattern the Genie agent instructions prescribe
277
+ * ("2-6 ask_genie calls for any non-trivial question").
278
+ * Mastra's own `agent.generate` default of 5 would cut the
279
+ * Genie agent off after 2-3 Genie calls, so explicitly raising
280
+ * the ceiling here is what lets the agent-mode loop play out.
281
+ *
282
+ * Lower this when an unusually slow or expensive model makes
283
+ * long turns unaffordable; raise it for exploratory workloads
284
+ * that need to drill into a dataset.
285
+ */
286
+ genieAgentMaxSteps?: number;
183
287
  }
@@ -4,9 +4,52 @@
4
4
  * Kept in a leaf module so `plugin.ts`, `server.ts`, `model.ts`, and
5
5
  * `memory.ts` can import them without creating a cycle.
6
6
  */
7
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
7
8
  /**
8
9
  * `RequestContext` key under which {@link MastraServer} stores the
9
10
  * resolved AppKit user. `model.ts` reads it to mint user-scoped
10
11
  * Databricks tokens.
11
12
  */
12
13
  export const MASTRA_USER_KEY = "mastra__user";
14
+ /**
15
+ * `RequestContext` keys for AppKit user metadata stamped by
16
+ * {@link MastraServer}. Surfaced as trace metadata via
17
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so traces are filterable by who
18
+ * issued the request without leaking the full user object.
19
+ */
20
+ export const MASTRA_USER_NAME_KEY = "mastra__userName";
21
+ export const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
22
+ /**
23
+ * `RequestContext` key for the per-HTTP-request id stamped by
24
+ * {@link MastraServer}. Reads `X-Request-Id` from the incoming
25
+ * headers when present (so an upstream load balancer / API gateway
26
+ * can keep its trace correlation), falls back to a freshly minted
27
+ * UUID. Echoed back on the response and surfaced on every span via
28
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so logs and traces share a
29
+ * join key.
30
+ */
31
+ export const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
32
+ /**
33
+ * Canonical list of `RequestContext` keys we want Mastra to extract
34
+ * as metadata on every observability span (agent runs, model calls,
35
+ * tool invocations, workflow steps).
36
+ *
37
+ * Mirrors {@link https://mastra.ai/docs/observability/tracing/overview#automatic-metadata-from-requestcontext}:
38
+ * passed verbatim into `Observability.configs[*].requestContextKeys`,
39
+ * so any key listed here is read from `RequestContext` at trace
40
+ * start and attached as scalar span metadata. Keep the set to plain
41
+ * scalars - never include {@link MASTRA_USER_KEY} (it carries the
42
+ * full AppKit execution context with a `WorkspaceClient` reference).
43
+ *
44
+ * Order is purely cosmetic; Mastra de-dupes internally.
45
+ */
46
+ export const TRACE_REQUEST_CONTEXT_KEYS = [
47
+ MASTRA_RESOURCE_ID_KEY,
48
+ MASTRA_THREAD_ID_KEY,
49
+ MASTRA_REQUEST_ID_KEY,
50
+ MASTRA_USER_NAME_KEY,
51
+ MASTRA_USER_EMAIL_KEY,
52
+ // Model override key is owned by `serving.ts`; spelled inline here
53
+ // so this module stays leaf-level (no cycles with `serving.ts`).
54
+ "mastra__model_override",
55
+ ];