@dbx-tools/appkit-mastra 0.1.13 → 0.1.18

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,40 +1,32 @@
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";
@@ -47,6 +39,75 @@ import { ModelTier, modelForTier, buildModel } from "./model.js";
47
39
  * `write:ok(data)` → `planner:done` → `write:ok(option)`).
48
40
  */
49
41
  const log = logUtils.logger("mastra/chart");
42
+ /**
43
+ * One series data point. Wide variant set so the planner agent can
44
+ * faithfully pass through whatever the SQL row set contained
45
+ * (numbers, stringified numbers, nulls for missing measurements,
46
+ * `[x, y]` tuples for scatter, `{name, value}` slices for pie)
47
+ * without the structured-output guard rejecting the whole plan.
48
+ *
49
+ * Three layers of tolerance:
50
+ *
51
+ * 1. {@link z.preprocess} normalizes wire shapes BEFORE union
52
+ * dispatch: stringified numbers parse to numbers, finite
53
+ * checks reject `NaN` / `Infinity`, 2-element arrays coerce
54
+ * tuple components, and `{value}` objects with missing /
55
+ * stringified `value` get coerced or rejected uniformly.
56
+ * Anything not handleable becomes `null`.
57
+ * 2. The union accepts `null` as a first-class variant. Echarts
58
+ * renders null as a gap on bar / line / area (which is the
59
+ * right visual signal for "missing reading"). Scatter and
60
+ * pie filter nulls in {@link planToEchartsOption} because
61
+ * Echarts crashes on null tuples / slices.
62
+ * 3. {@link z.union#catch} backstops the whole thing: if
63
+ * preprocess somehow produces a shape that still doesn't
64
+ * match any variant, the bad item becomes `null` instead of
65
+ * taking down the entire chart with a
66
+ * `Structured output validation failed` error.
67
+ *
68
+ * Net effect: a 200-row dataset with a few sparse/null/string
69
+ * values still produces a chart; only a totally-malformed planner
70
+ * response (no items at all) falls through to the table fallback.
71
+ */
72
+ const chartDataPointSchema = z
73
+ .preprocess((v) => {
74
+ if (v === null || v === undefined)
75
+ return null;
76
+ if (typeof v === "number")
77
+ return Number.isFinite(v) ? v : null;
78
+ if (typeof v === "string") {
79
+ const n = Number(v);
80
+ return Number.isFinite(n) ? n : null;
81
+ }
82
+ if (Array.isArray(v) && v.length === 2) {
83
+ const x = typeof v[0] === "number" ? v[0] : Number(v[0]);
84
+ const y = typeof v[1] === "number" ? v[1] : Number(v[1]);
85
+ return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
86
+ }
87
+ if (typeof v === "object" && v !== null && "value" in v) {
88
+ const obj = v;
89
+ const val = typeof obj.value === "number" ? obj.value : Number(obj.value);
90
+ if (!Number.isFinite(val))
91
+ return null;
92
+ // Coerce numeric / boolean / nullish names to strings so a
93
+ // pie slice keyed on a year (`2024`) or category id is
94
+ // accepted without round-tripping through the catch arm.
95
+ const rawName = obj.name;
96
+ const name = typeof rawName === "string"
97
+ ? rawName
98
+ : rawName == null
99
+ ? ""
100
+ : String(rawName);
101
+ return { name, value: val };
102
+ }
103
+ return null;
104
+ }, z.union([
105
+ z.number(),
106
+ z.null(),
107
+ z.tuple([z.number(), z.number()]),
108
+ z.object({ name: z.string(), value: z.number() }),
109
+ ]))
110
+ .catch(null);
50
111
  /**
51
112
  * Compact, model-friendly representation of an Echarts spec. The
52
113
  * planner agent emits this; {@link planToEchartsOption} expands it
@@ -58,8 +119,7 @@ const log = logUtils.logger("mastra/chart");
58
119
  * across charts.
59
120
  */
60
121
  const chartPlanSchema = z.object({
61
- chartType: z
62
- .enum(["bar", "line", "area", "scatter", "pie"])
122
+ chartType: z.enum(["bar", "line", "area", "scatter", "pie"])
63
123
  .describe(stringUtils.toDescription `
64
124
  The chart shape that best matches the data and intent. Use
65
125
  \`bar\` for category-vs-value comparisons, \`line\` for
@@ -79,10 +139,7 @@ const chartPlanSchema = z.object({
79
139
  Axis label to the left of the chart. Used for bar / line /
80
140
  area / scatter; ignored for pie.
81
141
  `),
82
- categories: z
83
- .array(z.string())
84
- .optional()
85
- .describe(stringUtils.toDescription `
142
+ categories: z.array(z.string()).optional().describe(stringUtils.toDescription `
86
143
  X-axis category labels for \`bar\` / \`line\` / \`area\`
87
144
  charts (one per data point in each series). Omit for
88
145
  \`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
@@ -93,24 +150,14 @@ const chartPlanSchema = z.object({
93
150
  name: z.string().describe(stringUtils.toDescription `
94
151
  Legend name for this series.
95
152
  `),
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 `
153
+ data: z.array(chartDataPointSchema).describe(stringUtils.toDescription `
106
154
  Data points. For \`bar\` / \`line\` / \`area\`, an
107
155
  array of numbers aligned to \`categories\`. For
108
156
  \`scatter\`, an array of \`[x, y]\` numeric tuples.
109
157
  For \`pie\`, an array of \`{name, value}\` objects.
110
158
  `),
111
159
  }))
112
- .min(1)
113
- .describe(stringUtils.toDescription `
160
+ .min(1).describe(stringUtils.toDescription `
114
161
  One or more series to plot. Pie charts use exactly one
115
162
  series; bar/line/area can stack multiple series sharing
116
163
  the same \`categories\` axis.
@@ -193,13 +240,14 @@ function getPlannerAgent(config) {
193
240
  }
194
241
  /**
195
242
  * 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).
243
+ * full Echarts `EChartsOption` JSON. Pure async function: no
244
+ * writer side-effects, no id minting, no background work.
245
+ * Producers (the `render_data` tool, the Genie agent,
246
+ * anything else that needs a chart) await this and stitch the
247
+ * result into whatever shape their wire contract needs.
200
248
  */
201
249
  export async function runChartPlanner(opts) {
202
- const { config, requestContext, title, description, data } = opts;
250
+ const { config, requestContext, title, description, data, signal } = opts;
203
251
  const planner = getPlannerAgent(config);
204
252
  const prompt = [
205
253
  `Title: ${title}`,
@@ -215,122 +263,12 @@ export async function runChartPlanner(opts) {
215
263
  const result = await planner.generate(prompt, {
216
264
  structuredOutput: { schema: chartPlanSchema },
217
265
  ...(requestContext ? { requestContext } : {}),
266
+ ...(signal ? { abortSignal: signal } : {}),
218
267
  });
219
268
  const plan = result.object;
220
269
  const option = planToEchartsOption(plan, title);
221
270
  return { option, chartType: plan.chartType };
222
271
  }
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
272
  const renderDataInputSchema = z.object({
335
273
  title: z.string().describe(stringUtils.toDescription `
336
274
  Title shown above the rendered chart. Use a concise
@@ -343,9 +281,7 @@ const renderDataInputSchema = z.object({
343
281
  The chart-planner reads this when picking the chart type and
344
282
  axis encodings; the user does not see it directly.
345
283
  `),
346
- data: z
347
- .array(z.record(z.string(), z.unknown()))
348
- .min(1)
284
+ data: z.array(z.record(z.string(), z.unknown())).min(1)
349
285
  .describe(stringUtils.toDescription `
350
286
  Tabular dataset to chart. One object per row, keyed by
351
287
  column name. Values may be strings, numbers, booleans, or
@@ -365,16 +301,14 @@ const renderDataOutputSchema = z.object({
365
301
  /**
366
302
  * Build the `render_data` tool bound to the given plugin config.
367
303
  *
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.
304
+ * The tool awaits {@link runChartPlanner} so the planner's
305
+ * latency is attributed to this tool's trace span, then emits
306
+ * one `type: "chart"` writer event carrying the dataset and the
307
+ * resolved `EChartsOption`. The LLM-bound output is just
308
+ * `{ chartId }` so the model's context stays flat regardless of
309
+ * dataset size. Planner failures are caught and surfaced as a
310
+ * `type: "error"` writer event so the slot can fall back to
311
+ * "couldn't render chart" without taking the parent agent down.
378
312
  */
379
313
  export function buildRenderDataTool(config) {
380
314
  return createTool({
@@ -389,14 +323,14 @@ export function buildRenderDataTool(config) {
389
323
 
390
324
  Placement contract: embed \`[[chart:<chartId>]]\` on its own
391
325
  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.
326
+ chart to appear in your reply. The chart is fully resolved
327
+ by the time the tool returns, so it renders immediately at
328
+ that spot. You can call \`render_data\` multiple times in
329
+ the same turn (the tool is parallel-safe) and interleave
330
+ the markers with prose so each chart sits next to its
331
+ commentary. A chart whose marker is omitted falls through
332
+ to the end of your reply as a fallback - safe but less
333
+ polished.
400
334
 
401
335
  Use whenever a SQL row set, API response, or hand-built
402
336
  dataset would land better as a picture than as a list or
@@ -410,22 +344,85 @@ export function buildRenderDataTool(config) {
410
344
  const writer = ctx?.writer;
411
345
  const requestContext = ctx
412
346
  ?.requestContext;
413
- const { chartId, plannerPromise } = await emitChartWithPlanning({
414
- ...(writer ? { writer } : {}),
415
- config,
416
- ...(requestContext ? { requestContext } : {}),
347
+ // Marker-friendly short id. The LLM types this verbatim
348
+ // into `[[chart:<id>]]`; 8 hex chars is unique within a
349
+ // single assistant turn and easy for the model to copy.
350
+ const chartId = commonUtils.shortId();
351
+ const startedAt = Date.now();
352
+ log.debug("render:start", {
353
+ chartId,
417
354
  title,
418
- ...(description ? { description } : {}),
419
- data,
355
+ rows: data.length,
356
+ columns: data[0] ? Object.keys(data[0]) : [],
357
+ hasWriter: writer !== undefined,
420
358
  });
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;
359
+ try {
360
+ const { option, chartType } = await runChartPlanner({
361
+ config,
362
+ ...(requestContext ? { requestContext } : {}),
363
+ title,
364
+ ...(description ? { description } : {}),
365
+ data,
366
+ });
367
+ log.debug("render:done", {
368
+ chartId,
369
+ chartType,
370
+ elapsedMs: Date.now() - startedAt,
371
+ });
372
+ // Single chart event with everything resolved: dataset
373
+ // for the table-like fallback / hover, option for the
374
+ // actual render. Best-effort write so a closed
375
+ // downstream stream can't take the tool down.
376
+ await safeWrite(writer, chartId, {
377
+ type: "chart",
378
+ chartId,
379
+ title,
380
+ ...(description ? { description } : {}),
381
+ data,
382
+ option,
383
+ });
384
+ }
385
+ catch (err) {
386
+ log.warn("render:error", {
387
+ chartId,
388
+ elapsedMs: Date.now() - startedAt,
389
+ error: err instanceof Error ? err.message : String(err),
390
+ });
391
+ // Surface as a writer-level error so the slot can
392
+ // transition to "couldn't render chart" without the
393
+ // parent agent surfacing a stack trace.
394
+ await safeWrite(writer, chartId, {
395
+ type: "error",
396
+ error: err instanceof Error ? err.message : String(err),
397
+ });
398
+ }
425
399
  return { chartId };
426
400
  },
427
401
  });
428
402
  }
403
+ /**
404
+ * Best-effort writer.write. Failures are logged at `warn` (a
405
+ * persistently-closed writer is the most likely culprit when
406
+ * chart events go missing client-side) but swallowed so a closed
407
+ * downstream stream (cancelled request, client navigated away)
408
+ * can't take a tool down.
409
+ */
410
+ async function safeWrite(writer, chartId, chunk) {
411
+ if (!writer) {
412
+ log.debug("write:no-writer", { chartId });
413
+ return;
414
+ }
415
+ try {
416
+ await writer.write(chunk);
417
+ log.debug("write:ok", { chartId });
418
+ }
419
+ catch (err) {
420
+ log.warn("write:error", {
421
+ chartId,
422
+ error: err instanceof Error ? err.message : String(err),
423
+ });
424
+ }
425
+ }
429
426
  /**
430
427
  * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
431
428
  * JSON. Centralized here so the planner agent only fills in the
@@ -437,6 +434,11 @@ function planToEchartsOption(plan, fallbackTitle) {
437
434
  const baseTitle = plan.title ?? fallbackTitle;
438
435
  const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
439
436
  if (plan.chartType === "pie") {
437
+ // Echarts crashes on null pie slices - filter them out.
438
+ // `{name, value}` slices are the only valid pie data shape,
439
+ // so drop bare numbers / tuples / nulls the planner may
440
+ // have leaked into a pie series.
441
+ const slices = (plan.series[0]?.data ?? []).filter((d) => d !== null && typeof d === "object" && !Array.isArray(d));
440
442
  return {
441
443
  title: { text: baseTitle, left: "center" },
442
444
  tooltip: { trigger: "item" },
@@ -446,12 +448,15 @@ function planToEchartsOption(plan, fallbackTitle) {
446
448
  name: plan.series[0]?.name ?? baseTitle,
447
449
  type: "pie",
448
450
  radius: ["35%", "65%"],
449
- data: plan.series[0]?.data ?? [],
451
+ data: slices,
450
452
  },
451
453
  ],
452
454
  };
453
455
  }
454
456
  if (plan.chartType === "scatter") {
457
+ // Echarts crashes on null scatter points - keep only valid
458
+ // `[x, y]` tuples. Bare numbers / objects / nulls from a
459
+ // mismatched plan get dropped silently.
455
460
  return {
456
461
  title: { text: baseTitle, left: "center" },
457
462
  tooltip: { trigger: "item" },
@@ -462,7 +467,7 @@ function planToEchartsOption(plan, fallbackTitle) {
462
467
  series: plan.series.map((s) => ({
463
468
  name: s.name,
464
469
  type: "scatter",
465
- data: s.data,
470
+ data: s.data.filter((d) => Array.isArray(d) && d.length === 2),
466
471
  })),
467
472
  };
468
473
  }
@@ -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
  }