@dbx-tools/appkit-mastra 0.1.42 → 0.1.48

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
@@ -3,20 +3,22 @@
3
3
  An AppKit plugin that hosts [Mastra](https://mastra.ai) agents inside a
4
4
  Databricks App with user-scoped workspace auth (OBO), optional
5
5
  Lakebase-backed memory, and the standard Mastra agent stream the React
6
- client drives via `@mastra/client-js` (`getAgent(id).stream()`).
6
+ client drives via `@mastra/client-js`.
7
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)`
8
+ Wiring it up looks the same as the AppKit
9
+ [`agents`](https://developers.databricks.com/docs/appkit/v0/plugins/agents)
11
10
  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.
11
+ callback, same `ToolkitOptions`. Switching a given agent between the two
12
+ is a one-line import change.
14
13
 
15
- ## Quick start
14
+ The implementation lives under `src/`: agent registration in
15
+ [`agents.ts`](src/agents.ts), the plugin + routes in
16
+ [`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), Model
17
+ Serving resolution in [`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts),
18
+ Genie tooling in [`genie.ts`](src/genie.ts), and the chart pipeline in
19
+ [`chart.ts`](src/chart.ts).
16
20
 
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:
21
+ ## Quick start
20
22
 
21
23
  ```ts
22
24
  import { analytics, createApp, files, lakebase, server } from "@databricks/appkit";
@@ -44,34 +46,21 @@ await createApp({
44
46
  analytics(),
45
47
  files(),
46
48
  // 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
+ // storage plus shared semantic recall. Skip it for a stateless agent.
49
50
  lakebase(),
50
51
  mastra({ agents: support }),
51
52
  ],
52
53
  });
53
54
  ```
54
55
 
55
- `createAgent` is a no-op identity helper that anchors type inference.
56
+ `createAgent` is a no-op identity helper that anchors type inference;
56
57
  `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.
58
+ that adapts to Mastra's `createTool` under the hood. The full config
59
+ shapes (`MastraAgentDefinition`, the plugin config) - every field and
60
+ default - are typed in [`config.ts`](src/config.ts).
72
61
 
73
62
  On the React side, drop in the prebuilt chat UI from
74
- [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui) - it wires itself
63
+ [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui); it wires itself
75
64
  from the plugin's published client config and streams over
76
65
  `@mastra/client-js`, so there's no transport code to write:
77
66
 
@@ -83,473 +72,144 @@ export default function ChatPage() {
83
72
  }
84
73
  ```
85
74
 
86
- Under the hood that's `useMastraClient()` -> a `MastraPluginClient`
87
- (a `@mastra/client-js` `MastraClient` subclass) that streams turns and
88
- adds the plugin's custom routes (history, models, suggestions, embeds).
89
- Never hardcode `/api/mastra/...`; the client derives every URL from the
90
- `basePath` published in `clientConfig()`.
91
-
92
- See [Client wiring](#client-wiring) for the full `MastraClientConfig`
93
- shape and per-agent selection.
94
-
95
- ## Sensible defaults
96
-
97
- The plugin is opinionated about what "no config" should mean. Everything
98
- below can be overridden, but the bare path works:
99
-
75
+ ## Memory + storage
100
76
 
101
- | Scenario | What you get |
102
- | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
103
- | `mastra()` | One built-in `default` analyst agent, model from `/serving-endpoints`; memory + storage auto-on if the `lakebase` plugin is registered |
104
- | `mastra({ agents: def })` | Single-agent shorthand - `def` is registered and marked as default |
105
- | `mastra({ agents: [def1, def2] })` | Array shorthand - keys come from each `def.name` (or `agent_<i>`); first one is default |
106
- | `mastra({ agents: { x: def, y: def }})` | Record - keys are the registered ids; first key is the default |
107
- | No `defaultAgent` set | First registered agent wins |
108
- | 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 |
109
- | No `name` on a definition | Uses the registry key as `Agent.name` |
110
- | No `tools` on an agent | Inherits plugin-level `config.tools` ambient set (if any) |
111
- | 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. |
112
- | `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. |
77
+ With no config, memory and storage are driven entirely by whether the
78
+ `lakebase` plugin is registered:
113
79
 
80
+ - **No `lakebase()`** - the agent is fully stateless: no threads, no
81
+ recall.
82
+ - **`lakebase()` registered** - both auto-enable. Each agent gets its
83
+ own `PostgresStore` schema (threads stay isolated per agent); every
84
+ agent shares one `PgVector` semantic-recall index.
114
85
 
115
- Every field on `MastraAgentDefinition`:
116
-
117
-
118
- | Field | Description |
119
- | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
120
- | `name` | Display name. Defaults to the registry key. |
121
- | `description` | Long-form description, surfaced as `Agent.description`. |
122
- | `instructions` | System prompt body. Required. |
123
- | `model` | Per-agent model override. String = `modelId` sugar; otherwise a Mastra `DynamicArgument`. |
124
- | `tools` | Plain record OR `(plugins) => tools` callback (see below). |
125
- | `memory` | `false`, `true`, or a `PgVectorConfig`. Cascades from `config.memory`. **Default: shared singleton `PgVector` across every agent** - object override switches to a dedicated index. |
126
- | `storage` | `false`, `true`, or a `PostgresStoreConfig`. Cascades from `config.storage`. **Default: per-agent namespace** via `schemaName: "mastra_<agentId>"` so threads stay isolated. |
127
-
128
-
129
- Plugin-level fields:
130
-
131
-
132
- | Field | Description |
133
- | ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
134
- | `agents` | The registry. Omit for a built-in `default` analyst. |
135
- | `defaultAgent` | Id the client talks to when no `:agentId` is supplied. Defaults to first registered. |
136
- | `defaultModel` | Fallback for any agent that omits `model`. Same shape (string sugar or `DynamicArgument`). |
137
- | `defaultModelFallbacks` | Priority-ordered list tried first when no `model` / env / override is set, ahead of the dynamic score-classified catalogue. First entry whose endpoint exists in the workspace wins. When unset, resolution is driven by the live Foundation Model API scores (see [Model resolution](#model-resolution)); set this to pin a regulated workspace to an approved subset. Compose one with `modelsForTier(ModelTier.Fast)`. |
138
- | `tools` | Ambient tools merged into every agent (per-agent tools win on collisions). |
139
- | `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>"`. |
140
- | `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. |
141
- | `modelFuzzyMatch` | `false` to disable fuzzy snapping of model ids against the workspace's Model Serving catalogue. Defaults to `true`. |
142
- | `modelFuzzyThreshold` | Fuse.js score threshold (`0` exact, `1` anything). Defaults to `0.4`. |
143
- | `modelCacheTtlMs` | TTL for the cached endpoint list, per workspace host. Defaults to 5 minutes. Concurrent callers share one in-flight fetch. |
144
- | `modelOverride` | `false` to disable per-request `X-Mastra-Model` / `?model=` / body overrides. Defaults to `true`. |
145
- | `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. |
146
-
86
+ Override per plugin or per agent by passing `memory` / `storage` as
87
+ `false` (opt out), `true` (explicit on), or a config object (dedicated
88
+ index / schema). The fields are typed in [`config.ts`](src/config.ts);
89
+ the wiring is in [`memory.ts`](src/memory.ts).
147
90
 
148
91
  ## The `tools(plugins)` callback
149
92
 
150
- Each registered agent can supply either a static `tools: { ... }` record
151
- or a `tools(plugins)` callback. The returned record accepts **any**
152
- tool shape Mastra understands:
153
-
154
- - Mastra tools built with `createTool` or `new Tool(...)`
155
- - AppKit-shaped tools built with the `tool()` wrapper (below)
156
- - Vercel AI SDK tools (`tool({...})` from `ai`)
157
- - Provider-defined tools (e.g. `openai.tools.webSearch(...)`)
158
- - Toolkits returned from `plugins.<name>.toolkit(...)`
93
+ Each agent supplies either a static `tools: { ... }` record or a
94
+ `tools(plugins)` callback. The returned record accepts any tool shape
95
+ Mastra understands - Mastra `createTool` tools, AppKit-shaped `tool()`
96
+ tools, Vercel AI SDK tools, provider-defined tools, and toolkits from
97
+ `plugins.<name>.toolkit(...)`.
159
98
 
160
99
  ```ts
161
100
  tools(plugins) {
162
101
  return {
163
- // Sibling plugin toolkits.
164
- ...plugins.analytics.toolkit(), // every analytics tool
165
- ...plugins.files.toolkit({ only: ["uploads.read"] }), // filtered subset
166
-
167
- // AppKit-shaped inline tool.
102
+ ...plugins.analytics.toolkit(), // every analytics tool
103
+ ...plugins.files.toolkit({ only: ["uploads.read"] }), // filtered subset
168
104
  get_weather: tool({
169
105
  description: "Weather",
170
106
  schema: z.object({ city: z.string() }),
171
107
  execute: async ({ city }) => `Sunny in ${city}`,
172
108
  }),
173
-
174
- // Existing Mastra tool dropped in unchanged.
175
- save_doc: existingMastraTool,
176
109
  };
177
110
  }
178
111
  ```
179
112
 
180
- `plugins` is a typed `Record<string, { toolkit(opts?): tools }>` matching
181
- AppKit's `Plugins` type. It's backed by a runtime Proxy that
182
- **auto-discovers any registered AppKit `ToolProvider` plugin** -
183
- `analytics`, `files`, `lakebase`, `genie`, plus any third-party plugin
184
- that implements the standard `getAgentTools()` + `executeAgentTool()` +
185
- `toolkit()` interface. Tool calls dispatch through the plugin's
186
- `executeAgentTool`, so OBO auth (`asUser`) and telemetry spans stay
187
- intact.
188
-
189
- `plugins.genie` is special-cased: it returns a flat set of
190
- Mastra tools the central agent drives directly (no inner
191
- orchestrator agent). Two shared, space-agnostic tools register
192
- once regardless of how many spaces are wired:
193
-
194
- - `get_statement(statement_id, limit?)` - fetch rows for a
195
- Genie statement. Use only when the agent needs to read values
196
- to reason about them; otherwise embed `[data:<statement_id>]`.
197
- - `prepare_chart({statement_id, title?, description?})` - mint a
198
- `chartId` (v4 UUID), kick off a background chart-planner task,
199
- cache the resolved Echarts spec under the id for one hour.
200
- Returns `{chartId}` synchronously so the agent can embed
201
- `[chart:<chartId>]` in prose without blocking.
202
-
203
- Three per-space tools register per wired alias (`ask_genie`,
204
- `get_space_description`, `get_space_serialized` for the
205
- `default` alias; `ask_genie_<alias>`, etc. for additional
206
- aliases):
207
-
208
- - `ask_genie({question})` - drives one `genieEventChat` turn
209
- and streams wire events (status / thinking / sql) through
210
- `ctx.writer`. Returns `{message: GenieMessage}`; rows are
211
- NOT fetched eagerly.
212
- - `get_space_description()` - cheap title / description /
213
- warehouse id lookup.
214
- - `get_space_serialized()` - full `GenieSpace` JSON for
215
- column-level grounding when the description isn't enough.
216
-
217
- AppKit's `genie()` plugin is honored only for its `spaces`
218
- config (and the matching `app.yaml`-declared resources). The
219
- tools talk to Genie directly via `@dbx-tools/genie`
220
- (`genieEventChat`) and the workspace
221
- `statementExecution.getStatement` API.
222
-
223
- The orchestration prompt (decompose, ask Genie focused
224
- sub-questions, place markers in prose) ships as the exported
225
- `GENIE_INSTRUCTIONS` string - compose it into your agent's
226
- `instructions` to get the canonical behavior:
113
+ `plugins` is a runtime Proxy that auto-discovers any registered AppKit
114
+ `ToolProvider` plugin (`analytics`, `files`, `lakebase`, `genie`, and
115
+ any third-party plugin implementing the standard toolkit interface).
116
+ Tool calls dispatch through the plugin's `executeAgentTool`, so OBO auth
117
+ and telemetry spans stay intact. Plugins that aren't registered resolve
118
+ to `undefined`, so guard optional backings with `?.` / `?? {}`.
227
119
 
228
- ```ts
229
- import { createAgent, GENIE_INSTRUCTIONS } from "@dbx-tools/appkit-mastra";
120
+ `toolkit(opts)` takes the same `ToolkitOptions` AppKit exposes
121
+ (`prefix`, `only`, `except`, `rename`), passed through verbatim.
230
122
 
231
- const support = createAgent({
232
- instructions: `${baseInstructions}\n\n${GENIE_INSTRUCTIONS}`,
233
- tools(plugins) {
234
- return { ...plugins.genie?.toolkit() };
235
- },
236
- });
237
- ```
238
-
239
- `GENIE_INSTRUCTIONS` references the default (`default` alias)
240
- tool names. Multi-space deployments should write a custom
241
- variant that names the suffixed per-space tools
242
- (e.g. `ask_genie_sales`).
243
-
244
- Genie writer event flow:
245
-
246
- - Each `ask_genie` call emits a Mastra-only `started` event
247
- before any network round-trip, then forwards every wire
248
- `GenieChatEvent` (status / thinking / sql / rows / suggested
249
- / message / result / error) through `ctx.writer`.
250
- - Charts ride out-of-band: `prepare_chart` mints a `chartId`
251
- synchronously, the planner runs in the background and writes
252
- the spec to the chart cache (1h TTL). The host UI long-polls
253
- `${basePath}/embed/chart/:id` (via `MastraPluginClient.chart(id)`)
254
- by id and renders inline at the matching `[chart:<chartId>]`
255
- marker.
256
- - After a hard reload, the live `started` / `status` / `sql`
257
- events are gone (they only ride the writer, not persisted
258
- tool results); the central agent's text reply (with embedded
259
- `[data:<statement_id>]` / `[chart:<chartId>]` markers) is
260
- preserved in the message history and the host UI re-resolves
261
- the markers on its own.
262
-
263
- ### `render_data` (system-default ambient tool)
264
-
265
- `buildAgents` registers a system-level `render_data` tool on
266
- every agent so the model can submit any tabular dataset for
267
- inline charting. Users can shadow it by including a same-named
268
- tool in `config.tools` or in a per-agent `tools` map; otherwise
269
- it's just there.
270
-
271
- The tool is generic - not coupled to Genie or any particular
272
- upstream. Input is `{ title, description?, data: Row[] }` where
273
- `data` is an array of objects keyed by column name (a SQL row
274
- set, an API response, a hand-built array, etc.).
275
-
276
- #### How it works
277
-
278
- `render_data` is a thin wrapper around `prepareChart`, the same
279
- orchestrator the Genie `prepare_chart` tool uses. It mints a
280
- `chartId` synchronously, caches an empty placeholder, and kicks
281
- off the chart-planner in the background. The tool returns just
282
- `{ chartId }` so the model can embed `[chart:<chartId>]` in
283
- prose immediately without blocking on chart generation.
284
-
285
- 1. Mint a `chartId` (v4 UUID via `crypto.randomUUID()`).
286
- 2. Cache an empty `{ chartId }` placeholder so the first
287
- `fetchChart` call always sees an entry (no spurious 404 race).
288
- 3. Fire-and-forget: resolve the dataset (trivial for
289
- `render_data` since the rows are already in hand) and run
290
- `runChartPlanner` in the background. On success the cache
291
- entry settles with `{ chartId, result }` (containing the
292
- `chartType` and full `EChartsOption`). On failure it settles
293
- with `{ chartId, error }`.
294
- 4. Return `{ chartId }` to the LLM immediately.
295
-
296
- The host UI resolves `[chart:<chartId>]` markers by hitting
297
- the plugin's generic `GET /embed/chart/:id` route, which long-polls
298
- until the entry settles or the server-side timeout elapses.
299
-
300
- #### Inline placement contract
301
-
302
- The model embeds `[chart:<chartId>]` on its own line in its
303
- markdown reply at the position where the chart should appear
304
- (the `<chartId>` is the v4 UUID the tool returned):
305
-
306
- ```markdown
307
- ## Audit Score
308
-
309
- Audit Score is stable at ~94%, hovering between 93.5 and 95.0.
310
-
311
- [chart:a3f9c1d2-7b4e-4c1a-9f2d-1e6b8c0a5d31]
312
-
313
- ## Service Time
314
-
315
- Service time is the outlier at 162.5s, up from a target of 150s.
316
-
317
- [chart:b7e2d4f1-3a9c-4e58-8d10-2f7a4b6c9e02]
318
- ```
319
-
320
- The chat client splits the assistant text on these markers and
321
- drops a `<ChartSlot>` in at each spot. A marker that arrives
322
- before its chart settles in the cache shows a "Queueing chart"
323
- skeleton; a chart whose marker the model forgot to place falls
324
- through to the end of the reply as a fallback. All three states
325
- (queueing, rendering, rendered) share the same fixed-height
326
- frame so the layout doesn't jump as charts resolve.
327
-
328
- #### Trade-offs
329
-
330
- - Chart entries live in the cache with a 1h TTL. After expiry
331
- the `/embed/chart/:id` route returns 404. The model can call
332
- `render_data` again on the next turn if the user wants the
333
- chart back.
334
- - The chart-planner is a separate model call per dataset (fast
335
- tier, but still ~1-3s each). For an N-chart turn, latency is
336
- `max(planners)` since the planners run concurrently in the
337
- background and the tool returns immediately.
338
-
339
- Plugins that aren't registered (or don't implement the toolkit
340
- interface) resolve to `undefined` at runtime, so guard with `?.` /
341
- `?? {}` when a backing plugin is optional in some environments:
342
-
343
- ```ts
344
- tools(plugins) {
345
- return {
346
- ...(plugins.analytics?.toolkit() ?? {}),
347
- ...(plugins.genie?.toolkit({ prefix: "g_" }) ?? {}),
348
- };
349
- }
350
- ```
123
+ ### `tool()` vs `createTool()`
351
124
 
352
- `plugins.<name>.toolkit(opts)` accepts the same `ToolkitOptions` shape
353
- AppKit's own toolkits expose (passed through verbatim):
125
+ `tool()` mirrors `@databricks/appkit/beta`'s shape so tool code is
126
+ portable between the AppKit `agents` plugin and this one. Reach for
127
+ Mastra's `createTool` when you need Mastra-only fields (`outputSchema`,
128
+ `suspendSchema`, `requireApproval`, `mcp`, ...). An omitted `tool()` `id`
129
+ is auto-derived from the description (slug + short hash) so traces stay
130
+ stable across runs.
354
131
 
355
- - `prefix?: string` - prepended to every key (AppKit default: `${pluginName}.`)
356
- - `only?: string[]` / `except?: string[]` - allow/deny list against the
357
- local tool name
358
- - `rename?: Record<string, string>` - remap individual keys
132
+ ## Genie
359
133
 
360
- ### `tool()` vs `createTool()`
134
+ `plugins.genie` returns a flat set of Mastra tools the central agent
135
+ drives directly - no inner orchestrator agent. The agent asks the
136
+ configured Genie space focused sub-questions, embeds inline markers in
137
+ its prose, and the host UI resolves them (see [Embeds](#embeds)).
361
138
 
362
- The `tool()` factory mirrors `@databricks/appkit/beta`'s shape so
363
- sharing tool code between the AppKit `agents` plugin and this one is a
364
- single-line import change:
139
+ The orchestration prompt ships as the exported `GENIE_INSTRUCTIONS`
140
+ string; compose it into your agent's `instructions` to get the canonical
141
+ behavior:
365
142
 
366
143
  ```ts
367
- import { tool, createTool } from "@dbx-tools/appkit-mastra";
368
-
369
- // AppKit-shaped (description / schema / flat-arg execute).
370
- const weather = tool({
371
- description: "Weather",
372
- schema: z.object({ city: z.string() }),
373
- execute: async ({ city }) => `Sunny in ${city}`,
374
- });
144
+ import { createAgent, GENIE_INSTRUCTIONS } from "@dbx-tools/appkit-mastra";
375
145
 
376
- // Full Mastra `createTool` (id required, inputSchema, advanced fields).
377
- const saveDoc = createTool({
378
- id: "save-doc",
379
- description: "Persist a document",
380
- inputSchema: z.object({ key: z.string(), value: z.any() }),
381
- outputSchema: z.object({ saved: z.boolean() }),
382
- requireApproval: true,
383
- execute: async (input, ctx) => {
384
- await ctx.mastra?.getStorage()?.set(input.key, input.value);
385
- return { saved: true };
146
+ const support = createAgent({
147
+ instructions: `${baseInstructions}\n\n${GENIE_INSTRUCTIONS}`,
148
+ tools(plugins) {
149
+ return { ...plugins.genie?.toolkit() };
386
150
  },
387
151
  });
388
152
  ```
389
153
 
390
- When `tool()`'s `id` is omitted it's auto-derived from a slugified
391
- description plus a 6-char FNV-1a base-32 suffix - stable across runs
392
- so traces stay readable. Pass an explicit `id` when you want to pin
393
- one.
394
-
395
- Reach for `createTool` when you need Mastra-only fields (`outputSchema`,
396
- `suspendSchema`, `requireApproval`, `mcp`, etc.).
154
+ AppKit's stock `genie()` plugin is honored only for its `spaces` config
155
+ (and the matching `app.yaml` resources); the tools talk to Genie
156
+ directly via `@dbx-tools/genie` (`genieEventChat`) and the workspace
157
+ `statementExecution` API. Each Genie turn forwards its wire
158
+ `GenieChatEvent`s through `ctx.writer` for live UI progress. The exact
159
+ tool set, ids, and writer-event contract live in [`genie.ts`](src/genie.ts)
160
+ and the `GenieWriterEvent` docs in
161
+ [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
162
+
163
+ ## Embeds
164
+
165
+ Charts and data tables ride out-of-band via inline markers so the model
166
+ never blocks on or round-trips large payloads:
167
+
168
+ - `render_data` (a system-default ambient tool) and the Genie
169
+ `prepare_chart` tool both mint a `chartId` synchronously, kick chart
170
+ planning into the background, and return `{ chartId }` immediately. The
171
+ model embeds `[chart:<chartId>]` on its own line where the chart should
172
+ appear.
173
+ - `[data:<statement_id>]` markers resolve to inline table data.
174
+
175
+ The host UI splits the assistant text on these markers and long-polls
176
+ the plugin's generic `${basePath}/embed/:type/:id` route until each
177
+ entry settles. Cache entries carry a 1h TTL; after expiry the route
178
+ 404s and the marker falls through harmlessly. Implementation and the
179
+ placement contract are in [`chart.ts`](src/chart.ts); the wire shapes
180
+ are in [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
397
181
 
398
182
  ## Model resolution
399
183
 
400
- Each agent call resolves a `MastraModelConfig` lazily so concurrent
401
- requests get distinct user identities. There are two paths through
402
- the resolver depending on whether the caller asked for a specific
403
- model.
404
-
405
- ### Explicit ask (override / agent / plugin / env)
406
-
407
- When any of these is set the resolver fuzzy-matches that single id
408
- against the live `/serving-endpoints` list, in priority order:
409
-
410
- 1. Per-request override (`X-Mastra-Model` header, `?model=` query,
411
- or `model` / `modelId` body field; see below)
412
- 2. Per-agent `def.model` (string sugar or `DynamicArgument`)
413
- 3. Plugin-level `config.defaultModel`
414
- 4. `DATABRICKS_SERVING_ENDPOINT_NAME` env var
415
-
416
- The matcher is `fuse.js` extended search with tokens split on
417
- non-word characters and AND-joined. Exact matches win immediately;
418
- loose tokens like `"claude sonnet"` snap to
419
- `databricks-claude-sonnet-4-6`, `"llama 70b"` to
420
- `databricks-meta-llama-3-3-70b-instruct`, `"DBRX"` to
421
- `databricks-dbrx-instruct`, and so on. If no candidate scores below
422
- `modelFuzzyThreshold` (default `0.4`) the input is returned verbatim
423
- and Databricks surfaces the canonical 404.
424
-
425
- ### No explicit ask (dynamic, score-based tiers)
426
-
427
- When nothing is set, the resolver classifies the **live** workspace
428
- catalogue and returns **the first id that is actually present in the
429
- endpoint listing**. This is how a workspace without Claude Opus still
430
- gets a sensible default automatically - the resolver skips ahead to
431
- whichever model is the best fit and actually wired up.
432
-
433
- Tiers are derived, not hard-coded. Databricks publishes a
434
- `quality` / `speed` / `cost` profile per Foundation Model API endpoint
435
- (the bars in the AI Playground), surfaced on `ServingEndpointSummary.profile`.
436
- `classifyEndpoints(endpoints)` buckets the scored chat models by the
437
- **relative distribution** of `quality`: the observed scores are split
438
- at their 1/3 and 2/3 quantiles, so the top third is
439
- `ModelTier.Thinking`, the bottom third `ModelTier.Fast`, and the
440
- middle `ModelTier.Balanced`. Because the thresholds come from the data,
441
- the split adapts as Databricks adds or rescores models - nothing is
442
- pinned to a fixed score band, and a brand-new model that lands outside
443
- today's range still slots in next to its peers.
444
-
445
- Two escape hatches cover missing scores:
446
-
447
- - **Unscored but recognizable** endpoints (a model Databricks hasn't
448
- scored yet, e.g. a fresh release) are placed by a small **family
449
- heuristic** keyed on provider + variant words (`opus`/`sonnet`/`haiku`,
450
- `pro`/`mini`/`nano`, `flash`/`flash-lite`, Llama parameter sizes).
451
- Unrecognized, unscored endpoints are never auto-selected.
452
- - **Catalogue unreachable**: the resolver falls back to the small,
453
- family-classified `FALLBACK_MODEL_IDS` floor (Thinking → Balanced →
454
- Fast).
455
-
456
- The default chain is therefore: any `defaultModelFallbacks` you set,
457
- then the live score-classified catalogue in descending tier order,
458
- then the `FALLBACK_MODEL_IDS` floor.
459
-
460
- #### Pick a tier-appropriate model for one agent
461
-
462
- `modelForTier(tier)` / `modelsForTier(tier)` return the **static
463
- fallback opinion** for a tier (the small built-in list, no workspace
464
- call). They're handy for seeding a default; the agent-step resolver
465
- still fuzzy-matches the result against the live catalogue at call time
466
- so it works even when the literal pick isn't deployed.
467
-
468
- ```ts
469
- import { createAgent, ModelTier, modelForTier } from "@dbx-tools/appkit-mastra";
470
-
471
- const classifier = createAgent({
472
- instructions: "Classify this email into one of: billing, support, spam.",
473
- model: modelForTier(ModelTier.Fast),
474
- });
475
-
476
- const planner = createAgent({
477
- instructions: "Plan a multi-step data migration.",
478
- model: modelForTier(ModelTier.Thinking),
479
- });
480
- ```
184
+ Each agent call resolves a model lazily (so concurrent requests keep
185
+ distinct user identities), delegating to
186
+ [`@dbx-tools/model`](../model) for the actual workspace-aware selection
187
+ (fuzzy name matching, score-based class classification, offline
188
+ fallbacks). Its exports are re-exported here, so `ModelClass` /
189
+ `modelForClass` and friends are available straight off
190
+ `@dbx-tools/appkit-mastra`.
481
191
 
482
- #### Bias the plugin-level fallback toward a tier
483
-
484
- `modelsForTier(tier)` returns the static fallback list for one tier;
485
- pass it to `defaultModelFallbacks` to scope the auto-resolver:
192
+ The plugin layers on the config sources for *which* model an agent asks
193
+ for, in priority order: a per-request override, the per-agent `model`,
194
+ the plugin `defaultModel`, then `DATABRICKS_SERVING_ENDPOINT_NAME`. With
195
+ none set, resolution falls through to the dynamic class picker.
486
196
 
487
197
  ```ts
488
- import { mastra, ModelTier, modelsForTier } from "@dbx-tools/appkit-mastra";
489
-
490
- mastra({
491
- // All agents that omit `model` will land on a Fast-tier endpoint.
492
- defaultModelFallbacks: modelsForTier(ModelTier.Fast),
493
- });
494
- ```
198
+ import { createAgent, ModelClass, modelForClass } from "@dbx-tools/appkit-mastra";
495
199
 
496
- #### Pin a custom approved subset
497
-
498
- Mix in your own endpoint names (internal fine-tunes, regulated
499
- allowlists, etc) in front of the catalogue:
500
-
501
- ```ts
502
- mastra({
503
- defaultModelFallbacks: [
504
- "my-org-finetune-v2", // try internal endpoint first
505
- "databricks-claude-sonnet-4-6", // approved fallback
506
- ],
200
+ const classifier = createAgent({
201
+ instructions: "Classify this email into billing, support, or spam.",
202
+ model: modelForClass(ModelClass.ChatFast),
507
203
  });
508
204
  ```
509
205
 
510
- If the workspace has none of the listed ids, the top fallback is
511
- returned and Databricks surfaces the canonical error.
512
-
513
- The endpoint list is cached per workspace host through AppKit's
514
- built-in `CacheManager` (`CacheManager.getInstanceSync().getOrExecute`),
515
- which is the TypeScript counterpart of Python's `cachetools.TTLCache`
516
- plus `cachetools-async` rolled into one: per-entry TTL (default 5
517
- minutes via `modelCacheTtlMs`), bounded size, in-flight request
518
- coalescing (the manager's internal `inFlightRequests` map shares one
519
- fetch across every concurrent caller), telemetry spans, and optional
520
- Lakebase persistence when the `lakebase` plugin is wired up. No extra
521
- dependency lives in this package; the catalogue piggybacks on whatever
522
- storage backend AppKit picked at boot.
523
-
524
- String values (`"databricks-claude-sonnet-4-6"`) are `modelId` sugar
525
- layered on top of the auto-resolver - workspace URL, provider, and OBO
526
- auth stay default. Pass a `DynamicArgument<MastraModelConfig>` on
527
- `def.model` / `config.defaultModel` when you need full control over
528
- auth, provider, or URL; that path bypasses the fuzzy matcher and
529
- per-request override.
530
-
531
- ### `GET /api/mastra/models`
532
-
533
- The plugin exposes the cached endpoint catalogue at `/models` (mounted
534
- under the plugin prefix, default `/api/mastra`) so clients can populate
535
- model pickers and validate `?model=` choices without a separate
536
- Databricks SDK round-trip:
537
-
538
- ```bash
539
- curl -s http://localhost:8000/api/mastra/models | jq
540
- # {
541
- # "endpoints": [
542
- # { "name": "databricks-claude-sonnet-4-6", "task": "llm/v1/chat", "state": "READY", ... },
543
- # { "name": "databricks-meta-llama-3-3-70b-instruct", ... },
544
- # ...
545
- # ]
546
- # }
547
- ```
548
-
549
- Same payload from a sibling plugin or script (no HTTP round-trip):
206
+ Per-request overrides arrive via the `X-Mastra-Model` header, a
207
+ `?model=` query, or a `model` / `modelId` body field (see
208
+ [`serving.ts`](src/serving.ts)). The cached endpoint catalogue is
209
+ exposed at `GET ${basePath}/models` for model pickers; the same data is
210
+ available in-process from a sibling plugin via
211
+ `appkitUtils.require(this.context, mastra).exports()`.
550
212
 
551
- ```ts
552
- import { appkitUtils } from "@dbx-tools/shared";
553
- import { mastra } from "@dbx-tools/appkit-mastra";
213
+ ## License
554
214
 
555
- const m = appkitUtils.require(this.context, ma
215
+ Apache-2.0