@dbx-tools/appkit-mastra 0.1.13 → 0.1.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +304 -645
- package/index.ts +46 -38
- package/package.json +58 -45
- package/src/agents.ts +224 -66
- package/src/chart.ts +531 -429
- package/src/config.ts +270 -19
- package/src/filesystems.ts +1090 -0
- package/src/genie.ts +1000 -660
- package/src/history.ts +166 -79
- package/src/mcp.ts +105 -0
- package/src/memory.ts +94 -92
- package/src/mlflow.ts +149 -0
- package/src/model.ts +75 -408
- package/src/observability.ts +121 -69
- package/src/pagination.ts +34 -0
- package/src/plugin.ts +552 -67
- package/src/processors.ts +168 -0
- package/src/rest.ts +67 -0
- package/src/server.ts +232 -45
- package/src/serving-sanitize.ts +167 -0
- package/src/serving.ts +27 -243
- package/src/statement.ts +89 -0
- package/src/storage-schema.ts +41 -0
- package/src/summarize.ts +176 -0
- package/src/threads.ts +338 -0
- package/src/workspaces.ts +346 -0
- package/src/writer.ts +44 -0
- package/tsconfig.json +41 -0
- package/dist/index.d.ts +0 -20
- package/dist/index.js +0 -20
- package/dist/src/agents.d.ts +0 -306
- package/dist/src/agents.js +0 -403
- package/dist/src/chart.d.ts +0 -170
- package/dist/src/chart.js +0 -491
- package/dist/src/config.d.ts +0 -183
- package/dist/src/config.js +0 -12
- package/dist/src/genie.d.ts +0 -131
- package/dist/src/genie.js +0 -630
- package/dist/src/history.d.ts +0 -67
- package/dist/src/history.js +0 -172
- package/dist/src/memory.d.ts +0 -100
- package/dist/src/memory.js +0 -242
- package/dist/src/model.d.ts +0 -159
- package/dist/src/model.js +0 -427
- package/dist/src/observability.d.ts +0 -33
- package/dist/src/observability.js +0 -71
- package/dist/src/plugin.d.ts +0 -130
- package/dist/src/plugin.js +0 -283
- package/dist/src/processors/strip-stale-charts.d.ts +0 -29
- package/dist/src/processors/strip-stale-charts.js +0 -96
- package/dist/src/server.d.ts +0 -46
- package/dist/src/server.js +0 -123
- package/dist/src/serving.d.ts +0 -156
- package/dist/src/serving.js +0 -231
- package/dist/src/tools/email.d.ts +0 -74
- package/dist/src/tools/email.js +0 -122
- package/dist/tsconfig.build.tsbuildinfo +0 -1
- package/src/processors/strip-stale-charts.ts +0 -105
- package/src/tools/email.ts +0 -147
package/src/plugin.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* AppKit plugin that builds one or more Mastra `Agent` instances and
|
|
3
|
-
* mounts the `@mastra/express` server
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* mounts the `@mastra/express` server. Clients drive the conversation
|
|
4
|
+
* over the standard Mastra agent stream (`@mastra/client-js`'s
|
|
5
|
+
* `getAgent(id).stream()`), so there's no bespoke chat transport to
|
|
6
|
+
* keep in sync.
|
|
7
7
|
*
|
|
8
8
|
* - Agents: registered through `config.agents` at plugin creation
|
|
9
9
|
* ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
|
|
@@ -18,13 +18,25 @@
|
|
|
18
18
|
* from `./memory.js`. Both auto-default to `true` when the
|
|
19
19
|
* `lakebase` plugin is registered (unless the caller passed
|
|
20
20
|
* `false` or a custom config). Storage namespaces per agent via
|
|
21
|
-
*
|
|
21
|
+
* {@link agentStorageSchemaName} per agent; the vector store is a single
|
|
22
22
|
* shared singleton across every agent.
|
|
23
23
|
* - Server: the Express subapp wiring lives in `./server.js`.
|
|
24
|
-
* - HTTP: AppKit mounts this plugin under `/api/mastra`.
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
|
|
25
|
+
* Mastra agent routes, the plugin registers `/route/history`
|
|
26
|
+
* (load + clear a thread's messages), `/route/threads` (list the
|
|
27
|
+
* caller's conversations + delete one), `/models`, `/suggestions`,
|
|
28
|
+
* `/route/feedback` (log a thumbs / comment to MLflow when feedback
|
|
29
|
+
* is enabled), and the generic `/embed/:type/:id` resolver for inline
|
|
30
|
+
* chart / data markers. The stock `@mastra/express` surface is gated
|
|
31
|
+
* by `config.apiAccess` (default `"scoped"`): only agent inference,
|
|
32
|
+
* read-only agent metadata, the `/route/*` routes, and (when enabled)
|
|
33
|
+
* MCP are dispatched to Mastra; admin / mutating / bulk-export routes
|
|
34
|
+
* are refused with `403`. See {@link isMastraRequestAllowed}.
|
|
35
|
+
* - MCP: opt in with `config.mcp` to expose the agents (and optionally
|
|
36
|
+
* tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
|
|
37
|
+
* instance via `mcpServers`, so `@mastra/express` serves the stock
|
|
38
|
+
* MCP transport routes (`/mcp/<serverId>/...`) under the mount. See
|
|
39
|
+
* `./mcp.js`.
|
|
28
40
|
*/
|
|
29
41
|
|
|
30
42
|
import {
|
|
@@ -37,28 +49,38 @@ import {
|
|
|
37
49
|
type PluginManifest,
|
|
38
50
|
type ResourceRequirement,
|
|
39
51
|
} from "@databricks/appkit";
|
|
40
|
-
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
41
|
-
import { chatRoute } from "@mastra/ai-sdk";
|
|
42
52
|
import type { Agent } from "@mastra/core/agent";
|
|
43
53
|
import { Mastra } from "@mastra/core/mastra";
|
|
44
54
|
import express from "express";
|
|
55
|
+
import type { Pool } from "pg";
|
|
45
56
|
|
|
46
|
-
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
47
|
-
import type { MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
|
|
48
|
-
import type { MastraPluginConfig } from "./config.js";
|
|
49
|
-
import { historyRoute } from "./history.js";
|
|
50
|
-
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
51
|
-
import { buildPhoenixObservability } from "./observability.js";
|
|
52
|
-
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
53
57
|
import {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
type
|
|
58
|
-
|
|
58
|
+
feedback,
|
|
59
|
+
routes,
|
|
60
|
+
type MastraClientConfig,
|
|
61
|
+
type MastraFeedbackRequest,
|
|
62
|
+
type StatementData,
|
|
63
|
+
} from "@dbx-tools/shared-mastra";
|
|
64
|
+
import { serving as nodeServing } from "@dbx-tools/model";
|
|
65
|
+
import { type ServingEndpointSummary } from "@dbx-tools/shared-model";
|
|
66
|
+
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents";
|
|
67
|
+
import { fetchChart } from "./chart";
|
|
68
|
+
import type { MastraPluginConfig } from "./config";
|
|
69
|
+
import { collectSpaceSuggestions, resolveGenieSpaces } from "./genie";
|
|
70
|
+
import { historyRoute } from "./history";
|
|
71
|
+
import { buildMcpServer, type ResolvedMcp } from "./mcp";
|
|
72
|
+
import { createMemoryBuilder, createServicePrincipalPool, needsLakebase } from "./memory";
|
|
73
|
+
import { logFeedback, resolveFeedbackEnabled } from "./mlflow";
|
|
74
|
+
import { buildObservability } from "./observability";
|
|
75
|
+
import { attachRoutePatchMiddleware, isMastraRequestAllowed, MastraServer } from "./server";
|
|
76
|
+
import { resolveServingConfig } from "./serving";
|
|
77
|
+
import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement";
|
|
78
|
+
import { threadsRoute } from "./threads";
|
|
79
|
+
import { error, log } from "@dbx-tools/shared-core";
|
|
80
|
+
import { plugin } from "@dbx-tools/appkit";
|
|
59
81
|
|
|
60
|
-
const GENIE_MANIFEST =
|
|
61
|
-
const LAKEBASE_MANIFEST =
|
|
82
|
+
const GENIE_MANIFEST = plugin.data(genie).plugin.manifest;
|
|
83
|
+
const LAKEBASE_MANIFEST = plugin.data(lakebase).plugin.manifest;
|
|
62
84
|
|
|
63
85
|
/**
|
|
64
86
|
* AppKit plugin (registered name: `mastra`) that hosts Mastra agents
|
|
@@ -76,6 +98,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
76
98
|
resources: {
|
|
77
99
|
required: [],
|
|
78
100
|
optional: [
|
|
101
|
+
// Surface the Genie resource binding (space id) declared by
|
|
102
|
+
// AppKit's `genie` plugin manifest. The Mastra plugin no
|
|
103
|
+
// longer uses the genie plugin's tools at runtime - the
|
|
104
|
+
// built-in Genie agent talks to Genie directly via
|
|
105
|
+
// `@dbx-tools/genie` - but reusing the manifest keeps the
|
|
106
|
+
// resource-binding shape identical to AppKit's so existing
|
|
107
|
+
// `app.yaml` configs and `genie({ spaces })` wiring keep
|
|
108
|
+
// working without change.
|
|
79
109
|
...GENIE_MANIFEST.resources.required,
|
|
80
110
|
...LAKEBASE_MANIFEST.resources.required,
|
|
81
111
|
],
|
|
@@ -102,18 +132,32 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
102
132
|
return resources;
|
|
103
133
|
}
|
|
104
134
|
|
|
105
|
-
private
|
|
135
|
+
private logger = log.logger(this);
|
|
106
136
|
private built: BuiltAgents | null = null;
|
|
107
137
|
private mastra: Mastra | null = null;
|
|
108
138
|
private mastraApp: express.Express | null = null;
|
|
109
139
|
private mastraServer: MastraServer | null = null;
|
|
140
|
+
/**
|
|
141
|
+
* The optional MCP server exposing this plugin's agents / tools, or
|
|
142
|
+
* `null` when `config.mcp` is disabled (the default). Built in
|
|
143
|
+
* {@link buildAgentAndServer} and registered on the Mastra instance.
|
|
144
|
+
*/
|
|
145
|
+
private mcp: ResolvedMcp | null = null;
|
|
146
|
+
/**
|
|
147
|
+
* Dedicated service-principal Lakebase pool backing Mastra memory /
|
|
148
|
+
* storage. Built once in {@link buildAgentAndServer} (outside any
|
|
149
|
+
* `asUser` scope, so it never inherits a request's OBO identity) and
|
|
150
|
+
* drained in {@link abortActiveOperations}. `null` until setup runs
|
|
151
|
+
* or when Lakebase isn't needed.
|
|
152
|
+
*/
|
|
153
|
+
private servicePrincipalPool: Pool | null = null;
|
|
110
154
|
|
|
111
155
|
override async setup(): Promise<void> {
|
|
112
156
|
// Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
|
|
113
157
|
// the lakebase pool is valid when storage/memory are enabled.
|
|
114
158
|
this.context?.onLifecycle("setup:complete", async () => {
|
|
115
159
|
this.applyLakebaseAutoDefaults();
|
|
116
|
-
this.
|
|
160
|
+
this.logger.info("setup:complete");
|
|
117
161
|
await this.buildAgentAndServer();
|
|
118
162
|
});
|
|
119
163
|
}
|
|
@@ -126,12 +170,32 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
126
170
|
* already in the registry by the time this fires.
|
|
127
171
|
*/
|
|
128
172
|
private applyLakebaseAutoDefaults(): void {
|
|
129
|
-
const hasLakebase =
|
|
173
|
+
const hasLakebase = plugin.instance(this.context, lakebase) !== undefined;
|
|
130
174
|
if (!hasLakebase) return;
|
|
131
175
|
if (this.config.storage === undefined) this.config.storage = true;
|
|
132
176
|
if (this.config.memory === undefined) this.config.memory = true;
|
|
133
177
|
}
|
|
134
178
|
|
|
179
|
+
/**
|
|
180
|
+
* Drain the memory service-principal pool on shutdown. AppKit calls
|
|
181
|
+
* this during teardown; the lakebase plugin closes its own SP / OBO
|
|
182
|
+
* pools the same way. Fire-and-forget so shutdown isn't blocked on a
|
|
183
|
+
* slow drain, and clear the handle so a re-`setup()` rebuilds it.
|
|
184
|
+
*/
|
|
185
|
+
override abortActiveOperations(): void {
|
|
186
|
+
super.abortActiveOperations();
|
|
187
|
+
if (this.servicePrincipalPool) {
|
|
188
|
+
this.logger.info("closing memory SP pool");
|
|
189
|
+
const pool = this.servicePrincipalPool;
|
|
190
|
+
this.servicePrincipalPool = null;
|
|
191
|
+
pool.end().catch((err) => {
|
|
192
|
+
this.logger.error("error closing memory SP pool", {
|
|
193
|
+
error: error.errorMessage(err),
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
135
199
|
override exports() {
|
|
136
200
|
return {
|
|
137
201
|
/**
|
|
@@ -147,14 +211,34 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
147
211
|
*/
|
|
148
212
|
get: (id: string): Agent | null => this.built?.agents[id] ?? null,
|
|
149
213
|
/**
|
|
150
|
-
* The agent
|
|
151
|
-
*
|
|
152
|
-
*
|
|
214
|
+
* The agent the client converses with when it doesn't name one.
|
|
215
|
+
* Resolves to `config.defaultAgent`, the first registered id, or
|
|
216
|
+
* the built-in `default` fallback.
|
|
153
217
|
*/
|
|
154
218
|
getDefault: (): Agent | null =>
|
|
155
219
|
(this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
|
|
156
220
|
/** Underlying Mastra instance for advanced use (custom routes etc.). */
|
|
157
221
|
getMastra: () => this.mastra,
|
|
222
|
+
/**
|
|
223
|
+
* MCP endpoint info when `config.mcp` is enabled, else `null`.
|
|
224
|
+
* Paths are absolute (under the plugin mount), ready to hand to an
|
|
225
|
+
* MCP client. Streamable HTTP is `http`; the SSE pair is the
|
|
226
|
+
* legacy transport.
|
|
227
|
+
*/
|
|
228
|
+
getMcp: (): {
|
|
229
|
+
serverId: string;
|
|
230
|
+
http: string;
|
|
231
|
+
sse: string;
|
|
232
|
+
messages: string;
|
|
233
|
+
} | null =>
|
|
234
|
+
this.mcp
|
|
235
|
+
? {
|
|
236
|
+
serverId: this.mcp.serverId,
|
|
237
|
+
http: `/api/${this.name}${this.mcp.httpPath}`,
|
|
238
|
+
sse: `/api/${this.name}${this.mcp.ssePath}`,
|
|
239
|
+
messages: `/api/${this.name}${this.mcp.messagePath}`,
|
|
240
|
+
}
|
|
241
|
+
: null,
|
|
158
242
|
/** Express subapp Mastra is mounted on; mostly for tests. */
|
|
159
243
|
getMastraServer: () => this.mastraServer,
|
|
160
244
|
/**
|
|
@@ -172,50 +256,348 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
172
256
|
* Returns the underlying `CacheManager.delete`/`clear` promise.
|
|
173
257
|
*/
|
|
174
258
|
clearModelsCache: (host?: string): Promise<void> =>
|
|
175
|
-
clearServingEndpointsCache(host),
|
|
259
|
+
nodeServing.clearServingEndpointsCache(host),
|
|
176
260
|
};
|
|
177
261
|
}
|
|
178
262
|
|
|
179
263
|
override clientConfig(): Record<string, unknown> {
|
|
180
264
|
// AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
|
|
181
|
-
// honors `config.name` overrides, so
|
|
182
|
-
//
|
|
265
|
+
// honors `config.name` overrides, so publishing `basePath` is
|
|
266
|
+
// enough for the client to stay correct under a custom mount id -
|
|
267
|
+
// the per-route segments are fixed (`routes.MASTRA_ROUTES`) and the
|
|
268
|
+
// client (`MastraPluginClient`) derives every endpoint from
|
|
269
|
+
// `basePath`.
|
|
183
270
|
// Return widens to `Record<string, unknown>` to satisfy the
|
|
184
271
|
// base-class signature; consumers read it through the typed
|
|
185
272
|
// `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
|
|
186
|
-
const basePath = `/api/${this.name}`;
|
|
187
273
|
const config: MastraClientConfig = {
|
|
188
|
-
basePath
|
|
189
|
-
chatPath: `${basePath}/route/chat`,
|
|
190
|
-
chatPathTemplate: `${basePath}/route/chat/:agentId`,
|
|
191
|
-
modelsPath: `${basePath}/models`,
|
|
192
|
-
historyPath: `${basePath}/route/history`,
|
|
193
|
-
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
274
|
+
basePath: `/api/${this.name}`,
|
|
194
275
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
195
276
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
277
|
+
feedbackEnabled: this.feedbackEnabled(),
|
|
196
278
|
};
|
|
197
279
|
return config as unknown as Record<string, unknown>;
|
|
198
280
|
}
|
|
199
281
|
|
|
282
|
+
/**
|
|
283
|
+
* Whether user feedback can be logged to MLflow. Delegates to
|
|
284
|
+
* {@link resolveFeedbackEnabled} so the client-config flag and the
|
|
285
|
+
* feedback route share the same gate as the server's trace-id header.
|
|
286
|
+
*/
|
|
287
|
+
private feedbackEnabled(): boolean {
|
|
288
|
+
return resolveFeedbackEnabled(this.config.feedback);
|
|
289
|
+
}
|
|
290
|
+
|
|
200
291
|
override injectRoutes(router: IAppRouter): void {
|
|
292
|
+
// Expose the MCP transport at the clean `/mcp` (plus the legacy
|
|
293
|
+
// `/sse` + `/messages`) under the plugin mount. `@mastra/express`
|
|
294
|
+
// mounts MCP under `/mcp/<serverId>/<transport>`, and the serverId
|
|
295
|
+
// defaults to the plugin name, so the raw route reads
|
|
296
|
+
// `/api/mastra/mcp/mastra/mcp` (doubled segment). This runs before
|
|
297
|
+
// the catch-all and rewrites the alias to the underlying route, so
|
|
298
|
+
// the public path is just `/api/<plugin>/mcp`; the query string
|
|
299
|
+
// (e.g. the SSE `sessionId`) is preserved.
|
|
300
|
+
router.use((req, _res, next) => {
|
|
301
|
+
const target = this.mcpRouteAlias(req.path);
|
|
302
|
+
if (target) {
|
|
303
|
+
const q = req.url.indexOf("?");
|
|
304
|
+
req.url = q >= 0 ? target + req.url.slice(q) : target;
|
|
305
|
+
}
|
|
306
|
+
next();
|
|
307
|
+
});
|
|
308
|
+
|
|
201
309
|
// `GET /models` exposes the cached endpoint list so clients can
|
|
202
310
|
// populate model pickers, validate `?model=` choices, etc. Must
|
|
203
311
|
// be registered before the catch-all that forwards everything to
|
|
204
312
|
// the Mastra subapp. Errors propagate to Express's default error
|
|
205
313
|
// handler via `next(err)` so callers see the real SDK message.
|
|
206
|
-
router.get(
|
|
314
|
+
router.get(routes.MASTRA_ROUTES.models, (req, res, next) => {
|
|
207
315
|
this.userScopedSelf(req)
|
|
208
316
|
.listModels()
|
|
209
317
|
.then((endpoints) => res.json({ endpoints }))
|
|
210
318
|
.catch(next);
|
|
211
319
|
});
|
|
212
320
|
|
|
213
|
-
|
|
321
|
+
// `GET /embed/:type/:id` is the single resolver for every embed
|
|
322
|
+
// marker the agent emits in prose (`[chart:<id>]`,
|
|
323
|
+
// `[data:<id>]`, ...). `:type` selects a resolver from the
|
|
324
|
+
// registry below; `:id` is that resolver's lookup key. The
|
|
325
|
+
// grammar (see `marker.ts`) is type-agnostic on purpose - new
|
|
326
|
+
// embed kinds are added by registering a resolver here, with no
|
|
327
|
+
// client or grammar change.
|
|
328
|
+
//
|
|
329
|
+
// Status codes:
|
|
330
|
+
// - 200 with the resolver's JSON body when the id resolves.
|
|
331
|
+
// - 404 when `:type` isn't registered (unsupported embed
|
|
332
|
+
// type) OR a registered resolver can't find `:id` (unknown
|
|
333
|
+
// / expired - e.g. a chart past its 1h TTL or a fabricated
|
|
334
|
+
// id the model never minted).
|
|
335
|
+
// - 400 when `:id` is empty.
|
|
336
|
+
//
|
|
337
|
+
// Per-type query knobs and behavior:
|
|
338
|
+
// - `chart`: long-polls the chart cache until the entry
|
|
339
|
+
// settles (`result` / `error`) or the budget elapses (then
|
|
340
|
+
// returns the still-processing entry to poll again).
|
|
341
|
+
// `?timeoutMs=<n>` (default 60s, capped 5min) tunes it.
|
|
342
|
+
// - `data`: one OBO-scoped Statement Execution fetch.
|
|
343
|
+
// `?limit=<n>` caps rows (clamped to STATEMENT_ROW_CAP).
|
|
344
|
+
//
|
|
345
|
+
// Built once (this handler is registered once) and keyed by the
|
|
346
|
+
// raw `:type` token. Each resolver gets the request (for query
|
|
347
|
+
// parsing + OBO scoping) and an `AbortSignal` bridged off the
|
|
348
|
+
// connection `close` event so a long-poll unblocks the instant
|
|
349
|
+
// the client disconnects. `undefined` from a resolver maps to a
|
|
350
|
+
// clean 404; thrown errors bubble through `next(err)`.
|
|
351
|
+
const embedResolvers: Record<string, EmbedResolver> = {
|
|
352
|
+
chart: (req, id, signal) => {
|
|
353
|
+
const timeoutMs = parseTimeoutMs(req.query["timeoutMs"]);
|
|
354
|
+
return fetchChart(id, {
|
|
355
|
+
...(timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
356
|
+
signal,
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
data: (req, id, signal) => {
|
|
360
|
+
const limit = parseStatementLimit(req.query["limit"]);
|
|
361
|
+
return this.userScopedSelf(req).fetchStatement(id, {
|
|
362
|
+
...(limit !== undefined ? { limit } : {}),
|
|
363
|
+
signal,
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
router.get(`${routes.MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
|
|
369
|
+
const type = req.params["type"] ?? "";
|
|
370
|
+
const id = req.params["id"];
|
|
371
|
+
const resolve = embedResolvers[type];
|
|
372
|
+
if (!resolve) {
|
|
373
|
+
res.status(404).json({ error: `unsupported embed type: ${type}` });
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (!id) {
|
|
377
|
+
res.status(400).json({ error: "id is required" });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
// Express's `req` predates `AbortSignal`; bridge the `close`
|
|
381
|
+
// event onto an `AbortController` so a closed connection
|
|
382
|
+
// unblocks any long-poll immediately and frees the request
|
|
383
|
+
// thread. The listener is GC'd with the request on normal
|
|
384
|
+
// completion.
|
|
385
|
+
const controller = new AbortController();
|
|
386
|
+
req.on("close", () => controller.abort());
|
|
387
|
+
resolve(req, id, controller.signal)
|
|
388
|
+
.then((entry) => {
|
|
389
|
+
if (entry === undefined) {
|
|
390
|
+
res.status(404).json({ error: `${type} not found` });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
res.json(entry);
|
|
394
|
+
})
|
|
395
|
+
.catch(next);
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
// `GET /suggestions` (and `/suggestions/:agentId`) returns the
|
|
399
|
+
// curated starter questions for the agent's Genie space(s) - the
|
|
400
|
+
// author-configured `sample_questions`, surfaced as one-tap
|
|
401
|
+
// prompts on the chat empty state. Returns `{ questions: [] }`
|
|
402
|
+
// when no Genie space is wired so the client renders a bare
|
|
403
|
+
// empty state (no built-in example prompts). The `:agentId`
|
|
404
|
+
// segment is accepted for URL symmetry with the chat / history
|
|
405
|
+
// routes; Genie spaces are resolved per-plugin, not per-agent,
|
|
406
|
+
// so it doesn't change the result. OBO-scoped like the other
|
|
407
|
+
// data routes so the space lookup runs as the calling user.
|
|
408
|
+
const handleSuggestions = (req: express.Request, res: express.Response): void => {
|
|
409
|
+
const controller = new AbortController();
|
|
410
|
+
req.on("close", () => controller.abort());
|
|
411
|
+
this.userScopedSelf(req)
|
|
412
|
+
.fetchSuggestions(controller.signal)
|
|
413
|
+
.then((questions) => res.json({ questions }))
|
|
414
|
+
.catch((err: unknown) => {
|
|
415
|
+
// Suggestions are a non-critical enhancement; a lookup
|
|
416
|
+
// failure should leave the chat usable with a bare empty
|
|
417
|
+
// state rather than surfacing a 500. Log and degrade.
|
|
418
|
+
this.logger.warn("suggestions:error", {
|
|
419
|
+
error: error.errorMessage(err),
|
|
420
|
+
});
|
|
421
|
+
res.json({ questions: [] });
|
|
422
|
+
});
|
|
423
|
+
};
|
|
424
|
+
router.get(routes.MASTRA_ROUTES.suggestions, handleSuggestions);
|
|
425
|
+
router.get(`${routes.MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
|
|
426
|
+
|
|
427
|
+
// `POST /route/feedback` logs a thumbs / comment assessment against
|
|
428
|
+
// a turn's MLflow trace (the `traceId` the client captured from the
|
|
429
|
+
// stream response's trace-id header). Registered on the AppKit
|
|
430
|
+
// router (like `/models`) rather than the Mastra subapp so it runs
|
|
431
|
+
// under the same OBO scope - the feedback is attributed to the
|
|
432
|
+
// signed-in user. Returns 404 when feedback is disabled so the
|
|
433
|
+
// client treats the capability as absent; 400 on a malformed body.
|
|
434
|
+
// A recorded assessment yields `{ ok: true }`; a soft failure (most
|
|
435
|
+
// often the trace hasn't finished exporting to MLflow yet) yields
|
|
436
|
+
// `{ ok: false }` without a 5xx so the UI can prompt a retry.
|
|
437
|
+
router.post(routes.MASTRA_ROUTES.feedback, (req, res, next) => {
|
|
438
|
+
if (!this.feedbackEnabled()) {
|
|
439
|
+
res.status(404).json({ ok: false });
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
const parsed = feedback.MastraFeedbackRequestSchema.safeParse(req.body);
|
|
443
|
+
if (!parsed.success) {
|
|
444
|
+
res.status(400).json({ ok: false, error: parsed.error.message });
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
this.userScopedSelf(req)
|
|
448
|
+
.logFeedback(parsed.data)
|
|
449
|
+
.then((assessmentId) =>
|
|
450
|
+
res.json({
|
|
451
|
+
ok: assessmentId !== undefined,
|
|
452
|
+
...(assessmentId ? { assessmentId } : {}),
|
|
453
|
+
}),
|
|
454
|
+
)
|
|
455
|
+
.catch(next);
|
|
456
|
+
});
|
|
457
|
+
|
|
458
|
+
router.use((req, res, next) => {
|
|
214
459
|
if (!this.mastraApp) return res.status(503).end();
|
|
215
|
-
|
|
460
|
+
// Gate the stock Mastra surface before dispatch. In the default
|
|
461
|
+
// "scoped" mode only agent inference, read-only agent metadata, this
|
|
462
|
+
// plugin's own `/route/*` routes, and (when enabled) MCP reach Mastra;
|
|
463
|
+
// admin / mutating / bulk-export routes are refused here. `req.path`
|
|
464
|
+
// is mount-relative under the plugin mount. See `server.ts`.
|
|
465
|
+
if (
|
|
466
|
+
!isMastraRequestAllowed(req.method, req.path, {
|
|
467
|
+
access: this.config.apiAccess ?? "scoped",
|
|
468
|
+
// Reflect the *resolved* MCP state, not raw `config.mcp`: MCP is
|
|
469
|
+
// on by default (`config.mcp` undefined), so gate on whether the
|
|
470
|
+
// server was actually built and mounted.
|
|
471
|
+
mcpEnabled: this.mcp !== null,
|
|
472
|
+
})
|
|
473
|
+
) {
|
|
474
|
+
res.status(403).json({ error: "Endpoint not exposed to the client (apiAccess=scoped)" });
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Dispatch through a real method, NOT the `mastraApp` property. The
|
|
478
|
+
// AppKit `asUser(req)` proxy wraps function-valued props with
|
|
479
|
+
// `value.bind(target)`. `mastraApp` is an express app whose `.bind` is
|
|
480
|
+
// the HTTP BIND route registrar (express defines a method per HTTP verb,
|
|
481
|
+
// and BIND is one), not `Function.prototype.bind` - so binding it through
|
|
482
|
+
// the proxy registers a bogus route and crashes `pathToRegexp`
|
|
483
|
+
// ("path must be a string ..."). This only manifests in production where
|
|
484
|
+
// an OBO token makes `userScopedSelf` return the proxy. `dispatchMastra`
|
|
485
|
+
// is a plain method (its `.bind` is the normal one) and invokes
|
|
486
|
+
// `this.mastraApp` off the real target, keeping the OBO scope active.
|
|
487
|
+
return this.userScopedSelf(req).dispatchMastra(req, res, next);
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Invoke the Mastra express sub-app. Exists as a method (instead of reading
|
|
493
|
+
* `this.mastraApp` through the `asUser(req)` proxy at the call site) so the
|
|
494
|
+
* proxy binds this plain method - whose `.bind` is `Function.prototype.bind`
|
|
495
|
+
* - rather than the express app, whose `.bind` is the HTTP BIND route
|
|
496
|
+
* registrar (see the note in `injectRoutes`). Runs inside the user scope so
|
|
497
|
+
* `getExecutionContext()` returns the OBO client for the agent/model
|
|
498
|
+
* resolvers.
|
|
499
|
+
*/
|
|
500
|
+
private dispatchMastra(
|
|
501
|
+
req: express.Request,
|
|
502
|
+
res: express.Response,
|
|
503
|
+
next: express.NextFunction,
|
|
504
|
+
): void {
|
|
505
|
+
this.mastraApp!(req, res, next);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* Map a clean, mount-relative MCP alias path to the underlying
|
|
510
|
+
* `@mastra/express` route. Returns `null` when MCP is off or the path
|
|
511
|
+
* isn't an alias. Collapses the stock `/mcp/<serverId>/<transport>`
|
|
512
|
+
* layout (serverId defaults to the plugin name) down to `/mcp`,
|
|
513
|
+
* `/sse`, and `/messages`.
|
|
514
|
+
*/
|
|
515
|
+
private mcpRouteAlias(path: string): string | null {
|
|
516
|
+
if (!this.mcp) return null;
|
|
517
|
+
const id = this.mcp.serverId;
|
|
518
|
+
if (path === "/mcp") return `/mcp/${id}/mcp`;
|
|
519
|
+
if (path === "/sse") return `/mcp/${id}/sse`;
|
|
520
|
+
if (path === "/messages") return `/mcp/${id}/messages`;
|
|
521
|
+
return null;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Implementation backing the `/suggestions` route. Runs inside the
|
|
526
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
527
|
+
* OBO-scoped client. Resolves the plugin's Genie spaces and merges
|
|
528
|
+
* their curated `sample_questions` (see {@link collectSpaceSuggestions}).
|
|
529
|
+
* Returns `[]` when no Genie space is configured so the client
|
|
530
|
+
* shows a bare empty state instead of built-in example prompts.
|
|
531
|
+
*/
|
|
532
|
+
private async fetchSuggestions(signal?: AbortSignal): Promise<string[]> {
|
|
533
|
+
const spaces = resolveGenieSpaces(this.config, this.context);
|
|
534
|
+
if (Object.keys(spaces).length === 0) return [];
|
|
535
|
+
const client = getExecutionContext().client;
|
|
536
|
+
return collectSpaceSuggestions({
|
|
537
|
+
spaces,
|
|
538
|
+
client,
|
|
539
|
+
...(signal ? { signal } : {}),
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Implementation backing the `/route/feedback` route. Runs inside the
|
|
545
|
+
* AppKit user-context proxy so `getExecutionContext()` returns the
|
|
546
|
+
* OBO-scoped client and the assessment is attributed to the signed-in
|
|
547
|
+
* user (their email / id as the assessment source). Returns the
|
|
548
|
+
* created assessment id on success, or `undefined` on a soft failure
|
|
549
|
+
* (see {@link logFeedback} in `./mlflow.js`).
|
|
550
|
+
*/
|
|
551
|
+
private async logFeedback(feedback: MastraFeedbackRequest): Promise<string | undefined> {
|
|
552
|
+
const ctx = getExecutionContext();
|
|
553
|
+
const sourceId =
|
|
554
|
+
"userEmail" in ctx && ctx.userEmail
|
|
555
|
+
? ctx.userEmail
|
|
556
|
+
: "userId" in ctx
|
|
557
|
+
? ctx.userId
|
|
558
|
+
: ctx.serviceUserId;
|
|
559
|
+
return logFeedback(ctx.client, {
|
|
560
|
+
...feedback,
|
|
561
|
+
...(sourceId ? { sourceId } : {}),
|
|
216
562
|
});
|
|
217
563
|
}
|
|
218
564
|
|
|
565
|
+
/**
|
|
566
|
+
* Implementation backing the `data` embed resolver
|
|
567
|
+
* (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
|
|
568
|
+
* `getExecutionContext()` returns the OBO-scoped workspace
|
|
569
|
+
* client, then reuses the same `fetchStatementData` pipeline
|
|
570
|
+
* the `get_statement` tool runs so the LLM and the UI see the
|
|
571
|
+
* exact same shape for the same statement.
|
|
572
|
+
*
|
|
573
|
+
* Returns `undefined` for upstream 404s so the route can map
|
|
574
|
+
* them to a clean HTTP 404; any other failure bubbles up.
|
|
575
|
+
*/
|
|
576
|
+
private async fetchStatement(
|
|
577
|
+
statementId: string,
|
|
578
|
+
options: { limit?: number; signal?: AbortSignal } = {},
|
|
579
|
+
): Promise<StatementData | undefined> {
|
|
580
|
+
const client = getExecutionContext().client;
|
|
581
|
+
const limit = Math.min(options.limit ?? STATEMENT_ROW_CAP, STATEMENT_ROW_CAP);
|
|
582
|
+
try {
|
|
583
|
+
const data = await fetchStatementData(client, statementId, {
|
|
584
|
+
limit,
|
|
585
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
586
|
+
});
|
|
587
|
+
return {
|
|
588
|
+
columns: data.columns,
|
|
589
|
+
rows: data.rows,
|
|
590
|
+
rowCount: data.rowCount,
|
|
591
|
+
truncated: data.rows.length < data.rowCount,
|
|
592
|
+
};
|
|
593
|
+
} catch (err) {
|
|
594
|
+
// The Databricks SDK throws on 404; surface as `undefined`
|
|
595
|
+
// so the route maps to a clean HTTP 404 instead of a 500.
|
|
596
|
+
if (error.errorContext(err).notAccessible) return undefined;
|
|
597
|
+
throw err;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
|
|
219
601
|
/**
|
|
220
602
|
* Return `this.asUser(req)` when the request carries an OBO token,
|
|
221
603
|
* otherwise return `this` directly. Prevents the noisy AppKit warn
|
|
@@ -238,19 +620,32 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
238
620
|
const client = getExecutionContext().client;
|
|
239
621
|
const host = (await client.config.getHost()).toString();
|
|
240
622
|
const serving = resolveServingConfig(this.config);
|
|
241
|
-
return listServingEndpoints(client, host, { ttlMs: serving.ttlMs });
|
|
623
|
+
return nodeServing.listServingEndpoints(client, host, { ttlMs: serving.ttlMs });
|
|
242
624
|
}
|
|
243
625
|
|
|
244
626
|
private async buildAgentAndServer(): Promise<void> {
|
|
245
|
-
// Per-agent memory factory.
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
249
|
-
|
|
250
|
-
|
|
627
|
+
// Per-agent memory factory. When any storage / memory setting needs
|
|
628
|
+
// Postgres, stand up a dedicated service-principal pool first so
|
|
629
|
+
// memory acts as the app SP (owner of the `mastra_*` schemas),
|
|
630
|
+
// never the per-request OBO identity the chat turn runs under.
|
|
631
|
+
// `getPgConfig()` is read here, outside any `asUser` scope, so it
|
|
632
|
+
// returns the SP connection target + token refresh plus any
|
|
633
|
+
// `lakebase({ pool })` overrides; `require` turns a missing
|
|
634
|
+
// sibling into a clear wiring error. The builder caches the shared
|
|
635
|
+
// `PgVector` singleton so registering N agents stays cheap. See
|
|
636
|
+
// `./memory.js`.
|
|
637
|
+
if (needsLakebase(this.config)) {
|
|
638
|
+
const spPgConfig = plugin
|
|
639
|
+
.require(this.context, lakebase, this.config)
|
|
640
|
+
.exports()
|
|
641
|
+
.getPgConfig();
|
|
642
|
+
this.servicePrincipalPool = await createServicePrincipalPool(spPgConfig);
|
|
643
|
+
}
|
|
644
|
+
const memoryBuilder = this.servicePrincipalPool
|
|
645
|
+
? createMemoryBuilder(this.config, this.servicePrincipalPool)
|
|
251
646
|
: undefined;
|
|
252
647
|
|
|
253
|
-
this.
|
|
648
|
+
this.logger.debug("build:start", {
|
|
254
649
|
lakebase: memoryBuilder !== undefined,
|
|
255
650
|
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
256
651
|
});
|
|
@@ -265,7 +660,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
265
660
|
config: this.config,
|
|
266
661
|
context: this.context,
|
|
267
662
|
memoryBuilder,
|
|
268
|
-
log: this.
|
|
663
|
+
log: this.logger,
|
|
269
664
|
});
|
|
270
665
|
|
|
271
666
|
// `mastra.server.apiRoutes` is only honored by Mastra's standalone
|
|
@@ -280,17 +675,33 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
280
675
|
// `agent.resumeStream()` errors with "could not find a suspended
|
|
281
676
|
// run" and the approval UI hangs after the user clicks Approve.
|
|
282
677
|
const instanceStorage = memoryBuilder?.instanceStorage();
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
678
|
+
// Wire Mastra's tracer into AppKit's global OTel pipeline via
|
|
679
|
+
// `@mastra/otel-bridge`. Mastra spans become native OTel spans on
|
|
680
|
+
// whatever tracer provider `TelemetryManager` registered during
|
|
681
|
+
// `createApp`, so the OTLP endpoint / headers / sampling are
|
|
682
|
+
// env-driven and shared with every other AppKit plugin.
|
|
683
|
+
const observability = await buildObservability({
|
|
684
|
+
serviceName: this.name,
|
|
685
|
+
enabled: this.config.observability,
|
|
686
|
+
});
|
|
687
|
+
// Optional MCP exposure: build a Mastra MCP server from the
|
|
688
|
+
// registered agents (and, opt-in, the ambient tools) and register
|
|
689
|
+
// it on the Mastra instance. `@mastra/express` serves the stock MCP
|
|
690
|
+
// transport routes (`/mcp/<serverId>/...`) off `mcpServers`, so the
|
|
691
|
+
// catch-all dispatch below already routes MCP requests under OBO -
|
|
692
|
+
// no bespoke route needed. See `./mcp.js`.
|
|
693
|
+
this.mcp = buildMcpServer({
|
|
694
|
+
config: this.config,
|
|
695
|
+
pluginName: this.name,
|
|
696
|
+
displayName: MastraPlugin.manifest.displayName,
|
|
697
|
+
agents: this.built.agents,
|
|
698
|
+
ambientTools: this.built.ambientTools,
|
|
699
|
+
});
|
|
290
700
|
this.mastra = new Mastra({
|
|
291
701
|
agents: this.built.agents,
|
|
292
702
|
...(instanceStorage ? { storage: instanceStorage } : {}),
|
|
293
703
|
...(observability ? { observability } : {}),
|
|
704
|
+
...(this.mcp ? { mcpServers: { [this.mcp.serverId]: this.mcp.server } } : {}),
|
|
294
705
|
});
|
|
295
706
|
this.mastraApp = express();
|
|
296
707
|
attachRoutePatchMiddleware(this.mastraApp);
|
|
@@ -299,21 +710,95 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
299
710
|
mastra: this.mastra,
|
|
300
711
|
prefix: "",
|
|
301
712
|
customApiRoutes: [
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
historyRoute({
|
|
713
|
+
// `historyRoute` registers both GET (load) and DELETE
|
|
714
|
+
// (clear) on the same path, so it returns an array we
|
|
715
|
+
// splice in.
|
|
716
|
+
...historyRoute({
|
|
717
|
+
path: routes.MASTRA_ROUTES.history,
|
|
718
|
+
agent: this.built.defaultAgentId,
|
|
719
|
+
}),
|
|
720
|
+
// Assert the `:agentId` template type: the per-package build's
|
|
721
|
+
// NodeNext resolution widens the imported `routes.MASTRA_ROUTES.history`
|
|
722
|
+
// to `string` (the source/bundler typecheck keeps it a literal),
|
|
723
|
+
// which would otherwise drop this out of the dynamic-agent
|
|
724
|
+
// overload and demand a fixed `agent`.
|
|
725
|
+
...historyRoute({
|
|
726
|
+
path: `${routes.MASTRA_ROUTES.history}/:agentId` as `${string}:agentId`,
|
|
727
|
+
}),
|
|
728
|
+
// `threadsRoute` registers GET (list the caller's conversation
|
|
729
|
+
// threads) and DELETE (remove the targeted thread) on the same
|
|
730
|
+
// path; both the default-agent and dynamic-agent mounts are
|
|
731
|
+
// spliced in, mirroring the history routes above.
|
|
732
|
+
...threadsRoute({
|
|
733
|
+
path: routes.MASTRA_ROUTES.threads,
|
|
734
|
+
agent: this.built.defaultAgentId,
|
|
735
|
+
}),
|
|
736
|
+
...threadsRoute({
|
|
737
|
+
path: `${routes.MASTRA_ROUTES.threads}/:agentId` as `${string}:agentId`,
|
|
738
|
+
}),
|
|
306
739
|
],
|
|
307
740
|
});
|
|
308
741
|
await this.mastraServer.init();
|
|
309
|
-
this.
|
|
742
|
+
this.logger.debug("build:done", {
|
|
310
743
|
agents: Object.keys(this.built.agents),
|
|
311
744
|
defaultAgent: this.built.defaultAgentId,
|
|
312
|
-
routes: [
|
|
745
|
+
routes: [
|
|
746
|
+
"/route/history",
|
|
747
|
+
"/route/threads",
|
|
748
|
+
"/models",
|
|
749
|
+
"/suggestions",
|
|
750
|
+
"/route/feedback",
|
|
751
|
+
"/embed/:type/:id",
|
|
752
|
+
],
|
|
313
753
|
instanceStorage: instanceStorage !== undefined,
|
|
314
|
-
observability: observability !== undefined ? "
|
|
754
|
+
observability: observability !== undefined ? "mlflow" : "off",
|
|
755
|
+
mcp: this.mcp ? `/api/${this.name}${this.mcp.httpPath}` : "off",
|
|
315
756
|
});
|
|
316
757
|
}
|
|
317
758
|
}
|
|
318
759
|
|
|
760
|
+
/**
|
|
761
|
+
* Resolver for one embed `<type>` behind the generic
|
|
762
|
+
* `GET /embed/:type/:id` route. Returns the JSON body to send on
|
|
763
|
+
* success, or `undefined` to signal a 404 (unknown / expired id).
|
|
764
|
+
* `signal` aborts when the client disconnects so long-polling
|
|
765
|
+
* resolvers (e.g. `chart`) unblock immediately.
|
|
766
|
+
*/
|
|
767
|
+
type EmbedResolver = (
|
|
768
|
+
req: express.Request,
|
|
769
|
+
id: string,
|
|
770
|
+
signal: AbortSignal,
|
|
771
|
+
) => Promise<unknown | undefined>;
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Parse the optional `?timeoutMs=<n>` query parameter from a
|
|
775
|
+
* `GET /embed/chart/:id` request. Accepts a positive integer up
|
|
776
|
+
* to 5 minutes (clamped) and rejects everything else as
|
|
777
|
+
* `undefined` so {@link fetchChart} falls back to its default.
|
|
778
|
+
* Express produces `string | string[] | undefined`; we normalize
|
|
779
|
+
* to the first scalar before parsing.
|
|
780
|
+
*/
|
|
781
|
+
function parseTimeoutMs(raw: unknown): number | undefined {
|
|
782
|
+
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
783
|
+
if (typeof v !== "string") return undefined;
|
|
784
|
+
const n = Number(v);
|
|
785
|
+
if (!Number.isFinite(n) || n <= 0) return undefined;
|
|
786
|
+
return Math.min(Math.floor(n), 5 * 60_000);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Parse the optional `?limit=<n>` query parameter from a
|
|
791
|
+
* `GET /embed/data/:id` request. Accepts a non-negative
|
|
792
|
+
* integer and lets the route clamp to `STATEMENT_ROW_CAP`;
|
|
793
|
+
* rejects anything else as `undefined` so the route falls back
|
|
794
|
+
* to the server-side cap.
|
|
795
|
+
*/
|
|
796
|
+
function parseStatementLimit(raw: unknown): number | undefined {
|
|
797
|
+
const v = Array.isArray(raw) ? raw[0] : raw;
|
|
798
|
+
if (typeof v !== "string") return undefined;
|
|
799
|
+
const n = Number(v);
|
|
800
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
801
|
+
return Math.floor(n);
|
|
802
|
+
}
|
|
803
|
+
|
|
319
804
|
export const mastra = toPlugin(MastraPlugin);
|