@dbx-tools/appkit-mastra 0.1.13 → 0.1.14

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.
Files changed (59) hide show
  1. package/README.md +304 -645
  2. package/index.ts +46 -38
  3. package/package.json +58 -45
  4. package/src/agents.ts +224 -66
  5. package/src/chart.ts +531 -429
  6. package/src/config.ts +270 -19
  7. package/src/filesystems.ts +1090 -0
  8. package/src/genie.ts +1000 -660
  9. package/src/history.ts +166 -79
  10. package/src/mcp.ts +105 -0
  11. package/src/memory.ts +94 -92
  12. package/src/mlflow.ts +149 -0
  13. package/src/model.ts +75 -408
  14. package/src/observability.ts +121 -69
  15. package/src/pagination.ts +34 -0
  16. package/src/plugin.ts +552 -67
  17. package/src/processors.ts +168 -0
  18. package/src/rest.ts +67 -0
  19. package/src/server.ts +232 -45
  20. package/src/serving-sanitize.ts +167 -0
  21. package/src/serving.ts +27 -243
  22. package/src/statement.ts +89 -0
  23. package/src/storage-schema.ts +41 -0
  24. package/src/summarize.ts +176 -0
  25. package/src/threads.ts +338 -0
  26. package/src/workspaces.ts +346 -0
  27. package/src/writer.ts +44 -0
  28. package/tsconfig.json +41 -0
  29. package/dist/index.d.ts +0 -20
  30. package/dist/index.js +0 -20
  31. package/dist/src/agents.d.ts +0 -306
  32. package/dist/src/agents.js +0 -403
  33. package/dist/src/chart.d.ts +0 -170
  34. package/dist/src/chart.js +0 -491
  35. package/dist/src/config.d.ts +0 -183
  36. package/dist/src/config.js +0 -12
  37. package/dist/src/genie.d.ts +0 -131
  38. package/dist/src/genie.js +0 -630
  39. package/dist/src/history.d.ts +0 -67
  40. package/dist/src/history.js +0 -172
  41. package/dist/src/memory.d.ts +0 -100
  42. package/dist/src/memory.js +0 -242
  43. package/dist/src/model.d.ts +0 -159
  44. package/dist/src/model.js +0 -427
  45. package/dist/src/observability.d.ts +0 -33
  46. package/dist/src/observability.js +0 -71
  47. package/dist/src/plugin.d.ts +0 -130
  48. package/dist/src/plugin.js +0 -283
  49. package/dist/src/processors/strip-stale-charts.d.ts +0 -29
  50. package/dist/src/processors/strip-stale-charts.js +0 -96
  51. package/dist/src/server.d.ts +0 -46
  52. package/dist/src/server.js +0 -123
  53. package/dist/src/serving.d.ts +0 -156
  54. package/dist/src/serving.js +0 -231
  55. package/dist/src/tools/email.d.ts +0 -74
  56. package/dist/src/tools/email.js +0 -122
  57. package/dist/tsconfig.build.tsbuildinfo +0 -1
  58. package/src/processors/strip-stale-charts.ts +0 -105
  59. package/src/tools/email.ts +0 -147
package/README.md CHANGED
@@ -1,36 +1,70 @@
1
- # @dbx-tools/appkit-mastra
2
-
3
- An AppKit plugin that hosts [Mastra](https://mastra.ai) agents inside a
4
- Databricks App with user-scoped workspace auth (OBO), optional
5
- Lakebase-backed memory, and an AI SDK chat route the React client can
6
- consume with `useChat()`.
7
-
8
- The plugin is designed so that wiring it up looks the same as the
9
- AppKit
10
- `[agents](https://developers.databricks.com/docs/appkit/v0/plugins/agents)`
11
- plugin - same `createAgent` / `tool` helpers, same `tools(plugins)`
12
- callback shape, same `ToolkitOptions`. Switching between the two for a
13
- given agent is a one-line import change.
14
-
15
- ## Quick start
16
-
17
- The pattern below is the direct counterpart of AppKit's `agents` plugin
18
- example - swap `agents` for `mastra` and the imports stay structurally
19
- identical:
1
+ # @dbx-tools/node-appkit-mastra
2
+
3
+ AppKit plugin and server-side toolkit for hosting Mastra agents inside a
4
+ Databricks App.
5
+
6
+ Import this package when an AppKit backend needs an agent service with
7
+ Databricks on-behalf-of auth, optional Lakebase-backed memory, Databricks Genie
8
+ tools, model selection, chart/data embeds, MLflow feedback, and MCP exposure.
9
+ The package mounts the standard Mastra agent stream under the AppKit server, so
10
+ clients can use Mastra-compatible chat transports instead of a custom protocol.
11
+
12
+ Key features:
13
+
14
+ - AppKit plugin lifecycle integration: routes, setup, shutdown, sibling plugin
15
+ access, and AppKit request context are handled inside `plugin.mastra()`.
16
+ - Agent composition: define one or more Mastra agents, give each one local tools,
17
+ AppKit plugin toolkits, workspace skills, model defaults, and approval-gated
18
+ tools.
19
+ - Databricks execution model: tool calls run with the active AppKit OBO client
20
+ where available, while storage and background work use service-principal
21
+ connections.
22
+ - Durable conversations: Lakebase-backed Mastra storage provides thread
23
+ history, message persistence, and optional vector memory.
24
+ - Rich data answers: Genie tools, statement fetches, chart preparation, and
25
+ embed markers let an agent answer with text plus delayed chart/table payloads.
26
+ - Operational surfaces: model-list routes, feedback routes, MCP exposure,
27
+ scoped API gating, tracing, and MLflow feedback are bundled with the plugin.
28
+
29
+ ## Why Not Just AppKit Agents?
30
+
31
+ Native AppKit includes a beta Agents plugin with markdown and TypeScript agent
32
+ definitions, AppKit tool-provider integration, streaming chat, thread
33
+ management, cancellation, and HITL approval. Use it when you want the AppKit
34
+ agent model and do not need a separate agent framework.
35
+
36
+ Use this package when you specifically want Mastra inside AppKit:
37
+
38
+ - Mastra's larger plugin/tool ecosystem, MCP support, memory/storage model,
39
+ workflow primitives, and `@mastra/client-js` stream shape.
40
+ - AppKit toolkits as Mastra tools, so Analytics, Files, Genie, and other AppKit
41
+ ToolProvider plugins stay available without rewriting them.
42
+ - Genie as an agent tool that emits typed progress events, result metadata, and
43
+ delayed chart/data markers into the same assistant turn.
44
+ - A paired React client in [`@dbx-tools/ui-mastra`](../../ui/mastra) with model
45
+ picking, thread sidebar, approvals, feedback, exports, and inline embeds.
46
+ - Per-request model override and fuzzy endpoint resolution through
47
+ [`@dbx-tools/node-model`](../model), instead of binding every agent to a fixed
48
+ endpoint name.
49
+
50
+ ## Quick Start
20
51
 
21
52
  ```ts
22
- import { analytics, createApp, files, lakebase, server } from "@databricks/appkit";
23
- import { createAgent, mastra, tool } from "@dbx-tools/appkit-mastra";
53
+ import { analytics, createApp, lakebase, server } from "@databricks/appkit";
54
+ import { agents, genie, plugin } from "@dbx-tools/node-appkit-mastra";
24
55
  import { z } from "zod";
25
56
 
26
- const support = createAgent({
27
- instructions: "You help customers with data and files.",
57
+ const analyst = agents.createAgent({
58
+ name: "analyst",
59
+ instructions: ["You answer questions about workspace data.", genie.GENIE_INSTRUCTIONS].join(
60
+ "\n\n",
61
+ ),
28
62
  tools(plugins) {
29
63
  return {
30
- ...plugins.analytics.toolkit(), // every analytics tool
31
- ...plugins.files.toolkit({ only: ["uploads.read"] }), // filtered subset
32
- get_weather: tool({
33
- description: "Weather",
64
+ ...plugins.analytics.toolkit(),
65
+ ...plugins.genie?.toolkit(),
66
+ get_weather: agents.tool({
67
+ description: "Get a simple weather report.",
34
68
  schema: z.object({ city: z.string() }),
35
69
  execute: async ({ city }) => `Sunny in ${city}`,
36
70
  }),
@@ -42,694 +76,319 @@ await createApp({
42
76
  plugins: [
43
77
  server(),
44
78
  analytics(),
45
- files(),
46
- // Drop `lakebase()` in and `mastra` auto-enables per-agent thread
47
- // storage (`schemaName: "mastra_<agentId>"`) plus a shared
48
- // semantic-recall vector index. Skip it for a stateless agent.
49
79
  lakebase(),
50
- mastra({ agents: support }),
80
+ plugin.mastra({
81
+ agents: { analyst },
82
+ defaultAgent: "analyst",
83
+ genie: { spaces: { sales: "01ef..." } },
84
+ }),
51
85
  ],
52
86
  });
53
87
  ```
54
88
 
55
- `createAgent` is a no-op identity helper that anchors type inference.
56
- `tool` is the AppKit-shaped factory (`{ description, schema, execute }`)
57
- that auto-adapts to Mastra's `createTool` under the hood.
58
-
59
- Memory + storage cascades:
60
-
61
- - **No `lakebase()` registered** ▸ agent is fully stateless. No threads,
62
- no recall. Same as `mastra()` alone.
63
- - `**lakebase()` registered, no `storage` / `memory` config** ▸ both
64
- auto-turn on. Each agent gets its own `PostgresStore` schema; every
65
- agent shares one `PgVector` recall index.
66
- - **Per-agent opt-out** ▸ `createAgent({ ..., memory: false, storage: false })`
67
- for routing / one-shot agents that don't need history.
68
- - **Per-agent override** ▸ pass a `PgVectorConfig` / `PostgresStoreConfig` object on the agent for a private index or a shared external schema.
69
-
70
- See [Memory + storage](#memory--storage) for the full cascade and worked
71
- examples.
72
-
73
- On the React side, never hardcode `/api/mastra/...`. Pull the published
74
- paths from `usePluginClientConfig` and use the `chatUrl` helper. Import
75
- them from the dependency-free `@dbx-tools/appkit-mastra-shared` package
76
- so your browser bundle doesn't pull in `pg`, `fastembed`, or Mastra:
77
-
78
- ```tsx
79
- import { usePluginClientConfig } from "@databricks/appkit-ui/react";
80
- import { chatUrl, type MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
81
- import { useChat } from "@ai-sdk/react";
82
- import { DefaultChatTransport } from "ai";
83
- import { useMemo } from "react";
84
-
85
- function Chat() {
86
- const config = usePluginClientConfig<MastraClientConfig>("mastra");
87
- const transport = useMemo(
88
- () => new DefaultChatTransport({ api: chatUrl(config) }),
89
- [config],
90
- );
91
- const { messages, sendMessage } = useChat({ transport });
92
- // ...
93
- }
94
- ```
95
-
96
- See [Client wiring](#client-wiring) for the full `MastraClientConfig`
97
- shape and per-agent selection.
98
-
99
- ## Sensible defaults
100
-
101
- The plugin is opinionated about what "no config" should mean. Everything
102
- below can be overridden, but the bare path works:
103
-
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 |
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. |
117
-
89
+ Benefits of importing the package:
118
90
 
119
- Every field on `MastraAgentDefinition`:
91
+ - `plugin.mastra()` registers a full AppKit plugin named `mastra`.
92
+ - `agents.createAgent()` keeps agent definitions typed and applies the default
93
+ Databricks workspace/skill mounts.
94
+ - `agents.tool()` lets the same AppKit-shaped tool body work in this Mastra
95
+ plugin.
96
+ - `genie.GENIE_INSTRUCTIONS` and `plugins.genie.toolkit()` give agents a
97
+ Databricks Genie workflow without embedding a second agent.
98
+ - Lakebase registration automatically enables durable thread storage and vector
99
+ memory unless you opt out.
120
100
 
101
+ ## Agent Registration
121
102
 
122
- | Field | Description |
123
- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
124
- | `name` | Display name. Defaults to the registry key. |
125
- | `description` | Long-form description, surfaced as `Agent.description`. |
126
- | `instructions` | System prompt body. Required. |
127
- | `model` | Per-agent model override. String = `modelId` sugar; otherwise a Mastra `DynamicArgument`. |
128
- | `tools` | Plain record OR `(plugins) => tools` callback (see below). |
129
- | `memory` | `false`, `true`, or a `PgVectorConfig`. Cascades from `config.memory`. **Default: shared singleton `PgVector` across every agent** - object override switches to a dedicated index. |
130
- | `storage` | `false`, `true`, or a `PostgresStoreConfig`. Cascades from `config.storage`. **Default: per-agent namespace** via `schemaName: "mastra_<agentId>"` so threads stay isolated. |
131
-
132
-
133
- Plugin-level fields:
134
-
135
-
136
- | Field | Description |
137
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
138
- | `agents` | The registry. Omit for a built-in `default` analyst. |
139
- | `defaultAgent` | Id that `chatRoute` binds to when no `:agentId` is supplied. Defaults to first registered. |
140
- | `defaultModel` | Fallback for any agent that omits `model`. Same shape (string sugar or `DynamicArgument`). |
141
- | `defaultModelFallbacks` | Priority-ordered list walked when no `model` / env / override is set. First entry whose endpoint exists in the workspace wins. Default chains the three `ModelTier`s (Thinking → Balanced → Fast); within each tier providers are interleaved Claude ↔ GPT ↔ Gemini with open-weights appended. Compose your own with `modelsForTier(ModelTier.Fast)` or read straight from `MODEL_CATALOG`. |
142
- | `tools` | Ambient tools merged into every agent (per-agent tools win on collisions). |
143
- | `storage` | `undefined` (default) auto-enables when `lakebase()` is registered; `true` does the same explicitly; `false` opts out; an object opens a dedicated `PostgresStore`. Per-agent default is `schemaName: "mastra_<agentId>"`. |
144
- | `memory` | `undefined` (default) auto-enables when `lakebase()` is registered; `true` does the same explicitly; `false` opts out; an object opens a dedicated `PgVector`. Default behavior: one shared `PgVector` singleton across every agent. |
145
- | `modelFuzzyMatch` | `false` to disable fuzzy snapping of model ids against the workspace's Model Serving catalogue. Defaults to `true`. |
146
- | `modelFuzzyThreshold` | Fuse.js score threshold (`0` exact, `1` anything). Defaults to `0.4`. |
147
- | `modelCacheTtlMs` | TTL for the cached endpoint list, per workspace host. Defaults to 5 minutes. Concurrent callers share one in-flight fetch. |
148
- | `modelOverride` | `false` to disable per-request `X-Mastra-Model` / `?model=` / body overrides. Defaults to `true`. |
149
- | `styleInstructions` | Style guardrails appended to every agent's `instructions` to curb LLM-isms (em dashes, emojis, sycophantic openers, throwaway closers). `undefined` (default) uses the built-in `DEFAULT_STYLE_INSTRUCTIONS`; a string replaces it; `false` disables. |
150
-
151
-
152
- ## The `tools(plugins)` callback
153
-
154
- Each registered agent can supply either a static `tools: { ... }` record
155
- or a `tools(plugins)` callback. The returned record accepts **any**
156
- tool shape Mastra understands:
157
-
158
- - Mastra tools built with `createTool` or `new Tool(...)`
159
- - AppKit-shaped tools built with the `tool()` wrapper (below)
160
- - Vercel AI SDK tools (`tool({...})` from `ai`)
161
- - Provider-defined tools (e.g. `openai.tools.webSearch(...)`)
162
- - Toolkits returned from `plugins.<name>.toolkit(...)`
103
+ `plugin.mastra({ agents })` accepts a single definition, an array, or a record.
104
+ Records are best when clients need stable agent ids:
163
105
 
164
106
  ```ts
165
- tools(plugins) {
166
- return {
167
- // Sibling plugin toolkits.
168
- ...plugins.analytics.toolkit(), // every analytics tool
169
- ...plugins.files.toolkit({ only: ["uploads.read"] }), // filtered subset
170
-
171
- // AppKit-shaped inline tool.
172
- get_weather: tool({
173
- description: "Weather",
174
- schema: z.object({ city: z.string() }),
175
- execute: async ({ city }) => `Sunny in ${city}`,
176
- }),
177
-
178
- // Existing Mastra tool dropped in unchanged.
179
- save_doc: existingMastraTool,
180
- };
181
- }
182
- ```
183
-
184
- `plugins` is a typed `Record<string, { toolkit(opts?): tools }>` matching
185
- AppKit's `Plugins` type. It's backed by a runtime Proxy that
186
- **auto-discovers any registered AppKit `ToolProvider` plugin** -
187
- `analytics`, `files`, `lakebase`, `genie`, plus any third-party plugin
188
- that implements the standard `getAgentTools()` + `executeAgentTool()` +
189
- `toolkit()` interface. Tool calls dispatch through the plugin's
190
- `executeAgentTool`, so OBO auth (`asUser`) and telemetry spans stay
191
- intact.
192
-
193
- `plugins.genie` is special-cased: it swaps the generic AppKit toolkit
194
- (which only emits a single final result chunk per call) for a
195
- streaming-aware tools record built on top of the plugin's
196
- `exports().sendMessage` AsyncGenerator. Each Genie wire event
197
- (`FETCHING_METADATA`, `ASKING_AI`, `EXECUTING_QUERY`, attached SQL,
198
- errors) is normalised into a `GenieProgress` payload and pushed
199
- mid-flight through Mastra's `ctx.writer`, surfacing as
200
- `tool-output` chunks the React client can render as inline status
201
- pills and SQL blocks while the LLM is still waiting on the final
202
- `tool-result`. Tool ids are stable: `genie` for the default alias,
203
- `genie-<alias>` for additional aliases, and one shared
204
- `genie_get_conversation`.
205
-
206
- Genie tool-result shape (LLM-bound):
207
-
208
- ```
209
- {
210
- conversationId?: string, // pass back to continue the same Genie thread
211
- genieAnswer?: string, // Genie's prose answer; pass through verbatim
212
- datasets?: [{ // metadata only, one per executed SQL statement
213
- chartId: string, // embed [[chart:<chartId>]] to render inline
214
- title?: string,
215
- description?: string,
216
- columns: string[],
217
- rowCount: number,
218
- sql?: string,
219
- }],
220
- suggestedFollowUps?: string[], // UI shows as buttons; don't list in reply
221
- error?: string,
222
- }
107
+ plugin.mastra({
108
+ agents: {
109
+ support: agents.createAgent({ instructions: "Answer support questions." }),
110
+ analyst: agents.createAgent({ instructions: "Analyze workspace data." }),
111
+ },
112
+ defaultAgent: "support",
113
+ });
223
114
  ```
224
115
 
225
- `datasets[]` is metadata only - column names, row count, the SQL
226
- Genie ran. The actual rows ride a separate `kind: "chart"` writer
227
- event so the LLM never has them in context (token cost stays flat
228
- regardless of dataset size). The model references each dataset by
229
- its `chartId` via the `[[chart:<chartId>]]` marker to display the
230
- chart inline; see the `render_data` section below for how the
231
- client resolves those markers.
232
-
233
- Genie data flow:
234
-
235
- - The writer emits `kind: "started" | "status" | "sql" |
236
- "suggested" | "error"` events for the live loading pill. SQL
237
- text is shown via a small Shiki-highlighted block.
238
- - The writer **also** emits two `kind: "chart"` events per
239
- executed SQL statement, sharing the same `chartId`: the first
240
- carries `{chartId, title, description?, data}` (rows converted
241
- to objects keyed by column name with best-effort numeric
242
- coercion); the second, when the chart-planner agent finishes,
243
- carries `{chartId, option}` (the resolved Echarts spec). The
244
- chat client's `<ChartSlot>` merges them by `chartId` and
245
- renders inline at the matching `[[chart:<chartId>]]` marker.
246
- This is the exact same wire format as the `render_data` tool,
247
- so Genie and hand-built charts are indistinguishable on the
248
- client.
249
- - After a hard reload, `synthesizeToolEventsFromHistory` rebuilds
250
- `suggested` events from the persisted tool-result; the SQL pill
251
- and chart events are live-only and don't replay.
252
-
253
- ### `render_data` (system-default ambient tool)
254
-
255
- `buildAgents` registers a system-level `render_data` tool on
256
- every agent so the model can submit any tabular dataset for
257
- inline charting. Users can shadow it by including a same-named
258
- tool in `config.tools` or in a per-agent `tools` map; otherwise
259
- it's just there.
260
-
261
- The tool is generic - not coupled to Genie or any particular
262
- upstream. Input is `{ title, description?, data: Row[] }` where
263
- `data` is an array of objects keyed by column name (a SQL row
264
- set, an API response, a hand-built array, etc.).
265
-
266
- #### How it works (shared pipeline with Genie)
267
-
268
- Both `render_data` and Genie's `drainGenieStream` route through
269
- one helper, `emitChartWithPlanning`, so the wire format and
270
- trace shape are consistent everywhere:
271
-
272
- 1. Mint a short `chartId` (8 hex chars).
273
- 2. Push a `{ kind: "chart", chartId, title, description?, data }`
274
- event to `ctx.writer` immediately. The chat client mounts a
275
- `<ChartSlot>` showing a "Rendering chart" skeleton.
276
- 3. Kick off the chart-planner agent
277
- (`modelForTier(ModelTier.Fast)`) with the dataset in the
278
- background. The agent's `structuredOutput` schema is a compact
279
- "chart plan" (chart type + axis labels + categories +
280
- series); the helper expands the plan into an Echarts
281
- `EChartsOption` JSON.
282
- 4. When the planner resolves, push a follow-up
283
- `{ kind: "chart", chartId, option }` event. The client's
284
- `<ChartSlot>` merges it with the dataset event (last write
285
- wins per field) and swaps in `<ReactECharts>`.
286
- 5. If the planner fails, no follow-up event fires. Once the
287
- parent tool finishes, the slot transitions to a "couldn't
288
- render chart" fallback frame.
289
-
290
- The parent tool (`render_data` or Genie) `await`s the planner
291
- promise(s) before its `execute` returns, so chart latency shows
292
- up under the tool's trace span. The dataset event fires
293
- **immediately**, though, so the calling LLM gets back the
294
- `chartId` synchronously and the chat client has a layout slot
295
- ready before the planner resolves.
296
-
297
- The LLM-bound payload is just `{ chartId }` (for `render_data`)
298
- or `datasets[]` metadata (for Genie); row data and option JSON
299
- never reach the LLM, so token cost stays flat regardless of
300
- dataset size.
301
-
302
- #### Inline placement contract
303
-
304
- The model embeds `[[chart:<chartId>]]` on its own line in its
305
- markdown reply at the position where the chart should appear:
306
-
307
- ```markdown
308
- ## Audit Score
309
-
310
- Audit Score is stable at ~94%, hovering between 93.5 and 95.0.
311
-
312
- [[chart:a3f9c1d2]]
313
-
314
- ## Service Time
315
-
316
- Service time is the outlier at 162.5s, up from a target of 150s.
317
-
318
- [[chart:b7e2d4f1]]
319
- ```
116
+ When no agents are supplied, the plugin registers a built-in `default` analyst so
117
+ the route surface still works for smoke tests. Each agent is streamed through the
118
+ Mastra agent API mounted below the plugin path, typically `/api/mastra`.
320
119
 
321
- The chat client splits the assistant text on these markers and
322
- drops a `<ChartSlot>` in at each spot. A marker that arrives
323
- before its `chart` event (rare; only during fast streaming) shows
324
- a "Queueing chart" skeleton; a chart whose marker the model
325
- forgot to place falls through to the end of the reply as a
326
- fallback. All three states (queueing, rendering, rendered) share
327
- the same fixed-height frame so the layout doesn't jump as
328
- charts resolve.
329
-
330
- #### Trade-offs
331
-
332
- - Both events ride the writer, not the persisted tool-result, so
333
- charts don't re-render after a hard reload. The model can call
334
- `render_data` again (or re-ask Genie) on the next turn if the
335
- user wants the chart back.
336
- - The chart-planner is a separate model call per dataset (fast
337
- tier, but still ~1-3s each). For an N-chart turn, latency is
338
- `Genie + max(planners)` since the planners run concurrently
339
- with the rest of Genie's stream and with each other.
340
-
341
- Plugins that aren't registered (or don't implement the toolkit
342
- interface) resolve to `undefined` at runtime, so guard with `?.` /
343
- `?? {}` when a backing plugin is optional in some environments:
120
+ Use `agents.createTool` when you need Mastra-native fields such as
121
+ `outputSchema`, `suspendSchema`, `requireApproval`, or MCP metadata. Use
122
+ `agents.tool` for the smaller AppKit-compatible shape:
344
123
 
345
124
  ```ts
346
- tools(plugins) {
347
- return {
348
- ...(plugins.analytics?.toolkit() ?? {}),
349
- ...(plugins.genie?.toolkit({ prefix: "g_" }) ?? {}),
350
- };
351
- }
125
+ const approveRefund = agents.createTool({
126
+ id: "approve_refund",
127
+ description: "Approve a refund request.",
128
+ inputSchema: z.object({ orderId: z.string(), amount: z.number() }),
129
+ requireApproval: true,
130
+ execute: async ({ context }) => approve(context.orderId, context.amount),
131
+ });
352
132
  ```
353
133
 
354
- `plugins.<name>.toolkit(opts)` accepts the same `ToolkitOptions` shape
355
- AppKit's own toolkits expose (passed through verbatim):
134
+ ## AppKit Toolkits
356
135
 
357
- - `prefix?: string` - prepended to every key (AppKit default: `${pluginName}.`)
358
- - `only?: string[]` / `except?: string[]` - allow/deny list against the
359
- local tool name
360
- - `rename?: Record<string, string>` - remap individual keys
361
-
362
- ### `tool()` vs `createTool()`
363
-
364
- The `tool()` factory mirrors `@databricks/appkit/beta`'s shape so
365
- sharing tool code between the AppKit `agents` plugin and this one is a
366
- single-line import change:
136
+ The `tools(plugins)` callback receives a dynamic index of registered AppKit
137
+ tool-provider plugins. Each entry exposes `.toolkit(opts)` with AppKit-compatible
138
+ `prefix`, `only`, `except`, and `rename` options.
367
139
 
368
140
  ```ts
369
- import { tool, createTool } from "@dbx-tools/appkit-mastra";
370
-
371
- // AppKit-shaped (description / schema / flat-arg execute).
372
- const weather = tool({
373
- description: "Weather",
374
- schema: z.object({ city: z.string() }),
375
- execute: async ({ city }) => `Sunny in ${city}`,
376
- });
377
-
378
- // Full Mastra `createTool` (id required, inputSchema, advanced fields).
379
- const saveDoc = createTool({
380
- id: "save-doc",
381
- description: "Persist a document",
382
- inputSchema: z.object({ key: z.string(), value: z.any() }),
383
- outputSchema: z.object({ saved: z.boolean() }),
384
- requireApproval: true,
385
- execute: async (input, ctx) => {
386
- await ctx.mastra?.getStorage()?.set(input.key, input.value);
387
- return { saved: true };
141
+ const agent = agents.createAgent({
142
+ instructions: "Use the narrowest tool that answers the question.",
143
+ tools(plugins) {
144
+ return {
145
+ ...plugins.analytics.toolkit({ only: ["query"] }),
146
+ ...plugins.files?.toolkit({ prefix: "files.", except: ["delete"] }),
147
+ };
388
148
  },
389
149
  });
390
150
  ```
391
151
 
392
- When `tool()`'s `id` is omitted it's auto-derived from a slugified
393
- description plus a 6-char FNV-1a base-32 suffix - stable across runs
394
- so traces stay readable. Pass an explicit `id` when you want to pin
395
- one.
396
-
397
- Reach for `createTool` when you need Mastra-only fields (`outputSchema`,
398
- `suspendSchema`, `requireApproval`, `mcp`, etc.).
399
-
400
- ## Model resolution
401
-
402
- Each agent call resolves a `MastraModelConfig` lazily so concurrent
403
- requests get distinct user identities. There are two paths through
404
- the resolver depending on whether the caller asked for a specific
405
- model.
406
-
407
- ### Explicit ask (override / agent / plugin / env)
408
-
409
- When any of these is set the resolver fuzzy-matches that single id
410
- against the live `/serving-endpoints` list, in priority order:
152
+ Tool calls dispatch back through the owning AppKit plugin, preserving OBO auth
153
+ and AppKit telemetry behavior. Optional plugins should be guarded with `?.` when
154
+ you spread their tools.
411
155
 
412
- 1. Per-request override (`X-Mastra-Model` header, `?model=` query,
413
- or `model` / `modelId` body field; see below)
414
- 2. Per-agent `def.model` (string sugar or `DynamicArgument`)
415
- 3. Plugin-level `config.defaultModel`
416
- 4. `DATABRICKS_SERVING_ENDPOINT_NAME` env var
156
+ ## Memory And Storage
417
157
 
418
- The matcher is `fuse.js` extended search with tokens split on
419
- non-word characters and AND-joined. Exact matches win immediately;
420
- loose tokens like `"claude sonnet"` snap to
421
- `databricks-claude-sonnet-4-6`, `"llama 70b"` to
422
- `databricks-meta-llama-3-3-70b-instruct`, `"DBRX"` to
423
- `databricks-dbrx-instruct`, and so on. If no candidate scores below
424
- `modelFuzzyThreshold` (default `0.4`) the input is returned verbatim
425
- and Databricks surfaces the canonical 404.
158
+ The `memory` and `storage` config fields can be `false`, `true`, or a concrete
159
+ Mastra Postgres/PgVector config.
426
160
 
427
- ### No explicit ask (tier-aware fallback list)
428
-
429
- When nothing is set, the resolver walks an opinionated
430
- priority-ordered list and returns **the first id that is actually
431
- present in the workspace's endpoint listing**. This is how a workspace
432
- without Claude Opus still gets a sensible default automatically -
433
- the resolver skips ahead to whichever Sonnet / GPT-5 / Gemini / Llama
434
- variant is wired up.
435
-
436
- The catalogue is grouped two ways:
437
-
438
- - By **capability tier** via the `ModelTier` enum:
439
- `ModelTier.Thinking` (deepest reasoning), `ModelTier.Balanced`
440
- (cost/latency sweet spot), `ModelTier.Fast` (cheap & quick for
441
- classification / routing / simple summarisation).
442
- - By **provider** within each tier: `claude`, `gpt`, `gemini`,
443
- `openSource`.
444
-
445
- Both views live on `MODEL_CATALOG[tier][provider]`. The walked
446
- `FALLBACK_MODEL_IDS` chains the three tiers in descending power
447
- (Thinking → Balanced → Fast); within each tier providers are
448
- round-robin-zipped (Claude ↔ GPT ↔ Gemini) before the open-weights
449
- tail is appended as the universal floor.
161
+ ```ts
162
+ plugin.mastra({
163
+ agents: analyst,
164
+ storage: true,
165
+ memory: { id: "analytics_memory", tableName: "agent_memory" },
166
+ });
167
+ ```
450
168
 
169
+ With `lakebase()` registered, both default to enabled:
451
170
 
452
- | Tier (most powerful first) | Claude | GPT | Gemini | Open weights |
453
- | -------------------------- | -------------------------------- | ------------------------------------- | ------------------------------- | ---------------------------------------------- |
454
- | `ModelTier.Thinking` | Opus 4.8 4.7 4.6 4.5 4.1 | 5.5 Pro | 3.1 Pro → 3 Pro → 2.5 Pro | Llama 4 Maverick, GPT-OSS 120B, Llama 3.1 405B |
455
- | `ModelTier.Balanced` | Sonnet 4.6 4.5 4 | 5.5 → 5.4 → 5.2 → 5.1 → 5 | 3.5 Flash → 3 Flash → 2.5 Flash | Llama 3.3 70B, Qwen3-Next 80B, Qwen35 122B |
456
- | `ModelTier.Fast` | Haiku 4.5 | 5.4 mini → 5.4 nano → 5 mini → 5 nano | 3.1 Flash Lite | GPT-OSS 20B, Gemma 3 12B, Llama 3.1 8B |
171
+ - storage uses a per-agent schema for durable threads and messages;
172
+ - memory uses a shared vector index for semantic recall;
173
+ - the service-principal pool is created outside any request so OBO user
174
+ identities are not captured in background storage work.
457
175
 
176
+ Without `lakebase()`, agents are stateless unless you provide explicit storage
177
+ and memory configs.
458
178
 
459
- #### Pick a tier-appropriate model for one agent
179
+ ## Workspace Skills
460
180
 
461
- Use `modelForTier(tier)` to grab the top of a tier as a string; the
462
- agent-step resolver fuzzy-matches it against the live catalogue at
463
- call time so it still works when the literal top pick isn't deployed.
181
+ Every `agents.createAgent()` gets a default Mastra `Workspace` from
182
+ `workspaces.createWorkspace()`. It mounts Databricks Workspace files through the
183
+ current OBO user's `WorkspaceClient`, so Mastra can discover Assistant-style
184
+ `SKILL.md` files at request time.
464
185
 
465
186
  ```ts
466
- import { createAgent, ModelTier, modelForTier } from "@dbx-tools/appkit-mastra";
467
-
468
- const classifier = createAgent({
469
- instructions: "Classify this email into one of: billing, support, spam.",
470
- model: modelForTier(ModelTier.Fast),
471
- });
472
-
473
- const planner = createAgent({
474
- instructions: "Plan a multi-step data migration.",
475
- model: modelForTier(ModelTier.Thinking),
187
+ const agent = agents.createAgent({
188
+ instructions: "Use mounted workspace skills when relevant.",
189
+ workspace: workspaces.createWorkspace({
190
+ assistantSkills: true,
191
+ mounts: [
192
+ async () => ({
193
+ mounts: { "/reference": myFilesystem },
194
+ skillPaths: ["/reference/skills"],
195
+ }),
196
+ ],
197
+ }),
476
198
  });
477
199
  ```
478
200
 
479
- #### Bias the plugin-level fallback toward a tier
201
+ Production workspace mounts require a forwarded token with `workspace`,
202
+ `workspace.workspace`, or `all-apis` scope. Development mode skips that gate for
203
+ local iteration.
480
204
 
481
- `modelsForTier(tier)` returns the priority-ordered list for one tier;
482
- pass it to `defaultModelFallbacks` to scope the auto-resolver:
205
+ ## Genie Tools
483
206
 
484
- ```ts
485
- import { mastra, ModelTier, modelsForTier } from "@dbx-tools/appkit-mastra";
207
+ `genie.buildGenieTools()` and `plugins.genie.toolkit()` expose tools for:
486
208
 
487
- mastra({
488
- // All agents that omit `model` will land on a Fast-tier endpoint.
489
- defaultModelFallbacks: modelsForTier(ModelTier.Fast),
490
- });
491
- ```
492
-
493
- #### Pin a custom approved subset
209
+ - asking a configured Genie space;
210
+ - reading space descriptions and serialized space metadata;
211
+ - fetching statement rows by `statement_id`;
212
+ - preparing charts from Genie result sets.
494
213
 
495
- Mix in your own endpoint names (internal fine-tunes, regulated
496
- allowlists, etc) in front of the catalogue:
214
+ The central agent drives those tools directly. Genie events stream through the
215
+ Mastra writer using the shared contract from
216
+ [`@dbx-tools/shared-mastra`](../../shared/mastra), so clients can show thinking,
217
+ SQL, row counts, summaries, chart markers, and data markers as the turn runs.
497
218
 
498
219
  ```ts
499
- mastra({
500
- defaultModelFallbacks: [
501
- "my-org-finetune-v2", // try internal endpoint first
502
- "databricks-claude-sonnet-4-6", // approved fallback
503
- ],
220
+ const agent = agents.createAgent({
221
+ instructions: `${baseInstructions}\n\n${genie.GENIE_INSTRUCTIONS}`,
222
+ tools(plugins) {
223
+ return { ...plugins.genie?.toolkit({ prefix: "" }) };
224
+ },
504
225
  });
505
226
  ```
506
227
 
507
- If the workspace has none of the listed ids, the top fallback is
508
- returned and Databricks surfaces the canonical error.
509
-
510
- The endpoint list is cached per workspace host through AppKit's
511
- built-in `CacheManager` (`CacheManager.getInstanceSync().getOrExecute`),
512
- which is the TypeScript counterpart of Python's `cachetools.TTLCache`
513
- plus `cachetools-async` rolled into one: per-entry TTL (default 5
514
- minutes via `modelCacheTtlMs`), bounded size, in-flight request
515
- coalescing (the manager's internal `inFlightRequests` map shares one
516
- fetch across every concurrent caller), telemetry spans, and optional
517
- Lakebase persistence when the `lakebase` plugin is wired up. No extra
518
- dependency lives in this package; the catalogue piggybacks on whatever
519
- storage backend AppKit picked at boot.
520
-
521
- String values (`"databricks-claude-sonnet-4-6"`) are `modelId` sugar
522
- layered on top of the auto-resolver - workspace URL, provider, and OBO
523
- auth stay default. Pass a `DynamicArgument<MastraModelConfig>` on
524
- `def.model` / `config.defaultModel` when you need full control over
525
- auth, provider, or URL; that path bypasses the fuzzy matcher and
526
- per-request override.
527
-
528
- ### `GET /api/mastra/models`
529
-
530
- The plugin exposes the cached endpoint catalogue at `/models` (mounted
531
- under the plugin prefix, default `/api/mastra`) so clients can populate
532
- model pickers and validate `?model=` choices without a separate
533
- Databricks SDK round-trip:
534
-
535
- ```bash
536
- curl -s http://localhost:8000/api/mastra/models | jq
537
- # {
538
- # "endpoints": [
539
- # { "name": "databricks-claude-sonnet-4-6", "task": "llm/v1/chat", "state": "READY", ... },
540
- # { "name": "databricks-meta-llama-3-3-70b-instruct", ... },
541
- # ...
542
- # ]
543
- # }
544
- ```
228
+ ## Charts And Data Embeds
545
229
 
546
- Same payload from a sibling plugin or script (no HTTP round-trip):
230
+ `chart.prepareChart()` mints a chart id immediately, caches an in-progress
231
+ record, resolves the data in the background, and stores a terminal chart or
232
+ error. `chart.fetchChart()` long-polls that cache for route handlers and custom
233
+ clients.
547
234
 
548
235
  ```ts
549
- import { pluginUtils } from "@dbx-tools/appkit-shared";
550
- import { mastra } from "@dbx-tools/appkit-mastra";
236
+ const { chartId } = await chart.prepareChart({
237
+ config,
238
+ request: {
239
+ title: "Revenue by region",
240
+ chartType: "bar",
241
+ instructions: "Compare total revenue by region.",
242
+ data: rows,
243
+ },
244
+ resolveData: async () => rows,
245
+ });
551
246
 
552
- const m = pluginUtils.require(this.context, mastra).exports();
553
- const endpoints = await m.asUser(req).listModels(); // user-scoped
554
- m.clearModelsCache(); // force the next call to re-fetch
247
+ const resolved = await chart.fetchChart(chartId);
555
248
  ```
556
249
 
557
- ### Per-request model override
250
+ Agents can return `[chart:<id>]` and `[data:<statement_id>]` markers in prose.
251
+ The embed route resolves them later, which avoids forcing the language model to
252
+ inline large tables or wait for chart planning before continuing its answer.
558
253
 
559
- Any in-flight request can pick a different backing endpoint without
560
- redeploying. Sources, checked in priority order:
254
+ ## Model Selection
561
255
 
256
+ `model.buildModel()` adapts the generic resolver from
257
+ [`@dbx-tools/node-model`](../model) to Mastra. It resolves the model per request,
258
+ so OBO identity and request-specific overrides stay isolated.
562
259
 
563
- | Source | Example |
564
- | ------------------------- | ------------------------------------------------ |
565
- | `X-Mastra-Model` header | `curl -H 'X-Mastra-Model: claude-haiku' ...` |
566
- | `?model=` query parameter | `POST /api/mastra/route/chat?model=llama-70b` |
567
- | Body `model` or `modelId` | `{ "messages": [...], "model": "claude-haiku" }` |
260
+ Model priority is:
568
261
 
262
+ 1. request override (`X-Mastra-Model`, `?model=`, body `model` / `modelId`);
263
+ 2. per-agent `model`;
264
+ 3. plugin `defaultModel`;
265
+ 4. `DATABRICKS_SERVING_ENDPOINT_NAME`;
266
+ 5. workspace catalogue ranking and static fallback floor.
569
267
 
570
- The override flows through the same fuzzy matcher as static ids, so
571
- `X-Mastra-Model: claude sonnet` still snaps to
572
- `databricks-claude-sonnet-4-6`. Set `modelOverride: false` on the
573
- plugin config to disable the override path entirely (e.g. for a
574
- multi-tenant deployment where untrusted clients shouldn't pick the
575
- endpoint).
268
+ ```ts
269
+ plugin.mastra({
270
+ agents: analyst,
271
+ defaultModel: "claude sonnet",
272
+ modelFuzzyMatch: true,
273
+ modelOverride: true,
274
+ });
275
+ ```
276
+
277
+ Use `serving.extractModelOverride()` and `serving.resolveServingConfig()` when
278
+ building custom routes that should behave like the plugin's `/models` and stream
279
+ routes.
576
280
 
577
- ## Memory + storage
281
+ ## Threads, History, And Suggestions
578
282
 
579
- Memory and storage are split into two independent knobs and both auto-on
580
- the moment the `lakebase` plugin is registered. Bare `mastra()` next to
581
- `lakebase()` already gets you per-agent threads + shared semantic recall;
582
- zero extra config required.
283
+ When storage is enabled, the plugin provides route helpers and in-process
284
+ functions for conversation management:
583
285
 
286
+ - `history.loadHistory()` and `history.clearHistory()` read or clear one thread;
287
+ - `threads.listThreads()`, `threads.renameThread()`, and
288
+ `threads.deleteThread()` operate on the caller's scoped conversations;
289
+ - `genie.collectSpaceSuggestions()` reads starter questions from the configured
290
+ Genie space.
584
291
 
585
- | Knob | Default when `lakebase()` is registered | What it backs |
586
- | --------- | ----------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- |
587
- | `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()`). |
588
- | `memory` | **Shared singleton** `PgVector` across every agent (cross-agent semantic recall on one index). | RAG-style recall over past messages via FastEmbed vectors. |
292
+ The plugin resolves the active thread from `x-mastra-thread-id`, `?threadId=`,
293
+ or a per-session fallback cookie. That keeps streaming, history, and clear
294
+ operations aligned around the same conversation id.
589
295
 
296
+ ## Feedback And Observability
590
297
 
591
- Override either at the plugin level, the agent level, or both. The agent
592
- value wins when set; otherwise the plugin value cascades.
298
+ `observability.buildObservability()` wires Mastra tracing when OTLP export is
299
+ configured. `mlflow.resolveFeedbackEnabled()` turns MLflow feedback on when both
300
+ trace export and an MLflow experiment are configured, unless the plugin config
301
+ forces a value.
593
302
 
594
303
  ```ts
595
- mastra({
596
- // Plugin defaults. Either field becomes the cascading baseline.
597
- // Omit entirely to inherit "auto-on when lakebase is present".
598
- storage: true, // (default behavior when lakebase is registered)
599
- memory: true, // (default behavior when lakebase is registered)
304
+ plugin.mastra({
305
+ agents: analyst,
306
+ feedback: true,
307
+ });
308
+ ```
600
309
 
601
- agents: {
602
- analyst: createAgent({
603
- instructions: "...",
604
- // No overrides: inherits the auto-on defaults above.
605
- // - threads stored under schema "mastra_analyst"
606
- // - recalls from the shared vector index
607
- }),
310
+ `mlflow.logFeedback()` logs a human assessment against the active MLflow trace.
311
+ The response header name and request/response schemas live in
312
+ [`@dbx-tools/shared-mastra`](../../shared/mastra).
608
313
 
609
- router: createAgent({
610
- instructions: "Stateless routing agent.",
611
- // Opt out of both for a fully stateless agent.
612
- storage: false,
613
- memory: false,
614
- }),
314
+ ## MCP Exposure
615
315
 
616
- legal: createAgent({
617
- instructions: "Compliance-bounded assistant.",
618
- // Private vector index so legal's recall doesn't bleed into
619
- // analyst's. Threads still get their own per-agent schema.
620
- memory: { connectionString: process.env.LEGAL_PG_URL!, /* ... */ },
621
- }),
316
+ `mcp.buildMcpServer()` exposes registered agents as MCP tools by default. The
317
+ AppKit plugin publishes clean aliases under its base path:
622
318
 
623
- archive: createAgent({
624
- instructions: "Read-only archive viewer.",
625
- // Pin to a specific schema (e.g. shared with another service).
626
- storage: {
627
- schemaName: "shared_history",
628
- pool: archivePool,
629
- },
630
- }),
319
+ ```ts
320
+ plugin.mastra({
321
+ agents: analyst,
322
+ mcp: {
323
+ serverId: "analytics",
324
+ name: "Analytics MCP",
325
+ tools: false,
631
326
  },
632
327
  });
633
328
  ```
634
329
 
635
- Notes:
636
-
637
- - `PostgresStore` runs `CREATE SCHEMA IF NOT EXISTS` on `init()`, so
638
- per-agent schemas (and the shared `mastra_instance` schema) spring
639
- into existence the first time the agent saves anything. No bundle /
640
- migration step required.
641
- - The `mastra_instance` schema exists so that Mastra-instance-level
642
- artifacts (workflow snapshots used by `agent.resumeStream()`,
643
- `requireApproval` flows, etc.) live in their own namespace and never
644
- collide with per-agent thread / message tables. Disable the instance
645
- store by setting `storage: false` at plugin level - approval-gated
646
- tool calls will then error on resume.
647
- - Disabling `lakebase()` from your plugin list while leaving `storage` /
648
- `memory` truthy fails fast at setup with a clear "lakebase plugin not
649
- registered" error.
650
- - The `lakebase` plugin is declared as a **required** resource only when
651
- `storage` / `memory` is explicitly truthy at registration time. Auto-on
652
- defaults activate inside `setup:complete`, after lakebase is already
653
- proven to be present.
654
-
655
- ## Runtime exports
656
-
657
- Other plugins / route handlers can introspect the registry via the
658
- `exports()` surface, modeled on AppKit's:
659
-
660
- ```ts
661
- import { pluginUtils } from "@dbx-tools/appkit-shared";
662
- import { mastra } from "@dbx-tools/appkit-mastra";
663
-
664
- const m = pluginUtils.require(this.context, mastra).exports();
665
- m.list(); // ["analyst", "helper"]
666
- m.get("analyst"); // Agent | null
667
- m.getDefault(); // Agent | null
668
- m.getMastra(); // underlying Mastra instance (advanced)
669
- m.listModels(); // Promise<ServingEndpointSummary[]> - cached + OBO when wrapped with asUser(req)
670
- m.clearModelsCache(); // force the next listModels() to re-fetch
671
- ```
672
-
673
- ## Client wiring
674
-
675
- `clientConfig()` publishes the mount paths, default agent id, and the
676
- full registry to `usePluginClientConfig("mastra")` so the React client
677
- never has to hardcode `/api/mastra` or rely on `DEFAULT_AGENT_ID`
678
- constants. A tiny URL helper (`chatUrl`) and the `MastraClientConfig`
679
- type ship from the standalone `@dbx-tools/appkit-mastra-shared`
680
- package; that package is pure (no `pg` / `fastembed` / Mastra
681
- dependencies) so it imports cleanly into Vite / Webpack / esbuild
682
- builds.
683
-
684
- ```tsx
685
- import { usePluginClientConfig } from "@databricks/appkit-ui/react";
686
- import { chatUrl, type MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
687
- import { useChat } from "@ai-sdk/react";
688
- import { DefaultChatTransport } from "ai";
689
- import { useMemo, useState } from "react";
690
-
691
- function Chat() {
692
- const config = usePluginClientConfig<MastraClientConfig>("mastra");
693
- const [selected, setSelected] = useState<string>();
694
- const api = chatUrl(config, selected); // defaults to config.defaultAgent
695
-
696
- const transport = useMemo(() => new DefaultChatTransport({ api }), [api]);
697
- const { messages, sendMessage } = useChat({ transport });
698
-
699
- return (
700
- <>
701
- <select onChange={(e) => setSelected(e.target.value)}>
702
- {config.agents.map((id) => (
703
- <option key={id} value={id}>
704
- {id}
705
- </option>
706
- ))}
707
- </select>
708
- {/* render messages, etc. */}
709
- </>
710
- );
711
- }
712
- ```
713
-
714
- `MastraClientConfig` fields (all derived from the server-side plugin
715
- mount, so a custom `mastra({ name: "myMastra" })` rewrites every path):
716
-
717
-
718
- | Field | Example | Description |
719
- | ------------------ | ----------------------------------- | -------------------------------------------------------- |
720
- | `basePath` | `"/api/mastra"` | Plugin mount path. |
721
- | `chatPath` | `"/api/mastra/route/chat"` | Default-agent chat URL. Use `chatUrl(config)` to get it. |
722
- | `chatPathTemplate` | `"/api/mastra/route/chat/:agentId"` | OpenAPI-style template for tools / docs. |
723
- | `modelsPath` | `"/api/mastra/models"` | `GET` cached endpoint catalogue. |
724
- | `defaultAgent` | `"analyst"` | Agent id `chatRoute` binds to when none is supplied. |
725
- | `agents` | `["analyst", "helper"]` | Every registered agent id in order. |
726
-
727
-
728
- `chatUrl(config, agentId?)` returns `config.chatPath` for the default
729
- agent (the registered `chatRoute` mount that omits `:agentId`), and
730
- `${config.chatPath}/${encodeURIComponent(agentId)}` otherwise. Pure
731
- function: no React, no hooks, safe in service workers and SSR.
732
-
733
- ## License
734
-
735
- Apache-2.0
330
+ Use `mcp: false` to disable MCP. Turn on `tools: true` only for ambient tools
331
+ that are safe outside an in-process chat turn.
332
+
333
+ ## API Gate
334
+
335
+ The stock `@mastra/express` app has broad management routes. The plugin's
336
+ default `apiAccess: "scoped"` allows only the chat, read-only metadata,
337
+ plugin-owned `/route/*`, embed, model, suggestion, and MCP surfaces that the
338
+ client needs. Use `apiAccess: "full"` only for a trusted first-party console.
339
+
340
+ `server.isMastraRequestAllowed()` is exported for tests and custom dispatch
341
+ logic that need the same allowlist.
342
+
343
+ ## Configuration Reference
344
+
345
+ The plugin config is intentionally centered on the AppKit lifecycle instead of
346
+ requiring callers to assemble a Mastra server by hand.
347
+
348
+ - `agents` registers a single agent, an array, or a record keyed by stable agent
349
+ ids. Records are best for UIs because the ids become route-visible.
350
+ - `defaultAgent` controls which registered agent handles requests that do not
351
+ name an agent explicitly.
352
+ - `storage` and `memory` accept `true`, `false`, or concrete Mastra Postgres /
353
+ PgVector options. `true` resolves from `lakebase()` when present.
354
+ - `genie.spaces` maps aliases to Genie Space IDs. Those aliases flow into tools,
355
+ suggestions, and chart/data workflows.
356
+ - `defaultModel`, `modelOverride`, and `modelFuzzyMatch` control how loose model
357
+ names are resolved through Databricks Model Serving.
358
+ - `feedback` controls whether MLflow feedback routes are exposed. The automatic
359
+ mode enables feedback when tracing and an MLflow experiment are configured.
360
+ - `mcp` controls whether agents are exposed as MCP tools and how that server is
361
+ named.
362
+ - `apiAccess` chooses the route allowlist. Keep the default scoped mode for
363
+ deployed apps.
364
+
365
+ Use this package when you want an AppKit-native agent runtime. Use the shared
366
+ schemas in [`@dbx-tools/shared-mastra`](../../shared/mastra) when building a
367
+ client that talks to these routes.
368
+
369
+ ## Modules
370
+
371
+ - `plugin` - `MastraPlugin` and `mastra()` AppKit plugin factory.
372
+ - `agents` - `createAgent`, `tool`, `createTool`, agent build helpers, fallback
373
+ defaults, and approval-gated tool inspection.
374
+ - `config` - plugin config types and RequestContext key constants.
375
+ - `model` / `serving` / `servingSanitize` - Mastra model config, request
376
+ overrides, serving-endpoint config, and request-body cleanup.
377
+ - `genie` - Genie prompt, space normalization, Genie toolkits, and suggestions.
378
+ - `chart` / `statement` / `writer` - chart cache, statement row fetches, and
379
+ safe writer events.
380
+ - `history` / `threads` / `pagination` - conversation persistence helpers and
381
+ route handlers.
382
+ - `memory` / `storageSchema` - Lakebase-backed Mastra store/vector setup.
383
+ - `workspaces` / `filesystems` - Mastra workspace creation and Databricks
384
+ Workspace file adapters.
385
+ - `mcp` - MCP server construction.
386
+ - `observability` / `mlflow` - tracing and feedback.
387
+ - `server` / `rest` / `processors` - Express dispatch, Databricks REST helpers,
388
+ stream/result processors.
389
+
390
+ Browser-facing wire types are in
391
+ [`@dbx-tools/shared-mastra`](../../shared/mastra). Genie event contracts are in
392
+ [`@dbx-tools/shared-genie`](../../shared/genie). Model request/result contracts
393
+ are in [`@dbx-tools/shared-model`](../../shared/model). The matching React chat
394
+ surface is [`@dbx-tools/ui-mastra`](../../ui/mastra).