@dbx-tools/appkit-mastra 0.1.41 → 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
@@ -2,21 +2,23 @@
2
2
 
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
- Lakebase-backed memory, and an AI SDK chat route the React client can
6
- consume with `useChat()`.
5
+ Lakebase-backed memory, and the standard Mastra agent stream the React
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,511 +46,170 @@ 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
- 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:
62
+ On the React side, drop in the prebuilt chat UI from
63
+ [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui); it wires itself
64
+ from the plugin's published client config and streams over
65
+ `@mastra/client-js`, so there's no transport code to write:
77
66
 
78
67
  ```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
- // ...
68
+ import { MastraChat } from "@dbx-tools/appkit-mastra-ui/react";
69
+
70
+ export default function ChatPage() {
71
+ return <MastraChat showModelPicker />;
93
72
  }
94
73
  ```
95
74
 
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. |
75
+ ## Memory + storage
117
76
 
77
+ With no config, memory and storage are driven entirely by whether the
78
+ `lakebase` plugin is registered:
118
79
 
119
- Every field on `MastraAgentDefinition`:
120
-
121
-
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. |
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.
150
85
 
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).
151
90
 
152
91
  ## The `tools(plugins)` callback
153
92
 
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(...)`
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(...)`.
163
98
 
164
99
  ```ts
165
100
  tools(plugins) {
166
101
  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.
102
+ ...plugins.analytics.toolkit(), // every analytics tool
103
+ ...plugins.files.toolkit({ only: ["uploads.read"] }), // filtered subset
172
104
  get_weather: tool({
173
105
  description: "Weather",
174
106
  schema: z.object({ city: z.string() }),
175
107
  execute: async ({ city }) => `Sunny in ${city}`,
176
108
  }),
177
-
178
- // Existing Mastra tool dropped in unchanged.
179
- save_doc: existingMastraTool,
180
109
  };
181
110
  }
182
111
  ```
183
112
 
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 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:
231
-
232
- ```ts
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
- });
241
- ```
242
-
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`).
247
-
248
- Genie writer event flow:
249
-
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.
266
-
267
- ### `render_data` (system-default ambient tool)
268
-
269
- `buildAgents` registers a system-level `render_data` tool on
270
- every agent so the model can submit any tabular dataset for
271
- inline charting. Users can shadow it by including a same-named
272
- tool in `config.tools` or in a per-agent `tools` map; otherwise
273
- it's just there.
274
-
275
- The tool is generic - not coupled to Genie or any particular
276
- upstream. Input is `{ title, description?, data: Row[] }` where
277
- `data` is an array of objects keyed by column name (a SQL row
278
- set, an API response, a hand-built array, etc.).
279
-
280
- #### How it works
281
-
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
-
304
- #### Inline placement contract
305
-
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):
309
-
310
- ```markdown
311
- ## Audit Score
312
-
313
- Audit Score is stable at ~94%, hovering between 93.5 and 95.0.
314
-
315
- [chart:a3f9c1d2-7b4e-4c1a-9f2d-1e6b8c0a5d31]
316
-
317
- ## Service Time
318
-
319
- Service time is the outlier at 162.5s, up from a target of 150s.
320
-
321
- [chart:b7e2d4f1-3a9c-4e58-8d10-2f7a4b6c9e02]
322
- ```
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 `?.` / `?? {}`.
323
119
 
324
- The chat client splits the assistant text on these markers and
325
- drops a `<ChartSlot>` in at each spot. A marker that arrives
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
-
332
- #### Trade-offs
333
-
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
- - The chart-planner is a separate model call per dataset (fast
339
- tier, but still ~1-3s each). For an N-chart turn, latency is
340
- `max(planners)` since the planners run concurrently in the
341
- background and the tool returns immediately.
342
-
343
- Plugins that aren't registered (or don't implement the toolkit
344
- interface) resolve to `undefined` at runtime, so guard with `?.` /
345
- `?? {}` when a backing plugin is optional in some environments:
120
+ `toolkit(opts)` takes the same `ToolkitOptions` AppKit exposes
121
+ (`prefix`, `only`, `except`, `rename`), passed through verbatim.
346
122
 
347
- ```ts
348
- tools(plugins) {
349
- return {
350
- ...(plugins.analytics?.toolkit() ?? {}),
351
- ...(plugins.genie?.toolkit({ prefix: "g_" }) ?? {}),
352
- };
353
- }
354
- ```
123
+ ### `tool()` vs `createTool()`
355
124
 
356
- `plugins.<name>.toolkit(opts)` accepts the same `ToolkitOptions` shape
357
- 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.
358
131
 
359
- - `prefix?: string` - prepended to every key (AppKit default: `${pluginName}.`)
360
- - `only?: string[]` / `except?: string[]` - allow/deny list against the
361
- local tool name
362
- - `rename?: Record<string, string>` - remap individual keys
132
+ ## Genie
363
133
 
364
- ### `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)).
365
138
 
366
- The `tool()` factory mirrors `@databricks/appkit/beta`'s shape so
367
- sharing tool code between the AppKit `agents` plugin and this one is a
368
- 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:
369
142
 
370
143
  ```ts
371
- import { tool, createTool } from "@dbx-tools/appkit-mastra";
372
-
373
- // AppKit-shaped (description / schema / flat-arg execute).
374
- const weather = tool({
375
- description: "Weather",
376
- schema: z.object({ city: z.string() }),
377
- execute: async ({ city }) => `Sunny in ${city}`,
378
- });
144
+ import { createAgent, GENIE_INSTRUCTIONS } from "@dbx-tools/appkit-mastra";
379
145
 
380
- // Full Mastra `createTool` (id required, inputSchema, advanced fields).
381
- const saveDoc = createTool({
382
- id: "save-doc",
383
- description: "Persist a document",
384
- inputSchema: z.object({ key: z.string(), value: z.any() }),
385
- outputSchema: z.object({ saved: z.boolean() }),
386
- requireApproval: true,
387
- execute: async (input, ctx) => {
388
- await ctx.mastra?.getStorage()?.set(input.key, input.value);
389
- return { saved: true };
146
+ const support = createAgent({
147
+ instructions: `${baseInstructions}\n\n${GENIE_INSTRUCTIONS}`,
148
+ tools(plugins) {
149
+ return { ...plugins.genie?.toolkit() };
390
150
  },
391
151
  });
392
152
  ```
393
153
 
394
- When `tool()`'s `id` is omitted it's auto-derived from a slugified
395
- description plus a 6-char FNV-1a base-32 suffix - stable across runs
396
- so traces stay readable. Pass an explicit `id` when you want to pin
397
- one.
398
-
399
- Reach for `createTool` when you need Mastra-only fields (`outputSchema`,
400
- `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).
401
181
 
402
182
  ## Model resolution
403
183
 
404
- Each agent call resolves a `MastraModelConfig` lazily so concurrent
405
- requests get distinct user identities. There are two paths through
406
- the resolver depending on whether the caller asked for a specific
407
- model.
408
-
409
- ### Explicit ask (override / agent / plugin / env)
410
-
411
- When any of these is set the resolver fuzzy-matches that single id
412
- against the live `/serving-endpoints` list, in priority order:
413
-
414
- 1. Per-request override (`X-Mastra-Model` header, `?model=` query,
415
- or `model` / `modelId` body field; see below)
416
- 2. Per-agent `def.model` (string sugar or `DynamicArgument`)
417
- 3. Plugin-level `config.defaultModel`
418
- 4. `DATABRICKS_SERVING_ENDPOINT_NAME` env var
419
-
420
- The matcher is `fuse.js` extended search with tokens split on
421
- non-word characters and AND-joined. Exact matches win immediately;
422
- loose tokens like `"claude sonnet"` snap to
423
- `databricks-claude-sonnet-4-6`, `"llama 70b"` to
424
- `databricks-meta-llama-3-3-70b-instruct`, `"DBRX"` to
425
- `databricks-dbrx-instruct`, and so on. If no candidate scores below
426
- `modelFuzzyThreshold` (default `0.4`) the input is returned verbatim
427
- and Databricks surfaces the canonical 404.
428
-
429
- ### No explicit ask (tier-aware fallback list)
430
-
431
- When nothing is set, the resolver walks an opinionated
432
- priority-ordered list and returns **the first id that is actually
433
- present in the workspace's endpoint listing**. This is how a workspace
434
- without Claude Opus still gets a sensible default automatically -
435
- the resolver skips ahead to whichever Sonnet / GPT-5 / Gemini / Llama
436
- variant is wired up.
437
-
438
- The catalogue is grouped two ways:
439
-
440
- - By **capability tier** via the `ModelTier` enum:
441
- `ModelTier.Thinking` (deepest reasoning), `ModelTier.Balanced`
442
- (cost/latency sweet spot), `ModelTier.Fast` (cheap & quick for
443
- classification / routing / simple summarisation).
444
- - By **provider** within each tier: `claude`, `gpt`, `gemini`,
445
- `openSource`.
446
-
447
- Both views live on `MODEL_CATALOG[tier][provider]`. The walked
448
- `FALLBACK_MODEL_IDS` chains the three tiers in descending power
449
- (Thinking → Balanced → Fast); within each tier providers are
450
- round-robin-zipped (Claude ↔ GPT ↔ Gemini) before the open-weights
451
- tail is appended as the universal floor.
452
-
453
-
454
- | Tier (most powerful first) | Claude | GPT | Gemini | Open weights |
455
- | -------------------------- | -------------------------------- | ------------------------------------- | ------------------------------- | ---------------------------------------------- |
456
- | `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 |
457
- | `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 |
458
- | `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 |
459
-
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`.
460
191
 
461
- #### Pick a tier-appropriate model for one agent
462
-
463
- Use `modelForTier(tier)` to grab the top of a tier as a string; the
464
- agent-step resolver fuzzy-matches it against the live catalogue at
465
- call time so it still works when the literal top pick isn't deployed.
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.
466
196
 
467
197
  ```ts
468
- import { createAgent, ModelTier, modelForTier } from "@dbx-tools/appkit-mastra";
198
+ import { createAgent, ModelClass, modelForClass } from "@dbx-tools/appkit-mastra";
469
199
 
470
200
  const classifier = createAgent({
471
- instructions: "Classify this email into one of: billing, support, spam.",
472
- model: modelForTier(ModelTier.Fast),
473
- });
474
-
475
- const planner = createAgent({
476
- instructions: "Plan a multi-step data migration.",
477
- model: modelForTier(ModelTier.Thinking),
478
- });
479
- ```
480
-
481
- #### Bias the plugin-level fallback toward a tier
482
-
483
- `modelsForTier(tier)` returns the priority-ordered list for one tier;
484
- pass it to `defaultModelFallbacks` to scope the auto-resolver:
485
-
486
- ```ts
487
- import { mastra, ModelTier, modelsForTier } from "@dbx-tools/appkit-mastra";
488
-
489
- mastra({
490
- // All agents that omit `model` will land on a Fast-tier endpoint.
491
- defaultModelFallbacks: modelsForTier(ModelTier.Fast),
201
+ instructions: "Classify this email into billing, support, or spam.",
202
+ model: modelForClass(ModelClass.ChatFast),
492
203
  });
493
204
  ```
494
205
 
495
- #### Pin a custom approved subset
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()`.
496
212
 
497
- Mix in your own endpoint names (internal fine-tunes, regulated
498
- allowlists, etc) in front of the catalogue:
499
-
500
- ```ts
501
- mastra({
502
- defaultModelFallbacks: [
503
- "my-org-finetune-v2", // try internal endpoint first
504
- "databricks-claude-sonnet-4-6", // approved fallback
505
- ],
506
- });
507
- ```
508
-
509
- If the workspace has none of the listed ids, the top fallback is
510
- returned and Databricks surfaces the canonical error.
511
-
512
- The endpoint list is cached per workspace host through AppKit's
513
- built-in `CacheManager` (`CacheManager.getInstanceSync().getOrExecute`),
514
- which is the TypeScript counterpart of Python's `cachetools.TTLCache`
515
- plus `cachetools-async` rolled into one: per-entry TTL (default 5
516
- minutes via `modelCacheTtlMs`), bounded size, in-flight request
517
- coalescing (the manager's internal `inFlightRequests` map shares one
518
- fetch across every concurrent caller), telemetry spans, and optional
519
- Lakebase persistence when the `lakebase` plugin is wired up. No extra
520
- dependency lives in this package; the catalogue piggybacks on whatever
521
- storage backend AppKit picked at boot.
522
-
523
- String values (`"databricks-claude-sonnet-4-6"`) are `modelId` sugar
524
- layered on top of the auto-resolver - workspace URL, provider, and OBO
525
- auth stay default. Pass a `DynamicArgument<MastraModelConfig>` on
526
- `def.model` / `config.defaultModel` when you need full control over
527
- auth, provider, or URL; that path bypasses the fuzzy matcher and
528
- per-request override.
529
-
530
- ### `GET /api/mastra/models`
531
-
532
- The plugin exposes the cached endpoint catalogue at `/models` (mounted
533
- under the plugin prefix, default `/api/mastra`) so clients can populate
534
- model pickers and validate `?model=` choices without a separate
535
- Databricks SDK round-trip:
536
-
537
- ```bash
538
- curl -s http://localhost:8000/api/mastra/models | jq
539
- # {
540
- # "endpoints": [
541
- # { "name": "databricks-claude-sonnet-4-6", "task": "llm/v1/chat", "state": "READY", ... },
542
- # { "name": "databricks-meta-llama-3-3-70b-instruct", ... },
543
- # ...
544
- # ]
545
- # }
546
- ```
547
-
548
- Same payload from a sibling plugin or script (no HTTP round-trip):
549
-
550
- ```ts
551
- import { appkitUtils } from "@dbx-tools/shared";
552
- import { mastra } from "@dbx-tools/appkit-mastra";
213
+ ## License
553
214
 
554
- const m = appkitUtils.require(this.context, ma
215
+ Apache-2.0