@dbx-tools/appkit-mastra 0.1.4 → 0.1.12

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.
@@ -0,0 +1,491 @@
1
+ /**
2
+ * Chart-rendering primitives.
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.
21
+ *
22
+ * - {@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.
27
+ *
28
+ * The model wires the chart into its reply by emitting the marker
29
+ * `[[chart:<chartId>]]` on its own line in markdown. The chat
30
+ * 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.
35
+ */
36
+ import { randomUUID } from "node:crypto";
37
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
38
+ import { Agent } from "@mastra/core/agent";
39
+ import { createTool } from "@mastra/core/tools";
40
+ import { z } from "zod";
41
+ import { ModelTier, modelForTier, buildModel } from "./model.js";
42
+ /**
43
+ * Module-level logger tagged `[mastra/chart]`. Uses the shared
44
+ * {@link logUtils.logger} so calls below `LOG_LEVEL` are
45
+ * discarded for free. Default `LOG_LEVEL` is `info`; flip to
46
+ * `debug` to see the per-chart timeline (`emit:start` →
47
+ * `write:ok(data)` → `planner:done` → `write:ok(option)`).
48
+ */
49
+ const log = logUtils.logger("mastra/chart");
50
+ /**
51
+ * Compact, model-friendly representation of an Echarts spec. The
52
+ * planner agent emits this; {@link planToEchartsOption} expands it
53
+ * into a real `EChartsOption` JSON. Two layers because letting the
54
+ * model fill in a fully-typed `EChartsOption` is brittle (hundreds
55
+ * of optional fields, deep unions, version-dependent shapes). A
56
+ * small "chart plan" schema is much more reliable for a fast model
57
+ * and keeps animation / tooltip / styling defaults consistent
58
+ * across charts.
59
+ */
60
+ const chartPlanSchema = z.object({
61
+ chartType: z
62
+ .enum(["bar", "line", "area", "scatter", "pie"])
63
+ .describe(stringUtils.toDescription `
64
+ The chart shape that best matches the data and intent. Use
65
+ \`bar\` for category-vs-value comparisons, \`line\` for
66
+ trends over an ordered axis, \`area\` for stacked-trend
67
+ emphasis, \`scatter\` for two-numeric-axis correlations,
68
+ \`pie\` for parts-of-a-whole when categories are few.
69
+ `),
70
+ title: z.string().optional().describe(stringUtils.toDescription `
71
+ Short title shown above the chart. Optional; defaults to the
72
+ \`title\` argument the caller passed in.
73
+ `),
74
+ xAxisLabel: z.string().optional().describe(stringUtils.toDescription `
75
+ Axis label below the chart. Used for bar / line / area /
76
+ scatter; ignored for pie.
77
+ `),
78
+ yAxisLabel: z.string().optional().describe(stringUtils.toDescription `
79
+ Axis label to the left of the chart. Used for bar / line /
80
+ area / scatter; ignored for pie.
81
+ `),
82
+ categories: z
83
+ .array(z.string())
84
+ .optional()
85
+ .describe(stringUtils.toDescription `
86
+ X-axis category labels for \`bar\` / \`line\` / \`area\`
87
+ charts (one per data point in each series). Omit for
88
+ \`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
89
+ carries its own \`name\`).
90
+ `),
91
+ series: z
92
+ .array(z.object({
93
+ name: z.string().describe(stringUtils.toDescription `
94
+ Legend name for this series.
95
+ `),
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 `
106
+ Data points. For \`bar\` / \`line\` / \`area\`, an
107
+ array of numbers aligned to \`categories\`. For
108
+ \`scatter\`, an array of \`[x, y]\` numeric tuples.
109
+ For \`pie\`, an array of \`{name, value}\` objects.
110
+ `),
111
+ }))
112
+ .min(1)
113
+ .describe(stringUtils.toDescription `
114
+ One or more series to plot. Pie charts use exactly one
115
+ series; bar/line/area can stack multiple series sharing
116
+ the same \`categories\` axis.
117
+ `),
118
+ });
119
+ /**
120
+ * System prompt for the inner chart-planning agent. Tuned for a
121
+ * fast-tier model (Haiku, GPT-5-mini, Gemini Flash Lite).
122
+ */
123
+ const CHART_PLANNER_INSTRUCTIONS = stringUtils.toDescription `
124
+ You design Apache Echarts visualizations. The user gives you a
125
+ tabular dataset (rows of objects) plus a title and an optional
126
+ description of the intent. You produce a small chart plan
127
+ (chart type, axis labels, categories, series) that best
128
+ conveys the data.
129
+
130
+ Decision guide:
131
+
132
+ - bar: comparing a numeric value across a small/medium set of
133
+ discrete categories (top-N, ranking, group-by).
134
+ - line: ordered-axis trend (time series, sequence).
135
+ - area: same as line but emphasises magnitude or stacked
136
+ composition.
137
+ - scatter: two numeric axes, correlation between fields.
138
+ - pie: parts of a whole when 2-7 categories sum to a
139
+ meaningful total.
140
+
141
+ When in doubt between bar and line, prefer bar for unordered
142
+ categories and line for ordered ones (dates, time buckets,
143
+ ranks). Never pick pie for more than 7 slices.
144
+
145
+ For bar / line / area: pick one column as the category axis
146
+ (usually the only string-valued column) and one or more
147
+ numeric columns as series. Sort categories by the primary
148
+ series value descending unless the data is naturally ordered
149
+ (dates, ranks).
150
+
151
+ For pie: pick the category column for slice names and one
152
+ numeric column for slice values. Emit a single series.
153
+
154
+ For scatter: pick two numeric columns and emit \`[x, y]\`
155
+ tuples in a single series.
156
+
157
+ Keep series names human-readable (use the column name; title
158
+ case it lightly if needed). Keep titles concise; do not
159
+ repeat the user's title in xAxisLabel / yAxisLabel.
160
+ `;
161
+ /**
162
+ * Lazily-constructed inner agent shared across all calls in this
163
+ * process. The agent is stateless (no memory, no tools) so a
164
+ * single instance per plugin config is safe; model resolution
165
+ * still happens per-call against the live `requestContext`, so
166
+ * OBO auth stays user-scoped.
167
+ */
168
+ function createChartPlannerAgent(config) {
169
+ return new Agent({
170
+ id: "render_chart_planner",
171
+ name: "Chart Planner",
172
+ description: "Picks chart type and axis encodings for a dataset.",
173
+ instructions: CHART_PLANNER_INSTRUCTIONS,
174
+ model: ({ requestContext }) => buildModel(config, requestContext, {
175
+ modelId: modelForTier(ModelTier.Fast),
176
+ }),
177
+ });
178
+ }
179
+ /**
180
+ * Module-level cache: one chart-planner agent per plugin config
181
+ * instance. Keyed on the config object identity since each plugin
182
+ * mount provides its own resolver / fallbacks. Re-used across
183
+ * tool invocations and the render-chart HTTP route.
184
+ */
185
+ const _plannerByConfig = new WeakMap();
186
+ function getPlannerAgent(config) {
187
+ let agent = _plannerByConfig.get(config);
188
+ if (!agent) {
189
+ agent = createChartPlannerAgent(config);
190
+ _plannerByConfig.set(config, agent);
191
+ }
192
+ return agent;
193
+ }
194
+ /**
195
+ * 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).
200
+ */
201
+ export async function runChartPlanner(opts) {
202
+ const { config, requestContext, title, description, data } = opts;
203
+ const planner = getPlannerAgent(config);
204
+ const prompt = [
205
+ `Title: ${title}`,
206
+ description ? `Intent: ${description}` : null,
207
+ "",
208
+ "Dataset (JSON, one row per object):",
209
+ "```json",
210
+ JSON.stringify(data, null, 2),
211
+ "```",
212
+ ]
213
+ .filter((line) => line !== null)
214
+ .join("\n");
215
+ const result = await planner.generate(prompt, {
216
+ structuredOutput: { schema: chartPlanSchema },
217
+ ...(requestContext ? { requestContext } : {}),
218
+ });
219
+ const plan = result.object;
220
+ const option = planToEchartsOption(plan, title);
221
+ return { option, chartType: plan.chartType };
222
+ }
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
+ const renderDataInputSchema = z.object({
335
+ title: z.string().describe(stringUtils.toDescription `
336
+ Title shown above the rendered chart. Use a concise
337
+ sentence-case label (e.g. "Top 10 SKUs by On-Hand Units").
338
+ `),
339
+ description: z.string().optional().describe(stringUtils.toDescription `
340
+ Optional one-line intent describing what insight the chart
341
+ should convey (e.g. "highlight the steep drop-off after
342
+ position 5", "compare quarterly revenue across regions").
343
+ The chart-planner reads this when picking the chart type and
344
+ axis encodings; the user does not see it directly.
345
+ `),
346
+ data: z
347
+ .array(z.record(z.string(), z.unknown()))
348
+ .min(1)
349
+ .describe(stringUtils.toDescription `
350
+ Tabular dataset to chart. One object per row, keyed by
351
+ column name. Values may be strings, numbers, booleans, or
352
+ null. The chart-planner decides which columns are
353
+ categories vs. numeric series. Cap at a few hundred rows
354
+ for legibility; sample / aggregate larger datasets first.
355
+ `),
356
+ });
357
+ const renderDataOutputSchema = z.object({
358
+ chartId: z.string().describe(stringUtils.toDescription `
359
+ Identifier of the queued chart. To position the chart in
360
+ your reply, embed the marker \`[[chart:<chartId>]]\` on its
361
+ own line where the chart should appear; the client renders
362
+ it inline.
363
+ `),
364
+ });
365
+ /**
366
+ * Build the `render_data` tool bound to the given plugin config.
367
+ *
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.
378
+ */
379
+ export function buildRenderDataTool(config) {
380
+ return createTool({
381
+ id: "render_data",
382
+ description: stringUtils.toDescription `
383
+ Submit a tabular dataset for inline rendering as a chart in
384
+ the user's view. Pass a title, the raw rows (array of
385
+ objects keyed by column name), and an optional one-line
386
+ description of the insight to highlight. Returns a short
387
+ \`chartId\`; the chart renders inline at the position you
388
+ embed the matching \`[[chart:<chartId>]]\` marker.
389
+
390
+ Placement contract: embed \`[[chart:<chartId>]]\` on its own
391
+ 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.
400
+
401
+ Use whenever a SQL row set, API response, or hand-built
402
+ dataset would land better as a picture than as a list or
403
+ table. Cap input at a few hundred rows; sample or
404
+ aggregate larger datasets first.
405
+ `,
406
+ inputSchema: renderDataInputSchema,
407
+ outputSchema: renderDataOutputSchema,
408
+ execute: async (input, ctx) => {
409
+ const { title, description, data } = input;
410
+ const writer = ctx?.writer;
411
+ const requestContext = ctx
412
+ ?.requestContext;
413
+ const { chartId, plannerPromise } = await emitChartWithPlanning({
414
+ ...(writer ? { writer } : {}),
415
+ config,
416
+ ...(requestContext ? { requestContext } : {}),
417
+ title,
418
+ ...(description ? { description } : {}),
419
+ data,
420
+ });
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;
425
+ return { chartId };
426
+ },
427
+ });
428
+ }
429
+ /**
430
+ * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
431
+ * JSON. Centralized here so the planner agent only fills in the
432
+ * compact plan shape; tooltip / animation / color / grid defaults
433
+ * stay consistent across charts and are easy to tune without
434
+ * retraining model behaviour.
435
+ */
436
+ function planToEchartsOption(plan, fallbackTitle) {
437
+ const baseTitle = plan.title ?? fallbackTitle;
438
+ const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
439
+ if (plan.chartType === "pie") {
440
+ return {
441
+ title: { text: baseTitle, left: "center" },
442
+ tooltip: { trigger: "item" },
443
+ legend: { bottom: 0 },
444
+ series: [
445
+ {
446
+ name: plan.series[0]?.name ?? baseTitle,
447
+ type: "pie",
448
+ radius: ["35%", "65%"],
449
+ data: plan.series[0]?.data ?? [],
450
+ },
451
+ ],
452
+ };
453
+ }
454
+ if (plan.chartType === "scatter") {
455
+ return {
456
+ title: { text: baseTitle, left: "center" },
457
+ tooltip: { trigger: "item" },
458
+ legend: { bottom: 0 },
459
+ grid,
460
+ xAxis: { type: "value", name: plan.xAxisLabel },
461
+ yAxis: { type: "value", name: plan.yAxisLabel },
462
+ series: plan.series.map((s) => ({
463
+ name: s.name,
464
+ type: "scatter",
465
+ data: s.data,
466
+ })),
467
+ };
468
+ }
469
+ // bar / line / area share the same axis layout.
470
+ const isArea = plan.chartType === "area";
471
+ const seriesType = plan.chartType === "bar" ? "bar" : "line";
472
+ return {
473
+ title: { text: baseTitle, left: "center" },
474
+ tooltip: { trigger: "axis" },
475
+ legend: { bottom: 0 },
476
+ grid,
477
+ xAxis: {
478
+ type: "category",
479
+ data: plan.categories ?? [],
480
+ name: plan.xAxisLabel,
481
+ },
482
+ yAxis: { type: "value", name: plan.yAxisLabel },
483
+ series: plan.series.map((s) => ({
484
+ name: s.name,
485
+ type: seriesType,
486
+ data: s.data,
487
+ smooth: seriesType === "line",
488
+ ...(isArea ? { areaStyle: {} } : {}),
489
+ })),
490
+ };
491
+ }
@@ -152,6 +152,19 @@ export interface MastraPluginConfig extends BasePluginConfig {
152
152
  * or to add custom endpoints in front of the public catalogue.
153
153
  */
154
154
  defaultModelFallbacks?: readonly string[];
155
+ /**
156
+ * When `true` (default), every agent gets a built-in input
157
+ * processor that strips `chartId` fields from prior assistant
158
+ * tool-invocation results before they reach the model. This
159
+ * prevents the model from reusing turn-scoped chartIds it sees
160
+ * in memory recall (which would leave `[[chart:<id>]]` markers
161
+ * pointing at writer events that no longer exist).
162
+ *
163
+ * Set to `false` to opt out - useful if a non-default agent
164
+ * needs full visibility into prior chartIds (e.g. an audit
165
+ * agent reasoning about chart lineage).
166
+ */
167
+ stripStaleCharts?: boolean;
155
168
  /**
156
169
  * Style guardrails appended to every agent's `instructions` to curb
157
170
  * common LLM-isms (em dashes, emojis, sycophantic openers, throwaway
@@ -11,15 +11,17 @@
11
11
  * upstream change in `@databricks/appkit` flows in automatically.
12
12
  *
13
13
  * As Genie streams its long-running events (`FETCHING_METADATA` →
14
- * `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL queries and
15
- * row data in `message_result.attachments` / `query_result`), the tool
16
- * forwards a normalised {@link GenieProgress} discriminated union out
17
- * through `ctx.writer` so the client can render incremental feedback
18
- * (status pill, SQL code block, row count) while the LLM still sees a
19
- * single clean final payload.
14
+ * `ASKING_AI` → `EXECUTING_QUERY` → `COMPLETED`, plus SQL text and
15
+ * follow-ups in `message_result.attachments`), the tool forwards a
16
+ * normalised {@link GenieProgress} discriminated union out through
17
+ * `ctx.writer` so the client can render an incremental loading pill.
18
+ * Row payloads from `query_result` are intentionally discarded - the
19
+ * LLM never sees rows, and charts come from the separate
20
+ * `render_data` tool when the model decides one is useful.
20
21
  */
21
22
  import { genie } from "@databricks/appkit";
22
23
  import { createTool } from "@mastra/core/tools";
24
+ import type { MastraPluginConfig } from "./config.js";
23
25
  /** Live AppKit `GeniePlugin` instance. */
24
26
  export type GeniePluginInstance = InstanceType<ReturnType<typeof genie>["plugin"]>;
25
27
  /** Full `exports()` shape of the AppKit `genie` plugin. */
@@ -33,10 +35,19 @@ export type GenieStreamEvent = ReturnType<GenieExports["sendMessage"]> extends A
33
35
  /** Conversation history returned by `genie.exports().getConversation`. */
34
36
  export type GenieConversation = Awaited<ReturnType<GenieExports["getConversation"]>>;
35
37
  /**
36
- * Normalised progress event surfaced to the UI as a Mastra `tool-output`
37
- * chunk. The discriminator (`kind`) keeps the union open for future
38
- * Genie features (charts, attachments, retries) without forcing the
39
- * client to know any Genie wire format.
38
+ * Normalised progress event surfaced to the UI as a Mastra
39
+ * `tool-output` chunk. Loading pill events (`started`, `status`,
40
+ * `sql`, `suggested`, `error`) are pure UI metadata and never reach
41
+ * the LLM.
42
+ *
43
+ * The `chart` variant is the wire shape emitted by
44
+ * {@link emitChartWithPlanning} (used by both this Genie
45
+ * draining loop and the system-level `render_data` tool). All
46
+ * fields except `chartId` are optional because two events per
47
+ * chartId arrive on the wire: the first carries the rows
48
+ * (`title` + `description?` + `data`); the second, on planner
49
+ * success, carries just the resolved Echarts spec (`option`).
50
+ * The host UI's `<ChartSlot>` merges them by `chartId`.
40
51
  */
41
52
  export type GenieProgress = {
42
53
  kind: "started";
@@ -54,9 +65,12 @@ export type GenieProgress = {
54
65
  description?: string;
55
66
  statementId?: string;
56
67
  } | {
57
- kind: "data";
58
- rowCount: number;
59
- columns: string[];
68
+ kind: "chart";
69
+ chartId: string;
70
+ title?: string;
71
+ description?: string;
72
+ data?: Array<Record<string, unknown>>;
73
+ option?: Record<string, unknown>;
60
74
  } | {
61
75
  kind: "text";
62
76
  content: string;
@@ -79,10 +93,16 @@ export declare function defaultGenieToolName(alias: string): string;
79
93
  * Build one `sendMessage` tool per configured Genie alias plus a single
80
94
  * `getConversation` tool. Returns a record keyed by tool id, ready to
81
95
  * spread into an `Agent`'s `tools` map.
96
+ *
97
+ * `config` must be the active plugin config; Genie's
98
+ * `query_result` events are routed through
99
+ * {@link emitChartWithPlanning} which uses it to resolve the
100
+ * chart-planner's model.
82
101
  */
83
102
  export declare function buildGenieTools(opts: {
84
103
  aliases: string[];
85
104
  exports: GenieExports;
105
+ config: MastraPluginConfig;
86
106
  signal?: AbortSignal;
87
107
  }): Record<string, ReturnType<typeof createTool>>;
88
108
  /**
@@ -104,6 +124,8 @@ export declare function buildGenieTools(opts: {
104
124
  * all-or-nothing bundle. Wire `only` / `except` / `prefix` / `rename`
105
125
  * later if a caller needs them.
106
126
  */
107
- export declare function buildGenieProvider(plugin: GeniePluginInstance): {
127
+ export declare function buildGenieProvider(plugin: GeniePluginInstance, opts: {
128
+ config: MastraPluginConfig;
129
+ }): {
108
130
  toolkit(opts?: unknown): Record<string, ReturnType<typeof createTool>>;
109
131
  };