@dbx-tools/appkit-mastra 0.1.25 → 0.1.26

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/README.md CHANGED
@@ -102,18 +102,18 @@ The plugin is opinionated about what "no config" should mean. Everything
102
102
  below can be overridden, but the bare path works:
103
103
 
104
104
 
105
- | Scenario | What you get |
106
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
- | `mastra()` | One built-in `default` analyst agent, model from `/serving-endpoints`; memory + storage auto-on if the `lakebase` plugin is registered |
108
- | `mastra({ agents: def })` | Single-agent shorthand - `def` is registered and marked as default |
109
- | `mastra({ agents: [def1, def2] })` | Array shorthand - keys come from each `def.name` (or `agent_<i>`); first one is default |
110
- | `mastra({ agents: { x: def, y: def }})` | Record - keys are the registered ids; first key is the default |
111
- | No `defaultAgent` set | First registered agent wins |
105
+ | Scenario | What you get |
106
+ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
107
+ | `mastra()` | One built-in `default` analyst agent, model from `/serving-endpoints`; memory + storage auto-on if the `lakebase` plugin is registered |
108
+ | `mastra({ agents: def })` | Single-agent shorthand - `def` is registered and marked as default |
109
+ | `mastra({ agents: [def1, def2] })` | Array shorthand - keys come from each `def.name` (or `agent_<i>`); first one is default |
110
+ | `mastra({ agents: { x: def, y: def }})` | Record - keys are the registered ids; first key is the default |
111
+ | No `defaultAgent` set | First registered agent wins |
112
112
  | No `model` on an agent | Falls back to `config.defaultModel`, then `DATABRICKS_SERVING_ENDPOINT_NAME`, then walks `defaultModelFallbacks` (Thinking → Balanced → Fast tiers, Claude ↔ GPT ↔ Gemini interleaved within each, then open-weights) and picks the first endpoint actually present in the workspace |
113
- | No `name` on a definition | Uses the registry key as `Agent.name` |
114
- | No `tools` on an agent | Inherits plugin-level `config.tools` ambient set (if any) |
115
- | No `storage` / `memory` and `lakebase()` registered | Both auto-default to `true`. Pass `false` (or a custom config object) on the plugin or an agent to opt out / override. |
116
- | `storage` / `memory` on an agent | Cascades from plugin: storage namespaces **per-agent** (`schemaName: "mastra_<agentId>"`), vector recall is **shared** across agents on one `PgVector` singleton. |
113
+ | No `name` on a definition | Uses the registry key as `Agent.name` |
114
+ | No `tools` on an agent | Inherits plugin-level `config.tools` ambient set (if any) |
115
+ | No `storage` / `memory` and `lakebase()` registered | Both auto-default to `true`. Pass `false` (or a custom config object) on the plugin or an agent to opt out / override. |
116
+ | `storage` / `memory` on an agent | Cascades from plugin: storage namespaces **per-agent** (`schemaName: "mastra_<agentId>"`), vector recall is **shared** across agents on one `PgVector` singleton. |
117
117
 
118
118
 
119
119
  Every field on `MastraAgentDefinition`:
@@ -190,72 +190,78 @@ that implements the standard `getAgentTools()` + `executeAgentTool()` +
190
190
  `executeAgentTool`, so OBO auth (`asUser`) and telemetry spans stay
191
191
  intact.
192
192
 
193
- `plugins.genie` is special-cased: it talks to Genie directly via
194
- `@dbx-tools/genie` (`genieEventChat`) rather than calling AppKit's
195
- stock `genie` toolkit. Each invocation spins up a per-call inner
196
- Mastra `Agent` with three tools (`ask_genie`,
197
- `get_space_description`, `get_space_serialized`), runs it with
198
- `structuredOutput`, and produces a hydrated
199
- {@link GenieAgentResult}. AppKit's `genie()` plugin is honored
200
- only for its `spaces` config (and the matching
201
- `app.yaml`-declared resources). Tool ids are stable: `genie` for
202
- the default alias, `genie_<alias>` for additional aliases.
203
-
204
- Genie tool-result shape (LLM-bound) is the
205
- [`GenieAgentResult`](../appkit-mastra-shared/src/genie.ts) type:
193
+ `plugins.genie` is special-cased: it returns a flat set of
194
+ Mastra tools the central agent drives directly (no inner
195
+ orchestrator agent). Two shared, space-agnostic tools register
196
+ once regardless of how many spaces are wired:
197
+
198
+ - `get_statement(statement_id, limit?)` - fetch rows for a
199
+ Genie statement. Use only when the agent needs to read values
200
+ to reason about them; otherwise embed `[data:<statement_id>]`.
201
+ - `prepare_chart({statement_id, title?, description?})` - mint a
202
+ short `chartId`, kick off a background chart-planner task,
203
+ cache the resolved Echarts spec under the id for one hour.
204
+ Returns `{chartId}` synchronously so the agent can embed
205
+ `[chart:<chartId>]` in prose without blocking.
206
+
207
+ Three per-space tools register per wired alias (`ask_genie`,
208
+ `get_space_description`, `get_space_serialized` for the
209
+ `default` alias; `ask_genie_<alias>`, etc. for additional
210
+ aliases):
211
+
212
+ - `ask_genie({question})` - drives one `genieEventChat` turn
213
+ and streams wire events (status / thinking / sql) through
214
+ `ctx.writer`. Returns `{message: GenieMessage}`; rows are
215
+ NOT fetched eagerly.
216
+ - `get_space_description()` - cheap title / description /
217
+ warehouse id lookup.
218
+ - `get_space_serialized()` - full `GenieSpace` JSON for
219
+ column-level grounding when the description isn't enough.
220
+
221
+ AppKit's `genie()` plugin is honored only for its `spaces`
222
+ config (and the matching `app.yaml`-declared resources). The
223
+ tools talk to Genie directly via `@dbx-tools/genie`
224
+ (`genieEventChat`) and the workspace
225
+ `statementExecution.getStatement` API.
226
+
227
+ The orchestration prompt (decompose, ask Genie focused
228
+ sub-questions, place markers in prose) ships as the exported
229
+ `GENIE_INSTRUCTIONS` string - compose it into your agent's
230
+ `instructions` to get the canonical behavior:
206
231
 
207
232
  ```ts
208
- type GenieAgentResult = {
209
- spaceId: string;
210
- conversationId?: string; // thread back to continue the same Genie thread
211
- summary: Array< // ordered prose + visualize slots
212
- | { type: "string"; text: string }
213
- | {
214
- type: "visualize";
215
- statementId: string;
216
- title?: string;
217
- description?: string;
218
- dataset: {
219
- data: { columns: string[]; rows: Row[]; rowCount: number };
220
- chart?: { chartId: string; chartType: "bar" | "line" | "area" | "scatter" | "pie" };
221
- };
222
- }
223
- >;
224
- error?: string;
225
- };
233
+ import { createAgent, GENIE_INSTRUCTIONS } from "@dbx-tools/appkit-mastra";
234
+
235
+ const support = createAgent({
236
+ instructions: `${baseInstructions}\n\n${GENIE_INSTRUCTIONS}`,
237
+ tools(plugins) {
238
+ return { ...plugins.genie?.toolkit() };
239
+ },
240
+ });
226
241
  ```
227
242
 
228
- The `summary` is the user-facing renderable: a mixed sequence of
229
- prose paragraphs (`type: "string"`) and `visualize` slots that
230
- mark where a chart should appear in the model's final answer.
231
- Visualize slots embed the dataset (rows + columns) plus a slim
232
- `chart` reference (just `chartId` + `chartType`); the resolved
233
- Echarts spec rides a separate `type: "chart"` writer event so the
234
- LLM never holds it in context. The host UI joins the dataset and
235
- the writer event by `chartId` and renders inline at the matching
236
- `[[chart:<chartId>]]` marker.
243
+ `GENIE_INSTRUCTIONS` references the default (`default` alias)
244
+ tool names. Multi-space deployments should write a custom
245
+ variant that names the suffixed per-space tools
246
+ (e.g. `ask_genie_sales`).
237
247
 
238
248
  Genie writer event flow:
239
249
 
240
- - The writer emits the flat
241
- [`GenieWriterEvent`](../appkit-mastra-shared/src/genie.ts)
242
- union for the live loading pill - the wire-derived
243
- `GenieChatEvent` (status / thinking / sql / rows / suggested)
244
- plus the Mastra-only lifecycle events (`started`,
245
- `ask_genie_done`, `summary`, `chart`, `error`).
246
- - The writer emits one `type: "chart"` event per executed SQL
247
- statement carrying `{chartId, title, description?, data,
248
- option, statementId, messageId}`. The chat client's
249
- `<ChartSlot>` renders inline at the matching
250
- `[[chart:<chartId>]]` marker. This is the exact same wire
251
- format as the `render_data` tool, so Genie and hand-built
252
- charts are indistinguishable on the client.
253
- - After a hard reload, `synthesizeToolEventsFromHistory`
254
- reconstructs lifecycle events from the persisted summary
255
- (`error` events via `genieResultToWriterEvents`); live-only
256
- events (rows, SQL pill, chart specs) don't replay because the
257
- resolved Echarts spec is held off-band on the per-request
258
- `RequestContext`.
250
+ - Each `ask_genie` call emits a Mastra-only `started` event
251
+ before any network round-trip, then forwards every wire
252
+ `GenieChatEvent` (status / thinking / sql / rows / suggested
253
+ / message / result / error) through `ctx.writer`.
254
+ - Charts ride out-of-band: `prepare_chart` mints a `chartId`
255
+ synchronously, the planner runs in the background and writes
256
+ the spec to the chart cache (1h TTL). The host UI long-polls
257
+ `${MastraClientConfig.chartsPathTemplate}` by id and renders
258
+ inline at the matching `[chart:<chartId>]` marker.
259
+ - After a hard reload, the live `started` / `status` / `sql`
260
+ events are gone (they only ride the writer, not persisted
261
+ tool results); the central agent's text reply (with embedded
262
+ `[data:<statement_id>]` / `[chart:<chartId>]` markers) is
263
+ preserved in the message history and the host UI re-resolves
264
+ the markers on its own.
259
265
 
260
266
  ### `render_data` (system-default ambient tool)
261
267
 
@@ -272,34 +278,27 @@ set, an API response, a hand-built array, etc.).
272
278
 
273
279
  #### How it works
274
280
 
275
- `render_data` is a thin wrapper around `runChartPlanner`, a
276
- pure async function that takes a dataset and returns a fully-
277
- resolved Echarts `EChartsOption`. No id-then-promise dance, no
278
- background work: the planner runs inline, and the tool emits a
279
- single chart event with everything attached.
281
+ `render_data` is a thin wrapper around `prepareChart`, the same
282
+ orchestrator the Genie `prepare_chart` tool uses. It mints a
283
+ `chartId` synchronously, caches an empty placeholder, and kicks
284
+ off the chart-planner in the background. The tool returns just
285
+ `{ chartId }` so the model can embed `[chart:<chartId>]` in
286
+ prose immediately without blocking on chart generation.
280
287
 
281
288
  1. Mint a short `chartId` (8 hex chars).
282
- 2. `await runChartPlanner({ title, description?, data })`. The
283
- planner is its own Mastra `Agent` (`modelForTier(ModelTier.Fast)`)
284
- whose `structuredOutput` schema is a compact "chart plan"
285
- (chart type + axis labels + categories + series); the helper
286
- expands the plan into an `EChartsOption` JSON.
287
- 3. Push one `{ type: "chart", chartId, title, description?, data,
288
- option }` event to `ctx.writer`. The chat client's
289
- `<ChartSlot>` mounts straight to the rendered Echarts
290
- visualisation - no skeleton frame, because the option arrives
291
- in the same event as the dataset.
292
- 4. If the planner throws, the tool emits a `type: "error"` writer
293
- event instead. The slot transitions to a "couldn't render
294
- chart" fallback frame.
295
-
296
- Planner latency lands under the tool's trace span (it's
297
- `await`ed directly), and the LLM-bound payload is just
298
- `{ chartId }` so the model's context stays flat regardless of
299
- dataset size. The Genie agent reuses `runChartPlanner` the
300
- same way to embed charts in its structured summary, so the
301
- backbone is shared without coupling the two paths to a
302
- fire-and-forget writer contract.
289
+ 2. Cache an empty `{ chartId }` placeholder so the first
290
+ `fetchChart` call always sees an entry (no spurious 404 race).
291
+ 3. Fire-and-forget: resolve the dataset (trivial for
292
+ `render_data` since the rows are already in hand) and run
293
+ `runChartPlanner` in the background. On success the cache
294
+ entry settles with `{ chartId, result }` (containing the
295
+ `chartType` and full `EChartsOption`). On failure it settles
296
+ with `{ chartId, error }`.
297
+ 4. Return `{ chartId }` to the LLM immediately.
298
+
299
+ The host UI resolves `[chart:<chartId>]` markers by hitting
300
+ the plugin's `GET /charts/:chartId` route, which long-polls
301
+ until the entry settles or the server-side timeout elapses.
303
302
 
304
303
  #### Inline placement contract
305
304
 
@@ -322,23 +321,22 @@ Service time is the outlier at 162.5s, up from a target of 150s.
322
321
 
323
322
  The chat client splits the assistant text on these markers and
324
323
  drops a `<ChartSlot>` in at each spot. A marker that arrives
325
- before its `chart` event (rare; only during fast streaming) shows
326
- a "Queueing chart" skeleton; a chart whose marker the model
327
- forgot to place falls through to the end of the reply as a
328
- fallback. All three states (queueing, rendering, rendered) share
329
- the same fixed-height frame so the layout doesn't jump as
330
- charts resolve.
324
+ before its chart settles in the cache shows a "Queueing chart"
325
+ skeleton; a chart whose marker the model forgot to place falls
326
+ through to the end of the reply as a fallback. All three states
327
+ (queueing, rendering, rendered) share the same fixed-height
328
+ frame so the layout doesn't jump as charts resolve.
331
329
 
332
330
  #### Trade-offs
333
331
 
334
- - Both events ride the writer, not the persisted tool-result, so
335
- charts don't re-render after a hard reload. The model can call
336
- `render_data` again (or re-ask Genie) on the next turn if the
337
- user wants the chart back.
332
+ - Chart entries live in the cache with a 1h TTL. After expiry
333
+ the `/charts/:chartId` route returns 404. The model can call
334
+ `render_data` again on the next turn if the user wants the
335
+ chart back.
338
336
  - The chart-planner is a separate model call per dataset (fast
339
337
  tier, but still ~1-3s each). For an N-chart turn, latency is
340
- `Genie + max(planners)` since the planners run concurrently
341
- with the rest of Genie's stream and with each other.
338
+ `max(planners)` since the planners run concurrently in the
339
+ background and the tool returns immediately.
342
340
 
343
341
  Plugins that aren't registered (or don't implement the toolkit
344
342
  interface) resolve to `undefined` at runtime, so guard with `?.` /
@@ -551,192 +549,4 @@ Same payload from a sibling plugin or script (no HTTP round-trip):
551
549
  import { appkitUtils } from "@dbx-tools/shared";
552
550
  import { mastra } from "@dbx-tools/appkit-mastra";
553
551
 
554
- const m = appkitUtils.require(this.context, mastra).exports();
555
- const endpoints = await m.asUser(req).listModels(); // user-scoped
556
- m.clearModelsCache(); // force the next call to re-fetch
557
- ```
558
-
559
- ### Per-request model override
560
-
561
- Any in-flight request can pick a different backing endpoint without
562
- redeploying. Sources, checked in priority order:
563
-
564
-
565
- | Source | Example |
566
- | ------------------------- | ------------------------------------------------ |
567
- | `X-Mastra-Model` header | `curl -H 'X-Mastra-Model: claude-haiku' ...` |
568
- | `?model=` query parameter | `POST /api/mastra/route/chat?model=llama-70b` |
569
- | Body `model` or `modelId` | `{ "messages": [...], "model": "claude-haiku" }` |
570
-
571
-
572
- The override flows through the same fuzzy matcher as static ids, so
573
- `X-Mastra-Model: claude sonnet` still snaps to
574
- `databricks-claude-sonnet-4-6`. Set `modelOverride: false` on the
575
- plugin config to disable the override path entirely (e.g. for a
576
- multi-tenant deployment where untrusted clients shouldn't pick the
577
- endpoint).
578
-
579
- ## Memory + storage
580
-
581
- Memory and storage are split into two independent knobs and both auto-on
582
- the moment the `lakebase` plugin is registered. Bare `mastra()` next to
583
- `lakebase()` already gets you per-agent threads + shared semantic recall;
584
- zero extra config required.
585
-
586
-
587
- | Knob | Default when `lakebase()` is registered | What it backs |
588
- | --------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
589
- | `storage` | **Per-agent** `PostgresStore` namespaced by `schemaName: "mastra_<agentId>"` so threads + messages stay isolated, plus a Mastra-instance-level `PostgresStore` in schema `mastra_instance`. | Mastra threads, messages, working memory; Mastra-instance-level workflow snapshots (`requireApproval` / `agent.resumeStream()`). |
590
- | `memory` | **Shared singleton** `PgVector` across every agent (cross-agent semantic recall on one index). | RAG-style recall over past messages via FastEmbed vectors. |
591
-
592
-
593
- Override either at the plugin level, the agent level, or both. The agent
594
- value wins when set; otherwise the plugin value cascades.
595
-
596
- ```ts
597
- mastra({
598
- // Plugin defaults. Either field becomes the cascading baseline.
599
- // Omit entirely to inherit "auto-on when lakebase is present".
600
- storage: true, // (default behavior when lakebase is registered)
601
- memory: true, // (default behavior when lakebase is registered)
602
-
603
- agents: {
604
- analyst: createAgent({
605
- instructions: "...",
606
- // No overrides: inherits the auto-on defaults above.
607
- // - threads stored under schema "mastra_analyst"
608
- // - recalls from the shared vector index
609
- }),
610
-
611
- router: createAgent({
612
- instructions: "Stateless routing agent.",
613
- // Opt out of both for a fully stateless agent.
614
- storage: false,
615
- memory: false,
616
- }),
617
-
618
- legal: createAgent({
619
- instructions: "Compliance-bounded assistant.",
620
- // Private vector index so legal's recall doesn't bleed into
621
- // analyst's. Threads still get their own per-agent schema.
622
- memory: { connectionString: process.env.LEGAL_PG_URL!, /* ... */ },
623
- }),
624
-
625
- archive: createAgent({
626
- instructions: "Read-only archive viewer.",
627
- // Pin to a specific schema (e.g. shared with another service).
628
- storage: {
629
- schemaName: "shared_history",
630
- pool: archivePool,
631
- },
632
- }),
633
- },
634
- });
635
- ```
636
-
637
- Notes:
638
-
639
- - `PostgresStore` runs `CREATE SCHEMA IF NOT EXISTS` on `init()`, so
640
- per-agent schemas (and the shared `mastra_instance` schema) spring
641
- into existence the first time the agent saves anything. No bundle /
642
- migration step required.
643
- - The `mastra_instance` schema exists so that Mastra-instance-level
644
- artifacts (workflow snapshots used by `agent.resumeStream()`,
645
- `requireApproval` flows, etc.) live in their own namespace and never
646
- collide with per-agent thread / message tables. Disable the instance
647
- store by setting `storage: false` at plugin level - approval-gated
648
- tool calls will then error on resume.
649
- - Disabling `lakebase()` from your plugin list while leaving `storage` /
650
- `memory` truthy fails fast at setup with a clear "lakebase plugin not
651
- registered" error.
652
- - The `lakebase` plugin is declared as a **required** resource only when
653
- `storage` / `memory` is explicitly truthy at registration time. Auto-on
654
- defaults activate inside `setup:complete`, after lakebase is already
655
- proven to be present.
656
-
657
- ## Runtime exports
658
-
659
- Other plugins / route handlers can introspect the registry via the
660
- `exports()` surface, modeled on AppKit's:
661
-
662
- ```ts
663
- import { appkitUtils } from "@dbx-tools/shared";
664
- import { mastra } from "@dbx-tools/appkit-mastra";
665
-
666
- const m = appkitUtils.require(this.context, mastra).exports();
667
- m.list(); // ["analyst", "helper"]
668
- m.get("analyst"); // Agent | null
669
- m.getDefault(); // Agent | null
670
- m.getMastra(); // underlying Mastra instance (advanced)
671
- m.listModels(); // Promise<ServingEndpointSummary[]> - cached + OBO when wrapped with asUser(req)
672
- m.clearModelsCache(); // force the next listModels() to re-fetch
673
- ```
674
-
675
- ## Client wiring
676
-
677
- `clientConfig()` publishes the mount paths, default agent id, and the
678
- full registry to `usePluginClientConfig("mastra")` so the React client
679
- never has to hardcode `/api/mastra` or rely on `DEFAULT_AGENT_ID`
680
- constants. A tiny URL helper (`chatUrl`) and the `MastraClientConfig`
681
- type ship from the standalone `@dbx-tools/appkit-mastra-shared`
682
- package; that package is pure (no `pg` / `fastembed` / Mastra
683
- dependencies) so it imports cleanly into Vite / Webpack / esbuild
684
- builds.
685
-
686
- ```tsx
687
- import { usePluginClientConfig } from "@databricks/appkit-ui/react";
688
- import { chatUrl, type MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
689
- import { useChat } from "@ai-sdk/react";
690
- import { DefaultChatTransport } from "ai";
691
- import { useMemo, useState } from "react";
692
-
693
- function Chat() {
694
- const config = usePluginClientConfig<MastraClientConfig>("mastra");
695
- const [selected, setSelected] = useState<string>();
696
- const api = chatUrl(config, selected); // defaults to config.defaultAgent
697
-
698
- const transport = useMemo(() => new DefaultChatTransport({ api }), [api]);
699
- const { messages, sendMessage } = useChat({ transport });
700
-
701
- return (
702
- <>
703
- <select onChange={(e) => setSelected(e.target.value)}>
704
- {config.agents.map((id) => (
705
- <option key={id} value={id}>
706
- {id}
707
- </option>
708
- ))}
709
- </select>
710
- {/* render messages, etc. */}
711
- </>
712
- );
713
- }
714
- ```
715
-
716
- `MastraClientConfig` fields (all derived from the server-side plugin
717
- mount, so a custom `mastra({ name: "myMastra" })` rewrites every path):
718
-
719
-
720
- | Field | Example | Description |
721
- | --------------------- | -------------------------------------- | -------------------------------------------------------------------- |
722
- | `basePath` | `"/api/mastra"` | Plugin mount path. |
723
- | `chatPath` | `"/api/mastra/route/chat"` | Default-agent chat URL. Use `chatUrl(config)` to get it. |
724
- | `chatPathTemplate` | `"/api/mastra/route/chat/:agentId"` | OpenAPI-style template for tools / docs. |
725
- | `modelsPath` | `"/api/mastra/models"` | `GET` cached endpoint catalogue. |
726
- | `historyPath` | `"/api/mastra/route/history"` | Default-agent thread history. Use `historyUrl(config, opts)`. |
727
- | `historyPathTemplate` | `"/api/mastra/route/history/:agentId"` | OpenAPI-style template for tools / docs. |
728
- | `defaultAgent` | `"analyst"` | Agent id `chatRoute` binds to when none is supplied. |
729
- | `agents` | `["analyst", "helper"]` | Every registered agent id in order. |
730
-
731
-
732
- `chatUrl(config, agentId?)` returns `config.chatPath` for the default
733
- agent (the registered `chatRoute` mount that omits `:agentId`), and
734
- `${config.chatPath}/${encodeURIComponent(agentId)}` otherwise. The
735
- sibling `historyUrl(config, { agentId?, page?, perPage? })` mirrors
736
- that pattern for the `/history` endpoint and appends pagination
737
- query parameters when provided. Both are pure functions: no React,
738
- no hooks, safe in service workers and SSR.
739
-
740
- ## License
741
-
742
- Apache-2.0
552
+ const m = appkitUtils.require(this.context, ma
@@ -274,6 +274,16 @@ export interface BuiltAgents {
274
274
  }
275
275
  /** Fallback agent id used when `config.agents` is omitted entirely. */
276
276
  export declare const FALLBACK_AGENT_ID = "default";
277
+ /**
278
+ * Default per-turn step ceiling applied to every registered agent
279
+ * when {@link MastraPluginConfig.agentMaxSteps} is unset. Sized to
280
+ * fit a decomposed Genie turn (grounding + several `ask_genie`
281
+ * calls + `prepare_chart` per dataset + the final-text reply) with
282
+ * headroom for the model to chain a couple of follow-ups before
283
+ * answering - well above Mastra's own `agent.generate` default of
284
+ * 5, which would cut multi-step orchestration off mid-loop.
285
+ */
286
+ export declare const DEFAULT_AGENT_MAX_STEPS = 25;
277
287
  /**
278
288
  * Style guardrails appended to every agent's `instructions` to curb
279
289
  * common LLM-isms (em dashes, emojis, sycophantic openers, excessive
@@ -15,10 +15,10 @@
15
15
  import { appkitUtils, logUtils, stringUtils } from "@dbx-tools/shared";
16
16
  import { Agent } from "@mastra/core/agent";
17
17
  import { createTool } from "@mastra/core/tools";
18
- import { buildRenderDataTool } from "./chart.js";
19
18
  import { buildGenieToolkitProvider, resolveGenieSpaces } from "./genie.js";
20
19
  import { buildModel, FALLBACK_MODEL_IDS } from "./model.js";
21
20
  import { stripStaleChartsProcessor } from "./processors/strip-stale-charts.js";
21
+ import { buildRenderDataTool } from "./chart.js";
22
22
  /** Re-export of Mastra's native `createTool` for full-feature access. */
23
23
  export { createTool } from "@mastra/core/tools";
24
24
  /**
@@ -90,6 +90,16 @@ export function createAgent(def) {
90
90
  }
91
91
  /** Fallback agent id used when `config.agents` is omitted entirely. */
92
92
  export const FALLBACK_AGENT_ID = "default";
93
+ /**
94
+ * Default per-turn step ceiling applied to every registered agent
95
+ * when {@link MastraPluginConfig.agentMaxSteps} is unset. Sized to
96
+ * fit a decomposed Genie turn (grounding + several `ask_genie`
97
+ * calls + `prepare_chart` per dataset + the final-text reply) with
98
+ * headroom for the model to chain a couple of follow-ups before
99
+ * answering - well above Mastra's own `agent.generate` default of
100
+ * 5, which would cut multi-step orchestration off mid-loop.
101
+ */
102
+ export const DEFAULT_AGENT_MAX_STEPS = 25;
93
103
  const FALLBACK_AGENT_INSTRUCTIONS = `You are a data analyst. The user will ask questions about
94
104
  business metrics and may share personal preferences you should remember across turns.
95
105
 
@@ -191,6 +201,9 @@ export async function buildAgents(opts) {
191
201
  ...(def.description !== undefined ? { description: def.description } : {}),
192
202
  instructions: composeInstructions(def.instructions, style),
193
203
  model: resolveModel(config, def.model),
204
+ defaultOptions: {
205
+ maxSteps: config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS,
206
+ },
194
207
  tools,
195
208
  ...(memory ? { memory } : {}),
196
209
  ...(inputProcessors.length > 0 ? { inputProcessors } : {}),