@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.
package/src/chart.ts ADDED
@@ -0,0 +1,593 @@
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
+
37
+ import { randomUUID } from "node:crypto";
38
+
39
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
40
+ import { Agent } from "@mastra/core/agent";
41
+ import type { RequestContext } from "@mastra/core/request-context";
42
+ import { createTool } from "@mastra/core/tools";
43
+ import { z } from "zod";
44
+
45
+ import type { MastraPluginConfig } from "./config.js";
46
+ import { ModelTier, modelForTier, buildModel } from "./model.js";
47
+
48
+ /**
49
+ * Module-level logger tagged `[mastra/chart]`. Uses the shared
50
+ * {@link logUtils.logger} so calls below `LOG_LEVEL` are
51
+ * discarded for free. Default `LOG_LEVEL` is `info`; flip to
52
+ * `debug` to see the per-chart timeline (`emit:start` →
53
+ * `write:ok(data)` → `planner:done` → `write:ok(option)`).
54
+ */
55
+ const log = logUtils.logger("mastra/chart");
56
+
57
+ /**
58
+ * Compact, model-friendly representation of an Echarts spec. The
59
+ * planner agent emits this; {@link planToEchartsOption} expands it
60
+ * into a real `EChartsOption` JSON. Two layers because letting the
61
+ * model fill in a fully-typed `EChartsOption` is brittle (hundreds
62
+ * of optional fields, deep unions, version-dependent shapes). A
63
+ * small "chart plan" schema is much more reliable for a fast model
64
+ * and keeps animation / tooltip / styling defaults consistent
65
+ * across charts.
66
+ */
67
+ const chartPlanSchema = z.object({
68
+ chartType: z
69
+ .enum(["bar", "line", "area", "scatter", "pie"])
70
+ .describe(stringUtils.toDescription`
71
+ The chart shape that best matches the data and intent. Use
72
+ \`bar\` for category-vs-value comparisons, \`line\` for
73
+ trends over an ordered axis, \`area\` for stacked-trend
74
+ emphasis, \`scatter\` for two-numeric-axis correlations,
75
+ \`pie\` for parts-of-a-whole when categories are few.
76
+ `),
77
+ title: z.string().optional().describe(stringUtils.toDescription`
78
+ Short title shown above the chart. Optional; defaults to the
79
+ \`title\` argument the caller passed in.
80
+ `),
81
+ xAxisLabel: z.string().optional().describe(stringUtils.toDescription`
82
+ Axis label below the chart. Used for bar / line / area /
83
+ scatter; ignored for pie.
84
+ `),
85
+ yAxisLabel: z.string().optional().describe(stringUtils.toDescription`
86
+ Axis label to the left of the chart. Used for bar / line /
87
+ area / scatter; ignored for pie.
88
+ `),
89
+ categories: z
90
+ .array(z.string())
91
+ .optional()
92
+ .describe(stringUtils.toDescription`
93
+ X-axis category labels for \`bar\` / \`line\` / \`area\`
94
+ charts (one per data point in each series). Omit for
95
+ \`scatter\` (uses [x, y] tuples) and \`pie\` (each slice
96
+ carries its own \`name\`).
97
+ `),
98
+ series: z
99
+ .array(
100
+ z.object({
101
+ name: z.string().describe(stringUtils.toDescription`
102
+ Legend name for this series.
103
+ `),
104
+ data: z
105
+ .array(
106
+ z.union([
107
+ z.number(),
108
+ z.tuple([z.number(), z.number()]),
109
+ z.object({
110
+ name: z.string(),
111
+ value: z.number(),
112
+ }),
113
+ ]),
114
+ )
115
+ .describe(stringUtils.toDescription`
116
+ Data points. For \`bar\` / \`line\` / \`area\`, an
117
+ array of numbers aligned to \`categories\`. For
118
+ \`scatter\`, an array of \`[x, y]\` numeric tuples.
119
+ For \`pie\`, an array of \`{name, value}\` objects.
120
+ `),
121
+ }),
122
+ )
123
+ .min(1)
124
+ .describe(stringUtils.toDescription`
125
+ One or more series to plot. Pie charts use exactly one
126
+ series; bar/line/area can stack multiple series sharing
127
+ the same \`categories\` axis.
128
+ `),
129
+ });
130
+
131
+ type ChartPlan = z.infer<typeof chartPlanSchema>;
132
+
133
+ /**
134
+ * System prompt for the inner chart-planning agent. Tuned for a
135
+ * fast-tier model (Haiku, GPT-5-mini, Gemini Flash Lite).
136
+ */
137
+ const CHART_PLANNER_INSTRUCTIONS = stringUtils.toDescription`
138
+ You design Apache Echarts visualizations. The user gives you a
139
+ tabular dataset (rows of objects) plus a title and an optional
140
+ description of the intent. You produce a small chart plan
141
+ (chart type, axis labels, categories, series) that best
142
+ conveys the data.
143
+
144
+ Decision guide:
145
+
146
+ - bar: comparing a numeric value across a small/medium set of
147
+ discrete categories (top-N, ranking, group-by).
148
+ - line: ordered-axis trend (time series, sequence).
149
+ - area: same as line but emphasises magnitude or stacked
150
+ composition.
151
+ - scatter: two numeric axes, correlation between fields.
152
+ - pie: parts of a whole when 2-7 categories sum to a
153
+ meaningful total.
154
+
155
+ When in doubt between bar and line, prefer bar for unordered
156
+ categories and line for ordered ones (dates, time buckets,
157
+ ranks). Never pick pie for more than 7 slices.
158
+
159
+ For bar / line / area: pick one column as the category axis
160
+ (usually the only string-valued column) and one or more
161
+ numeric columns as series. Sort categories by the primary
162
+ series value descending unless the data is naturally ordered
163
+ (dates, ranks).
164
+
165
+ For pie: pick the category column for slice names and one
166
+ numeric column for slice values. Emit a single series.
167
+
168
+ For scatter: pick two numeric columns and emit \`[x, y]\`
169
+ tuples in a single series.
170
+
171
+ Keep series names human-readable (use the column name; title
172
+ case it lightly if needed). Keep titles concise; do not
173
+ repeat the user's title in xAxisLabel / yAxisLabel.
174
+ `;
175
+
176
+ /**
177
+ * Lazily-constructed inner agent shared across all calls in this
178
+ * process. The agent is stateless (no memory, no tools) so a
179
+ * single instance per plugin config is safe; model resolution
180
+ * still happens per-call against the live `requestContext`, so
181
+ * OBO auth stays user-scoped.
182
+ */
183
+ function createChartPlannerAgent(config: MastraPluginConfig): Agent {
184
+ return new Agent({
185
+ id: "render_chart_planner",
186
+ name: "Chart Planner",
187
+ description: "Picks chart type and axis encodings for a dataset.",
188
+ instructions: CHART_PLANNER_INSTRUCTIONS,
189
+ model: ({ requestContext }) =>
190
+ buildModel(config, requestContext, {
191
+ modelId: modelForTier(ModelTier.Fast),
192
+ }),
193
+ });
194
+ }
195
+
196
+ /** Inputs to {@link runChartPlanner}. */
197
+ export interface RunChartPlannerOptions {
198
+ config: MastraPluginConfig;
199
+ requestContext?: RequestContext;
200
+ title: string;
201
+ description?: string;
202
+ data: ReadonlyArray<Record<string, unknown>>;
203
+ }
204
+
205
+ /** Output of {@link runChartPlanner}: a fully-formed Echarts spec. */
206
+ export interface RunChartPlannerResult {
207
+ option: Record<string, unknown>;
208
+ chartType: ChartPlan["chartType"];
209
+ }
210
+
211
+ /**
212
+ * Module-level cache: one chart-planner agent per plugin config
213
+ * instance. Keyed on the config object identity since each plugin
214
+ * mount provides its own resolver / fallbacks. Re-used across
215
+ * tool invocations and the render-chart HTTP route.
216
+ */
217
+ const _plannerByConfig = new WeakMap<MastraPluginConfig, Agent>();
218
+ function getPlannerAgent(config: MastraPluginConfig): Agent {
219
+ let agent = _plannerByConfig.get(config);
220
+ if (!agent) {
221
+ agent = createChartPlannerAgent(config);
222
+ _plannerByConfig.set(config, agent);
223
+ }
224
+ return agent;
225
+ }
226
+
227
+ /**
228
+ * Run the chart planner against the given dataset and return a
229
+ * full Echarts `EChartsOption` JSON. Used by
230
+ * {@link emitChartWithPlanning}; tools and producers shouldn't
231
+ * call this directly (use the helper instead so chart events
232
+ * follow the same wire-format contract everywhere).
233
+ */
234
+ export async function runChartPlanner(
235
+ opts: RunChartPlannerOptions,
236
+ ): Promise<RunChartPlannerResult> {
237
+ const { config, requestContext, title, description, data } = opts;
238
+ const planner = getPlannerAgent(config);
239
+
240
+ const prompt = [
241
+ `Title: ${title}`,
242
+ description ? `Intent: ${description}` : null,
243
+ "",
244
+ "Dataset (JSON, one row per object):",
245
+ "```json",
246
+ JSON.stringify(data, null, 2),
247
+ "```",
248
+ ]
249
+ .filter((line): line is string => line !== null)
250
+ .join("\n");
251
+
252
+ const result = await planner.generate(prompt, {
253
+ structuredOutput: { schema: chartPlanSchema },
254
+ ...(requestContext ? { requestContext } : {}),
255
+ });
256
+ const plan = result.object;
257
+ const option = planToEchartsOption(plan, title);
258
+ return { option, chartType: plan.chartType };
259
+ }
260
+
261
+ /**
262
+ * Minimal `ToolStream`-shaped writer surface. Defined locally so
263
+ * helpers can take any object with a `.write` method without
264
+ * importing Mastra's full `ToolStream` (which would also drag in
265
+ * agent / tool types this module doesn't otherwise need).
266
+ */
267
+ interface MinimalWriter {
268
+ write: (chunk: unknown) => unknown;
269
+ }
270
+
271
+ /** Inputs to {@link emitChartWithPlanning}. */
272
+ export interface EmitChartWithPlanningOptions {
273
+ /** Mastra `ctx.writer`; missing or closed writers are tolerated. */
274
+ writer?: MinimalWriter;
275
+ /** Plugin config; used to resolve the planner's model. */
276
+ config: MastraPluginConfig;
277
+ /** Per-request context (OBO auth). */
278
+ requestContext?: RequestContext;
279
+ /** Title shown above the rendered chart. Required. */
280
+ title: string;
281
+ /** Optional one-line intent biasing the planner. */
282
+ description?: string;
283
+ /** Tabular dataset to chart (one object per row). */
284
+ data: ReadonlyArray<Record<string, unknown>>;
285
+ }
286
+
287
+ /** Output of {@link emitChartWithPlanning}. */
288
+ export interface EmitChartWithPlanningResult {
289
+ /** Short id matching the marker `[[chart:<chartId>]]`. */
290
+ chartId: string;
291
+ /**
292
+ * Promise that resolves once the planner has finished and the
293
+ * `kind: "chart"` event with the option has been emitted (or
294
+ * once the planner has failed silently). Callers that want
295
+ * trace observability should `await` this before returning
296
+ * from their tool's `execute`; callers that want pure
297
+ * fire-and-forget can ignore it.
298
+ */
299
+ plannerPromise: Promise<void>;
300
+ }
301
+
302
+ /**
303
+ * Shared chart-emission primitive used by both the `render_data`
304
+ * tool and Genie's `drainGenieStream`. Keeps both producers on
305
+ * one wire-format contract so the chat client only ever has to
306
+ * understand a single chart event shape.
307
+ *
308
+ * Behaviour:
309
+ *
310
+ * 1. Generates a short `chartId` (8 hex chars).
311
+ * 2. Immediately emits `{ kind: "chart", chartId, title,
312
+ * description?, data }` via the writer so the chat client can
313
+ * mount its `<ChartSlot>` with the rows in hand.
314
+ * 3. Kicks off the chart-planner agent in the background. On
315
+ * success, emits a second `{ kind: "chart", chartId, option }`
316
+ * event - same `chartId`, just the spec - so the client merges
317
+ * the two into one rendered chart. On failure, no follow-up
318
+ * event fires; the client falls back to whatever it can do
319
+ * with the dataset alone (typically a "render failed" frame
320
+ * after the parent tool finishes).
321
+ *
322
+ * Returns `chartId` synchronously so the caller can include it in
323
+ * the tool result (model uses it in `[[chart:<chartId>]]`
324
+ * markers), and `plannerPromise` so the caller can choose
325
+ * trace-spanning vs. snappy-return semantics.
326
+ */
327
+ export async function emitChartWithPlanning(
328
+ opts: EmitChartWithPlanningOptions,
329
+ ): Promise<EmitChartWithPlanningResult> {
330
+ const { writer, config, requestContext, title, description, data } = opts;
331
+
332
+ // Short, marker-friendly id. The LLM types this verbatim into
333
+ // `[[chart:<id>]]`; an 8-hex-char prefix is unique within a
334
+ // single assistant turn (collision odds ~1 in 4 billion) and
335
+ // much less error-prone for the model to reproduce.
336
+ const chartId = randomUUID().replace(/-/g, "").slice(0, 8);
337
+
338
+ log.debug("emit:start", {
339
+ chartId,
340
+ title,
341
+ rows: data.length,
342
+ columns: data[0] ? Object.keys(data[0]) : [],
343
+ hasWriter: writer !== undefined,
344
+ });
345
+
346
+ // Initial event: rows + metadata, no option yet. The client
347
+ // mounts a chart slot that shows a skeleton until the option
348
+ // event arrives (or until the parent tool finishes without
349
+ // one, in which case it falls back).
350
+ await safeWrite(writer, chartId, "data", {
351
+ kind: "chart",
352
+ chartId,
353
+ title,
354
+ ...(description ? { description } : {}),
355
+ data,
356
+ });
357
+
358
+ // Background planner. Awaitable for trace observability via the
359
+ // returned `plannerPromise`; safe to ignore for pure
360
+ // fire-and-forget. Failures are intentionally swallowed (only
361
+ // logged): the dataset event already landed, so the client has
362
+ // enough to surface a fallback.
363
+ const plannerPromise = (async () => {
364
+ const startedAt = Date.now();
365
+ try {
366
+ const { option, chartType } = await runChartPlanner({
367
+ config,
368
+ ...(requestContext ? { requestContext } : {}),
369
+ title,
370
+ ...(description ? { description } : {}),
371
+ data,
372
+ });
373
+ log.debug("planner:done", {
374
+ chartId,
375
+ chartType,
376
+ elapsedMs: Date.now() - startedAt,
377
+ });
378
+ await safeWrite(writer, chartId, "option", { kind: "chart", chartId, option });
379
+ } catch (err) {
380
+ // No follow-up event on failure. The client treats a
381
+ // dataset-only chart slot as "render failed" once the
382
+ // parent tool's status flips to done. Surface as a `warn`
383
+ // so the failure is visible at the default log level
384
+ // without being mistaken for a fatal error.
385
+ log.warn("planner:error", {
386
+ chartId,
387
+ elapsedMs: Date.now() - startedAt,
388
+ error: err instanceof Error ? err.message : String(err),
389
+ });
390
+ }
391
+ })();
392
+
393
+ return { chartId, plannerPromise };
394
+ }
395
+
396
+ /**
397
+ * Best-effort writer.write. Failures are logged at `warn` (a
398
+ * persistently-closed writer is the most likely culprit when
399
+ * chart events go missing client-side) but swallowed so a closed
400
+ * downstream stream (cancelled request, client navigated away)
401
+ * can't take a tool down.
402
+ */
403
+ async function safeWrite(
404
+ writer: MinimalWriter | undefined,
405
+ chartId: string,
406
+ phase: "data" | "option",
407
+ chunk: unknown,
408
+ ): Promise<void> {
409
+ if (!writer) {
410
+ log.debug("write:no-writer", { chartId, phase });
411
+ return;
412
+ }
413
+ try {
414
+ await writer.write(chunk);
415
+ log.debug("write:ok", { chartId, phase });
416
+ } catch (err) {
417
+ log.warn("write:error", {
418
+ chartId,
419
+ phase,
420
+ error: err instanceof Error ? err.message : String(err),
421
+ });
422
+ }
423
+ }
424
+
425
+ const renderDataInputSchema = z.object({
426
+ title: z.string().describe(stringUtils.toDescription`
427
+ Title shown above the rendered chart. Use a concise
428
+ sentence-case label (e.g. "Top 10 SKUs by On-Hand Units").
429
+ `),
430
+ description: z.string().optional().describe(stringUtils.toDescription`
431
+ Optional one-line intent describing what insight the chart
432
+ should convey (e.g. "highlight the steep drop-off after
433
+ position 5", "compare quarterly revenue across regions").
434
+ The chart-planner reads this when picking the chart type and
435
+ axis encodings; the user does not see it directly.
436
+ `),
437
+ data: z
438
+ .array(z.record(z.string(), z.unknown()))
439
+ .min(1)
440
+ .describe(stringUtils.toDescription`
441
+ Tabular dataset to chart. One object per row, keyed by
442
+ column name. Values may be strings, numbers, booleans, or
443
+ null. The chart-planner decides which columns are
444
+ categories vs. numeric series. Cap at a few hundred rows
445
+ for legibility; sample / aggregate larger datasets first.
446
+ `),
447
+ });
448
+
449
+ const renderDataOutputSchema = z.object({
450
+ chartId: z.string().describe(stringUtils.toDescription`
451
+ Identifier of the queued chart. To position the chart in
452
+ your reply, embed the marker \`[[chart:<chartId>]]\` on its
453
+ own line where the chart should appear; the client renders
454
+ it inline.
455
+ `),
456
+ });
457
+
458
+ /**
459
+ * Build the `render_data` tool bound to the given plugin config.
460
+ *
461
+ * The tool is a thin wrapper around {@link emitChartWithPlanning}:
462
+ * a single `kind: "chart"` writer event ships the raw rows to
463
+ * the client immediately, the chart-planner agent runs alongside
464
+ * (so the calling LLM stays unblocked while the planner thinks),
465
+ * and a follow-up `kind: "chart"` event with the resolved
466
+ * `EChartsOption` lands when it's ready. The tool's `execute`
467
+ * awaits the planner promise so the planner work shows up under
468
+ * the tool's trace span; the LLM still gets back just
469
+ * `{ chartId }`, so its context stays small regardless of dataset
470
+ * size.
471
+ */
472
+ export function buildRenderDataTool(config: MastraPluginConfig) {
473
+ return createTool({
474
+ id: "render_data",
475
+ description: stringUtils.toDescription`
476
+ Submit a tabular dataset for inline rendering as a chart in
477
+ the user's view. Pass a title, the raw rows (array of
478
+ objects keyed by column name), and an optional one-line
479
+ description of the insight to highlight. Returns a short
480
+ \`chartId\`; the chart renders inline at the position you
481
+ embed the matching \`[[chart:<chartId>]]\` marker.
482
+
483
+ Placement contract: embed \`[[chart:<chartId>]]\` on its own
484
+ line (blank lines above and below) wherever you want the
485
+ chart to appear in your reply. The client shows a skeleton
486
+ at that spot until the chart is ready, then swaps in the
487
+ rendered Echarts visualization. You can call
488
+ \`render_data\` multiple times in the same turn (the tool
489
+ is parallel-safe) and interleave the markers with prose so
490
+ each chart sits next to its commentary. A chart whose
491
+ marker is omitted falls through to the end of your reply
492
+ as a fallback - safe but less polished.
493
+
494
+ Use whenever a SQL row set, API response, or hand-built
495
+ dataset would land better as a picture than as a list or
496
+ table. Cap input at a few hundred rows; sample or
497
+ aggregate larger datasets first.
498
+ `,
499
+ inputSchema: renderDataInputSchema,
500
+ outputSchema: renderDataOutputSchema,
501
+ execute: async (input, ctx) => {
502
+ const { title, description, data } = input as z.infer<
503
+ typeof renderDataInputSchema
504
+ >;
505
+ const writer = (ctx as { writer?: MinimalWriter } | undefined)?.writer;
506
+ const requestContext = (ctx as { requestContext?: RequestContext } | undefined)
507
+ ?.requestContext;
508
+ const { chartId, plannerPromise } = await emitChartWithPlanning({
509
+ ...(writer ? { writer } : {}),
510
+ config,
511
+ ...(requestContext ? { requestContext } : {}),
512
+ title,
513
+ ...(description ? { description } : {}),
514
+ data,
515
+ });
516
+ // Await the planner so its latency is attributed to this
517
+ // tool's trace span. The promise itself swallows planner
518
+ // failures, so this never throws.
519
+ await plannerPromise;
520
+ return { chartId };
521
+ },
522
+ });
523
+ }
524
+
525
+ /**
526
+ * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
527
+ * JSON. Centralized here so the planner agent only fills in the
528
+ * compact plan shape; tooltip / animation / color / grid defaults
529
+ * stay consistent across charts and are easy to tune without
530
+ * retraining model behaviour.
531
+ */
532
+ function planToEchartsOption(
533
+ plan: ChartPlan,
534
+ fallbackTitle: string,
535
+ ): Record<string, unknown> {
536
+ const baseTitle = plan.title ?? fallbackTitle;
537
+ const grid = { left: 48, right: 24, top: 56, bottom: 48, containLabel: true };
538
+
539
+ if (plan.chartType === "pie") {
540
+ return {
541
+ title: { text: baseTitle, left: "center" },
542
+ tooltip: { trigger: "item" },
543
+ legend: { bottom: 0 },
544
+ series: [
545
+ {
546
+ name: plan.series[0]?.name ?? baseTitle,
547
+ type: "pie",
548
+ radius: ["35%", "65%"],
549
+ data: plan.series[0]?.data ?? [],
550
+ },
551
+ ],
552
+ };
553
+ }
554
+
555
+ if (plan.chartType === "scatter") {
556
+ return {
557
+ title: { text: baseTitle, left: "center" },
558
+ tooltip: { trigger: "item" },
559
+ legend: { bottom: 0 },
560
+ grid,
561
+ xAxis: { type: "value", name: plan.xAxisLabel },
562
+ yAxis: { type: "value", name: plan.yAxisLabel },
563
+ series: plan.series.map((s) => ({
564
+ name: s.name,
565
+ type: "scatter",
566
+ data: s.data,
567
+ })),
568
+ };
569
+ }
570
+
571
+ // bar / line / area share the same axis layout.
572
+ const isArea = plan.chartType === "area";
573
+ const seriesType = plan.chartType === "bar" ? "bar" : "line";
574
+ return {
575
+ title: { text: baseTitle, left: "center" },
576
+ tooltip: { trigger: "axis" },
577
+ legend: { bottom: 0 },
578
+ grid,
579
+ xAxis: {
580
+ type: "category",
581
+ data: plan.categories ?? [],
582
+ name: plan.xAxisLabel,
583
+ },
584
+ yAxis: { type: "value", name: plan.yAxisLabel },
585
+ series: plan.series.map((s) => ({
586
+ name: s.name,
587
+ type: seriesType,
588
+ data: s.data,
589
+ smooth: seriesType === "line",
590
+ ...(isArea ? { areaStyle: {} } : {}),
591
+ })),
592
+ };
593
+ }
package/src/config.ts CHANGED
@@ -161,6 +161,19 @@ export interface MastraPluginConfig extends BasePluginConfig {
161
161
  * or to add custom endpoints in front of the public catalogue.
162
162
  */
163
163
  defaultModelFallbacks?: readonly string[];
164
+ /**
165
+ * When `true` (default), every agent gets a built-in input
166
+ * processor that strips `chartId` fields from prior assistant
167
+ * tool-invocation results before they reach the model. This
168
+ * prevents the model from reusing turn-scoped chartIds it sees
169
+ * in memory recall (which would leave `[[chart:<id>]]` markers
170
+ * pointing at writer events that no longer exist).
171
+ *
172
+ * Set to `false` to opt out - useful if a non-default agent
173
+ * needs full visibility into prior chartIds (e.g. an audit
174
+ * agent reasoning about chart lineage).
175
+ */
176
+ stripStaleCharts?: boolean;
164
177
  /**
165
178
  * Style guardrails appended to every agent's `instructions` to curb
166
179
  * common LLM-isms (em dashes, emojis, sycophantic openers, throwaway