@dbx-tools/appkit-mastra 0.1.25 → 0.1.27

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,79 @@ 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
+ `chartId` (v4 UUID), 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.embedPathTemplate}` (`/embed/chart/:id`)
258
+ by id and renders inline at the matching `[chart:<chartId>]`
259
+ marker.
260
+ - After a hard reload, the live `started` / `status` / `sql`
261
+ events are gone (they only ride the writer, not persisted
262
+ tool results); the central agent's text reply (with embedded
263
+ `[data:<statement_id>]` / `[chart:<chartId>]` markers) is
264
+ preserved in the message history and the host UI re-resolves
265
+ the markers on its own.
259
266
 
260
267
  ### `render_data` (system-default ambient tool)
261
268
 
@@ -272,73 +279,66 @@ set, an API response, a hand-built array, etc.).
272
279
 
273
280
  #### How it works
274
281
 
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.
280
-
281
- 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.
282
+ `render_data` is a thin wrapper around `prepareChart`, the same
283
+ orchestrator the Genie `prepare_chart` tool uses. It mints a
284
+ `chartId` synchronously, caches an empty placeholder, and kicks
285
+ off the chart-planner in the background. The tool returns just
286
+ `{ chartId }` so the model can embed `[chart:<chartId>]` in
287
+ prose immediately without blocking on chart generation.
288
+
289
+ 1. Mint a `chartId` (v4 UUID via `crypto.randomUUID()`).
290
+ 2. Cache an empty `{ chartId }` placeholder so the first
291
+ `fetchChart` call always sees an entry (no spurious 404 race).
292
+ 3. Fire-and-forget: resolve the dataset (trivial for
293
+ `render_data` since the rows are already in hand) and run
294
+ `runChartPlanner` in the background. On success the cache
295
+ entry settles with `{ chartId, result }` (containing the
296
+ `chartType` and full `EChartsOption`). On failure it settles
297
+ with `{ chartId, error }`.
298
+ 4. Return `{ chartId }` to the LLM immediately.
299
+
300
+ The host UI resolves `[chart:<chartId>]` markers by hitting
301
+ the plugin's generic `GET /embed/chart/:id` route, which long-polls
302
+ until the entry settles or the server-side timeout elapses.
303
303
 
304
304
  #### Inline placement contract
305
305
 
306
- The model embeds `[[chart:<chartId>]]` on its own line in its
307
- markdown reply at the position where the chart should appear:
306
+ The model embeds `[chart:<chartId>]` on its own line in its
307
+ markdown reply at the position where the chart should appear
308
+ (the `<chartId>` is the v4 UUID the tool returned):
308
309
 
309
310
  ```markdown
310
311
  ## Audit Score
311
312
 
312
313
  Audit Score is stable at ~94%, hovering between 93.5 and 95.0.
313
314
 
314
- [[chart:a3f9c1d2]]
315
+ [chart:a3f9c1d2-7b4e-4c1a-9f2d-1e6b8c0a5d31]
315
316
 
316
317
  ## Service Time
317
318
 
318
319
  Service time is the outlier at 162.5s, up from a target of 150s.
319
320
 
320
- [[chart:b7e2d4f1]]
321
+ [chart:b7e2d4f1-3a9c-4e58-8d10-2f7a4b6c9e02]
321
322
  ```
322
323
 
323
324
  The chat client splits the assistant text on these markers and
324
325
  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.
326
+ before its chart settles in the cache shows a "Queueing chart"
327
+ skeleton; a chart whose marker the model forgot to place falls
328
+ through to the end of the reply as a fallback. All three states
329
+ (queueing, rendering, rendered) share the same fixed-height
330
+ frame so the layout doesn't jump as charts resolve.
331
331
 
332
332
  #### Trade-offs
333
333
 
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.
334
+ - Chart entries live in the cache with a 1h TTL. After expiry
335
+ the `/embed/chart/:id` route returns 404. The model can call
336
+ `render_data` again on the next turn if the user wants the
337
+ chart back.
338
338
  - The chart-planner is a separate model call per dataset (fast
339
339
  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.
340
+ `max(planners)` since the planners run concurrently in the
341
+ background and the tool returns immediately.
342
342
 
343
343
  Plugins that aren't registered (or don't implement the toolkit
344
344
  interface) resolve to `undefined` at runtime, so guard with `?.` /
@@ -551,192 +551,4 @@ Same payload from a sibling plugin or script (no HTTP round-trip):
551
551
  import { appkitUtils } from "@dbx-tools/shared";
552
552
  import { mastra } from "@dbx-tools/appkit-mastra";
553
553
 
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
554
+ 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
 
@@ -178,7 +188,7 @@ export async function buildAgents(opts) {
178
188
  const style = resolveStyleInstructions(config);
179
189
  // Default-on protection against the model copying turn-scoped
180
190
  // chartIds from prior assistant tool results into the new
181
- // turn's `[[chart:<id>]]` markers. Opt out per-plugin via
191
+ // turn's `[chart:<id>]` markers. Opt out per-plugin via
182
192
  // `config.stripStaleCharts: false`.
183
193
  const inputProcessors = config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
184
194
  const agents = {};
@@ -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 } : {}),