@dbx-tools/appkit-mastra 0.1.5 → 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/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from "./src/config.js";
15
15
  export * from "./src/agents.js";
16
16
  export * from "./src/chart.js";
17
17
  export * from "./src/genie.js";
18
+ export * from "./src/tools/email.js";
18
19
  export {
19
20
  clearServingEndpointsCache,
20
21
  extractModelOverride,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
- "main": "dist/index.js",
3
- "types": "dist/index.d.ts",
2
+ "main": "./dist/index.js",
3
+ "types": "./dist/index.d.ts",
4
4
  "exports": {
5
5
  ".": {
6
6
  "source": "./index.ts",
@@ -8,32 +8,13 @@
8
8
  "default": "./dist/index.js"
9
9
  }
10
10
  },
11
- "files": [
12
- "dist",
13
- "index.ts",
14
- "src"
15
- ],
16
- "license": "Apache-2.0",
17
- "homepage": "https://github.com/reggie-db/dbx-tools-appkit#readme",
18
- "bugs": {
19
- "url": "https://github.com/reggie-db/dbx-tools-appkit/issues"
20
- },
21
- "publishConfig": {
22
- "registry": "https://registry.npmjs.org/",
23
- "access": "public"
24
- },
25
- "repository": {
26
- "type": "git",
27
- "url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
28
- "directory": "packages/mastra"
29
- },
30
11
  "name": "@dbx-tools/appkit-mastra",
31
- "version": "0.1.5",
32
- "module": "index.ts",
12
+ "version": "0.1.12",
33
13
  "type": "module",
14
+ "module": "index.ts",
34
15
  "dependencies": {
35
- "@dbx-tools/appkit-mastra-shared": "0.1.5",
36
- "@dbx-tools/appkit-shared": "0.1.5",
16
+ "@dbx-tools/appkit-mastra-shared": "0.1.12",
17
+ "@dbx-tools/appkit-shared": "0.1.12",
37
18
  "@mastra/ai-sdk": "^1.3",
38
19
  "@mastra/core": "^1.32",
39
20
  "@mastra/express": "^1.3",
@@ -51,5 +32,20 @@
51
32
  "@types/express": "^5",
52
33
  "@types/pg": "^8",
53
34
  "express": "^5"
35
+ },
36
+ "files": [
37
+ "dist",
38
+ "index*.ts",
39
+ "src"
40
+ ],
41
+ "license": "Apache-2.0",
42
+ "homepage": "https://github.com/reggie-db/dbx-tools-appkit#readme",
43
+ "bugs": {
44
+ "url": "https://github.com/reggie-db/dbx-tools-appkit/issues"
45
+ },
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/reggie-db/dbx-tools-appkit.git",
49
+ "directory": "packages/mastra"
54
50
  }
55
51
  }
package/src/agents.ts CHANGED
@@ -26,6 +26,7 @@ import type { MastraPluginConfig } from "./config.js";
26
26
  import { buildGenieProvider } from "./genie.js";
27
27
  import type { MemoryBuilder } from "./memory.js";
28
28
  import { buildModel } from "./model.js";
29
+ import { stripStaleChartsProcessor } from "./processors/strip-stale-charts.js";
29
30
 
30
31
  /**
31
32
  * Tool record accepted by every Mastra `Agent.tools` field and by the
@@ -122,8 +123,8 @@ export interface AppKitToolOptions {
122
123
  /**
123
124
  * Build a deterministic Mastra tool id from a description.
124
125
  * Delegates to {@link stringUtils.toUniqueSlug}: slug + always-on
125
- * SHA-1 suffix so two tools with the same leading words don't
126
- * collide in traces. Stable across runs.
126
+ * 6-char FNV-1a base-32 suffix so two tools with the same leading
127
+ * words don't collide in traces. Stable across runs.
127
128
  */
128
129
  function deriveToolId(description: string): string {
129
130
  return stringUtils.toUniqueSlug(description, { fallbackPrefix: "tool" });
@@ -407,7 +408,7 @@ export async function buildAgents(opts: {
407
408
  const ids = Object.keys(definitions);
408
409
  const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
409
410
 
410
- const plugins = buildPluginsMap(context);
411
+ const plugins = buildPluginsMap(config, context);
411
412
  // System-default ambient tools every agent gets out of the box.
412
413
  // Currently just `render_data` for inline visualizations; the
413
414
  // user can shadow it by including a same-named tool in their own
@@ -418,6 +419,12 @@ export async function buildAgents(opts: {
418
419
  };
419
420
  const ambientTools = { ...systemTools, ...(config.tools ?? {}) };
420
421
  const style = resolveStyleInstructions(config);
422
+ // Default-on protection against the model copying turn-scoped
423
+ // chartIds from prior assistant tool results into the new
424
+ // turn's `[[chart:<id>]]` markers. Opt out per-plugin via
425
+ // `config.stripStaleCharts: false`.
426
+ const inputProcessors =
427
+ config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
421
428
  const agents: Record<string, Agent> = {};
422
429
 
423
430
  for (const [id, def] of Object.entries(definitions)) {
@@ -431,6 +438,7 @@ export async function buildAgents(opts: {
431
438
  model: resolveModel(config, def.model),
432
439
  tools,
433
440
  ...(memory ? { memory } : {}),
441
+ ...(inputProcessors.length > 0 ? { inputProcessors } : {}),
434
442
  });
435
443
  }
436
444
 
@@ -581,6 +589,7 @@ async function resolveTools(
581
589
  * time instead of staring at a spinner for the full Genie round-trip.
582
590
  */
583
591
  function buildPluginsMap(
592
+ config: MastraPluginConfig,
584
593
  context: pluginUtils.PluginContextLike | undefined,
585
594
  ): MastraPlugins {
586
595
  const cache = new Map<string, MastraPluginToolkitProvider | null>();
@@ -588,7 +597,7 @@ function buildPluginsMap(
588
597
  get(_target, propName) {
589
598
  if (typeof propName !== "string") return undefined;
590
599
  if (cache.has(propName)) return cache.get(propName) ?? undefined;
591
- const provider = resolveProvider(context, propName);
600
+ const provider = resolveProvider(config, context, propName);
592
601
  cache.set(propName, provider);
593
602
  return provider ?? undefined;
594
603
  },
@@ -599,16 +608,20 @@ function buildPluginsMap(
599
608
  * Pick the right {@link MastraPluginToolkitProvider} for a sibling
600
609
  * plugin lookup. Returns the streaming-aware Genie adapter when the
601
610
  * caller asks for `genie`; falls back to the generic AppKit
602
- * `ToolProvider` adapter for every other plugin name.
611
+ * `ToolProvider` adapter for every other plugin name. `config` is
612
+ * threaded through so Genie's tool can run the chart planner
613
+ * inline against the same model resolver / fallback ladder the
614
+ * agents use.
603
615
  */
604
616
  function resolveProvider(
617
+ config: MastraPluginConfig,
605
618
  context: pluginUtils.PluginContextLike | undefined,
606
619
  propName: string,
607
620
  ): MastraPluginToolkitProvider | null {
608
621
  if (propName === "genie") {
609
622
  const geniePlugin = pluginUtils.instance(context, genie);
610
623
  if (!geniePlugin) return null;
611
- return buildGenieProvider(geniePlugin) as MastraPluginToolkitProvider;
624
+ return buildGenieProvider(geniePlugin, { config }) as MastraPluginToolkitProvider;
612
625
  }
613
626
  const plugin = context?.getPlugins().get(propName);
614
627
  return adaptPluginToolkit(plugin);
package/src/chart.ts CHANGED
@@ -1,36 +1,42 @@
1
1
  /**
2
2
  * Chart-rendering primitives.
3
3
  *
4
- * Two surfaces, one shared brain:
4
+ * Three surfaces, one shared brain:
5
5
  *
6
6
  * - {@link buildRenderDataTool}: a Mastra tool the model calls
7
- * ("here is a dataset, render it as a chart"). The tool is
8
- * fire-and-forget by design - it generates a short `chartId`,
9
- * pushes a single `kind: "chart"` event onto `ctx.writer` carrying
10
- * the raw rows, and returns the id to the model immediately. No
11
- * chart planning happens inside the agentic loop, so the model
12
- * never blocks on a downstream LLM call to get its identifier.
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.
13
21
  *
14
22
  * - {@link runChartPlanner}: the chart-planner Agent + ECOption
15
- * expansion as a plain async function. The HTTP route in
16
- * {@link ./render-chart-route.ts} calls this when the client
17
- * POSTs the dataset back; the result is an `EChartsOption` JSON
18
- * the React `<ChartSlot>` renders inline. Decoupling the planner
19
- * from the tool means the planning latency lives entirely
20
- * client-side: the model can finish writing its report while
21
- * the client is still rendering the charts.
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.
22
27
  *
23
28
  * The model wires the chart into its reply by emitting the marker
24
29
  * `[[chart:<chartId>]]` on its own line in markdown. The chat
25
30
  * client splits the assistant text on these markers and drops a
26
31
  * `<ChartSlot>` in at the position the model placed it; the slot
27
- * then fires the render-chart endpoint on mount and shows a
28
- * skeleton until the option lands.
32
+ * shows a skeleton until the second `kind: "chart"` event (with
33
+ * the resolved `EChartsOption`) arrives, then swaps in the
34
+ * rendered Echarts visualisation.
29
35
  */
30
36
 
31
37
  import { randomUUID } from "node:crypto";
32
38
 
33
- import { stringUtils } from "@dbx-tools/appkit-shared";
39
+ import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
34
40
  import { Agent } from "@mastra/core/agent";
35
41
  import type { RequestContext } from "@mastra/core/request-context";
36
42
  import { createTool } from "@mastra/core/tools";
@@ -39,6 +45,15 @@ import { z } from "zod";
39
45
  import type { MastraPluginConfig } from "./config.js";
40
46
  import { ModelTier, modelForTier, buildModel } from "./model.js";
41
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
+
42
57
  /**
43
58
  * Compact, model-friendly representation of an Echarts spec. The
44
59
  * planner agent emits this; {@link planToEchartsOption} expands it
@@ -211,10 +226,10 @@ function getPlannerAgent(config: MastraPluginConfig): Agent {
211
226
 
212
227
  /**
213
228
  * Run the chart planner against the given dataset and return a
214
- * full Echarts `EChartsOption` JSON. Used by the HTTP route the
215
- * client hits when it sees a `[[chart:<chartId>]]` marker; the
216
- * tool itself does not call this so the model never blocks on
217
- * planning latency.
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).
218
233
  */
219
234
  export async function runChartPlanner(
220
235
  opts: RunChartPlannerOptions,
@@ -243,6 +258,170 @@ export async function runChartPlanner(
243
258
  return { option, chartType: plan.chartType };
244
259
  }
245
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
+
246
425
  const renderDataInputSchema = z.object({
247
426
  title: z.string().describe(stringUtils.toDescription`
248
427
  Title shown above the rendered chart. Use a concise
@@ -269,31 +448,28 @@ const renderDataInputSchema = z.object({
269
448
 
270
449
  const renderDataOutputSchema = z.object({
271
450
  chartId: z.string().describe(stringUtils.toDescription`
272
- Identifier of the queued chart. The tool returned
273
- immediately - actual chart planning happens client-side
274
- asynchronously. To position the chart in your reply, embed
275
- the marker \`[[chart:<chartId>]]\` on its own line (with
276
- blank lines above and below) where the chart should appear.
277
- The client renders a skeleton there until the chart is
278
- ready, then swaps in the visualization in place. You can
279
- keep writing prose around the marker; the agent does not
280
- need to wait for the chart to render.
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.
281
455
  `),
282
456
  });
283
457
 
284
458
  /**
285
459
  * Build the `render_data` tool bound to the given plugin config.
286
460
  *
287
- * Fire-and-forget by design: the tool returns immediately with a
288
- * short `chartId` and emits a single `kind: "chart"` event over
289
- * `ctx.writer` carrying the raw dataset for the client. The
290
- * client's chart slot then POSTs the data to
291
- * `/route/render-chart` to get an `EChartsOption` back from the
292
- * planner agent. This keeps the calling LLM unblocked - it can
293
- * write the report referencing the chart by id while the client
294
- * is still rendering it.
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.
295
471
  */
296
- export function buildRenderDataTool(_config: MastraPluginConfig) {
472
+ export function buildRenderDataTool(config: MastraPluginConfig) {
297
473
  return createTool({
298
474
  id: "render_data",
299
475
  description: stringUtils.toDescription`
@@ -301,9 +477,8 @@ export function buildRenderDataTool(_config: MastraPluginConfig) {
301
477
  the user's view. Pass a title, the raw rows (array of
302
478
  objects keyed by column name), and an optional one-line
303
479
  description of the insight to highlight. Returns a short
304
- \`chartId\` immediately - chart planning happens
305
- asynchronously in the client, not in this turn, so the tool
306
- does not block your prose.
480
+ \`chartId\`; the chart renders inline at the position you
481
+ embed the matching \`[[chart:<chartId>]]\` marker.
307
482
 
308
483
  Placement contract: embed \`[[chart:<chartId>]]\` on its own
309
484
  line (blank lines above and below) wherever you want the
@@ -327,28 +502,21 @@ export function buildRenderDataTool(_config: MastraPluginConfig) {
327
502
  const { title, description, data } = input as z.infer<
328
503
  typeof renderDataInputSchema
329
504
  >;
330
-
331
- // Short, marker-friendly id. The LLM has to type this
332
- // verbatim into the `[[chart:<id>]]` marker; an 8-hex-char
333
- // prefix is unique within a single assistant turn (collision
334
- // odds ~1 in 4 billion) and much less error-prone for the
335
- // model to reproduce.
336
- const chartId = randomUUID().replace(/-/g, "").slice(0, 8);
337
-
338
- const writer = (ctx as { writer?: { write: (e: unknown) => unknown } } | undefined)
339
- ?.writer;
340
- try {
341
- await writer?.write({
342
- kind: "chart",
343
- chartId,
344
- title,
345
- ...(description ? { description } : {}),
346
- data,
347
- });
348
- } catch {
349
- // Ignore: the parent stream may have closed downstream.
350
- }
351
-
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;
352
520
  return { chartId };
353
521
  },
354
522
  });
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