@dbx-tools/appkit-mastra 0.3.29 → 0.3.31
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 +86 -15
- package/index.ts +3 -0
- package/package.json +10 -10
- package/src/agents.ts +12 -11
- package/src/chart.ts +69 -38
- package/src/config.ts +170 -18
- package/src/defaults.ts +133 -0
- package/src/filesystems.ts +17 -5
- package/src/genie.ts +80 -39
- package/src/history.ts +5 -2
- package/src/model.ts +7 -0
- package/src/plugin.ts +469 -194
- package/src/rest.ts +5 -2
- package/src/server.ts +2 -1
- package/src/serving-sanitize.ts +15 -5
- package/src/storage-schema.ts +4 -1
- package/src/threads.ts +10 -3
- package/src/validation.ts +22 -0
- package/test/config.test.ts +64 -0
package/src/plugin.ts
CHANGED
|
@@ -24,10 +24,13 @@
|
|
|
24
24
|
* - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
|
|
25
25
|
* Mastra agent routes, the plugin registers `/route/history`
|
|
26
26
|
* (load + clear a thread's messages), `/route/threads` (list the
|
|
27
|
-
* caller's conversations + delete one), `/models`, `/
|
|
28
|
-
* `/route/feedback` (log a thumbs / comment to MLflow
|
|
29
|
-
* is enabled), and the generic `/embed/:type/:id`
|
|
30
|
-
* chart / data markers.
|
|
27
|
+
* caller's conversations + delete one), `/models`, `/default-model`,
|
|
28
|
+
* `/suggestions`, `/route/feedback` (log a thumbs / comment to MLflow
|
|
29
|
+
* when feedback is enabled), and the generic `/embed/:type/:id`
|
|
30
|
+
* resolver for inline chart / data markers. Each is registered through
|
|
31
|
+
* AppKit's `route()` so it lands in the plugin's endpoint map, and
|
|
32
|
+
* every outbound call one makes runs through `execute()` (see
|
|
33
|
+
* `./defaults.js`). The stock `@mastra/express` surface is gated
|
|
31
34
|
* by `config.apiAccess` (default `"scoped"`): only agent inference,
|
|
32
35
|
* read-only agent metadata, the `/route/*` routes, and (when enabled)
|
|
33
36
|
* MCP are dispatched to Mastra; admin / mutating / bulk-export routes
|
|
@@ -42,21 +45,24 @@
|
|
|
42
45
|
*/
|
|
43
46
|
|
|
44
47
|
import {
|
|
48
|
+
ExecutionError,
|
|
45
49
|
genie,
|
|
46
50
|
getExecutionContext,
|
|
47
51
|
lakebase,
|
|
48
52
|
Plugin,
|
|
49
53
|
toPlugin,
|
|
54
|
+
type ExecutionResult,
|
|
50
55
|
type IAppRouter,
|
|
51
56
|
type PluginManifest,
|
|
52
57
|
type ResourceRequirement,
|
|
53
58
|
} from "@databricks/appkit";
|
|
54
59
|
import { plugin } from "@dbx-tools/appkit";
|
|
55
60
|
import { serving as nodeServing } from "@dbx-tools/model";
|
|
56
|
-
import { error, log, string } from "@dbx-tools/shared-core";
|
|
61
|
+
import { async, error, log, string } from "@dbx-tools/shared-core";
|
|
57
62
|
import {
|
|
58
63
|
feedback,
|
|
59
64
|
routes,
|
|
65
|
+
type Chart,
|
|
60
66
|
type MastraClientConfig,
|
|
61
67
|
type MastraFeedbackRequest,
|
|
62
68
|
type StatementData,
|
|
@@ -69,7 +75,14 @@ import type { Pool } from "pg";
|
|
|
69
75
|
|
|
70
76
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents";
|
|
71
77
|
import { fetchChart } from "./chart";
|
|
72
|
-
import type
|
|
78
|
+
import { MASTRA_CONFIG_SCHEMA, resolveUserKey, type MastraPluginConfig } from "./config";
|
|
79
|
+
import {
|
|
80
|
+
chartFetchDefaults,
|
|
81
|
+
feedbackWriteDefaults,
|
|
82
|
+
genieSuggestionDefaults,
|
|
83
|
+
modelCatalogueDefaults,
|
|
84
|
+
statementDataDefaults,
|
|
85
|
+
} from "./defaults";
|
|
73
86
|
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie";
|
|
74
87
|
import { historyRoute } from "./history";
|
|
75
88
|
import { buildMcpServer, type ResolvedMcp } from "./mcp";
|
|
@@ -80,17 +93,71 @@ import { attachRoutePatchMiddleware, isMastraRequestAllowed, MastraServer } from
|
|
|
80
93
|
import { resolveServingConfig } from "./serving";
|
|
81
94
|
import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement";
|
|
82
95
|
import { threadsRoute } from "./threads";
|
|
96
|
+
import { invalidFields } from "./validation";
|
|
83
97
|
|
|
84
98
|
const GENIE_MANIFEST = plugin.data(genie).plugin.manifest;
|
|
85
99
|
const LAKEBASE_MANIFEST = plugin.data(lakebase).plugin.manifest;
|
|
86
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Budget for draining the memory service-principal pool on shutdown. Well
|
|
103
|
+
* inside AppKit's 15s graceful-shutdown window, so a stuck connection can
|
|
104
|
+
* never be what holds the process open.
|
|
105
|
+
*/
|
|
106
|
+
const POOL_DRAIN_TIMEOUT_MS = 5_000;
|
|
107
|
+
|
|
108
|
+
/** Ceiling on the `?timeoutMs=` long-poll budget a client may request. */
|
|
109
|
+
const MAX_EMBED_POLL_TIMEOUT_MS = 5 * 60_000;
|
|
110
|
+
|
|
111
|
+
/** Stable client message when the workspace's model catalogue can't be read. */
|
|
112
|
+
const MODEL_CATALOGUE_FAILED_MESSAGE = "Could not read the workspace's model catalogue";
|
|
113
|
+
|
|
114
|
+
/** Stable client message for a feedback body that fails schema validation. */
|
|
115
|
+
const INVALID_FEEDBACK_MESSAGE = "Invalid feedback request";
|
|
116
|
+
|
|
117
|
+
/** Cache namespace for statement result sets, matching the chart cache's form. */
|
|
118
|
+
const STATEMENT_CACHE_NAMESPACE = "mastra:statement";
|
|
119
|
+
|
|
87
120
|
/**
|
|
88
121
|
* AppKit plugin (registered name: `mastra`) that hosts Mastra agents
|
|
89
122
|
* with optional Lakebase-backed memory and AI SDK chat routes under
|
|
90
123
|
* the plugin mount (typically `/api/mastra`).
|
|
124
|
+
*
|
|
125
|
+
* @example Register the plugin
|
|
126
|
+
* ```ts
|
|
127
|
+
* import { createApp, lakebase } from "@databricks/appkit";
|
|
128
|
+
* import { createAgent, mastra } from "@dbx-tools/appkit-mastra";
|
|
129
|
+
*
|
|
130
|
+
* // `lakebase` first: registering it auto-enables Mastra storage + memory.
|
|
131
|
+
* const app = await createApp({
|
|
132
|
+
* plugins: [
|
|
133
|
+
* lakebase(),
|
|
134
|
+
* mastra({
|
|
135
|
+
* genieSpaces: { default: process.env.DATABRICKS_GENIE_SPACE_ID! },
|
|
136
|
+
* agents: createAgent({
|
|
137
|
+
* name: "analyst",
|
|
138
|
+
* instructions: "You answer questions about revenue and returns.",
|
|
139
|
+
* tools(plugins) {
|
|
140
|
+
* return { ...plugins.genie?.toolkit() };
|
|
141
|
+
* },
|
|
142
|
+
* }),
|
|
143
|
+
* }),
|
|
144
|
+
* ],
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*
|
|
148
|
+
* @example Read the agents back off the AppKit instance
|
|
149
|
+
* ```ts
|
|
150
|
+
* const agentIds = app.mastra.list();
|
|
151
|
+
* const endpoints = await app.mastra.listModels();
|
|
152
|
+
* ```
|
|
91
153
|
*/
|
|
92
154
|
export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
93
|
-
|
|
155
|
+
// Annotated rather than left to `satisfies`: the config schema's type comes
|
|
156
|
+
// from `@types/json-schema`, which this package does not depend on, so an
|
|
157
|
+
// inferred manifest type cannot be named in the emitted declaration. The
|
|
158
|
+
// `PluginManifest<"mastra">` parameter still carries the literal name into
|
|
159
|
+
// `toPlugin()`'s factory type.
|
|
160
|
+
static manifest: PluginManifest<"mastra"> = {
|
|
94
161
|
name: "mastra",
|
|
95
162
|
displayName: "Mastra",
|
|
96
163
|
description:
|
|
@@ -100,19 +167,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
100
167
|
resources: {
|
|
101
168
|
required: [],
|
|
102
169
|
optional: [
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// built-in Genie
|
|
107
|
-
// `@dbx-tools/genie
|
|
108
|
-
//
|
|
109
|
-
// `app.yaml` configs and `genie({ spaces })` wiring keep
|
|
110
|
-
// working without change.
|
|
170
|
+
// Reuse the Genie space-id binding declared by AppKit's `genie`
|
|
171
|
+
// manifest so the resource-binding shape is identical to AppKit's and
|
|
172
|
+
// an existing `app.yaml` / `genie({ spaces })` wiring keeps working.
|
|
173
|
+
// The built-in Genie tools talk to Genie directly through
|
|
174
|
+
// `@dbx-tools/genie`, so only the binding is shared, not the plugin's
|
|
175
|
+
// tools.
|
|
111
176
|
...GENIE_MANIFEST.resources.required,
|
|
112
177
|
...LAKEBASE_MANIFEST.resources.required,
|
|
113
178
|
],
|
|
114
179
|
},
|
|
115
|
-
|
|
180
|
+
config: { schema: MASTRA_CONFIG_SCHEMA },
|
|
181
|
+
};
|
|
116
182
|
|
|
117
183
|
/**
|
|
118
184
|
* Tighten resource requirements based on which features are enabled.
|
|
@@ -128,7 +194,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
128
194
|
}
|
|
129
195
|
for (const m of enabledManifests) {
|
|
130
196
|
for (const resource of m.resources.required) {
|
|
131
|
-
resources.push({ ...resource, required: true }
|
|
197
|
+
resources.push({ ...resource, required: true });
|
|
132
198
|
}
|
|
133
199
|
}
|
|
134
200
|
return resources;
|
|
@@ -149,8 +215,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
149
215
|
* Dedicated service-principal Lakebase pool backing Mastra memory /
|
|
150
216
|
* storage. Built once in {@link buildAgentAndServer} (outside any
|
|
151
217
|
* `asUser` scope, so it never inherits a request's OBO identity) and
|
|
152
|
-
* drained in {@link
|
|
153
|
-
*
|
|
218
|
+
* drained in {@link shutdown}. `null` until setup runs or when
|
|
219
|
+
* Lakebase isn't needed.
|
|
154
220
|
*/
|
|
155
221
|
private servicePrincipalPool: Pool | null = null;
|
|
156
222
|
|
|
@@ -159,7 +225,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
159
225
|
// the lakebase pool is valid when storage/memory are enabled.
|
|
160
226
|
this.context?.onLifecycle("setup:complete", async () => {
|
|
161
227
|
this.applyLakebaseAutoDefaults();
|
|
162
|
-
this.logger.info("setup:complete");
|
|
163
228
|
await this.buildAgentAndServer();
|
|
164
229
|
});
|
|
165
230
|
}
|
|
@@ -179,25 +244,42 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
179
244
|
}
|
|
180
245
|
|
|
181
246
|
/**
|
|
182
|
-
* Drain the memory service-principal pool
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
247
|
+
* Drain the memory service-principal pool. Idempotent: the handle is
|
|
248
|
+
* cleared before the drain starts, so a second call is a no-op and a
|
|
249
|
+
* later `setup()` rebuilds the pool. Bounded by
|
|
250
|
+
* {@link POOL_DRAIN_TIMEOUT_MS} to stay well inside the 15s graceful
|
|
251
|
+
* shutdown budget.
|
|
186
252
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
253
|
+
async shutdown(): Promise<void> {
|
|
254
|
+
const pool = this.servicePrincipalPool;
|
|
255
|
+
if (!pool) return;
|
|
256
|
+
this.servicePrincipalPool = null;
|
|
257
|
+
this.logger.info("closing memory SP pool");
|
|
258
|
+
// The budget timer is cancelled on the way out so a drain that finished
|
|
259
|
+
// early cannot hold the event loop open for the rest of the window.
|
|
260
|
+
const budget = new AbortController();
|
|
261
|
+
try {
|
|
262
|
+
await Promise.race([pool.end(), async.sleep(POOL_DRAIN_TIMEOUT_MS, budget.signal)]);
|
|
263
|
+
} catch (err) {
|
|
264
|
+
this.logger.error("error closing memory SP pool", {
|
|
265
|
+
error: error.errorMessage(err),
|
|
197
266
|
});
|
|
267
|
+
} finally {
|
|
268
|
+
budget.abort();
|
|
198
269
|
}
|
|
199
270
|
}
|
|
200
271
|
|
|
272
|
+
/**
|
|
273
|
+
* Abort in-flight work. AppKit's graceful shutdown calls this hook
|
|
274
|
+
* synchronously and never awaits {@link shutdown}, so the pool drain is
|
|
275
|
+
* started here too; it cannot be awaited from a `void` hook, and
|
|
276
|
+
* {@link shutdown} is idempotent so the duplicate call is free.
|
|
277
|
+
*/
|
|
278
|
+
override abortActiveOperations(): void {
|
|
279
|
+
super.abortActiveOperations();
|
|
280
|
+
void this.shutdown();
|
|
281
|
+
}
|
|
282
|
+
|
|
201
283
|
override exports() {
|
|
202
284
|
return {
|
|
203
285
|
/**
|
|
@@ -223,12 +305,20 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
223
305
|
getMastra: () => this.mastra,
|
|
224
306
|
/**
|
|
225
307
|
* MCP endpoint info when `config.mcp` is enabled, else `null`.
|
|
226
|
-
*
|
|
227
|
-
*
|
|
228
|
-
*
|
|
308
|
+
* Streamable HTTP is `http`; the SSE pair is the legacy transport.
|
|
309
|
+
*
|
|
310
|
+
* Each path is given twice: mount-relative (`httpPath`, `ssePath`,
|
|
311
|
+
* `messagePath`) for anything that already knows where the plugin is
|
|
312
|
+
* mounted, and absolute (`http`, `sse`, `messages`) for MCP clients,
|
|
313
|
+
* which take a single URL and cannot compose one. The absolute form is
|
|
314
|
+
* built from {@link basePath}, so it honors a `config.name` override but
|
|
315
|
+
* still assumes AppKit's default `/api/<name>` mount.
|
|
229
316
|
*/
|
|
230
317
|
getMcp: (): {
|
|
231
318
|
serverId: string;
|
|
319
|
+
httpPath: string;
|
|
320
|
+
ssePath: string;
|
|
321
|
+
messagePath: string;
|
|
232
322
|
http: string;
|
|
233
323
|
sse: string;
|
|
234
324
|
messages: string;
|
|
@@ -236,9 +326,12 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
236
326
|
this.mcp
|
|
237
327
|
? {
|
|
238
328
|
serverId: this.mcp.serverId,
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
329
|
+
httpPath: this.mcp.httpPath,
|
|
330
|
+
ssePath: this.mcp.ssePath,
|
|
331
|
+
messagePath: this.mcp.messagePath,
|
|
332
|
+
http: `${this.basePath}${this.mcp.httpPath}`,
|
|
333
|
+
sse: `${this.basePath}${this.mcp.ssePath}`,
|
|
334
|
+
messages: `${this.basePath}${this.mcp.messagePath}`,
|
|
242
335
|
}
|
|
243
336
|
: null,
|
|
244
337
|
/** Express subapp Mastra is mounted on; mostly for tests. */
|
|
@@ -248,7 +341,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
248
341
|
* payload the `GET /models` route returns; surfaced here so
|
|
249
342
|
* other plugins / scripts can introspect the catalogue without
|
|
250
343
|
* an HTTP round-trip. AppKit wraps this with `asUser(req)` for
|
|
251
|
-
* OBO scoping automatically.
|
|
344
|
+
* OBO scoping automatically. Throws when the listing fails, since
|
|
345
|
+
* there is no status code to hand back on this surface.
|
|
252
346
|
*/
|
|
253
347
|
listModels: (): Promise<ServingEndpointSummary[]> => this.listModels(),
|
|
254
348
|
/**
|
|
@@ -262,18 +356,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
262
356
|
};
|
|
263
357
|
}
|
|
264
358
|
|
|
359
|
+
/**
|
|
360
|
+
* Absolute mount this plugin's routes answer on. AppKit mounts every plugin
|
|
361
|
+
* at `/api/<plugin.name>` and `this.name` honors a `config.name` override,
|
|
362
|
+
* so the per-route segments (`routes.MASTRA_ROUTES`) hang off this.
|
|
363
|
+
*/
|
|
364
|
+
private get basePath(): string {
|
|
365
|
+
return `/api/${this.name}`;
|
|
366
|
+
}
|
|
367
|
+
|
|
265
368
|
override clientConfig(): Record<string, unknown> {
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
// client (`MastraPluginClient`) derives every endpoint from
|
|
271
|
-
// `basePath`.
|
|
369
|
+
// Publishing `basePath` is enough for the client to stay correct under a
|
|
370
|
+
// custom mount id: the per-route segments are fixed
|
|
371
|
+
// (`routes.MASTRA_ROUTES`) and the client (`MastraPluginClient`) derives
|
|
372
|
+
// every endpoint from `basePath`.
|
|
272
373
|
// Return widens to `Record<string, unknown>` to satisfy the
|
|
273
374
|
// base-class signature; consumers read it through the typed
|
|
274
375
|
// `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
|
|
275
376
|
const config: MastraClientConfig = {
|
|
276
|
-
basePath:
|
|
377
|
+
basePath: this.basePath,
|
|
277
378
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
278
379
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
279
380
|
feedbackEnabled: this.feedbackEnabled(),
|
|
@@ -299,6 +400,10 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
299
400
|
// the catch-all and rewrites the alias to the underlying route, so
|
|
300
401
|
// the public path is just `/api/<plugin>/mcp`; the query string
|
|
301
402
|
// (e.g. the SSE `sessionId`) is preserved.
|
|
403
|
+
//
|
|
404
|
+
// Middleware rather than `this.route`: it rewrites the URL for routes
|
|
405
|
+
// `@mastra/express` owns further down the chain instead of answering the
|
|
406
|
+
// request, which `RouteConfig` has no shape for.
|
|
302
407
|
router.use((req, _res, next) => {
|
|
303
408
|
const target = this.mcpRouteAlias(req.path);
|
|
304
409
|
if (target) {
|
|
@@ -311,13 +416,19 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
311
416
|
// `GET /models` exposes the cached endpoint list so clients can
|
|
312
417
|
// populate model pickers, validate `?model=` choices, etc. Must
|
|
313
418
|
// be registered before the catch-all that forwards everything to
|
|
314
|
-
// the Mastra subapp.
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
.
|
|
419
|
+
// the Mastra subapp.
|
|
420
|
+
this.route(router, {
|
|
421
|
+
name: "models",
|
|
422
|
+
method: "get",
|
|
423
|
+
path: routes.MASTRA_ROUTES.models,
|
|
424
|
+
handler: async (req, res) => {
|
|
425
|
+
const result = await this.asUser(req).listModelsResult();
|
|
426
|
+
if (!result.ok) {
|
|
427
|
+
this.sendFailure(res, result, "models", MODEL_CATALOGUE_FAILED_MESSAGE);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
res.json({ endpoints: result.data });
|
|
431
|
+
},
|
|
321
432
|
});
|
|
322
433
|
|
|
323
434
|
// `GET /default-model` (and `/default-model/:agentId`) reports the static
|
|
@@ -326,11 +437,19 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
326
437
|
// name. Agent-scoped via the optional `/:agentId` suffix (URL symmetry
|
|
327
438
|
// with the history / threads / suggestions routes), defaulting to the
|
|
328
439
|
// default agent. `model` / `displayName` are null when the agent has no
|
|
329
|
-
// static default (a dynamic, call-time model)
|
|
330
|
-
//
|
|
331
|
-
// scoping. Registered before the catch-all,
|
|
332
|
-
|
|
440
|
+
// static default (a dynamic, call-time model); an unknown agent id is a
|
|
441
|
+
// 404, matching the history / threads routes. Reads only in-memory build
|
|
442
|
+
// state, so it needs no OBO scoping. Registered before the catch-all,
|
|
443
|
+
// same as `/models`.
|
|
444
|
+
const handleDefaultModel = async (
|
|
445
|
+
req: express.Request,
|
|
446
|
+
res: express.Response,
|
|
447
|
+
): Promise<void> => {
|
|
333
448
|
const requested = string.firstNonEmpty(req.params.agentId);
|
|
449
|
+
if (requested !== null && !this.built?.agents[requested]) {
|
|
450
|
+
res.status(404).json({ error: this.unknownAgentMessage(requested) });
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
334
453
|
const agentId = requested ?? this.built?.defaultAgentId ?? FALLBACK_AGENT_ID;
|
|
335
454
|
const raw = this.built?.defaultModels[agentId];
|
|
336
455
|
// `"<dynamic>"` (a call-time function) has no fixed id to advertise.
|
|
@@ -343,8 +462,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
343
462
|
displayName: model ? display.toModelDisplayName(model) : null,
|
|
344
463
|
});
|
|
345
464
|
};
|
|
346
|
-
|
|
347
|
-
|
|
465
|
+
this.route(router, {
|
|
466
|
+
name: "defaultModel",
|
|
467
|
+
method: "get",
|
|
468
|
+
path: routes.MASTRA_ROUTES.defaultModel,
|
|
469
|
+
handler: handleDefaultModel,
|
|
470
|
+
});
|
|
471
|
+
this.route(router, {
|
|
472
|
+
name: "defaultModelByAgent",
|
|
473
|
+
method: "get",
|
|
474
|
+
path: `${routes.MASTRA_ROUTES.defaultModel}/:agentId`,
|
|
475
|
+
handler: handleDefaultModel,
|
|
476
|
+
});
|
|
348
477
|
|
|
349
478
|
// `GET /embed/:type/:id` is the single resolver for every embed
|
|
350
479
|
// marker the agent emits in prose (`[chart:<id>]`,
|
|
@@ -379,48 +508,55 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
379
508
|
const embedResolvers: Record<string, EmbedResolver> = {
|
|
380
509
|
chart: (req, id, signal) => {
|
|
381
510
|
const timeoutMs = parseTimeoutMs(req.query.timeoutMs);
|
|
382
|
-
return
|
|
511
|
+
return this.asUser(req).fetchChartEntry(id, {
|
|
383
512
|
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
384
513
|
signal,
|
|
385
514
|
});
|
|
386
515
|
},
|
|
387
516
|
data: (req, id, signal) => {
|
|
388
517
|
const limit = parseStatementLimit(req.query.limit);
|
|
389
|
-
return this.
|
|
518
|
+
return this.asUser(req).fetchStatement(id, {
|
|
390
519
|
...(limit !== undefined ? { limit } : {}),
|
|
391
520
|
signal,
|
|
392
521
|
});
|
|
393
522
|
},
|
|
394
523
|
};
|
|
395
524
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
.
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
525
|
+
this.route(router, {
|
|
526
|
+
name: "embed",
|
|
527
|
+
method: "get",
|
|
528
|
+
path: `${routes.MASTRA_ROUTES.embed}/:type/:id`,
|
|
529
|
+
handler: async (req, res) => {
|
|
530
|
+
const type = string.firstNonEmpty(req.params.type) ?? "";
|
|
531
|
+
const id = string.firstNonEmpty(req.params.id);
|
|
532
|
+
const resolve = embedResolvers[type];
|
|
533
|
+
if (!resolve) {
|
|
534
|
+
res.status(404).json({ error: `unsupported embed type: ${type}` });
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (!id) {
|
|
538
|
+
res.status(400).json({ error: "id is required" });
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
// Express's `req` predates `AbortSignal`; bridge the `close`
|
|
542
|
+
// event onto an `AbortController` so a closed connection
|
|
543
|
+
// unblocks any long-poll immediately and frees the request
|
|
544
|
+
// thread. The listener is GC'd with the request on normal
|
|
545
|
+
// completion.
|
|
546
|
+
const controller = new AbortController();
|
|
547
|
+
req.on("close", () => controller.abort());
|
|
548
|
+
const result = await resolve(req, id, controller.signal);
|
|
549
|
+
if (controller.signal.aborted) return;
|
|
550
|
+
if (!result.ok) {
|
|
551
|
+
this.sendFailure(res, result, `embed:${type}`, `Could not resolve the ${type} embed`);
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
if (result.data === undefined) {
|
|
555
|
+
res.status(404).json({ error: `${type} not found` });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
res.json(result.data);
|
|
559
|
+
},
|
|
424
560
|
});
|
|
425
561
|
|
|
426
562
|
// `GET /suggestions` (and `/suggestions/:agentId`) returns the
|
|
@@ -433,24 +569,39 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
433
569
|
// routes; Genie spaces are resolved per-plugin, not per-agent,
|
|
434
570
|
// so it doesn't change the result. OBO-scoped like the other
|
|
435
571
|
// data routes so the space lookup runs as the calling user.
|
|
436
|
-
const handleSuggestions = (
|
|
572
|
+
const handleSuggestions = async (
|
|
573
|
+
req: express.Request,
|
|
574
|
+
res: express.Response,
|
|
575
|
+
): Promise<void> => {
|
|
437
576
|
const controller = new AbortController();
|
|
438
577
|
req.on("close", () => controller.abort());
|
|
439
|
-
this.
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
449
|
-
res.json({ questions: [] });
|
|
578
|
+
const result = await this.asUser(req).fetchSuggestions(controller.signal);
|
|
579
|
+
if (controller.signal.aborted) return;
|
|
580
|
+
if (!result.ok) {
|
|
581
|
+
// Suggestions are a non-critical enhancement; a lookup failure should
|
|
582
|
+
// leave the chat usable with a bare empty state rather than surfacing
|
|
583
|
+
// the upstream status. Log and degrade.
|
|
584
|
+
this.logger.warn("suggestions:error", {
|
|
585
|
+
status: result.status,
|
|
586
|
+
error: result.message,
|
|
450
587
|
});
|
|
588
|
+
res.json({ questions: [] });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
res.json({ questions: result.data });
|
|
451
592
|
};
|
|
452
|
-
|
|
453
|
-
|
|
593
|
+
this.route(router, {
|
|
594
|
+
name: "suggestions",
|
|
595
|
+
method: "get",
|
|
596
|
+
path: routes.MASTRA_ROUTES.suggestions,
|
|
597
|
+
handler: handleSuggestions,
|
|
598
|
+
});
|
|
599
|
+
this.route(router, {
|
|
600
|
+
name: "suggestionsByAgent",
|
|
601
|
+
method: "get",
|
|
602
|
+
path: `${routes.MASTRA_ROUTES.suggestions}/:agentId`,
|
|
603
|
+
handler: handleSuggestions,
|
|
604
|
+
});
|
|
454
605
|
|
|
455
606
|
// `POST /route/feedback` logs a thumbs / comment assessment against
|
|
456
607
|
// a turn's MLflow trace (the `traceId` the client captured from the
|
|
@@ -462,27 +613,43 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
462
613
|
// A recorded assessment yields `{ ok: true }`; a soft failure (most
|
|
463
614
|
// often the trace hasn't finished exporting to MLflow yet) yields
|
|
464
615
|
// `{ ok: false }` without a 5xx so the UI can prompt a retry.
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
616
|
+
this.route(router, {
|
|
617
|
+
name: "feedback",
|
|
618
|
+
method: "post",
|
|
619
|
+
path: routes.MASTRA_ROUTES.feedback,
|
|
620
|
+
handler: async (req, res) => {
|
|
621
|
+
if (!this.feedbackEnabled()) {
|
|
622
|
+
res.status(404).json({ ok: false });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const parsed = feedback.MastraFeedbackRequestSchema.safeParse(req.body);
|
|
626
|
+
if (!parsed.success) {
|
|
627
|
+
// A Zod issue string quotes the received body back, so only the
|
|
628
|
+
// offending field names travel; the full issue list is logged.
|
|
629
|
+
this.logger.warn("feedback:invalid", { error: parsed.error.message });
|
|
630
|
+
res.status(400).json({
|
|
631
|
+
ok: false,
|
|
632
|
+
error: INVALID_FEEDBACK_MESSAGE,
|
|
633
|
+
fields: invalidFields(parsed.error),
|
|
634
|
+
});
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
const result = await this.asUser(req).logFeedback(parsed.data);
|
|
638
|
+
if (!result.ok) {
|
|
639
|
+
this.sendFailure(res, result, "feedback", "Feedback could not be recorded");
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
const assessmentId = result.data;
|
|
643
|
+
res.json({
|
|
644
|
+
ok: assessmentId !== undefined,
|
|
645
|
+
...(assessmentId ? { assessmentId } : {}),
|
|
646
|
+
});
|
|
647
|
+
},
|
|
484
648
|
});
|
|
485
649
|
|
|
650
|
+
// Middleware rather than `this.route`: this is the catch-all that hands
|
|
651
|
+
// every remaining method / path pair to the Mastra sub-app, so it has no
|
|
652
|
+
// single method or path to register under.
|
|
486
653
|
router.use((req, res, next) => {
|
|
487
654
|
if (!this.mastraApp) return res.status(503).end();
|
|
488
655
|
// Gate the stock Mastra surface before dispatch. In the default
|
|
@@ -512,10 +679,36 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
512
679
|
// an OBO token makes `userScopedSelf` return the proxy. `dispatchMastra`
|
|
513
680
|
// is a plain method (its `.bind` is the normal one) and invokes
|
|
514
681
|
// `this.mastraApp` off the real target, keeping the OBO scope active.
|
|
515
|
-
return this.
|
|
682
|
+
return this.asUser(req).dispatchMastra(req, res, next);
|
|
516
683
|
});
|
|
517
684
|
}
|
|
518
685
|
|
|
686
|
+
/**
|
|
687
|
+
* Map a failed {@link ExecutionResult} onto the response: the status the
|
|
688
|
+
* execution pipeline resolved, paired with `clientMessage` rather than the
|
|
689
|
+
* result's own text, which can carry upstream detail. The full message is
|
|
690
|
+
* logged under `<operation>:error`.
|
|
691
|
+
*/
|
|
692
|
+
private sendFailure(
|
|
693
|
+
res: express.Response,
|
|
694
|
+
result: { status: number; message: string },
|
|
695
|
+
operation: string,
|
|
696
|
+
clientMessage: string,
|
|
697
|
+
): void {
|
|
698
|
+
this.logger.warn(`${operation}:error`, {
|
|
699
|
+
status: result.status,
|
|
700
|
+
error: result.message,
|
|
701
|
+
});
|
|
702
|
+
if (res.headersSent) return;
|
|
703
|
+
res.status(result.status).json({ error: clientMessage });
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** 404 body for a request naming an agent that isn't registered. */
|
|
707
|
+
private unknownAgentMessage(agentId: string): string {
|
|
708
|
+
const registered = Object.keys(this.built?.agents ?? {});
|
|
709
|
+
return `Unknown agent "${agentId}". Registered agents: ${registered.join(", ") || "none"}`;
|
|
710
|
+
}
|
|
711
|
+
|
|
519
712
|
/**
|
|
520
713
|
* Invoke the Mastra express sub-app. Exists as a method (instead of reading
|
|
521
714
|
* `this.mastraApp` through the `asUser(req)` proxy at the call site) so the
|
|
@@ -557,15 +750,18 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
557
750
|
* Returns `[]` when no Genie space is configured so the client
|
|
558
751
|
* shows a bare empty state instead of built-in example prompts.
|
|
559
752
|
*/
|
|
560
|
-
private async fetchSuggestions(signal?: AbortSignal): Promise<string[]
|
|
753
|
+
private async fetchSuggestions(signal?: AbortSignal): Promise<ExecutionResult<string[]>> {
|
|
561
754
|
const spaces = resolveGenieSpaces(this.config, this.context);
|
|
562
|
-
if (Object.keys(spaces).length === 0) return [];
|
|
755
|
+
if (Object.keys(spaces).length === 0) return { ok: true, data: [] };
|
|
563
756
|
const client = getExecutionContext().client;
|
|
564
|
-
return
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
757
|
+
return this.execute((executeSignal) => {
|
|
758
|
+
const combined = async.combineAbortSignals(signal, executeSignal);
|
|
759
|
+
return collectSpaceSuggestions({
|
|
760
|
+
spaces,
|
|
761
|
+
client,
|
|
762
|
+
...(combined ? { signal: combined } : {}),
|
|
763
|
+
});
|
|
764
|
+
}, genieSuggestionDefaults);
|
|
569
765
|
}
|
|
570
766
|
|
|
571
767
|
/**
|
|
@@ -576,7 +772,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
576
772
|
* created assessment id on success, or `undefined` on a soft failure
|
|
577
773
|
* (see {@link logFeedback} in `./mlflow.js`).
|
|
578
774
|
*/
|
|
579
|
-
private async logFeedback(
|
|
775
|
+
private async logFeedback(
|
|
776
|
+
feedback: MastraFeedbackRequest,
|
|
777
|
+
): Promise<ExecutionResult<string | undefined>> {
|
|
580
778
|
const ctx = getExecutionContext();
|
|
581
779
|
const sourceId =
|
|
582
780
|
"userEmail" in ctx && ctx.userEmail
|
|
@@ -584,10 +782,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
584
782
|
: "userId" in ctx
|
|
585
783
|
? ctx.userId
|
|
586
784
|
: ctx.serviceUserId;
|
|
587
|
-
return
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
785
|
+
return this.execute(
|
|
786
|
+
() =>
|
|
787
|
+
logFeedback(ctx.client, {
|
|
788
|
+
...feedback,
|
|
789
|
+
...(sourceId ? { sourceId } : {}),
|
|
790
|
+
}),
|
|
791
|
+
feedbackWriteDefaults,
|
|
792
|
+
);
|
|
591
793
|
}
|
|
592
794
|
|
|
593
795
|
/**
|
|
@@ -598,57 +800,101 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
598
800
|
* the `get_statement` tool runs so the LLM and the UI see the
|
|
599
801
|
* exact same shape for the same statement.
|
|
600
802
|
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
803
|
+
* Resolves to `undefined` data for upstream 404s so the route can map them
|
|
804
|
+
* to a clean HTTP 404; any other failure comes back as a non-`ok` result
|
|
805
|
+
* carrying the status to answer with.
|
|
603
806
|
*/
|
|
604
807
|
private async fetchStatement(
|
|
605
808
|
statementId: string,
|
|
606
809
|
options: { limit?: number; signal?: AbortSignal } = {},
|
|
607
|
-
): Promise<StatementData | undefined
|
|
810
|
+
): Promise<ExecutionResult<StatementData | undefined>> {
|
|
608
811
|
const client = getExecutionContext().client;
|
|
609
812
|
const limit = Math.min(options.limit ?? STATEMENT_ROW_CAP, STATEMENT_ROW_CAP);
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
813
|
+
return this.execute(
|
|
814
|
+
async (executeSignal) => {
|
|
815
|
+
const combined = async.combineAbortSignals(options.signal, executeSignal);
|
|
816
|
+
try {
|
|
817
|
+
const data = await fetchStatementData(client, statementId, {
|
|
818
|
+
limit,
|
|
819
|
+
...(combined ? { signal: combined } : {}),
|
|
820
|
+
});
|
|
821
|
+
return {
|
|
822
|
+
columns: data.columns,
|
|
823
|
+
rows: data.rows,
|
|
824
|
+
rowCount: data.rowCount,
|
|
825
|
+
truncated: data.rows.length < data.rowCount,
|
|
826
|
+
};
|
|
827
|
+
} catch (err) {
|
|
828
|
+
// The Databricks SDK throws on 404; surface as `undefined`
|
|
829
|
+
// so the route maps to a clean HTTP 404 instead of a 500.
|
|
830
|
+
if (error.errorContext(err).notAccessible) return undefined;
|
|
831
|
+
throw err;
|
|
832
|
+
}
|
|
833
|
+
},
|
|
834
|
+
{
|
|
835
|
+
default: {
|
|
836
|
+
...statementDataDefaults.default,
|
|
837
|
+
// Namespaced so the key can't collide with another cache sharing the
|
|
838
|
+
// manager, and keyed on `limit` because the slice is part of the
|
|
839
|
+
// payload.
|
|
840
|
+
cache: {
|
|
841
|
+
...statementDataDefaults.default.cache,
|
|
842
|
+
cacheKey: [STATEMENT_CACHE_NAMESPACE, statementId, limit],
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
},
|
|
846
|
+
);
|
|
627
847
|
}
|
|
628
848
|
|
|
629
849
|
/**
|
|
630
|
-
*
|
|
631
|
-
*
|
|
632
|
-
*
|
|
633
|
-
* user
|
|
634
|
-
* browser never sends `x-forwarded-access-token`. Behavior is
|
|
635
|
-
* unchanged in production: a missing token always means a real OBO
|
|
636
|
-
* proxy call (and AppKit will throw upstream if that's wrong).
|
|
850
|
+
* Implementation backing the `chart` embed resolver
|
|
851
|
+
* (`GET /embed/chart/:id`). Runs inside the AppKit user-context proxy so the
|
|
852
|
+
* lookup is namespaced to the calling identity: a chart minted for another
|
|
853
|
+
* user is indistinguishable from an unknown id, and the route answers 404.
|
|
637
854
|
*/
|
|
638
|
-
private
|
|
639
|
-
|
|
855
|
+
private async fetchChartEntry(
|
|
856
|
+
chartId: string,
|
|
857
|
+
options: { timeoutMs?: number; signal: AbortSignal },
|
|
858
|
+
): Promise<ExecutionResult<Chart | undefined>> {
|
|
859
|
+
const userKey = resolveUserKey();
|
|
860
|
+
return this.execute(
|
|
861
|
+
(executeSignal) =>
|
|
862
|
+
fetchChart(chartId, {
|
|
863
|
+
userKey,
|
|
864
|
+
...(options.timeoutMs !== undefined ? { timeoutMs: options.timeoutMs } : {}),
|
|
865
|
+
signal: async.combineAbortSignals(options.signal, executeSignal) ?? options.signal,
|
|
866
|
+
}),
|
|
867
|
+
chartFetchDefaults,
|
|
868
|
+
);
|
|
640
869
|
}
|
|
641
870
|
|
|
642
871
|
/**
|
|
643
|
-
* Implementation backing
|
|
644
|
-
* `
|
|
645
|
-
*
|
|
872
|
+
* Implementation backing the `/models` route. Runs inside the AppKit
|
|
873
|
+
* user-context proxy so `getExecutionContext()` returns the OBO-scoped
|
|
874
|
+
* client and the catalogue reflects what the caller can invoke.
|
|
875
|
+
*/
|
|
876
|
+
private async listModelsResult(): Promise<ExecutionResult<ServingEndpointSummary[]>> {
|
|
877
|
+
return this.execute(async () => {
|
|
878
|
+
const client = getExecutionContext().client;
|
|
879
|
+
const host = (await client.config.getHost()).toString();
|
|
880
|
+
const serving = resolveServingConfig(this.config);
|
|
881
|
+
return nodeServing.listServingEndpoints(client, host, { ttlMs: serving.ttlMs });
|
|
882
|
+
}, modelCatalogueDefaults);
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/**
|
|
886
|
+
* Implementation backing the `listModels` export. The programmatic surface
|
|
887
|
+
* carries no status code, so a failed execution becomes a throw with a
|
|
888
|
+
* stable message; the routes branch on the {@link ExecutionResult} instead.
|
|
646
889
|
*/
|
|
647
890
|
private async listModels(): Promise<ServingEndpointSummary[]> {
|
|
648
|
-
const
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
891
|
+
const result = await this.listModelsResult();
|
|
892
|
+
if (result.ok) return result.data;
|
|
893
|
+
this.logger.warn("listModels:error", {
|
|
894
|
+
status: result.status,
|
|
895
|
+
error: result.message,
|
|
896
|
+
});
|
|
897
|
+
throw new ExecutionError(MODEL_CATALOGUE_FAILED_MESSAGE);
|
|
652
898
|
}
|
|
653
899
|
|
|
654
900
|
private async buildAgentAndServer(): Promise<void> {
|
|
@@ -673,11 +919,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
673
919
|
? createMemoryBuilder(this.config, this.servicePrincipalPool)
|
|
674
920
|
: undefined;
|
|
675
921
|
|
|
676
|
-
this.logger.debug("build:start", {
|
|
677
|
-
lakebase: memoryBuilder !== undefined,
|
|
678
|
-
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
679
|
-
});
|
|
680
|
-
|
|
681
922
|
// Build every agent declared in `config.agents` (or the built-in
|
|
682
923
|
// fallback when none are declared). Each agent's `model` resolves
|
|
683
924
|
// workspace URL + bearer at call time so concurrent requests get
|
|
@@ -767,21 +1008,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
767
1008
|
],
|
|
768
1009
|
});
|
|
769
1010
|
await this.mastraServer.init();
|
|
770
|
-
this.logger.
|
|
1011
|
+
this.logger.info("ready", {
|
|
771
1012
|
agents: Object.keys(this.built.agents),
|
|
772
1013
|
defaultAgent: this.built.defaultAgentId,
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
"/default-model",
|
|
778
|
-
"/suggestions",
|
|
779
|
-
"/route/feedback",
|
|
780
|
-
"/embed/:type/:id",
|
|
781
|
-
],
|
|
782
|
-
instanceStorage: instanceStorage !== undefined,
|
|
1014
|
+
apiAccess: this.config.apiAccess ?? "scoped",
|
|
1015
|
+
mcp: this.mcp ? `${this.basePath}${this.mcp.httpPath}` : "off",
|
|
1016
|
+
lakebase: memoryBuilder !== undefined,
|
|
1017
|
+
feedback: this.feedbackEnabled(),
|
|
783
1018
|
observability: observability !== undefined ? "mlflow" : "off",
|
|
784
|
-
mcp: this.mcp ? `/api/${this.name}${this.mcp.httpPath}` : "off",
|
|
785
1019
|
});
|
|
786
1020
|
}
|
|
787
1021
|
}
|
|
@@ -797,13 +1031,13 @@ type EmbedResolver = (
|
|
|
797
1031
|
req: express.Request,
|
|
798
1032
|
id: string,
|
|
799
1033
|
signal: AbortSignal,
|
|
800
|
-
) => Promise<unknown
|
|
1034
|
+
) => Promise<ExecutionResult<unknown>>;
|
|
801
1035
|
|
|
802
1036
|
/**
|
|
803
1037
|
* Parse the optional `?timeoutMs=<n>` query parameter from a
|
|
804
1038
|
* `GET /embed/chart/:id` request. Accepts a positive integer up
|
|
805
|
-
* to
|
|
806
|
-
* `undefined` so {@link fetchChart} falls back to its default.
|
|
1039
|
+
* to {@link MAX_EMBED_POLL_TIMEOUT_MS} (clamped) and rejects everything else
|
|
1040
|
+
* as `undefined` so {@link fetchChart} falls back to its default.
|
|
807
1041
|
* Express produces `string | string[] | undefined`; we normalize
|
|
808
1042
|
* to the first scalar before parsing.
|
|
809
1043
|
*/
|
|
@@ -812,7 +1046,7 @@ function parseTimeoutMs(raw: unknown): number | undefined {
|
|
|
812
1046
|
if (typeof v !== "string") return undefined;
|
|
813
1047
|
const n = Number(v);
|
|
814
1048
|
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
815
|
-
return Math.min(Math.floor(n),
|
|
1049
|
+
return Math.min(Math.floor(n), MAX_EMBED_POLL_TIMEOUT_MS);
|
|
816
1050
|
}
|
|
817
1051
|
|
|
818
1052
|
/**
|
|
@@ -830,4 +1064,45 @@ function parseStatementLimit(raw: unknown): number | undefined {
|
|
|
830
1064
|
return Math.floor(n);
|
|
831
1065
|
}
|
|
832
1066
|
|
|
1067
|
+
/**
|
|
1068
|
+
* Register the Mastra plugin on an AppKit app. Mounts the agents, the
|
|
1069
|
+
* `/route/*` chat surface, and (unless disabled) the MCP transport under
|
|
1070
|
+
* `/api/mastra`.
|
|
1071
|
+
*
|
|
1072
|
+
* @example Minimal app
|
|
1073
|
+
* ```ts
|
|
1074
|
+
* import { createApp } from "@databricks/appkit";
|
|
1075
|
+
* import { mastra } from "@dbx-tools/appkit-mastra";
|
|
1076
|
+
*
|
|
1077
|
+
* // No `agents`: a single built-in `default` analyst is registered, so the
|
|
1078
|
+
* // chat endpoint works out of the box.
|
|
1079
|
+
* const app = await createApp({ plugins: [mastra()] });
|
|
1080
|
+
* ```
|
|
1081
|
+
*
|
|
1082
|
+
* @example Two agents, a pinned model, and Genie
|
|
1083
|
+
* ```ts
|
|
1084
|
+
* import { createApp, lakebase } from "@databricks/appkit";
|
|
1085
|
+
* import { createAgent, mastra } from "@dbx-tools/appkit-mastra";
|
|
1086
|
+
*
|
|
1087
|
+
* const app = await createApp({
|
|
1088
|
+
* plugins: [
|
|
1089
|
+
* lakebase(),
|
|
1090
|
+
* mastra({
|
|
1091
|
+
* defaultModel: "databricks-claude-sonnet-4-6",
|
|
1092
|
+
* defaultAgent: "analyst",
|
|
1093
|
+
* genieSpaces: { sales: { spaceId: "01ef...", hint: "orders, returns" } },
|
|
1094
|
+
* agents: {
|
|
1095
|
+
* analyst: createAgent({
|
|
1096
|
+
* instructions: "You answer questions about sales.",
|
|
1097
|
+
* tools(plugins) {
|
|
1098
|
+
* return { ...plugins.genie?.toolkit() };
|
|
1099
|
+
* },
|
|
1100
|
+
* }),
|
|
1101
|
+
* helper: createAgent({ instructions: "You explain the analyst's answers." }),
|
|
1102
|
+
* },
|
|
1103
|
+
* }),
|
|
1104
|
+
* ],
|
|
1105
|
+
* });
|
|
1106
|
+
* ```
|
|
1107
|
+
*/
|
|
833
1108
|
export const mastra = toPlugin(MastraPlugin);
|