@dbx-tools/appkit-mastra 0.1.58 → 0.1.68

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +41 -1
  2. package/dist/index.d.ts +1229 -17
  3. package/dist/index.js +3348 -19
  4. package/package.json +17 -30
  5. package/dist/src/agents.d.ts +0 -316
  6. package/dist/src/agents.js +0 -470
  7. package/dist/src/chart.d.ts +0 -153
  8. package/dist/src/chart.js +0 -570
  9. package/dist/src/config.d.ts +0 -295
  10. package/dist/src/config.js +0 -55
  11. package/dist/src/genie.d.ts +0 -161
  12. package/dist/src/genie.js +0 -973
  13. package/dist/src/history.d.ts +0 -95
  14. package/dist/src/history.js +0 -278
  15. package/dist/src/memory.d.ts +0 -109
  16. package/dist/src/memory.js +0 -253
  17. package/dist/src/model.d.ts +0 -52
  18. package/dist/src/model.js +0 -198
  19. package/dist/src/observability.d.ts +0 -64
  20. package/dist/src/observability.js +0 -83
  21. package/dist/src/plugin.d.ts +0 -177
  22. package/dist/src/plugin.js +0 -554
  23. package/dist/src/processor.d.ts +0 -8
  24. package/dist/src/processor.js +0 -40
  25. package/dist/src/processors/strip-stale-charts.d.ts +0 -32
  26. package/dist/src/processors/strip-stale-charts.js +0 -98
  27. package/dist/src/server.d.ts +0 -51
  28. package/dist/src/server.js +0 -152
  29. package/dist/src/serving.d.ts +0 -65
  30. package/dist/src/serving.js +0 -79
  31. package/dist/src/statement.d.ts +0 -66
  32. package/dist/src/statement.js +0 -111
  33. package/dist/src/writer.d.ts +0 -23
  34. package/dist/src/writer.js +0 -37
  35. package/dist/tsconfig.build.tsbuildinfo +0 -1
  36. package/index.ts +0 -27
  37. package/src/agents.ts +0 -772
  38. package/src/chart.ts +0 -716
  39. package/src/config.ts +0 -320
  40. package/src/genie.ts +0 -1123
  41. package/src/history.ts +0 -322
  42. package/src/memory.ts +0 -293
  43. package/src/model.ts +0 -257
  44. package/src/observability.ts +0 -114
  45. package/src/plugin.ts +0 -623
  46. package/src/processor.ts +0 -46
  47. package/src/processors/strip-stale-charts.ts +0 -102
  48. package/src/server.ts +0 -195
  49. package/src/serving.ts +0 -104
  50. package/src/statement.ts +0 -120
  51. package/src/writer.ts +0 -44
package/dist/index.js CHANGED
@@ -1,20 +1,3349 @@
1
+ import { ChartSchema, ChartTypeSchema, MASTRA_ROUTES, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
3
+ import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils } from "@dbx-tools/shared";
4
+ import { Agent } from "@mastra/core/agent";
5
+ import { createTool, createTool as createTool$1 } from "@mastra/core/tools";
6
+ import { CacheManager, Plugin, genie, getExecutionContext, getUsernameWithApiLookup, lakebase, toPlugin } from "@databricks/appkit";
7
+ import { z } from "zod";
8
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
9
+ import { WorkspaceClient } from "@databricks/sdk-experimental";
10
+ import { genieEventChat, genieSampleQuestions, getGenieSpace } from "@dbx-tools/genie";
11
+ import { GenieMessageSchema } from "@dbx-tools/genie-shared";
12
+ import { MCPServer } from "@mastra/mcp";
13
+ import { Mastra } from "@mastra/core/mastra";
14
+ import express from "express";
15
+ import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
16
+ import { registerApiRoute } from "@mastra/core/server";
17
+ import { fastembed } from "@mastra/fastembed";
18
+ import { Memory } from "@mastra/memory";
19
+ import { PgVector, PostgresStore } from "@mastra/pg";
20
+ import { randomUUID } from "node:crypto";
21
+ import { Pool } from "pg";
22
+ import { Observability } from "@mastra/observability";
23
+ import { OtelBridge } from "@mastra/otel-bridge";
24
+ import { MastraServer } from "@mastra/express";
25
+
26
+ export * from "@dbx-tools/appkit-mastra-shared"
27
+
28
+ export * from "@dbx-tools/model"
29
+
30
+ //#region packages/appkit-mastra/src/config.ts
1
31
  /**
2
- * Server-side entry point for the AppKit Mastra integration. Mounts
3
- * the plugin via {@link mastra} and re-exports the full server surface
4
- * (config, agent wiring, Genie and chart tooling, and dynamic Model
5
- * Serving resolution) so apps build agent backends from one import.
6
- *
7
- * Client-side consumers should import URL helpers and the
8
- * {@link MastraClientConfig} type from `@dbx-tools/appkit-mastra-shared`
9
- * instead - that package is pure (no pg / fastembed / Mastra deps) and
10
- * is the right surface for browser bundles and `usePluginClientConfig`
11
- * consumers.
12
- */
13
- export * from "@dbx-tools/appkit-mastra-shared";
14
- export * from "@dbx-tools/model";
15
- export * from "./src/agents.js";
16
- export * from "./src/chart.js";
17
- export * from "./src/config.js";
18
- export * from "./src/genie.js";
19
- export * from "./src/plugin.js";
20
- export { MASTRA_MODEL_OVERRIDE_KEY, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, extractModelOverride, } from "./src/serving.js";
32
+ * `RequestContext` key under which {@link MastraServer} stores the
33
+ * resolved AppKit user. `model.ts` reads it to mint user-scoped
34
+ * Databricks tokens.
35
+ */
36
+ const MASTRA_USER_KEY = "mastra__user";
37
+ /**
38
+ * `RequestContext` keys for AppKit user metadata stamped by
39
+ * {@link MastraServer}. Surfaced as trace metadata via
40
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so traces are filterable by who
41
+ * issued the request without leaking the full user object.
42
+ */
43
+ const MASTRA_USER_NAME_KEY = "mastra__userName";
44
+ const MASTRA_USER_EMAIL_KEY = "mastra__userEmail";
45
+ /**
46
+ * `RequestContext` key for the per-HTTP-request id stamped by
47
+ * {@link MastraServer}. Reads `X-Request-Id` from the incoming
48
+ * headers when present (so an upstream load balancer / API gateway
49
+ * can keep its trace correlation), falls back to a freshly minted
50
+ * UUID. Echoed back on the response and surfaced on every span via
51
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} so logs and traces share a
52
+ * join key.
53
+ */
54
+ const MASTRA_REQUEST_ID_KEY = "mastra__requestId";
55
+ /**
56
+ * Canonical list of `RequestContext` keys we want Mastra to extract
57
+ * as metadata on every observability span (agent runs, model calls,
58
+ * tool invocations, workflow steps).
59
+ *
60
+ * Mirrors {@link https://mastra.ai/docs/observability/tracing/overview#automatic-metadata-from-requestcontext}:
61
+ * passed verbatim into `Observability.configs[*].requestContextKeys`,
62
+ * so any key listed here is read from `RequestContext` at trace
63
+ * start and attached as scalar span metadata. Keep the set to plain
64
+ * scalars - never include {@link MASTRA_USER_KEY} (it carries the
65
+ * full AppKit execution context with a `WorkspaceClient` reference).
66
+ *
67
+ * Order is purely cosmetic; Mastra de-dupes internally.
68
+ */
69
+ const TRACE_REQUEST_CONTEXT_KEYS = [
70
+ MASTRA_RESOURCE_ID_KEY,
71
+ MASTRA_THREAD_ID_KEY,
72
+ MASTRA_REQUEST_ID_KEY,
73
+ MASTRA_USER_NAME_KEY,
74
+ MASTRA_USER_EMAIL_KEY,
75
+ "mastra__model_override"
76
+ ];
77
+
78
+ //#endregion
79
+ //#region packages/appkit-mastra/src/serving.ts
80
+ /**
81
+ * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
82
+ *
83
+ * The live `/serving-endpoints` catalogue access, fuzzy name
84
+ * resolution, and class/fallback selection all live in
85
+ * `@dbx-tools/model`; this module only adds what is specific to the
86
+ * Mastra plugin: pulling a per-request model override off an HTTP
87
+ * request (header / query / body) and projecting the plugin config
88
+ * onto the knobs `buildModel` and the `/models` route share.
89
+ */
90
+ /**
91
+ * `RequestContext` key under which {@link MastraServer} stores the
92
+ * per-request model override (header / query / body). `model.ts`
93
+ * reads it before falling back to the agent / plugin default.
94
+ */
95
+ const MASTRA_MODEL_OVERRIDE_KEY = "mastra__model_override";
96
+ /**
97
+ * Pull a model override out of a single HTTP request, checking
98
+ * sources in priority order:
99
+ *
100
+ * 1. `X-Mastra-Model` header
101
+ * 2. `?model=` query string parameter
102
+ * 3. Body field (`model` or `modelId`, in that order)
103
+ *
104
+ * Returns `null` when nothing is set, so callers can wrap with
105
+ * `if (override) ...` without juggling empty strings. Body inspection
106
+ * is lenient - any plain object with one of the configured keys
107
+ * counts, mirroring how AI SDK chat clients pass arbitrary metadata
108
+ * alongside `messages`.
109
+ */
110
+ function extractModelOverride(req) {
111
+ const headers = req.headers;
112
+ if (headers) {
113
+ const headerVal = stringUtils.firstNonEmpty(headers[MODEL_OVERRIDE_HEADER] ?? headers[MODEL_OVERRIDE_HEADER.toLowerCase()]);
114
+ if (headerVal) return headerVal;
115
+ }
116
+ if (req.query) {
117
+ const queryVal = stringUtils.firstNonEmpty(req.query[MODEL_OVERRIDE_QUERY]);
118
+ if (queryVal) return queryVal;
119
+ }
120
+ if (req.body && typeof req.body === "object") {
121
+ const record = req.body;
122
+ for (const field of MODEL_OVERRIDE_BODY_FIELDS) {
123
+ const bodyVal = stringUtils.firstNonEmpty(record[field]);
124
+ if (bodyVal) return bodyVal;
125
+ }
126
+ }
127
+ return null;
128
+ }
129
+ /**
130
+ * Read the fuzzy-resolution config knobs off the plugin config with
131
+ * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
132
+ * the `/models` route agree on what "enabled" means.
133
+ *
134
+ * `fallbacks` is the priority-ordered list `resolveModel` walks first
135
+ * when nothing explicit is set; defaults to an empty list so the
136
+ * generic resolver falls through to the live catalogue and its own
137
+ * `FALLBACK_MODEL_IDS` floor.
138
+ */
139
+ function resolveServingConfig(config) {
140
+ return {
141
+ ttlMs: config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
142
+ threshold: config.modelFuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD,
143
+ fuzzy: config.modelFuzzyMatch !== false,
144
+ allowOverride: config.modelOverride !== false,
145
+ fallbacks: config.defaultModelFallbacks ?? []
146
+ };
147
+ }
148
+
149
+ //#endregion
150
+ //#region packages/appkit-mastra/src/model.ts
151
+ /**
152
+ * Databricks Model Serving resolver for Mastra agents.
153
+ *
154
+ * Each agent step calls {@link buildModel} with the active
155
+ * `RequestContext`. The user stamped by `MastraServer` carries an
156
+ * AppKit `WorkspaceClient`; we ask it for the workspace host and a
157
+ * fresh bearer header, then point Mastra's OpenAI-compatible provider
158
+ * at `/serving-endpoints` on that host.
159
+ *
160
+ * This module only adds the Mastra-specific glue. The actual model
161
+ * selection - listing the workspace catalogue and resolving an
162
+ * explicit name / class / fallback chain to a real endpoint id - lives
163
+ * in `@dbx-tools/model` ({@link selectModel}) so non-Mastra consumers
164
+ * (e.g. a job that just needs a model name) can reuse it. Here we
165
+ * assemble the explicit ask from Mastra's request context (the
166
+ * per-request override under {@link MASTRA_MODEL_OVERRIDE_KEY}, the
167
+ * agent / plugin `modelId`, or `DATABRICKS_SERVING_ENDPOINT_NAME`),
168
+ * pass the plugin's fuzzy / class / fallback knobs through, and wrap
169
+ * the resolved id in the OpenAI-compatible provider config Mastra
170
+ * expects. Catalogue fetches fail loud: network / auth errors
171
+ * propagate so callers see the real SDK message.
172
+ */
173
+ /**
174
+ * Resolve a `MastraModelConfig` for the current agent step. Runs
175
+ * while `agent.stream` is inside the `asUser(req)` scope so tokens
176
+ * are user-scoped; outside an active user context the workspace
177
+ * client falls back to the service principal.
178
+ */
179
+ async function buildModel(config, requestContext, overrides = {}) {
180
+ setupFetchInterceptor();
181
+ const executionContext = requestContext.get(MASTRA_USER_KEY)?.executionContext ?? getExecutionContext();
182
+ const clientConfig = executionContext.client.config;
183
+ const host = (await clientConfig.getHost()).toString();
184
+ const headers = new Headers();
185
+ await clientConfig.authenticate(headers);
186
+ const url = new URL("/serving-endpoints", host).toString().replace(/\/$/, "");
187
+ const log = logUtils.logger(config);
188
+ const serving = resolveServingConfig(config);
189
+ const requested = (serving.allowOverride ? requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) : void 0) ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
190
+ const requestedClass = requested !== void 0 ? parseModelClass(requested) : null;
191
+ const explicit = requestedClass === null ? requested : void 0;
192
+ const modelClass = requestedClass ?? overrides.modelClass;
193
+ const { modelId, source } = await selectModel(executionContext.client, host, {
194
+ ...explicit !== void 0 ? { explicit } : {},
195
+ fuzzy: serving.fuzzy,
196
+ threshold: serving.threshold,
197
+ ...modelClass !== void 0 ? { modelClass } : {},
198
+ fallbacks: serving.fallbacks,
199
+ ttlMs: serving.ttlMs
200
+ });
201
+ log.debug("model selected", {
202
+ modelId,
203
+ source,
204
+ requested
205
+ });
206
+ return {
207
+ providerId: config.providerId ?? "databricks",
208
+ modelId,
209
+ url,
210
+ headers: Object.fromEntries(headers.entries())
211
+ };
212
+ }
213
+ /** Path prefix that identifies a Databricks Model Serving REST call. */
214
+ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
215
+ /**
216
+ * Install a single shared `globalThis.fetch` wrapper for every POST to
217
+ * `/serving-endpoints/...`. The wrapper does two things:
218
+ *
219
+ * 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
220
+ * stream-replay quirks that Databricks-hosted Claude rejects (see
221
+ * {@link sanitizeServingMessages}).
222
+ * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
223
+ * 4xx debugging doesn't have to fight AI SDK's `[Array]`
224
+ * formatter.
225
+ *
226
+ * Safe to call from any hot path: {@link commonUtils.memoize} ensures
227
+ * the wrapper is installed at most once per process, so subsequent
228
+ * calls collapse to a single cached promise even when
229
+ * {@link buildModel} fires on every agent step.
230
+ */
231
+ const setupFetchInterceptor = commonUtils.memoize(() => {
232
+ const log = logUtils.logger("mastra/llm");
233
+ const original = globalThis.fetch.bind(globalThis);
234
+ globalThis.fetch = (async (input, init) => {
235
+ const url = netUtils.urlBuilder(input);
236
+ if (!url || !url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) || typeof init?.body !== "string") return original(input, init);
237
+ const rewritten = rewriteServingBody(init.body);
238
+ if (rewritten !== init.body) init = {
239
+ ...init,
240
+ body: rewritten
241
+ };
242
+ try {
243
+ log.debug("POST", {
244
+ url: url.toString(),
245
+ body: JSON.parse(rewritten)
246
+ });
247
+ } catch {
248
+ log.debug("POST", {
249
+ url: url.toString(),
250
+ bodyType: "non-JSON"
251
+ });
252
+ }
253
+ return original(input, init);
254
+ });
255
+ });
256
+ /**
257
+ * Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
258
+ * body. Returns the original string verbatim when the body is not
259
+ * JSON, has no `messages`, or no rewrite was needed; this lets the
260
+ * caller skip the allocation of a new `init` object in the common
261
+ * pass-through case.
262
+ */
263
+ function rewriteServingBody(body) {
264
+ let parsed;
265
+ try {
266
+ parsed = JSON.parse(body);
267
+ } catch {
268
+ return body;
269
+ }
270
+ if (!Array.isArray(parsed.messages)) return body;
271
+ return sanitizeServingMessages(parsed.messages) ? JSON.stringify(parsed) : body;
272
+ }
273
+ /**
274
+ * Repair a Mastra/AI SDK message replay that Databricks-hosted Claude
275
+ * rejects with `"This model does not support assistant message
276
+ * prefill. The conversation must end with a user message."`.
277
+ *
278
+ * The bug pattern: when an assistant turn streams text *and* a
279
+ * `tool_call`, the AI SDK persists them as two separate assistant
280
+ * entries (text-only and tool-call-only). On the next agent step the
281
+ * tool-call entry is replayed *before* the tool result and the
282
+ * text entry is replayed *after* it, so the conversation ends with a
283
+ * trailing assistant text message. Anthropic interprets that as a
284
+ * prefill request and rejects it on Databricks (the upstream Bedrock
285
+ * route disallows prefill).
286
+ *
287
+ * Fix: when the last message is an assistant text with no `tool_calls`
288
+ * and the chain immediately before it is `assistant(tool_calls=...)`
289
+ * followed only by `tool(...)` results, fold the trailing text back
290
+ * into the `content` of that opening assistant and drop the duplicate.
291
+ * The result is the canonical OpenAI shape
292
+ * `[..., user, assistant(text + tool_calls), tool(...)]` which both
293
+ * Databricks Claude and every other endpoint accept.
294
+ *
295
+ * Mutates `messages` in place; returns `true` when something changed
296
+ * so the caller knows whether to re-serialize.
297
+ */
298
+ function sanitizeServingMessages(messages) {
299
+ if (messages.length < 2) return false;
300
+ const last = messages[messages.length - 1];
301
+ if (!last || last.role !== "assistant" || last.tool_calls && last.tool_calls.length > 0) return false;
302
+ let i = messages.length - 2;
303
+ while (i >= 0 && messages[i]?.role === "tool") i--;
304
+ if (i < 0) return false;
305
+ const opener = messages[i];
306
+ if (!opener || opener.role !== "assistant" || !opener.tool_calls || opener.tool_calls.length === 0) return false;
307
+ opener.content = [stringUtils.trimToNull(opener.content), stringUtils.trimToNull(last.content)].filter((s) => s !== null).join("\n\n");
308
+ messages.pop();
309
+ return true;
310
+ }
311
+
312
+ //#endregion
313
+ //#region packages/appkit-mastra/src/chart.ts
314
+ /**
315
+ * Chart planner + chart cache.
316
+ *
317
+ * Self-contained chart subsystem with two layers:
318
+ *
319
+ * 1. Inner planner agent (private). Pure dataset-in /
320
+ * `EChartsOption`-out brain. Driven by {@link prepareChart};
321
+ * callers never instantiate it directly.
322
+ * 2. {@link prepareChart}: orchestration on top of the planner.
323
+ * Mints a `chartId`, caches an empty `{ chartId }` record
324
+ * synchronously, then resolves the dataset and runs the
325
+ * planner in the background. The terminal entry settles with
326
+ * either `result` (success) or `error` (failure). Both
327
+ * undefined means the entry is still processing.
328
+ *
329
+ * The cache surface ({@link fetchChart}) is the only state the
330
+ * HTTP route and the chart-producing tools share. `prepareChart`
331
+ * is dataset-agnostic - callers supply a `resolveData` callback
332
+ * that fetches the rows however they like (Genie statement, inline
333
+ * dataset, custom API). The module has no knowledge of Genie or
334
+ * statement ids; those concerns live in the tools that wrap it.
335
+ *
336
+ * Wire-format schemas live in `@dbx-tools/appkit-mastra-shared` so
337
+ * the demo client and any other UI consumer share the exact same
338
+ * shape this module reads and writes.
339
+ */
340
+ const log$5 = logUtils.logger("mastra/chart");
341
+ /**
342
+ * TTL for cached chart entries. One hour balances "long enough for
343
+ * the host UI to fetch the chart well after the model finished
344
+ * talking" against "short enough that abandoned chart ids don't
345
+ * pin storage". Matches the typical Databricks OBO token lifetime
346
+ * so any data re-resolution stays inside the original auth window.
347
+ */
348
+ const CHART_CACHE_TTL_SEC = 3600;
349
+ /** Cache namespace; keeps the chart keyspace tidy. */
350
+ const CHART_CACHE_NAMESPACE = "mastra:chart";
351
+ /**
352
+ * `userKey` for `CacheManager.generateKey`. Chart ids are minted
353
+ * via `commonUtils.id()` (v4 UUID) and are unguessable, so a
354
+ * constant user key is fine. The HTTP route can re-scope to the
355
+ * requesting user when policy demands it.
356
+ */
357
+ const CHART_CACHE_USER_KEY = "mastra-chart";
358
+ /** Default server-side long-poll budget for {@link fetchChart}. */
359
+ const DEFAULT_FETCH_TIMEOUT_MS = 6e4;
360
+ /** Default inter-poll sleep for {@link fetchChart}. */
361
+ const DEFAULT_FETCH_INTERVAL_MS = 250;
362
+ /**
363
+ * One series data point. Wide variant set so the planner agent can
364
+ * faithfully pass through whatever the SQL row set contained
365
+ * (numbers, stringified numbers, nulls for missing measurements,
366
+ * `[x, y]` tuples for scatter, `{name, value}` slices for pie)
367
+ * without the structured-output guard rejecting the whole plan.
368
+ *
369
+ * Three layers of tolerance:
370
+ *
371
+ * 1. {@link z.preprocess} normalizes wire shapes BEFORE union
372
+ * dispatch: stringified numbers parse to numbers, finite
373
+ * checks reject `NaN` / `Infinity`, 2-element arrays coerce
374
+ * tuple components, and `{value}` objects with missing /
375
+ * stringified `value` get coerced or rejected uniformly.
376
+ * Anything not handleable becomes `null`.
377
+ * 2. The union accepts `null` as a first-class variant. Echarts
378
+ * renders null as a gap on bar / line / area (which is the
379
+ * right visual signal for "missing reading"). Scatter and
380
+ * pie filter nulls in {@link planToEchartsOption} because
381
+ * Echarts crashes on null tuples / slices.
382
+ * 3. {@link z.union#catch} backstops the whole thing: if
383
+ * preprocess somehow produces a shape that still doesn't
384
+ * match any variant, the bad item becomes `null` instead of
385
+ * taking down the entire chart with a
386
+ * `Structured output validation failed` error.
387
+ */
388
+ const chartDataPointSchema = z.preprocess((v) => {
389
+ if (v === null || v === void 0) return null;
390
+ if (typeof v === "number") return Number.isFinite(v) ? v : null;
391
+ if (typeof v === "string") {
392
+ const n = Number(v);
393
+ return Number.isFinite(n) ? n : null;
394
+ }
395
+ if (Array.isArray(v) && v.length === 2) {
396
+ const x = typeof v[0] === "number" ? v[0] : Number(v[0]);
397
+ const y = typeof v[1] === "number" ? v[1] : Number(v[1]);
398
+ return Number.isFinite(x) && Number.isFinite(y) ? [x, y] : null;
399
+ }
400
+ if (typeof v === "object" && v !== null && "value" in v) {
401
+ const obj = v;
402
+ const val = typeof obj.value === "number" ? obj.value : Number(obj.value);
403
+ if (!Number.isFinite(val)) return null;
404
+ const rawName = obj.name;
405
+ return {
406
+ name: typeof rawName === "string" ? rawName : rawName == null ? "" : String(rawName),
407
+ value: val
408
+ };
409
+ }
410
+ return null;
411
+ }, z.union([
412
+ z.number(),
413
+ z.null(),
414
+ z.tuple([z.number(), z.number()]),
415
+ z.object({
416
+ name: z.string(),
417
+ value: z.number()
418
+ })
419
+ ])).catch(null);
420
+ /**
421
+ * Compact, model-friendly representation of an Echarts spec. The
422
+ * planner agent emits this; {@link planToEchartsOption} expands it
423
+ * into a real `EChartsOption` JSON. Two layers because letting the
424
+ * model fill in a fully-typed `EChartsOption` is brittle (hundreds
425
+ * of optional fields, deep unions, version-dependent shapes). A
426
+ * small "chart plan" schema is much more reliable for a fast model
427
+ * and keeps animation / tooltip / styling defaults consistent
428
+ * across charts.
429
+ */
430
+ const chartPlanSchema = z.object({
431
+ chartType: ChartTypeSchema,
432
+ title: z.string().optional().describe(stringUtils.toDescription(`
433
+ Short title shown above the chart. Optional; defaults to the
434
+ \`title\` argument the caller passed in.
435
+ `)),
436
+ xAxisLabel: z.string().optional().describe(stringUtils.toDescription(`
437
+ Axis label below the chart. Used for bar / line / area / scatter;
438
+ ignored for pie.
439
+ `)),
440
+ yAxisLabel: z.string().optional().describe(stringUtils.toDescription(`
441
+ Axis label to the left of the chart. Used for bar / line / area /
442
+ scatter; ignored for pie.
443
+ `)),
444
+ categories: z.array(z.string()).optional().describe(stringUtils.toDescription(`
445
+ X-axis category labels for \`bar\` / \`line\` / \`area\` charts
446
+ (one per data point in each series). Omit for \`scatter\` (uses
447
+ [x, y] tuples) and \`pie\` (each slice carries its own \`name\`).
448
+ `)),
449
+ series: z.array(z.object({
450
+ name: z.string().describe(stringUtils.toDescription(`
451
+ Legend name for this series.
452
+ `)),
453
+ data: z.array(chartDataPointSchema).describe(stringUtils.toDescription(`
454
+ Data points. For \`bar\` / \`line\` / \`area\`, an array of
455
+ numbers aligned to \`categories\`. For \`scatter\`, an array
456
+ of \`[x, y]\` numeric tuples. For \`pie\`, an array of
457
+ \`{name, value}\` objects.
458
+ `))
459
+ })).min(1).describe(stringUtils.toDescription(`
460
+ One or more series to plot. Pie charts use exactly one series;
461
+ bar/line/area can stack multiple series sharing the same
462
+ \`categories\` axis.
463
+ `))
464
+ });
465
+ /**
466
+ * Canonical planner input shape. Tools that source rows from an
467
+ * inline dataset (`render_data`) use it as their `inputSchema`
468
+ * verbatim; tools that resolve rows from a remote (`prepare_chart`
469
+ * over a Genie statement) `omit({ data })` and `extend` with their
470
+ * own identifier field, so the field-level `.describe()` text
471
+ * stays a single source of truth. Server-only - the UI never
472
+ * sees a planner request, only the resolved {@link Chart}.
473
+ */
474
+ const chartPlannerRequestSchema = z.object({
475
+ title: z.string().describe(stringUtils.toDescription(`
476
+ Concise title shown above the chart (e.g. "Top 10 SKUs by Revenue").
477
+ `)),
478
+ description: z.string().optional().describe(stringUtils.toDescription(`
479
+ One-line intent the chart-planner uses when picking a chart type
480
+ and axis encodings (e.g. "compare quarterly revenue across
481
+ regions", "highlight the steep drop after position 5"). Not shown
482
+ to the user.
483
+ `)),
484
+ data: z.array(z.record(z.string(), z.unknown())).nonempty("Data must contain at least one row").readonly().describe(stringUtils.toDescription(`
485
+ Tabular dataset to chart. One object per row, keyed by column
486
+ name. Values may be strings, numbers, booleans, or null. The
487
+ chart-planner decides which columns are categories vs. numeric
488
+ series. Cap at a few hundred rows for legibility; sample /
489
+ aggregate larger datasets first.
490
+ `))
491
+ });
492
+ /**
493
+ * Format {@link ChartTypeSchema}'s variants as a single
494
+ * human-friendly string of `` `<value>` for <description> ``
495
+ * clauses joined by semicolons, drawn from each variant's own
496
+ * `.describe()` so the planner prompt stays in lock-step with
497
+ * the schema by construction.
498
+ */
499
+ function formatChartTypePicker() {
500
+ return ChartTypeSchema.options.map((opt) => `\`${opt.value}\` for ${opt.description ?? ""}`).join("; ");
501
+ }
502
+ /**
503
+ * System prompt for the inner chart-planning agent. Tuned for a
504
+ * fast-tier model (Haiku, GPT-5-mini, Gemini Flash Lite).
505
+ */
506
+ const CHART_PLANNER_INSTRUCTIONS = stringUtils.toDescription(`
507
+ You design Apache Echarts visualizations. The user gives you a
508
+ tabular dataset (rows of objects) plus a title and an optional
509
+ description of the intent. You produce a small chart plan (chart
510
+ type, axis labels, categories, series) that best conveys the data.
511
+
512
+ Decision guide. Pick the chart type whose data shape matches the
513
+ dataset and the user's intent: ${formatChartTypePicker()}.
514
+
515
+ When in doubt between bar and line, prefer bar for unordered
516
+ categories and line for ordered ones (dates, time buckets, ranks).
517
+ Never pick pie for more than 7 slices.
518
+
519
+ For bar / line / area: pick one column as the category axis (usually
520
+ the only string-valued column) and one or more numeric columns as
521
+ series. Sort categories by the primary series value descending unless
522
+ the data is naturally ordered (dates, ranks).
523
+
524
+ For pie: pick the category column for slice names and one numeric
525
+ column for slice values. Emit a single series.
526
+
527
+ For scatter: pick two numeric columns and emit \`[x, y]\` tuples in a
528
+ single series.
529
+
530
+ Keep series names human-readable (use the column name; title case it
531
+ lightly if needed). Keep titles concise; do not repeat the user's
532
+ title in xAxisLabel / yAxisLabel.
533
+ `);
534
+ /**
535
+ * One planner `Agent` per plugin config. Cached on config object
536
+ * identity so callers can `prepareChart({ config, ... })` from a
537
+ * hot path without paying the Agent-constructor cost every call.
538
+ * `WeakMap` lets retired configs (e.g. test reconfigurations)
539
+ * release their agent without manual eviction.
540
+ */
541
+ const plannerAgents = /* @__PURE__ */ new WeakMap();
542
+ function getPlannerAgent(config) {
543
+ let agent = plannerAgents.get(config);
544
+ if (!agent) {
545
+ agent = new Agent({
546
+ id: "chart_planner",
547
+ name: "Chart Planner",
548
+ description: "Picks chart type and axis encodings for a dataset.",
549
+ instructions: CHART_PLANNER_INSTRUCTIONS,
550
+ model: ({ requestContext }) => buildModel(config, requestContext, { modelClass: ModelClass.ChatFast })
551
+ });
552
+ plannerAgents.set(config, agent);
553
+ }
554
+ return agent;
555
+ }
556
+ /**
557
+ * Run the planner against `request` and return the resolved
558
+ * Echarts spec. Throws on planner failure - {@link prepareChart}
559
+ * catches and stashes the error in the cache entry.
560
+ */
561
+ async function runChartPlanner(config, request, options = {}) {
562
+ const { title, description, data } = request;
563
+ const { requestContext, abortSignal } = options;
564
+ const prompt = stringUtils.toDescription({
565
+ Title: title,
566
+ ...description ? { Description: description } : {},
567
+ "Dataset (JSON, one row per object)": JSON.stringify(data, null, 2)
568
+ });
569
+ const result = await getPlannerAgent(config).generate(prompt, {
570
+ structuredOutput: { schema: chartPlanSchema },
571
+ ...requestContext ? { requestContext } : {},
572
+ ...abortSignal ? { abortSignal } : {}
573
+ });
574
+ const plan = chartPlanSchema.parse(result.object);
575
+ const option = planToEchartsOption(plan, title);
576
+ return {
577
+ chartType: plan.chartType,
578
+ option
579
+ };
580
+ }
581
+ /** Build the canonical cache key for a `chartId`. */
582
+ async function chartCacheKey(chartId) {
583
+ return (await CacheManager.getInstance()).generateKey([CHART_CACHE_NAMESPACE, chartId], CHART_CACHE_USER_KEY);
584
+ }
585
+ /**
586
+ * Persist a {@link Chart} entry under its `chartId`. Refreshes
587
+ * the TTL on every write. Cache-layer failures are logged and
588
+ * swallowed so background runners never throw into the
589
+ * unhandled-rejection stream.
590
+ */
591
+ async function writeChart(entry) {
592
+ try {
593
+ const key = await chartCacheKey(entry.chartId);
594
+ await CacheManager.getInstanceSync().set(key, entry, { ttl: CHART_CACHE_TTL_SEC });
595
+ } catch (err) {
596
+ log$5.warn("write-error", {
597
+ chartId: entry.chartId,
598
+ error: commonUtils.errorMessage(err)
599
+ });
600
+ }
601
+ }
602
+ /**
603
+ * Look up a chart by id. Returns `undefined` on miss, on
604
+ * expiry, or when the cache layer is unhealthy - never throws.
605
+ */
606
+ async function readChart(chartId) {
607
+ try {
608
+ const key = await chartCacheKey(chartId);
609
+ return await CacheManager.getInstanceSync().get(key) ?? void 0;
610
+ } catch (err) {
611
+ log$5.warn("read-error", {
612
+ chartId,
613
+ error: commonUtils.errorMessage(err)
614
+ });
615
+ return;
616
+ }
617
+ }
618
+ /**
619
+ * Mint a `chartId`, cache an empty `{ chartId }` placeholder
620
+ * synchronously, and kick off a background task that resolves the
621
+ * dataset and runs the planner. Returns the `chartId` once the
622
+ * placeholder lands so the first {@link fetchChart} call always
623
+ * sees an entry (no spurious 404 race).
624
+ *
625
+ * The background task swallows its own failures and writes them
626
+ * as `error` entries, so callers never see a rejected promise.
627
+ * Cache state machine:
628
+ *
629
+ * - just after this call returns: `{ chartId }` (processing)
630
+ * - on planner success: `{ chartId, result }`
631
+ * - on data / planner failure: `{ chartId, error }`
632
+ */
633
+ async function prepareChart(opts) {
634
+ const chartId = commonUtils.id();
635
+ await writeChart({ chartId });
636
+ log$5.debug("queued", { chartId });
637
+ runPrepareChart(chartId, opts);
638
+ return { chartId };
639
+ }
640
+ async function runPrepareChart(chartId, opts) {
641
+ const startedAt = Date.now();
642
+ try {
643
+ const data = await opts.resolveData(opts.signal);
644
+ if (data.rows.length === 0) throw new Error("dataset has no rows; nothing to chart");
645
+ const result = await runChartPlanner(opts.config, {
646
+ title: opts.title ?? "Chart",
647
+ ...opts.description ? { description: opts.description } : {},
648
+ data: data.rows
649
+ }, {
650
+ ...opts.requestContext ? { requestContext: opts.requestContext } : {},
651
+ ...opts.signal ? { abortSignal: opts.signal } : {}
652
+ });
653
+ await writeChart({
654
+ chartId,
655
+ result
656
+ });
657
+ log$5.info("done", {
658
+ chartId,
659
+ chartType: result.chartType,
660
+ elapsedMs: Date.now() - startedAt
661
+ });
662
+ } catch (err) {
663
+ const error = commonUtils.errorMessage(err);
664
+ log$5.warn("error", {
665
+ chartId,
666
+ error
667
+ });
668
+ await writeChart({
669
+ chartId,
670
+ error
671
+ });
672
+ }
673
+ }
674
+ /**
675
+ * Long-poll the chart cache until the entry settles (`result` or
676
+ * `error` set), the entry is missing, or the server-side timeout
677
+ * elapses.
678
+ *
679
+ * Returns:
680
+ * - the resolved {@link Chart} when it settled, errored, or
681
+ * stayed in processing past `timeoutMs` (so the client can
682
+ * re-poll);
683
+ * - `undefined` when the entry is missing or expired (the
684
+ * consumer should treat as 404).
685
+ *
686
+ * `signal` lets the caller cancel ahead of timeout (e.g. the HTTP
687
+ * request closed). Cancellation propagates to the inter-poll sleep
688
+ * so the helper returns immediately.
689
+ */
690
+ async function fetchChart(chartId, options = {}) {
691
+ const timeoutMs = options.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
692
+ const intervalMs = options.intervalMs ?? DEFAULT_FETCH_INTERVAL_MS;
693
+ const deadline = Date.now() + timeoutMs;
694
+ let last;
695
+ while (true) {
696
+ options.signal?.throwIfAborted();
697
+ last = await readChart(chartId);
698
+ if (!last) return void 0;
699
+ if (last.result !== void 0 || last.error !== void 0) return last;
700
+ const remaining = deadline - Date.now();
701
+ if (remaining <= 0) return last;
702
+ await commonUtils.sleep(Math.min(intervalMs, remaining), options.signal);
703
+ }
704
+ }
705
+ /**
706
+ * Expand a {@link ChartPlan} into a full Echarts `EChartsOption`
707
+ * JSON. Centralized here so the planner agent only fills in the
708
+ * compact plan shape; tooltip / animation / color / grid defaults
709
+ * stay consistent across charts and are easy to tune without
710
+ * retraining model behaviour.
711
+ */
712
+ function planToEchartsOption(plan, fallbackTitle) {
713
+ const baseTitle = plan.title ?? fallbackTitle;
714
+ const grid = {
715
+ left: 48,
716
+ right: 24,
717
+ top: 56,
718
+ bottom: 48,
719
+ containLabel: true
720
+ };
721
+ if (plan.chartType === "pie") {
722
+ const slices = (plan.series[0]?.data ?? []).filter((d) => d !== null && typeof d === "object" && !Array.isArray(d));
723
+ return {
724
+ title: {
725
+ text: baseTitle,
726
+ left: "center"
727
+ },
728
+ tooltip: { trigger: "item" },
729
+ legend: { bottom: 0 },
730
+ series: [{
731
+ name: plan.series[0]?.name ?? baseTitle,
732
+ type: "pie",
733
+ radius: ["35%", "65%"],
734
+ data: slices
735
+ }]
736
+ };
737
+ }
738
+ if (plan.chartType === "scatter") return {
739
+ title: {
740
+ text: baseTitle,
741
+ left: "center"
742
+ },
743
+ tooltip: { trigger: "item" },
744
+ legend: { bottom: 0 },
745
+ grid,
746
+ xAxis: {
747
+ type: "value",
748
+ name: plan.xAxisLabel
749
+ },
750
+ yAxis: {
751
+ type: "value",
752
+ name: plan.yAxisLabel
753
+ },
754
+ series: plan.series.map((s) => ({
755
+ name: s.name,
756
+ type: "scatter",
757
+ data: s.data.filter((d) => Array.isArray(d) && d.length === 2)
758
+ }))
759
+ };
760
+ const isArea = plan.chartType === "area";
761
+ const seriesType = plan.chartType === "bar" ? "bar" : "line";
762
+ return {
763
+ title: {
764
+ text: baseTitle,
765
+ left: "center"
766
+ },
767
+ tooltip: { trigger: "axis" },
768
+ legend: { bottom: 0 },
769
+ grid,
770
+ xAxis: {
771
+ type: "category",
772
+ data: plan.categories ?? [],
773
+ name: plan.xAxisLabel
774
+ },
775
+ yAxis: {
776
+ type: "value",
777
+ name: plan.yAxisLabel
778
+ },
779
+ series: plan.series.map((s) => ({
780
+ name: s.name,
781
+ type: seriesType,
782
+ data: s.data,
783
+ smooth: seriesType === "line",
784
+ ...isArea ? { areaStyle: {} } : {}
785
+ }))
786
+ };
787
+ }
788
+ /**
789
+ * Build the `render_data` Mastra tool bound to the given plugin
790
+ * config. Auto-wired as a system tool on every agent (see
791
+ * `agents.ts`); per-agent tools can shadow it by registering a
792
+ * same-named entry.
793
+ *
794
+ * Thin wrapper over {@link prepareChart} for callers that already
795
+ * have a dataset in hand. Mints a `chartId` synchronously, caches
796
+ * an empty placeholder, and kicks off the chart-planner in the
797
+ * background. Returns just the `chartId`; the host UI resolves
798
+ * `[chart:<chartId>]` markers by hitting the plugin's
799
+ * `/embed/chart/:id` route.
800
+ *
801
+ * For Genie statement results, prefer the Genie agent's
802
+ * `prepare_chart` tool, which accepts a `statement_id` and
803
+ * resolves the rows lazily.
804
+ */
805
+ function buildRenderDataTool(config) {
806
+ return createTool$1({
807
+ id: "render_data",
808
+ description: stringUtils.toDescription([
809
+ `
810
+ Submit a tabular dataset for inline rendering as a chart in
811
+ the user's view. Pass a title, the raw rows (array of objects
812
+ keyed by column name), and an optional one-line description
813
+ of the insight to highlight. Returns a short \`chartId\`;
814
+ the chart renders inline at the position you embed the
815
+ matching \`[chart:<chartId>]\` marker.
816
+ `,
817
+ `
818
+ Placement contract: embed \`[chart:<chartId>]\` on its own
819
+ line (blank lines above and below) wherever you want the
820
+ chart to appear in your reply. The chart resolves
821
+ asynchronously - the tool returns the id immediately and the
822
+ host UI fetches the chart from the cache once the planner
823
+ lands. You can call \`render_data\` multiple times in the
824
+ same turn (the tool is parallel-safe) and interleave the
825
+ markers with prose so each chart sits next to its
826
+ commentary.
827
+ `,
828
+ `
829
+ Use whenever a SQL row set, API response, or hand-built
830
+ dataset would land better as a picture than as a list or
831
+ table. Cap input at a few hundred rows; sample or aggregate
832
+ larger datasets first.
833
+ `
834
+ ]),
835
+ inputSchema: chartPlannerRequestSchema,
836
+ outputSchema: ChartSchema.pick({ chartId: true }),
837
+ execute: async (input, ctxRaw) => {
838
+ const { title, description, data } = input;
839
+ const ctx = ctxRaw;
840
+ return prepareChart({
841
+ config,
842
+ title,
843
+ ...description ? { description } : {},
844
+ resolveData: () => Promise.resolve({ rows: data }),
845
+ ...ctx?.requestContext ? { requestContext: ctx.requestContext } : {}
846
+ });
847
+ }
848
+ });
849
+ }
850
+
851
+ //#endregion
852
+ //#region packages/appkit-mastra/src/statement.ts
853
+ /**
854
+ * Hard server-side cap on rows returned by the
855
+ * `/embed/data/:id` route. Sized to keep responses small
856
+ * enough for inline tables to render snappily; the route surfaces
857
+ * a `truncated` flag whenever the upstream `rowCount` exceeds
858
+ * this so end users know they're seeing a sample.
859
+ */
860
+ const STATEMENT_ROW_CAP = 500;
861
+ /**
862
+ * Best-effort numeric coercion for the Statement Execution API's
863
+ * all-strings cells. Leaves non-numeric strings (and explicit
864
+ * `null`s) intact; everything else flows through `Number`.
865
+ */
866
+ function coerceCell(cell) {
867
+ if (cell === null) return null;
868
+ if (/^-?\d+(\.\d+)?$/.test(cell)) {
869
+ const n = Number(cell);
870
+ if (Number.isFinite(n)) return n;
871
+ }
872
+ return cell;
873
+ }
874
+ /**
875
+ * Fetch a single statement's rows via the Statement Execution API
876
+ * and reshape into the shared {@link GenieDatasetData} shape
877
+ * (column array + row records).
878
+ *
879
+ * Optional `limit` slices the returned `rows` client-side so the
880
+ * agent can scan a small sample without paging the full result
881
+ * set into context. `rowCount` always reflects the upstream total
882
+ * so callers know when the slice truncated.
883
+ *
884
+ * Exported because every consumer in the plugin (the
885
+ * `get_statement` tool, the `prepare_chart` dataset resolver, and
886
+ * the `/embed/data/:id` route) needs the exact same
887
+ * fetch + coercion pipeline so LLM-side `get_statement` output
888
+ * and UI-side `[data:<id>]` rendering stay shape-identical for
889
+ * the same `statement_id`.
890
+ */
891
+ async function fetchStatementData(client, statementId, options) {
892
+ const ctx = options?.signal ? apiUtils.toContext(options.signal) : void 0;
893
+ const r = await client.statementExecution.getStatement({ statement_id: statementId }, ctx);
894
+ const columns = (r.manifest?.schema?.columns ?? []).map((c) => c.name ?? "");
895
+ const dataArray = r.result?.data_array ?? [];
896
+ return {
897
+ columns,
898
+ rows: (options?.limit !== void 0 && options.limit >= 0 ? dataArray.slice(0, options.limit) : dataArray).map((row) => {
899
+ const obj = {};
900
+ columns.forEach((col, i) => {
901
+ obj[col] = coerceCell(row[i] ?? null);
902
+ });
903
+ return obj;
904
+ }),
905
+ rowCount: r.manifest?.total_row_count ?? dataArray.length
906
+ };
907
+ }
908
+
909
+ //#endregion
910
+ //#region packages/appkit-mastra/src/writer.ts
911
+ /**
912
+ * Best-effort `writer.write`. No-op when `writer` is undefined;
913
+ * caught errors are logged via `log.warn("writer:error", ...)`
914
+ * along with any caller-supplied `context` fields (e.g. a
915
+ * `chartId` or `messageId`) so the warning is greppable per
916
+ * resource.
917
+ *
918
+ * Returns when the write resolves or rejects; never throws.
919
+ */
920
+ async function safeWrite(log, writer, chunk, context = {}) {
921
+ if (!writer) {
922
+ log.debug("writer:no-writer", context);
923
+ return;
924
+ }
925
+ try {
926
+ await writer.write(chunk);
927
+ log.debug("writer:ok", context);
928
+ } catch (err) {
929
+ log.warn("writer:error", {
930
+ ...context,
931
+ error: commonUtils.errorMessage(err)
932
+ });
933
+ }
934
+ }
935
+
936
+ //#endregion
937
+ //#region packages/appkit-mastra/src/genie.ts
938
+ /**
939
+ * Genie tools for Mastra.
940
+ *
941
+ * Surfaces each configured Genie space as a small set of flat Mastra
942
+ * tools the calling agent drives directly - no inner orchestrator
943
+ * agent. The central agent decomposes user questions, picks which
944
+ * space to ask, streams the per-turn wire events (status, thinking,
945
+ * sql, rows) through `ctx.writer`, and composes the final reply.
946
+ * Rows are never fetched eagerly: the agent reads a statement's
947
+ * values only when it needs to reason about them, otherwise it embeds
948
+ * a `[data:<statement_id>]` marker in prose and lets the host UI
949
+ * resolve the data. Charts are minted asynchronously and referenced
950
+ * by `[chart:<chartId>]` markers so prose isn't blocked on chart
951
+ * generation; the host UI fetches the cached spec by id once ready.
952
+ * Space description and serialized-space lookups are available for
953
+ * grounding when the agent needs schema context.
954
+ *
955
+ * Each tool's `execute` pulls the per-request
956
+ * {@link WorkspaceClient} off `ctx.requestContext` (stamped by
957
+ * `MastraServer` under {@link MASTRA_USER_KEY}) and the per-call
958
+ * `writer` / `abortSignal` off `ctx`, so the tools are stateless
959
+ * across requests and the central agent owns the loop.
960
+ *
961
+ * The tools talk to Genie directly via `@dbx-tools/genie`
962
+ * (`genieEventChat`); statement-row fetching is delegated to
963
+ * {@link fetchStatementData} from `./statement.js`, which wraps
964
+ * the workspace `statementExecution.getStatement` API. AppKit's
965
+ * stock `genie` plugin is honored only for its `spaces` config
966
+ * so existing AppKit-style wiring keeps working without change.
967
+ *
968
+ * Suggested orchestration prompt for the central agent lives in
969
+ * {@link GENIE_INSTRUCTIONS}; compose it into the agent's own
970
+ * `instructions` when you want the canonical "how to drive the
971
+ * Genie tools" guidance.
972
+ */
973
+ const log$4 = logUtils.logger("mastra/genie");
974
+ /** Default alias used when a single unnamed Genie space is wired up. */
975
+ const DEFAULT_GENIE_ALIAS = "default";
976
+ /**
977
+ * Pull the per-request {@link WorkspaceClient} off the active
978
+ * `RequestContext`. The Mastra plugin's server middleware stamps
979
+ * a {@link User} on the context under {@link MASTRA_USER_KEY}
980
+ * for every request; tools fail loudly when it's missing because
981
+ * that means the Mastra plugin isn't running (e.g. a tool was
982
+ * invoked outside the chat route).
983
+ */
984
+ function requireClient(ctx, toolId) {
985
+ const requestContext = ctx?.requestContext;
986
+ if (!requestContext) throw new Error(`${toolId}: missing requestContext (MastraServer must stamp MASTRA_USER_KEY)`);
987
+ const user = requestContext.get(MASTRA_USER_KEY);
988
+ if (!user) throw new Error(`${toolId}: no user on requestContext (MASTRA_USER_KEY not set)`);
989
+ return {
990
+ client: user.executionContext.client,
991
+ requestContext
992
+ };
993
+ }
994
+ /**
995
+ * Lowercased placeholder strings we reject at the `ask_genie`
996
+ * boundary so the LLM doesn't spend a Genie round-trip on a
997
+ * non-question. Genie politely answers any of these with "Your
998
+ * request '...' does not relate to..." which is pure UI noise.
999
+ * Kept narrow on purpose - real questions sometimes start with
1000
+ * one of these tokens, so we only match the FULL trimmed string.
1001
+ */
1002
+ const PLACEHOLDER_QUESTIONS = new Set([
1003
+ "noop",
1004
+ "no-op",
1005
+ "skip",
1006
+ "none",
1007
+ "n/a",
1008
+ "na",
1009
+ "null",
1010
+ "undefined",
1011
+ "test",
1012
+ "placeholder"
1013
+ ]);
1014
+ /**
1015
+ * Estimated Genie conversation lifetime in seconds. Databricks
1016
+ * publishes no official TTL on the conversation resource itself;
1017
+ * community projects (e.g. the open-source Databricks Genie Bot)
1018
+ * converge on 4 hours of inactivity as a safe operating window.
1019
+ * Treat this as an estimate that gets *extended on every use* by
1020
+ * re-setting the cache entry after each successful turn (sliding
1021
+ * TTL via re-`set`). When the estimate ends up wrong (conversation
1022
+ * deleted, expired upstream, cross-space referenced), `ask_genie`
1023
+ * catches the SDK's `RESOURCE_DOES_NOT_EXIST`/404 and transparently
1024
+ * starts a fresh conversation.
1025
+ */
1026
+ const CONVERSATION_TTL_SEC = 14400;
1027
+ /** Cache namespace prefix so coexisting Mastra caches don't collide. */
1028
+ const CONVERSATION_CACHE_NAMESPACE = "mastra:genie:conversation";
1029
+ /**
1030
+ * `userKey` for `CacheManager.getOrExecute` / `generateKey`. Genie
1031
+ * conversations are scoped to a single user + space + thread, and
1032
+ * `threadId` is already user-scoped (Mastra mints threads per
1033
+ * `resourceId`), so a constant user key here is safe and keeps the
1034
+ * cache key short.
1035
+ */
1036
+ const CONVERSATION_USER_KEY = "mastra-genie";
1037
+ /**
1038
+ * Build the per-request {@link RequestContext} key the active
1039
+ * Genie `conversation_id` lives under for `spaceId`. Scoped by
1040
+ * space so an app calling two Genie spaces in one request keeps
1041
+ * each conversation distinct (Genie conversation ids are
1042
+ * space-scoped on the wire). The same `RequestContext` instance
1043
+ * flows from the central agent through to every `ask_genie`
1044
+ * invocation, so writes on one call are visible on the next
1045
+ * without an explicit shared ref.
1046
+ */
1047
+ const conversationContextKey = (spaceId) => `mastra__genie_conversation__${spaceId}`;
1048
+ /**
1049
+ * Read the active Genie `conversation_id` for `spaceId` off the
1050
+ * per-request {@link RequestContext}. Returns `undefined` when no
1051
+ * conversation has been started yet this request.
1052
+ */
1053
+ function readContextConversationId(requestContext, spaceId) {
1054
+ return requestContext.get(conversationContextKey(spaceId));
1055
+ }
1056
+ /**
1057
+ * Write the active Genie `conversation_id` for `spaceId` onto the
1058
+ * per-request {@link RequestContext}. Subsequent `ask_genie` calls
1059
+ * in this request will reuse it.
1060
+ */
1061
+ function writeContextConversationId(requestContext, spaceId, conversationId) {
1062
+ requestContext.set(conversationContextKey(spaceId), conversationId);
1063
+ }
1064
+ /**
1065
+ * Build the canonical cache key for a `(spaceId, threadId)` pair.
1066
+ * Returns `undefined` when `threadId` is missing - callers should
1067
+ * skip caching entirely in that case (no Mastra memory wired up).
1068
+ */
1069
+ async function conversationCacheKey(spaceId, threadId) {
1070
+ if (!threadId) return void 0;
1071
+ return (await CacheManager.getInstance()).generateKey([
1072
+ CONVERSATION_CACHE_NAMESPACE,
1073
+ spaceId,
1074
+ threadId
1075
+ ], CONVERSATION_USER_KEY);
1076
+ }
1077
+ /**
1078
+ * Read the cached Genie conversation id for `(spaceId, threadId)`.
1079
+ * Returns `undefined` on miss, on expiry, or when the cache layer
1080
+ * is unhealthy - never throws. The TTL is renewed via re-`set`
1081
+ * after each successful turn (see {@link saveCachedConversationId}).
1082
+ */
1083
+ async function readCachedConversationId(cacheKey) {
1084
+ if (!cacheKey) return void 0;
1085
+ try {
1086
+ return await CacheManager.getInstanceSync().get(cacheKey) ?? void 0;
1087
+ } catch (err) {
1088
+ log$4.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1089
+ return;
1090
+ }
1091
+ }
1092
+ /**
1093
+ * Persist the active conversation id under `cacheKey`, refreshing
1094
+ * its TTL. Idempotent; no-op when `cacheKey` or `conversationId`
1095
+ * is missing. Re-setting the same key acts as a sliding TTL: every
1096
+ * turn that uses the conversation extends the window by another
1097
+ * {@link CONVERSATION_TTL_SEC} seconds.
1098
+ */
1099
+ async function saveCachedConversationId(cacheKey, conversationId) {
1100
+ if (!cacheKey || !conversationId) return;
1101
+ try {
1102
+ await CacheManager.getInstanceSync().set(cacheKey, conversationId, { ttl: CONVERSATION_TTL_SEC });
1103
+ } catch (err) {
1104
+ log$4.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1105
+ }
1106
+ }
1107
+ /** Force-evict a cached conversation id. Used on the stale-id recovery path. */
1108
+ async function evictCachedConversationId(cacheKey) {
1109
+ if (!cacheKey) return;
1110
+ try {
1111
+ await CacheManager.getInstanceSync().delete(cacheKey);
1112
+ } catch (err) {
1113
+ log$4.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1114
+ }
1115
+ }
1116
+ /**
1117
+ * Lazy-seed the active Genie `conversation_id` for `spaceId` from
1118
+ * the cross-request cache onto the per-request `RequestContext`.
1119
+ * No-op when the slot is already populated (subsequent
1120
+ * `ask_genie` calls in the same turn) so we hit the cache at most
1121
+ * once per request per space.
1122
+ */
1123
+ async function ensureConversationSeeded(requestContext, spaceId, cacheKey) {
1124
+ if (readContextConversationId(requestContext, spaceId)) return;
1125
+ const cached = await readCachedConversationId(cacheKey);
1126
+ if (cached) writeContextConversationId(requestContext, spaceId, cached);
1127
+ }
1128
+ /**
1129
+ * Agent-facing `prepare_chart` input schema. Reuses
1130
+ * {@link chartPlannerRequestSchema} (the dataset-driven planner
1131
+ * contract) but swaps the inline `data` field for a Genie
1132
+ * `statement_id` the tool resolves into rows server-side.
1133
+ * `title` is loosened to optional - the planner falls back to a
1134
+ * generic placeholder when the agent doesn't supply one.
1135
+ *
1136
+ * Shaped to match Genie's wire form - `statement_id` (snake)
1137
+ * mirrors `query_result.statement_id` and the `get_statement`
1138
+ * tool's input field name, so the LLM only ever sees one
1139
+ * spelling for the same identifier.
1140
+ */
1141
+ const prepareChartRequestSchema = chartPlannerRequestSchema.omit({
1142
+ data: true,
1143
+ title: true
1144
+ }).extend({
1145
+ statement_id: z.string().min(1, "statement_id is required").describe(stringUtils.toDescription(`
1146
+ Genie \`statement_id\` to chart. Read from
1147
+ \`message.query_result.statement_id\` or
1148
+ \`message.attachments[*].query.statement_id\` returned by
1149
+ \`ask_genie\`.
1150
+ `)),
1151
+ title: chartPlannerRequestSchema.shape.title.optional()
1152
+ });
1153
+ /**
1154
+ * Suffix appended to per-space tool ids when the alias isn't the
1155
+ * well-known `default`. Single-space deployments get the bare
1156
+ * names (`ask_genie`, `get_space_description`, ...); multi-space
1157
+ * deployments get `ask_genie_<alias>` etc. so each space's tools
1158
+ * stay disambiguated in the central agent's tool registry.
1159
+ */
1160
+ function aliasSuffix(alias) {
1161
+ if (alias === DEFAULT_GENIE_ALIAS) return "";
1162
+ const slug = stringUtils.toIdentifier(alias);
1163
+ return slug ? `_${slug}` : "";
1164
+ }
1165
+ /**
1166
+ * Drop `suggested_questions` attachments from a {@link GenieMessage}
1167
+ * before handing it to the central LLM. Those entries already
1168
+ * surface in the UI as one-tap pills via the writer's
1169
+ * `suggested_questions` events (see `collectSuggestions` on the
1170
+ * client); if we let them ride back in the tool result the LLM
1171
+ * tends to quote them inline in its prose, double-showing the
1172
+ * same questions and stepping on the dedicated suggestion UI.
1173
+ * Query / text / row attachments are preserved so the model can
1174
+ * still read `statement_id`, SQL, and the answer text.
1175
+ */
1176
+ function stripSuggestedQuestions(message) {
1177
+ const attachments = message.attachments;
1178
+ if (!attachments || attachments.length === 0) return message;
1179
+ const filtered = attachments.filter((att) => !att.suggested_questions);
1180
+ if (filtered.length === attachments.length) return message;
1181
+ return {
1182
+ ...message,
1183
+ attachments: filtered
1184
+ };
1185
+ }
1186
+ function buildAskGenieTool(opts) {
1187
+ const { spaceId, alias, hint } = opts;
1188
+ const toolId = `ask_genie${aliasSuffix(alias)}`;
1189
+ const hintLine = hint ? ` (${hint})` : "";
1190
+ return createTool$1({
1191
+ id: toolId,
1192
+ description: stringUtils.toDescription(`
1193
+ Ask the Genie space "${alias}"${hintLine} ONE focused
1194
+ natural-language sub-question and wait for the turn to
1195
+ complete. Genie answers best when each call covers a
1196
+ single metric / dimension / time window, so expect to
1197
+ call this tool MULTIPLE TIMES per user turn (typically
1198
+ two to six) and let the results from earlier calls inform
1199
+ later ones - that's the normal pattern, not the exception.
1200
+ Do NOT try to cram a multi-part question into a single
1201
+ call; decompose first, then ask each piece.
1202
+
1203
+ Returns the final \`GenieMessage\`. Rows are NOT included -
1204
+ the Genie wire response carries the \`statement_id\` for any
1205
+ SQL that ran (at \`message.query_result.statement_id\` or the
1206
+ first attachment's \`query.statement_id\`); call
1207
+ \`get_statement\` with that id only when you need to read
1208
+ the underlying values to reason about them. If you just
1209
+ want to display the rows to the user, embed a
1210
+ \`[data:<statement_id>]\` marker in your prose instead -
1211
+ the host UI fetches and renders the rows on its own. Wire
1212
+ events (status, thinking, sql) stream to the user
1213
+ automatically while the call is in flight.
1214
+ `),
1215
+ inputSchema: z.object({ question: z.string().min(1, "question is required") }),
1216
+ outputSchema: z.object({ message: GenieMessageSchema }),
1217
+ execute: async ({ question }, ctxRaw) => {
1218
+ const ctx = ctxRaw;
1219
+ const { client, requestContext } = requireClient(ctx, toolId);
1220
+ const writer = ctx?.writer;
1221
+ const signal = ctx?.abortSignal;
1222
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY);
1223
+ const trimmed = question.trim();
1224
+ if (trimmed.length === 0 || PLACEHOLDER_QUESTIONS.has(trimmed.toLowerCase())) throw new Error(`${toolId}: refusing placeholder question "${question}" - call ${toolId} only with a real natural-language question, or skip the call entirely`);
1225
+ const cacheKey = await conversationCacheKey(spaceId, threadId);
1226
+ await ensureConversationSeeded(requestContext, spaceId, cacheKey);
1227
+ await safeWrite(log$4, writer, {
1228
+ type: "started",
1229
+ spaceId,
1230
+ content: question
1231
+ });
1232
+ const runTurn = async () => {
1233
+ const seedConversationId = readContextConversationId(requestContext, spaceId);
1234
+ let finalMessage;
1235
+ for await (const event of genieEventChat(spaceId, question, {
1236
+ workspaceClient: client,
1237
+ ...seedConversationId ? { conversationId: seedConversationId } : {},
1238
+ ...signal ? { context: signal } : {}
1239
+ })) {
1240
+ if (event.type !== "message") await safeWrite(log$4, writer, event);
1241
+ const eventConversationId = event.conversation_id;
1242
+ if (eventConversationId) writeContextConversationId(requestContext, spaceId, eventConversationId);
1243
+ if (event.type === "result") finalMessage = event.message;
1244
+ }
1245
+ if (!finalMessage) throw new Error("Genie turn ended without a result event");
1246
+ return finalMessage;
1247
+ };
1248
+ let finalMessage;
1249
+ try {
1250
+ finalMessage = await runTurn();
1251
+ } catch (err) {
1252
+ const seeded = readContextConversationId(requestContext, spaceId);
1253
+ if (seeded && apiUtils.isNotFoundError(err)) {
1254
+ log$4.warn("conversation-cache:stale, resetting", {
1255
+ spaceId,
1256
+ conversationId: seeded,
1257
+ error: commonUtils.errorMessage(err)
1258
+ });
1259
+ await evictCachedConversationId(cacheKey);
1260
+ writeContextConversationId(requestContext, spaceId, void 0);
1261
+ finalMessage = await runTurn();
1262
+ } else throw err;
1263
+ }
1264
+ await saveCachedConversationId(cacheKey, readContextConversationId(requestContext, spaceId));
1265
+ return { message: stripSuggestedQuestions(finalMessage) };
1266
+ }
1267
+ });
1268
+ }
1269
+ function buildSpaceDescriptionTool(opts) {
1270
+ const { spaceId, alias } = opts;
1271
+ const toolId = `get_space_description${aliasSuffix(alias)}`;
1272
+ return createTool$1({
1273
+ id: toolId,
1274
+ description: stringUtils.toDescription(`
1275
+ Return the Genie space "${alias}"'s title, description, and
1276
+ warehouse id. Cheap (single REST call, no LLM round-trip).
1277
+ Call this FIRST on any user turn that's going to touch
1278
+ \`ask_genie\`, unless the same description already landed
1279
+ earlier in this conversation - the title + description tell
1280
+ you what tables, metrics, and time windows the space
1281
+ actually covers, which is what lets you decompose the
1282
+ user's question into the right \`ask_genie\` sub-questions.
1283
+ `),
1284
+ inputSchema: z.object({}),
1285
+ outputSchema: z.object({
1286
+ spaceId: z.string(),
1287
+ title: z.string().optional(),
1288
+ description: z.string().optional(),
1289
+ warehouseId: z.string().optional()
1290
+ }),
1291
+ execute: async (_input, ctxRaw) => {
1292
+ const ctx = ctxRaw;
1293
+ const { client } = requireClient(ctx, toolId);
1294
+ const signal = ctx?.abortSignal;
1295
+ const space = await getGenieSpace(spaceId, {
1296
+ workspaceClient: client,
1297
+ serialized: false,
1298
+ ...signal ? { context: signal } : {}
1299
+ });
1300
+ return {
1301
+ spaceId,
1302
+ ...space.title ? { title: space.title } : {},
1303
+ ...space.description ? { description: space.description } : {},
1304
+ ...space.warehouse_id ? { warehouseId: space.warehouse_id } : {}
1305
+ };
1306
+ }
1307
+ });
1308
+ }
1309
+ function buildSpaceSerializedTool(opts) {
1310
+ const { spaceId, alias } = opts;
1311
+ const toolId = `get_space_serialized${aliasSuffix(alias)}`;
1312
+ return createTool$1({
1313
+ id: toolId,
1314
+ description: stringUtils.toDescription(`
1315
+ Return the full \`GenieSpace\` JSON for the "${alias}" space.
1316
+ Use only when you need exact column / table identifiers
1317
+ \`get_space_description\` doesn't expose. Larger payload, so
1318
+ prefer the description tool when it's enough.
1319
+ `),
1320
+ inputSchema: z.object({}),
1321
+ outputSchema: z.object({ space: z.unknown() }),
1322
+ execute: async (_input, ctxRaw) => {
1323
+ const ctx = ctxRaw;
1324
+ const { client } = requireClient(ctx, toolId);
1325
+ const signal = ctx?.abortSignal;
1326
+ return { space: await getGenieSpace(spaceId, {
1327
+ workspaceClient: client,
1328
+ ...signal ? { context: signal } : {}
1329
+ }) };
1330
+ }
1331
+ });
1332
+ }
1333
+ /**
1334
+ * Default row cap for {@link buildGetStatementTool} when the agent
1335
+ * doesn't supply a `limit`. Sized to keep result sets out of the
1336
+ * model context unless the agent explicitly opts into more rows -
1337
+ * the cheap shape (column names + a handful of representative
1338
+ * rows) is usually enough to reason about a query.
1339
+ */
1340
+ const DEFAULT_STATEMENT_LIMIT = 50;
1341
+ function buildGetStatementTool() {
1342
+ const toolId = "get_statement";
1343
+ return createTool$1({
1344
+ id: toolId,
1345
+ description: stringUtils.toDescription(`
1346
+ Fetch the rows of a Genie statement by its \`statement_id\` (the
1347
+ value at \`message.query_result.statement_id\` or
1348
+ \`message.attachments[*].query.statement_id\` returned from
1349
+ \`ask_genie\`). Use this ONLY when you need to read the underlying
1350
+ values to reason about them in your reply - e.g. naming the
1351
+ largest row, computing a delta the visualization wouldn't already
1352
+ convey, or filtering down to a specific record. If you'd just be
1353
+ reciting numbers the user will see anyway, skip the call and
1354
+ embed a \`[data:<statement_id>]\` marker in your prose instead;
1355
+ the host UI fetches and renders the rows on its own. \`limit\`
1356
+ caps the number of rows returned (defaults to
1357
+ ${DEFAULT_STATEMENT_LIMIT}). \`rowCount\` reflects the full
1358
+ upstream total - compare to \`rows.length\` to detect truncation.
1359
+ `),
1360
+ inputSchema: z.object({
1361
+ statement_id: z.string().min(1, "statement_id is required"),
1362
+ limit: z.number().int().nonnegative().optional().describe("Max rows to return. Defaults to a small sample; raise only when more rows are genuinely needed.")
1363
+ }),
1364
+ outputSchema: z.object({
1365
+ columns: z.array(z.string()),
1366
+ rows: z.array(z.record(z.string(), z.unknown())),
1367
+ rowCount: z.number(),
1368
+ truncated: z.boolean()
1369
+ }),
1370
+ execute: async ({ statement_id, limit }, ctxRaw) => {
1371
+ const ctx = ctxRaw;
1372
+ const { client } = requireClient(ctx, toolId);
1373
+ const signal = ctx?.abortSignal;
1374
+ const data = await fetchStatementData(client, statement_id, {
1375
+ limit: limit ?? DEFAULT_STATEMENT_LIMIT,
1376
+ ...signal ? { signal } : {}
1377
+ });
1378
+ return {
1379
+ columns: data.columns,
1380
+ rows: data.rows,
1381
+ rowCount: data.rowCount,
1382
+ truncated: data.rows.length < data.rowCount
1383
+ };
1384
+ }
1385
+ });
1386
+ }
1387
+ /**
1388
+ * `prepare_chart` Mastra tool. Thin wrapper over
1389
+ * {@link prepareChart} that resolves the dataset by fetching the
1390
+ * Genie statement's rows on demand. The tool mints a `chartId`
1391
+ * synchronously, caches an empty placeholder, and kicks off the
1392
+ * planner in the background so the agent loop never blocks. The
1393
+ * host UI resolves `[chart:<chartId>]` markers by reading the
1394
+ * cached {@link Chart} entry (1h TTL).
1395
+ *
1396
+ * Space-agnostic: a Genie `statement_id` is workspace-scoped, so
1397
+ * one shared `prepare_chart` tool covers every wired Genie space.
1398
+ *
1399
+ * Cancellation: deliberately does NOT forward the per-call
1400
+ * `abortSignal` to {@link prepareChart}. The planner task is
1401
+ * fire-and-forget background work; the tool's own `execute`
1402
+ * resolves the moment the `chartId` is minted, so the per-call
1403
+ * signal aborts the second the tool returns. The 1h cache TTL
1404
+ * caps abandoned entries.
1405
+ */
1406
+ function buildPrepareChartTool(opts) {
1407
+ const { config } = opts;
1408
+ const toolId = "prepare_chart";
1409
+ return createTool$1({
1410
+ id: toolId,
1411
+ description: stringUtils.toDescription([
1412
+ `
1413
+ Queue a chart for the rows of a Genie statement. Mints a
1414
+ short \`chartId\` synchronously and kicks off a BACKGROUND
1415
+ task that fetches the statement's rows, runs the
1416
+ chart-planner to pick a chart type and Echarts spec, and
1417
+ caches the result under the \`chartId\` for one hour. The
1418
+ host UI fetches the cached chart on its own once it lands.
1419
+ `,
1420
+ `
1421
+ To display the chart in your reply, embed
1422
+ \`[chart:<chartId>]\` on its own line at the position you
1423
+ want it to appear, using the EXACT \`chartId\` string this
1424
+ call returned. Never construct a chart id yourself (it is
1425
+ not the \`statement_id\` or any variation of it) - only a
1426
+ value returned by this tool resolves to a real chart. The
1427
+ tool returns immediately - do NOT wait or call it again to
1428
+ "check progress"; the chart resolves asynchronously on the
1429
+ host UI's side.
1430
+ `,
1431
+ `
1432
+ Use this only when the data has a story a chart conveys
1433
+ better than a table (trends, rankings, distributions,
1434
+ parts-of-a-whole). For raw rows, embed
1435
+ \`[data:<statement_id>]\` instead and skip this tool.
1436
+ `
1437
+ ]),
1438
+ inputSchema: prepareChartRequestSchema,
1439
+ outputSchema: ChartSchema.pick({ chartId: true }),
1440
+ execute: async (request, ctxRaw) => {
1441
+ const ctx = ctxRaw;
1442
+ const { client } = requireClient(ctx, toolId);
1443
+ return prepareChart({
1444
+ config,
1445
+ ...request.title ? { title: request.title } : {},
1446
+ ...request.description ? { description: request.description } : {},
1447
+ resolveData: (taskSignal) => fetchStatementData(client, request.statement_id, { ...taskSignal ? { signal: taskSignal } : {} }),
1448
+ ...ctx?.requestContext ? { requestContext: ctx.requestContext } : {}
1449
+ });
1450
+ }
1451
+ });
1452
+ }
1453
+ /**
1454
+ * Suggested orchestration prompt for the central agent that owns
1455
+ * the Genie tools. Compose into your agent's `instructions` to
1456
+ * get the canonical "decompose questions, ask Genie focused
1457
+ * sub-questions, place data / chart markers in prose" behavior:
1458
+ *
1459
+ * ```ts
1460
+ * createAgent({
1461
+ * instructions: `${myAgentInstructions}\n\n${GENIE_INSTRUCTIONS}`,
1462
+ * tools(plugins) {
1463
+ * return { ...plugins.genie?.toolkit() };
1464
+ * },
1465
+ * });
1466
+ * ```
1467
+ *
1468
+ * The prompt references the bare tool names (`ask_genie`,
1469
+ * `get_space_description`, `get_space_serialized`,
1470
+ * `get_statement`, `prepare_chart`) used for the single-space
1471
+ * default alias. Multi-space deployments should write their own
1472
+ * variant that names the suffixed per-space tools
1473
+ * (e.g. `ask_genie_sales`).
1474
+ */
1475
+ const GENIE_INSTRUCTIONS = stringUtils.toDescription(["Genie orchestration. For every user question that needs SQL-backed data:", { numbered: [
1476
+ `
1477
+ Start by calling \`get_space_description\` to ground yourself
1478
+ in what the space covers (tables, metrics, time windows),
1479
+ unless you already saw the same description earlier in this
1480
+ conversation. Reach for \`get_space_serialized\` only when you
1481
+ need exact column / table identifiers the description doesn't
1482
+ expose - it's a much larger payload.
1483
+ `,
1484
+ `
1485
+ Decompose the user's question into focused sub-questions
1486
+ BEFORE asking Genie anything. One sub-question per distinct
1487
+ metric, dimension, or time window. Then call \`ask_genie\`
1488
+ once per sub-question - usually two to six calls per turn,
1489
+ and let earlier answers inform what you ask next. Cramming
1490
+ a multi-part question into one \`ask_genie\` call almost
1491
+ always produces a worse answer than asking the pieces
1492
+ separately. Only collapse to a single call when the question
1493
+ is genuinely atomic ("what was Q3 revenue?").
1494
+
1495
+ Worked example: user asks "How did SKU 1234's revenue
1496
+ compare to its category average last quarter, and which
1497
+ regions drove the gap?" Decomposes to: (a) \`ask_genie\`
1498
+ for SKU 1234's Q3 revenue, (b) \`ask_genie\` for the
1499
+ category-average Q3 revenue, (c) \`ask_genie\` for SKU
1500
+ 1234's Q3 revenue split by region. Three focused calls,
1501
+ each grounded in the prior results.
1502
+ `,
1503
+ `
1504
+ Each \`ask_genie\` call returns the terminal \`GenieMessage\`.
1505
+ When the turn ran SQL the result has a \`statement_id\` - read
1506
+ it from \`message.query_result.statement_id\` (or the first
1507
+ attachment's \`query.statement_id\`).
1508
+ `,
1509
+ [
1510
+ `
1511
+ To DISPLAY a result set in your reply, embed a marker on its
1512
+ own line where the visualization should appear. Two marker
1513
+ shapes:
1514
+ `,
1515
+ { bullets: [`
1516
+ \`[data:<statement_id>]\` - render the rows as a table.
1517
+ Use this when there's no clear visual story (long lists,
1518
+ reference data, single-row results, or the user just
1519
+ wants to see the data). Embed the marker directly; no
1520
+ tool call needed.
1521
+ `, `
1522
+ \`[chart:<chartId>]\` - render the rows as a chart. To
1523
+ get a \`<chartId>\`, call \`prepare_chart\` with the
1524
+ statement's id (and an optional \`title\` / one-line
1525
+ \`description\` of the insight to surface). The tool
1526
+ returns the \`chartId\` synchronously and prepares the
1527
+ chart spec in the background; embed the returned id as
1528
+ \`[chart:<chartId>]\` on its own line wherever the
1529
+ chart should appear. Use a chart when the data has a
1530
+ story a visual conveys better than a table (trends,
1531
+ rankings, distributions, parts-of-a-whole).
1532
+
1533
+ NEVER invent or hand-build a \`<chartId>\`. A valid
1534
+ \`<chartId>\` is the opaque token a \`prepare_chart\`
1535
+ call returned to you in THIS turn - nothing else. It
1536
+ is NOT a \`statement_id\`, and it is NOT a
1537
+ \`statement_id\` prefix with a label appended (e.g.
1538
+ \`01f1...-region-fill\`). If you have not called
1539
+ \`prepare_chart\` and received an id back, do not write
1540
+ a \`[chart:...]\` marker at all - use \`[data:...]\`
1541
+ instead. A fabricated chart id renders nothing and
1542
+ wastes a request.
1543
+ `] },
1544
+ `
1545
+ The host UI resolves both markers on its own once it sees
1546
+ them - you do NOT need to call \`get_statement\` just to
1547
+ display data, and you do NOT need to wait on
1548
+ \`prepare_chart\` (it returns the id immediately and the
1549
+ host UI fetches the cached chart later). Pick at most one
1550
+ marker per statement; don't chart AND table the same result
1551
+ side by side.
1552
+ `
1553
+ ],
1554
+ `
1555
+ Call \`get_statement(statement_id, limit?)\` ONLY when you need
1556
+ to read the actual values to reason about them (e.g. naming a
1557
+ specific row, computing a delta the table or chart wouldn't
1558
+ show, or sanity-checking a result before interpreting it). If
1559
+ you'd just be reciting numbers the visualization already shows,
1560
+ skip the call and use a marker instead. \`limit\` defaults to a
1561
+ small sample; raise it only when you genuinely need more rows.
1562
+ `,
1563
+ `
1564
+ Compose your final reply as plain prose. Interleave paragraphs
1565
+ with \`[data:...]\` / \`[chart:...]\` markers wherever a result
1566
+ should render. Don't dump all markers at the end - place each
1567
+ one next to the prose that interprets it. Don't restate every
1568
+ number the visualization already shows; call out deltas,
1569
+ anomalies, takeaways.
1570
+ `
1571
+ ] }]);
1572
+ /**
1573
+ * Normalize the {@link GenieSpacesConfig} record. Bare-string
1574
+ * entries (`{ default: "01ef..." }`) get wrapped as
1575
+ * `{ spaceId: "01ef..." }`; object entries pass through unchanged.
1576
+ * `undefined` and empty-string values are dropped so callers can
1577
+ * pass `process.env.X` directly (matches AppKit `genie()`'s
1578
+ * defensive treatment of unset env vars).
1579
+ */
1580
+ function normalizeGenieSpaces(spaces) {
1581
+ if (!spaces) return {};
1582
+ const out = {};
1583
+ for (const [alias, value] of Object.entries(spaces)) {
1584
+ if (value === void 0) continue;
1585
+ if (typeof value === "string") {
1586
+ if (!value) continue;
1587
+ out[alias] = { spaceId: value };
1588
+ continue;
1589
+ }
1590
+ if (!value.spaceId) continue;
1591
+ out[alias] = value;
1592
+ }
1593
+ return out;
1594
+ }
1595
+ /**
1596
+ * Discover Genie space aliases from every supported source and
1597
+ * merge them into a single record. Precedence (highest first):
1598
+ *
1599
+ * 1. {@link MastraPluginConfig.genieSpaces} on the `mastra(...)`
1600
+ * call. Explicit Mastra wiring always wins so users can
1601
+ * override AppKit's defaults per-agent.
1602
+ * 2. AppKit `genie({ spaces: { ... } })` plugin instance. Lets
1603
+ * users keep using the existing AppKit config format
1604
+ * (`genie({ spaces: { sales: "...", ops: "..." } })`)
1605
+ * without restating the same record on the Mastra plugin.
1606
+ * Read off the live plugin instance via a structural cast
1607
+ * since `Plugin.config` is TS-protected (not runtime-private).
1608
+ * 3. `DATABRICKS_GENIE_SPACE_ID` env var (registered under the
1609
+ * well-known `default` alias). Matches the AppKit `genie()`
1610
+ * plugin's fallback behavior so a bare `mastra()` + `genie()`
1611
+ * pair just works.
1612
+ *
1613
+ * Aliases collide cleanly: a higher-precedence source's value
1614
+ * replaces a lower one's wholesale. Sources that contribute zero
1615
+ * aliases (or contribute only `undefined` / empty entries) are
1616
+ * silently ignored.
1617
+ */
1618
+ function resolveGenieSpaces(config, context) {
1619
+ const merged = {};
1620
+ const envSpaceId = process.env["DATABRICKS_GENIE_SPACE_ID"];
1621
+ if (envSpaceId) merged[DEFAULT_GENIE_ALIAS] = { spaceId: envSpaceId };
1622
+ const geniePlugin = appkitUtils.instance(context, genie);
1623
+ if (geniePlugin) {
1624
+ const pluginSpaces = geniePlugin.config?.spaces;
1625
+ if (pluginSpaces) Object.assign(merged, normalizeGenieSpaces(pluginSpaces));
1626
+ }
1627
+ if (config.genieSpaces) Object.assign(merged, normalizeGenieSpaces(config.genieSpaces));
1628
+ return merged;
1629
+ }
1630
+ /**
1631
+ * Build the flat Mastra tools record for every configured Genie
1632
+ * space. Two shared, space-agnostic tools (`get_statement`,
1633
+ * `prepare_chart`) are registered once regardless of how many
1634
+ * spaces are wired; the per-space tools (`ask_genie`,
1635
+ * `get_space_description`, `get_space_serialized`) are suffixed
1636
+ * with `_<alias>` for non-default aliases so multi-space
1637
+ * deployments stay disambiguated.
1638
+ *
1639
+ * Returns a record keyed by tool id, ready to spread into the
1640
+ * central `Agent`'s `tools` map (or surfaced via the
1641
+ * `plugins.genie?.toolkit()` callback). Returns an empty record
1642
+ * when `spaces` resolves to zero entries so the caller can spread
1643
+ * safely.
1644
+ */
1645
+ function buildGenieTools(opts) {
1646
+ const normalized = normalizeGenieSpaces(opts.spaces);
1647
+ const tools = {};
1648
+ if (Object.keys(normalized).length === 0) return tools;
1649
+ tools.get_statement = buildGetStatementTool();
1650
+ tools.prepare_chart = buildPrepareChartTool({ config: opts.config });
1651
+ for (const [alias, space] of Object.entries(normalized)) {
1652
+ const askTool = buildAskGenieTool({
1653
+ spaceId: space.spaceId,
1654
+ alias,
1655
+ ...space.hint ? { hint: space.hint } : {}
1656
+ });
1657
+ const descTool = buildSpaceDescriptionTool({
1658
+ spaceId: space.spaceId,
1659
+ alias
1660
+ });
1661
+ const serTool = buildSpaceSerializedTool({
1662
+ spaceId: space.spaceId,
1663
+ alias
1664
+ });
1665
+ tools[askTool.id] = askTool;
1666
+ tools[descTool.id] = descTool;
1667
+ tools[serTool.id] = serTool;
1668
+ }
1669
+ return tools;
1670
+ }
1671
+ /**
1672
+ * Plugin-toolkit adapter so the `plugins.genie?.toolkit()` lookup
1673
+ * inside an agent's `tools(plugins)` callback returns the
1674
+ * flat Genie tools record instead of throwing on missing plugin.
1675
+ * Mirrors AppKit's `PluginToolkitProvider` shape.
1676
+ */
1677
+ function buildGenieToolkitProvider(opts) {
1678
+ return { toolkit(_opts) {
1679
+ return buildGenieTools(opts);
1680
+ } };
1681
+ }
1682
+ /**
1683
+ * Default cap on starter suggestions surfaced to the chat empty
1684
+ * state. Sample-question lists can run long (10+ on a curated
1685
+ * space); a handful of one-tap prompts reads better than a wall of
1686
+ * buttons.
1687
+ */
1688
+ const SUGGESTION_LIMIT = 6;
1689
+ /**
1690
+ * How long a space's parsed sample questions stay cached. They're
1691
+ * authored config that changes rarely, and the lookup is an extra
1692
+ * REST round-trip per chat mount, so a few minutes of caching keeps
1693
+ * the empty state instant on reload without going stale for long.
1694
+ */
1695
+ const SUGGESTION_CACHE_TTL_MS = 10 * 6e4;
1696
+ /** Space-id -> cached sample questions with an absolute expiry. */
1697
+ const _suggestionCache = /* @__PURE__ */ new Map();
1698
+ /** Read a space's cached questions, or `undefined` on miss / expiry. */
1699
+ function readSuggestionCache(spaceId) {
1700
+ const entry = _suggestionCache.get(spaceId);
1701
+ if (!entry) return void 0;
1702
+ if (entry.expires <= Date.now()) {
1703
+ _suggestionCache.delete(spaceId);
1704
+ return;
1705
+ }
1706
+ return entry.questions;
1707
+ }
1708
+ /** Cache a space's parsed questions for {@link SUGGESTION_CACHE_TTL_MS}. */
1709
+ function writeSuggestionCache(spaceId, questions) {
1710
+ _suggestionCache.set(spaceId, {
1711
+ questions,
1712
+ expires: Date.now() + SUGGESTION_CACHE_TTL_MS
1713
+ });
1714
+ }
1715
+ /**
1716
+ * Collect the curated starter questions across every resolved Genie
1717
+ * space, deduped and capped. Each space's `sample_questions` are
1718
+ * fetched once (cached for {@link SUGGESTION_CACHE_TTL_MS}) via
1719
+ * {@link getGenieSpace} + {@link genieSampleQuestions}, then merged in
1720
+ * alias-iteration order so a single-space app surfaces that space's
1721
+ * questions and a multi-space app round-trips breadth-first up to the
1722
+ * cap. A per-space fetch failure degrades to "no questions for that
1723
+ * space" (logged, not thrown) so one unreachable space never blanks
1724
+ * the whole list. Returns `[]` when no spaces are configured.
1725
+ */
1726
+ async function collectSpaceSuggestions(opts) {
1727
+ const limit = opts.limit ?? SUGGESTION_LIMIT;
1728
+ const merged = [];
1729
+ const seen = /* @__PURE__ */ new Set();
1730
+ for (const { spaceId } of Object.values(opts.spaces)) {
1731
+ let questions = readSuggestionCache(spaceId);
1732
+ if (!questions) try {
1733
+ questions = genieSampleQuestions(await getGenieSpace(spaceId, {
1734
+ workspaceClient: opts.client,
1735
+ ...opts.signal ? { context: opts.signal } : {}
1736
+ }));
1737
+ writeSuggestionCache(spaceId, questions);
1738
+ } catch (err) {
1739
+ log$4.warn("suggestions:fetch-error", {
1740
+ spaceId,
1741
+ error: commonUtils.errorMessage(err)
1742
+ });
1743
+ questions = [];
1744
+ }
1745
+ for (const question of questions) {
1746
+ if (seen.has(question)) continue;
1747
+ seen.add(question);
1748
+ merged.push(question);
1749
+ if (merged.length >= limit) return merged;
1750
+ }
1751
+ }
1752
+ return merged;
1753
+ }
1754
+
1755
+ //#endregion
1756
+ //#region packages/appkit-mastra/src/processor.ts
1757
+ var ResultProcessor = class {
1758
+ id = "result-processor";
1759
+ processDataParts = true;
1760
+ async processOutputStream({ part }) {
1761
+ if (!part || typeof part !== "object") return part;
1762
+ if (![
1763
+ "step-finish",
1764
+ "finish",
1765
+ "tool-result",
1766
+ "data-tool-agent"
1767
+ ].includes(part.type)) return part;
1768
+ const payload = part.payload;
1769
+ if (!payload || typeof payload !== "object") return part;
1770
+ for (const key of [
1771
+ "output",
1772
+ "messages",
1773
+ "response",
1774
+ "result"
1775
+ ]) if (key in payload) {
1776
+ const value = payload[key];
1777
+ if (typeof value === "object") payload[key] = {};
1778
+ else if (Array.isArray(value)) payload[key] = [];
1779
+ else delete payload[key];
1780
+ }
1781
+ return part;
1782
+ }
1783
+ };
1784
+
1785
+ //#endregion
1786
+ //#region packages/appkit-mastra/src/processors/strip-stale-charts.ts
1787
+ /**
1788
+ * Mastra input processor that strips `chartId` fields from every
1789
+ * tool-invocation result in prior assistant messages before they
1790
+ * reach the model.
1791
+ *
1792
+ * Why: chartIds are turn-scoped from the model's point of view -
1793
+ * each `prepare_chart` / `render_data` call mints a fresh id and
1794
+ * the host UI binds it to that turn's reply. Mastra Memory
1795
+ * replays prior tool results into the prompt; if old chartIds
1796
+ * leak through, the model is tempted to copy them verbatim into
1797
+ * the new turn's `[chart:<id>]` markers and the host UI ends up
1798
+ * rendering an unrelated chart from the chart cache (or
1799
+ * a 404 once the 1h TTL elapsed). This processor removes the
1800
+ * temptation by deleting `chartId` keys from every assistant
1801
+ * message's tool results before the prompt is built. The current
1802
+ * turn's tool results don't exist yet at `processInput` time, so
1803
+ * they pass through unmodified.
1804
+ *
1805
+ * The strip is recursive - any nested `chartId` field is removed,
1806
+ * regardless of which tool produced the result. This covers
1807
+ * `prepare_chart` / `render_data` top-level chartIds and any
1808
+ * legacy `datasets[].chartId` payloads uniformly without coupling
1809
+ * to specific tool ids.
1810
+ */
1811
+ const log$3 = logUtils.logger("mastra/processor/strip-stale-charts");
1812
+ /**
1813
+ * Recursively clone `value`, omitting any property whose key is
1814
+ * `chartId`. Arrays are mapped element-wise; primitives are
1815
+ * returned as-is. The result is structurally identical to the
1816
+ * input minus chartIds, so downstream message-shape consumers
1817
+ * keep working.
1818
+ */
1819
+ function stripChartIds(value) {
1820
+ if (Array.isArray(value)) return value.map(stripChartIds);
1821
+ if (value && typeof value === "object") {
1822
+ const obj = value;
1823
+ const out = {};
1824
+ for (const [key, val] of Object.entries(obj)) {
1825
+ if (key === "chartId") continue;
1826
+ out[key] = stripChartIds(val);
1827
+ }
1828
+ return out;
1829
+ }
1830
+ return value;
1831
+ }
1832
+ /**
1833
+ * Input processor that scrubs `chartId` from every tool-invocation
1834
+ * result in the message list. Wired onto every agent by default
1835
+ * via {@link buildAgents}; opt out with
1836
+ * `MastraPluginConfig.stripStaleCharts: false`.
1837
+ */
1838
+ const stripStaleChartsProcessor = {
1839
+ id: "strip-stale-charts",
1840
+ description: "Removes chartId fields from prior tool-invocation results so the model can't reuse turn-scoped ids from memory.",
1841
+ processInput(args) {
1842
+ let stripped = 0;
1843
+ for (const message of args.messages) {
1844
+ if (message.role !== "assistant") continue;
1845
+ const parts = message.content?.parts;
1846
+ if (!Array.isArray(parts)) continue;
1847
+ for (const part of parts) {
1848
+ if (part.type !== "tool-invocation") continue;
1849
+ const inv = part.toolInvocation;
1850
+ if (!inv || inv.result === void 0) continue;
1851
+ const before = inv.result;
1852
+ const after = stripChartIds(before);
1853
+ if (typeof before === "object" && before !== null && JSON.stringify(before).length !== JSON.stringify(after).length) {
1854
+ inv.result = after;
1855
+ stripped += 1;
1856
+ }
1857
+ }
1858
+ }
1859
+ if (stripped > 0) log$3.debug("stripped", { results: stripped });
1860
+ return args.messages;
1861
+ }
1862
+ };
1863
+
1864
+ //#endregion
1865
+ //#region packages/appkit-mastra/src/agents.ts
1866
+ /**
1867
+ * Agent registration for the Mastra AppKit plugin.
1868
+ *
1869
+ * Mirrors the shape of the AppKit `agents` plugin (`config.agents` map
1870
+ * of {@link MastraAgentDefinition}, dual-form `tools` accepting a plain
1871
+ * record or a `(plugins) => tools` callback). Resolves each definition
1872
+ * into a Mastra `Agent` instance during plugin setup; user-supplied
1873
+ * tool callbacks are invoked exactly once with a typed
1874
+ * {@link MastraPlugins} map built from registered sibling plugins.
1875
+ *
1876
+ * When no agents are registered the plugin falls back to a single
1877
+ * built-in analyst so the bare `mastra()` call still mounts a working
1878
+ * streamable agent for demos.
1879
+ */
1880
+ /**
1881
+ * AppKit-shaped tool factory. Lets users mix-and-match tools across
1882
+ * AppKit's `agents` plugin and `mastra` with a single import:
1883
+ *
1884
+ * ```ts
1885
+ * import { tool } from "@dbx-tools/appkit-mastra";
1886
+ * import { z } from "zod";
1887
+ *
1888
+ * get_weather: tool({
1889
+ * description: "Weather",
1890
+ * schema: z.object({ city: z.string() }),
1891
+ * execute: async ({ city }) => `Sunny in ${city}`,
1892
+ * }),
1893
+ * ```
1894
+ *
1895
+ * Maps onto Mastra's `createTool`:
1896
+ *
1897
+ * - `description` -> `description` (required)
1898
+ * - `schema` -> `inputSchema` (optional)
1899
+ * - `execute(input)` -> `execute(input, ctx)` - Mastra already calls
1900
+ * the first arg with the parsed inputs, so the body shape is
1901
+ * identical. The Mastra `context` arg is forwarded as the second
1902
+ * parameter when the caller declares it.
1903
+ * - `id`: optional. Defaults to a stable identifier derived from
1904
+ * `description` (slugified, with a short hash suffix for
1905
+ * uniqueness). Pass an explicit `id` when you need a stable string
1906
+ * for tracing or MCP exposure.
1907
+ *
1908
+ * Reach for {@link createTool} when you need Mastra-only fields
1909
+ * (`outputSchema`, `suspendSchema`, `requireApproval`, `mcp`, etc.).
1910
+ */
1911
+ function tool(opts) {
1912
+ return createTool$1({
1913
+ id: opts.id ?? deriveToolId(opts.description),
1914
+ description: opts.description,
1915
+ ...opts.schema ? { inputSchema: opts.schema } : {},
1916
+ execute: opts.execute
1917
+ });
1918
+ }
1919
+ /**
1920
+ * Build a deterministic Mastra tool id from a description.
1921
+ * Delegates to {@link stringUtils.toUniqueSlug}: slug + always-on
1922
+ * 6-char FNV-1a base-32 suffix so two tools with the same leading
1923
+ * words don't collide in traces. Stable across runs.
1924
+ */
1925
+ function deriveToolId(description) {
1926
+ return stringUtils.toUniqueSlug(description, { fallbackPrefix: "tool" });
1927
+ }
1928
+ /**
1929
+ * Identity helper that brands a definition as a Mastra agent. Mirrors
1930
+ * AppKit's `createAgent(def)` so the registration shape matches:
1931
+ *
1932
+ * ```ts
1933
+ * const support = createAgent({
1934
+ * instructions: "...",
1935
+ * model: "databricks-claude-sonnet-4-6",
1936
+ * tools(plugins) { return { ... }; },
1937
+ * });
1938
+ * ```
1939
+ *
1940
+ * Returns the definition unchanged - the wrapper exists only to anchor
1941
+ * type inference and to match the AppKit API surface.
1942
+ */
1943
+ function createAgent(def) {
1944
+ return def;
1945
+ }
1946
+ /** Fallback agent id used when `config.agents` is omitted entirely. */
1947
+ const FALLBACK_AGENT_ID = "default";
1948
+ /**
1949
+ * Default per-turn step ceiling applied to every registered agent
1950
+ * when {@link MastraPluginConfig.agentMaxSteps} is unset. Sized to
1951
+ * fit a decomposed Genie turn (grounding + several `ask_genie`
1952
+ * calls + `prepare_chart` per dataset + the final-text reply) with
1953
+ * headroom for the model to chain a couple of follow-ups before
1954
+ * answering - well above Mastra's own `agent.generate` default of
1955
+ * 5, which would cut multi-step orchestration off mid-loop.
1956
+ */
1957
+ const DEFAULT_AGENT_MAX_STEPS = 25;
1958
+ const FALLBACK_AGENT_INSTRUCTIONS = `You are a data analyst. The user will ask questions about
1959
+ business metrics and may share personal preferences you should remember across turns.
1960
+
1961
+ Rules:
1962
+
1963
+ 1. Quote numbers exactly. Never invent data.
1964
+ 2. When the user states a preference or durable fact about themselves
1965
+ ("I'm in EU so use EUR", "always show me the SQL"), acknowledge that
1966
+ you will remember it.
1967
+ 3. If you don't have enough information to answer, ask a clarifying
1968
+ question instead of guessing.`;
1969
+ /**
1970
+ * Style guardrails appended to every agent's `instructions` to curb
1971
+ * common LLM-isms (em dashes, emojis, sycophantic openers, excessive
1972
+ * hedging, throwaway closers). Appended rather than prepended so the
1973
+ * agent's role/context comes first; the model's recency bias then
1974
+ * helps the style rules dominate the response surface.
1975
+ *
1976
+ * Override globally via {@link MastraPluginConfig.styleInstructions}
1977
+ * (pass `false` to disable entirely, or a string to replace).
1978
+ */
1979
+ const DEFAULT_STYLE_INSTRUCTIONS = [
1980
+ "Output style:",
1981
+ "",
1982
+ "Use markdown formatting, including headings, lists, and code blocks.",
1983
+ "Avoid lists and headers for short replies.",
1984
+ "Plain prose.",
1985
+ "Use hyphens (-) only. Never use em dashes or en dashes.",
1986
+ "Never use emojis.",
1987
+ "Skip openers like 'Great question', 'Absolutely', and 'I'd be happy to help'.",
1988
+ "Skip closers like 'Let me know if you have any questions'.",
1989
+ "Skip self-disclaimers like 'I should mention' and 'It's important to note'.",
1990
+ "Answer directly.",
1991
+ "Do not include a preamble before the actual answer.",
1992
+ "Use lists and headers only when they clarify a multi-part answer."
1993
+ ].join("\n");
1994
+ /**
1995
+ * Resolve the style block to append to every agent's instructions.
1996
+ * Returns `null` when the caller opted out (`styleInstructions: false`).
1997
+ */
1998
+ function resolveStyleInstructions(config) {
1999
+ if (config.styleInstructions === false) return null;
2000
+ if (typeof config.styleInstructions === "string") return config.styleInstructions;
2001
+ return DEFAULT_STYLE_INSTRUCTIONS;
2002
+ }
2003
+ /**
2004
+ * Join an agent's bespoke instructions with the resolved style block.
2005
+ * Returns the bespoke text unchanged when the style block is disabled.
2006
+ */
2007
+ function composeInstructions(agentInstructions, style) {
2008
+ if (!style) return agentInstructions;
2009
+ return `${agentInstructions.trimEnd()}\n\n${style}`;
2010
+ }
2011
+ /**
2012
+ * Resolve every entry in `config.agents` into a Mastra `Agent`
2013
+ * instance. When `config.agents` is omitted the plugin registers a
2014
+ * single built-in `default` analyst so the bare `mastra()` call still
2015
+ * yields a working agent.
2016
+ *
2017
+ * Per-agent tool callbacks are invoked once with a typed
2018
+ * {@link MastraPlugins} index built from registered sibling plugins
2019
+ * (currently `genie`; extend `MastraPlugins` to surface more).
2020
+ *
2021
+ * @throws when `config.defaultAgent` is set to an id that isn't in the
2022
+ * resolved registry; this is a wiring bug, not a runtime condition.
2023
+ */
2024
+ async function buildAgents(opts) {
2025
+ const { config, context, memoryBuilder, log } = opts;
2026
+ const definitions = resolveDefinitions(config);
2027
+ const ids = Object.keys(definitions);
2028
+ const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
2029
+ const plugins = buildPluginsMap(config, context);
2030
+ const ambientTools = {
2031
+ render_data: buildRenderDataTool(config),
2032
+ ...config.tools ?? {}
2033
+ };
2034
+ const style = resolveStyleInstructions(config);
2035
+ const outputProcessors = [new ResultProcessor()];
2036
+ const inputProcessors = config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
2037
+ const agents = {};
2038
+ for (const [id, def] of Object.entries(definitions)) {
2039
+ const tools = await resolveTools(def.tools, plugins, ambientTools);
2040
+ const memory = memoryBuilder?.forAgent(id, def);
2041
+ agents[id] = new Agent({
2042
+ id,
2043
+ name: def.name ?? id,
2044
+ ...def.description !== void 0 ? { description: def.description } : {},
2045
+ instructions: composeInstructions(def.instructions, style),
2046
+ model: resolveModel(config, def.model),
2047
+ defaultOptions: { maxSteps: config.agentMaxSteps ?? DEFAULT_AGENT_MAX_STEPS },
2048
+ tools,
2049
+ ...memory ? { memory } : {},
2050
+ inputProcessors,
2051
+ outputProcessors
2052
+ });
2053
+ log.info("agent registered", {
2054
+ id,
2055
+ name: def.name ?? id,
2056
+ defaultModel: describeAgentDefaultModel(config, def),
2057
+ tools: Object.keys(tools)
2058
+ });
2059
+ }
2060
+ if (!agents[defaultAgentId]) throw new Error(`mastra: defaultAgent "${defaultAgentId}" not found in registered agents (${ids.join(", ") || "none"})`);
2061
+ log.info("agents ready", {
2062
+ ids,
2063
+ defaultAgentId
2064
+ });
2065
+ return {
2066
+ agents,
2067
+ defaultAgentId,
2068
+ ambientTools
2069
+ };
2070
+ }
2071
+ /**
2072
+ * Best-effort description of the *static* default model an agent will
2073
+ * resolve to at call time. Walks the same precedence ladder as
2074
+ * {@link resolveModel} / {@link buildModel}:
2075
+ *
2076
+ * 1. Per-agent `def.model` (string sugar -> the literal id;
2077
+ * function / `DynamicArgument` -> `"<dynamic>"` because the
2078
+ * resolver decides at call time).
2079
+ * 2. Plugin-level `config.defaultModel` (same rules).
2080
+ * 3. `DATABRICKS_SERVING_ENDPOINT_NAME` env var.
2081
+ * 4. First entry of `config.defaultModelFallbacks ?? FALLBACK_MODEL_IDS`.
2082
+ *
2083
+ * Used for the startup `agent registered` log so operators can see
2084
+ * which endpoint each agent points at by default. Per-request
2085
+ * overrides (`X-Mastra-Model` etc.) and the workspace-catalogue
2086
+ * fuzzy match are still applied at runtime.
2087
+ */
2088
+ function describeAgentDefaultModel(config, def) {
2089
+ const effective = def.model ?? config.defaultModel;
2090
+ if (typeof effective === "string") return effective;
2091
+ if (effective !== void 0) return "<dynamic>";
2092
+ return process.env.DATABRICKS_SERVING_ENDPOINT_NAME ?? config.defaultModelFallbacks?.[0] ?? FALLBACK_MODEL_IDS[0];
2093
+ }
2094
+ /**
2095
+ * Normalize `config.agents` into a `Record<id, definition>`. Accepts
2096
+ * any of the three shapes documented on
2097
+ * {@link MastraPluginConfig.agents}:
2098
+ *
2099
+ * - Record - returned as-is when non-empty.
2100
+ * - Single definition (detected via the required `instructions`
2101
+ * field) - keyed by `slugify(def.name)` or `FALLBACK_AGENT_ID`.
2102
+ * - Array - keyed by `slugify(def.name)` or `agent_${i}`; duplicate
2103
+ * slugs fail loudly so users know to set explicit names.
2104
+ *
2105
+ * Omitted or empty inputs fall back to a single built-in analyst so
2106
+ * the bare `mastra()` call still mounts a working chat route.
2107
+ */
2108
+ function resolveDefinitions(config) {
2109
+ const input = config.agents;
2110
+ if (!input) return fallbackDefinitions();
2111
+ if (Array.isArray(input)) {
2112
+ if (input.length === 0) return fallbackDefinitions();
2113
+ const out = {};
2114
+ input.forEach((def, i) => {
2115
+ const key = deriveAgentKey(def, i);
2116
+ if (out[key]) throw new Error(`mastra: duplicate agent id "${key}" derived from name "${def.name ?? ""}"; set unique \`name\`s on each definition`);
2117
+ out[key] = def;
2118
+ });
2119
+ return out;
2120
+ }
2121
+ if (typeof input.instructions === "string") {
2122
+ const def = input;
2123
+ return { [deriveAgentKey(def)]: def };
2124
+ }
2125
+ const record = input;
2126
+ if (Object.keys(record).length === 0) return fallbackDefinitions();
2127
+ return record;
2128
+ }
2129
+ /** Derive a registry id from a definition's `name`, with a fallback. */
2130
+ function deriveAgentKey(def, index) {
2131
+ if (def.name) {
2132
+ const slug = stringUtils.toIdentifier(def.name);
2133
+ if (slug) return slug;
2134
+ }
2135
+ return index === void 0 ? FALLBACK_AGENT_ID : `agent_${index}`;
2136
+ }
2137
+ /** Built-in fallback registry used when `agents` is omitted / empty. */
2138
+ function fallbackDefinitions() {
2139
+ return { [FALLBACK_AGENT_ID]: {
2140
+ name: "Default Agent",
2141
+ instructions: FALLBACK_AGENT_INSTRUCTIONS
2142
+ } };
2143
+ }
2144
+ /**
2145
+ * Pick the effective model spec for an agent. Fallback ladder, in
2146
+ * order:
2147
+ *
2148
+ * 1. Per-agent `def.model` (string sugar or `DynamicArgument`).
2149
+ * 2. Plugin-level `config.defaultModel` (string sugar or
2150
+ * `DynamicArgument`) - mirrors AppKit's `agents({ defaultModel })`.
2151
+ * 3. The auto-resolver that mints user-scoped tokens against
2152
+ * `/serving-endpoints` via {@link buildModel}.
2153
+ *
2154
+ * String values are treated as `modelId` sugar and threaded through
2155
+ * `buildModel`'s override hook so the runtime fuzzy matcher and the
2156
+ * per-request `X-Mastra-Model` override layer on top of the static
2157
+ * choice. Non-string `DynamicArgument`s are passed through verbatim;
2158
+ * callers that need full control over `providerId` / `headers` /
2159
+ * `modelId` bypass the resolver pipeline entirely.
2160
+ */
2161
+ function resolveModel(config, override) {
2162
+ const effective = override ?? config.defaultModel;
2163
+ if (effective === void 0) return ({ requestContext }) => buildModel(config, requestContext);
2164
+ if (typeof effective === "string") {
2165
+ const modelId = effective;
2166
+ return ({ requestContext }) => buildModel(config, requestContext, { modelId });
2167
+ }
2168
+ return effective;
2169
+ }
2170
+ /**
2171
+ * Resolve a definition's `tools` field to a flat `MastraTools` record,
2172
+ * merging in plugin-level ambient tools (per-agent tools win on key
2173
+ * collision). Callback errors propagate verbatim so the original stack
2174
+ * survives - the caller already knows which agent was registering.
2175
+ */
2176
+ async function resolveTools(defTools, plugins, ambientTools) {
2177
+ if (!defTools) return { ...ambientTools };
2178
+ const resolved = typeof defTools === "function" ? await defTools(plugins) : defTools;
2179
+ return {
2180
+ ...ambientTools,
2181
+ ...resolved
2182
+ };
2183
+ }
2184
+ /**
2185
+ * Build the {@link MastraPlugins} runtime proxy handed to
2186
+ * `tools(plugins)` callbacks.
2187
+ *
2188
+ * Implemented as a `Proxy` over the AppKit plugin context so
2189
+ * `plugins.<name>` resolves at first access. Any sibling plugin that
2190
+ * implements AppKit's standard `ToolProvider` interface
2191
+ * (`toolkit(opts?)` + `executeAgentTool(name, args, signal?)`) is
2192
+ * auto-adapted into Mastra tools. Unknown names return `undefined`,
2193
+ * matching AppKit's `Plugins` semantics so `plugins.foo?.toolkit()`
2194
+ * remains safe in environments where `foo` isn't registered.
2195
+ *
2196
+ * `genie` is special-cased to swap the generic AppKit toolkit (which
2197
+ * runs `executeAgentTool` and only emits a single final `tool-result`
2198
+ * chunk per call) for the streaming-aware tools built by
2199
+ * {@link buildGenieProvider}. The streaming variant forwards each
2200
+ * Genie wire event (status, SQL, row counts, errors) out through the
2201
+ * Mastra `ctx.writer`, so the UI gets `tool-output` chunks in real
2202
+ * time instead of staring at a spinner for the full Genie round-trip.
2203
+ */
2204
+ function buildPluginsMap(config, context) {
2205
+ const cache = /* @__PURE__ */ new Map();
2206
+ return new Proxy({}, { get(_target, propName) {
2207
+ if (typeof propName !== "string") return void 0;
2208
+ if (cache.has(propName)) return cache.get(propName) ?? void 0;
2209
+ const provider = resolveProvider(config, context, propName);
2210
+ cache.set(propName, provider);
2211
+ return provider ?? void 0;
2212
+ } });
2213
+ }
2214
+ /**
2215
+ * Pick the right {@link MastraPluginToolkitProvider} for a sibling
2216
+ * plugin lookup. Returns the Genie agent-backed adapter when
2217
+ * the caller asks for `genie` AND at least one space is reachable
2218
+ * via {@link resolveGenieSpaces} (the explicit
2219
+ * `config.genieSpaces`, the registered AppKit `genie()` plugin's
2220
+ * `spaces` config, or the `DATABRICKS_GENIE_SPACE_ID` env var).
2221
+ * Falls back to the generic AppKit `ToolProvider` adapter for
2222
+ * every other plugin name. `config` is threaded through so the
2223
+ * Genie agent inherits the same model resolver / fallback
2224
+ * ladder the calling agents use.
2225
+ *
2226
+ * The Genie agent talks to Genie directly via `@dbx-tools/genie`
2227
+ * (`genieEventChat`) and the workspace
2228
+ * `statementExecution.getStatement` API. AppKit's stock `genie`
2229
+ * plugin is honored only for its resource manifest and `spaces`
2230
+ * config so existing `app.yaml` configs and `genie({ spaces })`
2231
+ * wiring keep working without change.
2232
+ */
2233
+ function resolveProvider(config, context, propName) {
2234
+ if (propName === "genie") {
2235
+ const spaces = resolveGenieSpaces(config, context);
2236
+ if (Object.keys(spaces).length === 0) return null;
2237
+ return buildGenieToolkitProvider({
2238
+ spaces,
2239
+ config
2240
+ });
2241
+ }
2242
+ const plugin = context?.getPlugins().get(propName);
2243
+ return adaptPluginToolkit(plugin);
2244
+ }
2245
+ /**
2246
+ * Adapt an AppKit `ToolProvider` plugin instance into a
2247
+ * {@link MastraPluginToolkitProvider}. Returns `null` for any plugin
2248
+ * that doesn't implement both `toolkit` and `executeAgentTool` (e.g.
2249
+ * `server`, `lakebase` when used only as a Postgres pool, etc.).
2250
+ */
2251
+ function adaptPluginToolkit(plugin) {
2252
+ if (!plugin || typeof plugin !== "object") return null;
2253
+ const p = plugin;
2254
+ if (typeof p.toolkit !== "function" || typeof p.executeAgentTool !== "function") return null;
2255
+ return { toolkit(opts) {
2256
+ const entries = p.toolkit(opts);
2257
+ const tools = {};
2258
+ for (const [key, entry] of Object.entries(entries)) tools[key] = toolkitEntryToMastraTool(entry, p);
2259
+ return tools;
2260
+ } };
2261
+ }
2262
+ /**
2263
+ * Wrap a single {@link AppKitToolkitEntry} as a Mastra tool whose
2264
+ * `execute` dispatches back through `plugin.executeAgentTool(...)` so
2265
+ * AppKit's OBO auth (`asUser`) and telemetry spans stay intact. JSON
2266
+ * Schema parameters pass through unchanged - Mastra's `PublicSchema`
2267
+ * accepts `JSONSchema7` directly via `@mastra/schema-compat`.
2268
+ */
2269
+ function toolkitEntryToMastraTool(entry, plugin) {
2270
+ return createTool$1({
2271
+ id: `${entry.pluginName}__${entry.localName}`,
2272
+ description: entry.def.description,
2273
+ ...entry.def.parameters ? { inputSchema: entry.def.parameters } : {},
2274
+ execute: async (input, context) => {
2275
+ const signal = context?.abortSignal;
2276
+ return plugin.executeAgentTool(entry.localName, input, signal);
2277
+ }
2278
+ });
2279
+ }
2280
+
2281
+ //#endregion
2282
+ //#region packages/appkit-mastra/src/mcp.ts
2283
+ /** MCP server version advertised when the caller doesn't pin one. */
2284
+ const DEFAULT_MCP_VERSION = "1.0.0";
2285
+ /**
2286
+ * Build the plugin's MCP server, or `null` when `config.mcp` is falsy
2287
+ * (the default - no MCP endpoints).
2288
+ *
2289
+ * `true` exposes every registered agent as an `ask_<agentId>` MCP tool
2290
+ * under a server id equal to the plugin name. The object form
2291
+ * ({@link MastraMcpConfig}) tunes the id / advertised metadata and can
2292
+ * additionally expose the plugin's ambient tools or a set of extra
2293
+ * MCP-only tools.
2294
+ */
2295
+ function buildMcpServer(opts) {
2296
+ const { config, pluginName, displayName, agents, ambientTools } = opts;
2297
+ if (!config.mcp) return null;
2298
+ const settings = config.mcp === true ? {} : config.mcp;
2299
+ const serverId = settings.serverId ?? pluginName;
2300
+ const exposeAgents = settings.agents !== false;
2301
+ const tools = {
2302
+ ...settings.tools === true ? ambientTools : {},
2303
+ ...settings.extraTools ?? {}
2304
+ };
2305
+ return {
2306
+ serverId,
2307
+ server: new MCPServer({
2308
+ id: serverId,
2309
+ name: settings.name ?? `${displayName} MCP`,
2310
+ version: settings.version ?? DEFAULT_MCP_VERSION,
2311
+ ...settings.description ? { description: settings.description } : {},
2312
+ ...exposeAgents ? { agents } : {},
2313
+ tools
2314
+ }),
2315
+ httpPath: `/mcp/${serverId}/mcp`,
2316
+ ssePath: `/mcp/${serverId}/sse`,
2317
+ messagePath: `/mcp/${serverId}/messages`
2318
+ };
2319
+ }
2320
+
2321
+ //#endregion
2322
+ //#region packages/appkit-mastra/src/history.ts
2323
+ const log$2 = logUtils.logger("mastra/history");
2324
+ /** Default history page size; matches the Mastra storage default. */
2325
+ const DEFAULT_PER_PAGE = 20;
2326
+ /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
2327
+ const MAX_PER_PAGE = 200;
2328
+ /**
2329
+ * Fetch a page of UI-formatted messages for a thread.
2330
+ *
2331
+ * Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
2332
+ * storage namespaces (`mastra_<agentId>` schemas) and any future
2333
+ * memory-side filters apply automatically. When the agent has no
2334
+ * memory configured the response is a successful empty page so
2335
+ * callers don't have to special-case stateless agents.
2336
+ *
2337
+ * Pagination is descending-by-default: page 0 is the most recent
2338
+ * page, page 1 the page before that, etc. The returned `uiMessages`
2339
+ * are always re-sorted into chronological order (oldest -> newest)
2340
+ * so the client can prepend them above the existing transcript
2341
+ * without sorting locally.
2342
+ */
2343
+ async function loadHistory(opts) {
2344
+ const perPage = clampPerPage(opts.perPage);
2345
+ const page = Math.max(0, Math.trunc(opts.page ?? 0));
2346
+ const memory = await opts.agent.getMemory();
2347
+ if (!memory) {
2348
+ log$2.debug("recall:no-memory", {
2349
+ agentId: opts.agent.id,
2350
+ threadId: opts.threadId
2351
+ });
2352
+ return {
2353
+ uiMessages: [],
2354
+ page,
2355
+ perPage,
2356
+ total: 0,
2357
+ hasMore: false
2358
+ };
2359
+ }
2360
+ const startedAt = Date.now();
2361
+ const result = await memory.recall({
2362
+ threadId: opts.threadId,
2363
+ ...opts.resourceId ? { resourceId: opts.resourceId } : {},
2364
+ page,
2365
+ perPage,
2366
+ orderBy: {
2367
+ field: "createdAt",
2368
+ direction: opts.ascending ? "ASC" : "DESC"
2369
+ }
2370
+ });
2371
+ const uiMessages = toAISdkV5Messages(sortChronological(result.messages));
2372
+ log$2.debug("recall:done", {
2373
+ agentId: opts.agent.id,
2374
+ threadId: opts.threadId,
2375
+ page,
2376
+ perPage,
2377
+ returned: uiMessages.length,
2378
+ total: result.total,
2379
+ hasMore: result.hasMore,
2380
+ elapsedMs: Date.now() - startedAt
2381
+ });
2382
+ return {
2383
+ uiMessages,
2384
+ page,
2385
+ perPage,
2386
+ total: result.total,
2387
+ hasMore: result.hasMore
2388
+ };
2389
+ }
2390
+ /**
2391
+ * Wipe every persisted message tied to a thread. Returns the count
2392
+ * of messages that were on the thread at delete time so the caller
2393
+ * can render a "cleared N messages" affordance without an
2394
+ * additional round-trip.
2395
+ *
2396
+ * Agents without a configured `Memory` resolve to a no-op (count
2397
+ * 0), matching {@link loadHistory}'s "stateless agents return an
2398
+ * empty page" stance so callers don't have to special-case them.
2399
+ * Threads that don't exist yet are also a successful no-op - the
2400
+ * operation is idempotent so the UI can fire-and-forget without
2401
+ * tracking thread existence.
2402
+ */
2403
+ async function clearHistory(opts) {
2404
+ const memory = await opts.agent.getMemory();
2405
+ if (!memory) {
2406
+ log$2.debug("clear:no-memory", {
2407
+ agentId: opts.agent.id,
2408
+ threadId: opts.threadId
2409
+ });
2410
+ return { cleared: 0 };
2411
+ }
2412
+ let cleared = 0;
2413
+ try {
2414
+ cleared = (await memory.recall({
2415
+ threadId: opts.threadId,
2416
+ page: 0,
2417
+ perPage: 1
2418
+ })).total;
2419
+ } catch (err) {
2420
+ log$2.debug("clear:probe-failed", {
2421
+ agentId: opts.agent.id,
2422
+ threadId: opts.threadId,
2423
+ error: commonUtils.errorMessage(err)
2424
+ });
2425
+ }
2426
+ const startedAt = Date.now();
2427
+ try {
2428
+ await memory.deleteThread(opts.threadId);
2429
+ } catch (err) {
2430
+ log$2.warn("clear:delete-soft-failed", {
2431
+ agentId: opts.agent.id,
2432
+ threadId: opts.threadId,
2433
+ error: commonUtils.errorMessage(err)
2434
+ });
2435
+ }
2436
+ log$2.info("clear:done", {
2437
+ agentId: opts.agent.id,
2438
+ threadId: opts.threadId,
2439
+ cleared,
2440
+ elapsedMs: Date.now() - startedAt
2441
+ });
2442
+ return { cleared };
2443
+ }
2444
+ /**
2445
+ * Register the `<path>` Mastra custom API route. Handles two
2446
+ * methods on the same mount:
2447
+ *
2448
+ * - `GET`: return a page of AI SDK V5 `UIMessage`s for the
2449
+ * caller's current thread ({@link loadHistory}).
2450
+ * - `DELETE`: wipe every persisted message on the caller's
2451
+ * thread ({@link clearHistory}). The session cookie that
2452
+ * anchors the thread id is left alone so the user keeps the
2453
+ * same thread - only the contents go away.
2454
+ *
2455
+ * Follows the `@mastra/ai-sdk` `chatRoute` convention for agent
2456
+ * binding: pass `agent` for a fixed-agent mount, or include
2457
+ * `:agentId` in the path for dynamic routing. The plugin registers
2458
+ * both `/route/history` (default agent) and `/route/history/:agentId`.
2459
+ *
2460
+ * The handler reads `threadId` and `resourceId` from `RequestContext`
2461
+ * (populated upstream by `MastraServer.registerAuthMiddleware`), so
2462
+ * no cookie or user lookups happen here.
2463
+ */
2464
+ function historyRoute(options) {
2465
+ const { path } = options;
2466
+ const fixedAgent = "agent" in options ? options.agent : void 0;
2467
+ if (!fixedAgent && !path.includes(":agentId")) throw new Error("historyRoute path must include `:agentId` or `agent` must be passed explicitly");
2468
+ const resolveContext = (c) => {
2469
+ const mastra = c.get("mastra");
2470
+ const requestContext = c.get("requestContext");
2471
+ const agentId = fixedAgent ?? c.req.param("agentId");
2472
+ if (!agentId) return { error: c.json({ error: "agentId is required" }, 400) };
2473
+ const agent = mastra.getAgentById(agentId);
2474
+ if (!agent) return { error: c.json({ error: `Unknown agent "${agentId}"` }, 404) };
2475
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY);
2476
+ if (!threadId) return { error: c.json({ error: "thread id missing from request context" }, 400) };
2477
+ return {
2478
+ agentId,
2479
+ agent,
2480
+ threadId,
2481
+ resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY)
2482
+ };
2483
+ };
2484
+ return [registerApiRoute(path, {
2485
+ method: "GET",
2486
+ handler: async (c) => {
2487
+ const ctx = resolveContext(c);
2488
+ if ("error" in ctx) return ctx.error;
2489
+ const payload = await loadHistory({
2490
+ agent: ctx.agent,
2491
+ threadId: ctx.threadId,
2492
+ ...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
2493
+ page: parseIntParam(c.req.query("page")),
2494
+ perPage: parseIntParam(c.req.query("perPage"))
2495
+ });
2496
+ return c.json(payload);
2497
+ }
2498
+ }), registerApiRoute(path, {
2499
+ method: "DELETE",
2500
+ handler: async (c) => {
2501
+ const ctx = resolveContext(c);
2502
+ if ("error" in ctx) return ctx.error;
2503
+ const { cleared } = await clearHistory({
2504
+ agent: ctx.agent,
2505
+ threadId: ctx.threadId
2506
+ });
2507
+ const payload = {
2508
+ ok: true,
2509
+ agentId: ctx.agentId,
2510
+ threadId: ctx.threadId,
2511
+ cleared
2512
+ };
2513
+ return c.json(payload);
2514
+ }
2515
+ })];
2516
+ }
2517
+ /** Coerce / clamp `perPage`; falls back to the page-size default. */
2518
+ function clampPerPage(value) {
2519
+ if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
2520
+ const n = Math.trunc(value);
2521
+ if (n <= 0) return DEFAULT_PER_PAGE;
2522
+ return Math.min(n, MAX_PER_PAGE);
2523
+ }
2524
+ /**
2525
+ * Sort messages oldest-first by `createdAt`, falling back to whatever
2526
+ * order the storage returned them in. The native `recall` call honors
2527
+ * `orderBy` but doesn't guarantee a stable secondary sort, so we
2528
+ * normalize here before handing the page to the AI SDK converter.
2529
+ */
2530
+ function sortChronological(messages) {
2531
+ return [...messages].sort((a, b) => {
2532
+ return toEpoch(a.createdAt) - toEpoch(b.createdAt);
2533
+ });
2534
+ }
2535
+ function toEpoch(value) {
2536
+ if (value instanceof Date) return value.getTime();
2537
+ if (typeof value === "string" || typeof value === "number") {
2538
+ const parsed = new Date(value).getTime();
2539
+ return Number.isNaN(parsed) ? 0 : parsed;
2540
+ }
2541
+ return 0;
2542
+ }
2543
+ /**
2544
+ * Coerce a Hono query value into a non-negative integer. Returns
2545
+ * `undefined` for empty / non-numeric / negative inputs so
2546
+ * {@link loadHistory} can apply its built-in defaults.
2547
+ */
2548
+ function parseIntParam(value) {
2549
+ if (!value) return void 0;
2550
+ const n = Number(value);
2551
+ if (!Number.isFinite(n) || n < 0) return void 0;
2552
+ return Math.trunc(n);
2553
+ }
2554
+
2555
+ //#endregion
2556
+ //#region packages/appkit-mastra/src/memory.ts
2557
+ /**
2558
+ * Lakebase-backed Mastra memory wiring.
2559
+ *
2560
+ * Provides a {@link MemoryBuilder} that mints one `Memory` per agent
2561
+ * with two independent knobs:
2562
+ *
2563
+ * - **Storage** (threads / messages via `PostgresStore`): defaults to
2564
+ * **per-agent** namespacing via `schemaName: "mastra_<agentId>"` so
2565
+ * conversation history stays isolated between agents in the same
2566
+ * database. `PostgresStore` auto-creates the schema with
2567
+ * `CREATE SCHEMA IF NOT EXISTS` on init.
2568
+ * - **Memory** (semantic recall via `PgVector`): defaults to a single
2569
+ * **shared** instance across every agent. Cross-agent recall on one
2570
+ * index is almost always what users want; opt into per-agent recall
2571
+ * by passing a {@link MastraMemoryConfigOverride} on the agent.
2572
+ *
2573
+ * Additionally, {@link MemoryBuilder.instanceStorage} returns a
2574
+ * **Mastra-instance-level** `PostgresStore` (schema `mastra_instance`)
2575
+ * used for workflow snapshots - the persistence layer
2576
+ * `agent.resumeStream()` reads from when waking a suspended
2577
+ * `requireApproval` tool call. Per-agent stores are not enough for
2578
+ * this: workflow runs are scoped to the Mastra instance, not an
2579
+ * individual agent's `Memory`.
2580
+ *
2581
+ * Plugin-level `config.storage` / `config.memory` act as the baseline
2582
+ * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
2583
+ * is registered); per-agent settings cascade on top of that.
2584
+ */
2585
+ const log$1 = logUtils.logger("mastra/memory");
2586
+ /**
2587
+ * Build a dedicated **service-principal** Lakebase pool for Mastra
2588
+ * memory from the lakebase plugin's resolved SP pg config.
2589
+ *
2590
+ * The plugin's `exports().pool` is a `RoutingPool` that switches to
2591
+ * the per-user (OBO) pool whenever a query runs inside an `asUser`
2592
+ * scope - exactly the context the mastra plugin establishes around
2593
+ * every chat turn. Memory (threads / messages + semantic recall) must
2594
+ * instead always act as the app service principal: it owns the
2595
+ * auto-created `mastra_*` schemas (a per-user role usually can't
2596
+ * `CREATE SCHEMA`) and is shared across users, so it cannot inherit a
2597
+ * request's OBO identity.
2598
+ *
2599
+ * `pgConfig` must be the plugin's `exports().getPgConfig()` evaluated
2600
+ * **outside** any `asUser` scope (i.e. during setup), so it carries
2601
+ * the SP connection target, OAuth token-refresh `password` callback,
2602
+ * and any `lakebase({ pool })` tuning overrides - all of which this
2603
+ * pool inherits. See the call site in `plugin.ts`.
2604
+ */
2605
+ async function createServicePrincipalPool(pgConfig) {
2606
+ const user = pgConfig.user ?? await getUsernameWithApiLookup();
2607
+ return new Pool({
2608
+ ...pgConfig,
2609
+ user
2610
+ });
2611
+ }
2612
+ /**
2613
+ * True when any plugin-level or per-agent setting could need the
2614
+ * Lakebase pool. Used by `plugin.ts` to gate creation of the
2615
+ * service-principal pool and the {@link MemoryBuilder} that consumes
2616
+ * it; when false neither is built.
2617
+ */
2618
+ function needsLakebase(config) {
2619
+ if (settingNeedsSharedPool(config.storage)) return true;
2620
+ if (settingNeedsSharedPool(config.memory)) return true;
2621
+ return collectAgentDefinitions(config).some((d) => settingNeedsSharedPool(d.storage) || settingNeedsSharedPool(d.memory));
2622
+ }
2623
+ /**
2624
+ * Construct a per-agent {@link Memory} factory bound to the supplied
2625
+ * service-principal pool (see {@link createServicePrincipalPool}).
2626
+ * Caches the shared `PgVector` singleton (built on first need) so each
2627
+ * agent build is O(1) after the first.
2628
+ */
2629
+ function createMemoryBuilder(config, servicePrincipalPool) {
2630
+ return new MemoryBuilder(config, servicePrincipalPool);
2631
+ }
2632
+ /**
2633
+ * Builds one `Memory` per agent against a shared service-principal
2634
+ * Lakebase pool. Per-instance state keeps the shared `PgVector` alive
2635
+ * across calls so registering N agents stays cheap.
2636
+ */
2637
+ var MemoryBuilder = class {
2638
+ sharedVector;
2639
+ constructor(config, servicePrincipalPool) {
2640
+ this.config = config;
2641
+ this.servicePrincipalPool = servicePrincipalPool;
2642
+ }
2643
+ /**
2644
+ * Build a `Memory` for `agentId` after the plugin/agent cascade.
2645
+ * Returns `undefined` when the agent has neither storage nor a
2646
+ * vector store enabled - Mastra accepts a missing `memory` field
2647
+ * and treats the agent as stateless.
2648
+ */
2649
+ /**
2650
+ * Build the Mastra-instance-level storage used for workflow
2651
+ * snapshots. Returns `undefined` when plugin-level `storage` is
2652
+ * disabled, in which case `agent.resumeStream()` (and therefore
2653
+ * the `requireApproval` flow) will not be available.
2654
+ *
2655
+ * The store lives in a dedicated `mastra_instance` schema so it
2656
+ * never collides with per-agent `mastra_<agentId>` namespaces.
2657
+ * Workflow snapshots are not per-agent state; they belong to the
2658
+ * `Mastra` instance that owns the workflow execution.
2659
+ */
2660
+ instanceStorage() {
2661
+ const setting = this.config.storage;
2662
+ if (!setting) return void 0;
2663
+ if (typeof setting === "object") return new PostgresStore(withId(setting, "mastra-store__instance"));
2664
+ return new PostgresStore({
2665
+ id: "mastra-store__instance",
2666
+ schemaName: "mastra_instance",
2667
+ pool: this.servicePrincipalPool
2668
+ });
2669
+ }
2670
+ forAgent(agentId, def) {
2671
+ const storageSetting = def.storage ?? this.config.storage;
2672
+ const memorySetting = def.memory ?? this.config.memory;
2673
+ const storage = this.buildStorage(agentId, storageSetting);
2674
+ const vector = this.buildVector(memorySetting);
2675
+ if (!storage && !vector) {
2676
+ log$1.debug("agent:stateless", { agentId });
2677
+ return;
2678
+ }
2679
+ log$1.debug("agent:configured", {
2680
+ agentId,
2681
+ storage: storage !== void 0,
2682
+ vector: vector !== void 0,
2683
+ vectorMode: vector === void 0 ? "off" : typeof memorySetting === "object" ? "dedicated" : "shared"
2684
+ });
2685
+ return new Memory({
2686
+ ...storage ? { storage } : {},
2687
+ ...vector ? {
2688
+ vector,
2689
+ embedder: fastembed
2690
+ } : {},
2691
+ options: {
2692
+ lastMessages: 10,
2693
+ ...vector ? { semanticRecall: {
2694
+ topK: 3,
2695
+ messageRange: 2
2696
+ } } : {}
2697
+ }
2698
+ });
2699
+ }
2700
+ buildStorage(agentId, setting) {
2701
+ if (!setting) return void 0;
2702
+ if (typeof setting === "boolean") return new PostgresStore({
2703
+ id: `mastra-store__${agentId}`,
2704
+ schemaName: `mastra_${agentId}`,
2705
+ pool: this.servicePrincipalPool
2706
+ });
2707
+ return new PostgresStore(withId(setting, `mastra-store__${agentId}`));
2708
+ }
2709
+ /**
2710
+ * Resolve the agent's vector store. Cascade:
2711
+ *
2712
+ * - falsy: no vector.
2713
+ * - `boolean` / `undefined-inheriting-true`: return the shared
2714
+ * singleton (built lazily on first call). All agents that
2715
+ * default-enable memory write into and recall from one index.
2716
+ * - object: build a dedicated `PgVector` for this agent.
2717
+ */
2718
+ buildVector(setting) {
2719
+ if (!setting) return void 0;
2720
+ if (typeof setting === "boolean") return this.getSharedVector();
2721
+ return buildPgVector(setting);
2722
+ }
2723
+ getSharedVector() {
2724
+ if (!this.sharedVector) this.sharedVector = buildSharedPgVector(this.servicePrincipalPool);
2725
+ return this.sharedVector;
2726
+ }
2727
+ };
2728
+ /**
2729
+ * Build the shared `PgVector` that backs the default
2730
+ * `def.memory === true` case across every agent.
2731
+ *
2732
+ * `PgVector`'s constructor accepts only connection-style configs
2733
+ * (`HostConfig` / `ConnectionStringConfig` / `ClientConfig`); there is
2734
+ * no `{ pool }` shorthand the way `PostgresStore` has one. Worse, the
2735
+ * constructor synchronously kicks off a `cacheWarmupPromise` IIFE that
2736
+ * calls `this.pool.connect()` before returning, so we can't cleanly
2737
+ * hand it an inert config and patch the pool afterwards.
2738
+ *
2739
+ * The trick: pass illegal-but-validation-passing placeholders so the
2740
+ * warmup's `net.connect()` rejects synchronously with `RangeError`
2741
+ * (Node validates `0 <= port < 65536`). The IIFE's `catch {}` swallows
2742
+ * it, no DNS lookup or TCP attempt happens, and we then swap
2743
+ * `pgVector.pool` to the lakebase pool. Every subsequent `PgVector`
2744
+ * method reads `this.pool` at call time, so all real I/O goes through
2745
+ * the lakebase pool from then on. The placeholder pool is `.end()`'d
2746
+ * so its socket book-keeping is released.
2747
+ */
2748
+ function buildSharedPgVector(pool) {
2749
+ const vector = new PgVector({
2750
+ id: `pg${randomUUID()}`,
2751
+ host: "-1",
2752
+ port: -1,
2753
+ database: "_",
2754
+ user: "_",
2755
+ password: "_"
2756
+ });
2757
+ const placeholder = vector.pool;
2758
+ vector.pool = pool;
2759
+ placeholder.end().catch(() => void 0);
2760
+ return vector;
2761
+ }
2762
+ /** Per-agent dedicated `PgVector` (rare; opt-in via object override). */
2763
+ function buildPgVector(setting) {
2764
+ return new PgVector(withId(setting, `pg-vector__${randomUUID()}`));
2765
+ }
2766
+ /** True when this setting requires the shared Lakebase pool. */
2767
+ function settingNeedsSharedPool(setting) {
2768
+ return setting === true;
2769
+ }
2770
+ /** Walk the three shapes of `config.agents` into a flat list. */
2771
+ function collectAgentDefinitions(config) {
2772
+ const agents = config.agents;
2773
+ if (!agents) return [];
2774
+ if (Array.isArray(agents)) return agents;
2775
+ if (typeof agents.instructions === "string") return [agents];
2776
+ return Object.values(agents);
2777
+ }
2778
+ /** Fill in a default `id` when the caller didn't supply one. */
2779
+ function withId(value, fallback) {
2780
+ return value.id ? value : {
2781
+ ...value,
2782
+ id: fallback
2783
+ };
2784
+ }
2785
+
2786
+ //#endregion
2787
+ //#region packages/appkit-mastra/src/observability.ts
2788
+ /**
2789
+ * Mastra observability wired through the same OTel pipeline AppKit's
2790
+ * built-in plugins (e.g. `agents`) use, via `@mastra/otel-bridge`.
2791
+ *
2792
+ * How traces flow:
2793
+ *
2794
+ * 1. `@databricks/appkit` boots a global `NodeSDK` in
2795
+ * `TelemetryManager.initialize()` (during `createApp`) when
2796
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` is set in the process env.
2797
+ * 2. Every AppKit plugin span (e.g. the `agents` plugin's
2798
+ * `executeStream`) is created via the global OTel tracer
2799
+ * (`trace.getTracer(<plugin>)`), so it lands on that NodeSDK and
2800
+ * is shipped through its OTLP exporter.
2801
+ * 3. The Mastra `OtelBridge` ALSO creates real OTel spans on the same
2802
+ * global tracer for every Mastra operation (agent runs, model
2803
+ * calls, tool invocations, workflow steps). They inherit the
2804
+ * ambient OTel context, so when Mastra is invoked from inside an
2805
+ * AppKit HTTP span the trace stays connected.
2806
+ *
2807
+ * Net effect: Mastra spans get exactly the treatment AppKit's
2808
+ * `agents` plugin gets. No custom OTLP pipeline lives in this
2809
+ * package; the OTLP endpoint, headers, and resource attributes are
2810
+ * driven by the standard OTel env vars
2811
+ * (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`,
2812
+ * `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, ...) and consumed
2813
+ * by AppKit's `TelemetryManager`. Set those once and both AppKit and
2814
+ * Mastra spans end up at the same backend.
2815
+ *
2816
+ * When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the bridge's spans go
2817
+ * to the global noop tracer, mirroring how the `agents` plugin
2818
+ * silently no-ops in the same situation.
2819
+ */
2820
+ const log = logUtils.logger("mastra/observability");
2821
+ const DEFAULT_SERVICE_NAME = "mastra";
2822
+ /**
2823
+ * Build a Mastra `Observability` whose spans ride AppKit's global
2824
+ * OTel pipeline via `@mastra/otel-bridge`.
2825
+ *
2826
+ * Returns `undefined` only if someone explicitly opts out in the
2827
+ * future; today it always returns an `Observability` because the
2828
+ * bridge degrades gracefully (no-op tracer) when no global OTel SDK
2829
+ * is registered. Callers can spread `...(observability ? { observability } : {})`
2830
+ * either way to stay forward-compatible.
2831
+ */
2832
+ async function buildObservability(options) {
2833
+ const serviceName = options?.serviceName ?? await projectUtils.name() ?? DEFAULT_SERVICE_NAME;
2834
+ const requestContextKeys = [...options?.requestContextKeys ?? TRACE_REQUEST_CONTEXT_KEYS];
2835
+ const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
2836
+ const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
2837
+ const resolvedTracesUrl = otelTracesOverride ? otelTracesOverride : otelBase ? `${otelBase.replace(/\/+$/, "")}/v1/traces` : void 0;
2838
+ log.info("Mastra observability wired through OTel bridge", {
2839
+ serviceName,
2840
+ requestContextKeys,
2841
+ otelBase: otelBase ?? "<unset>",
2842
+ resolvedTracesUrl: resolvedTracesUrl ?? "<noop; OTLP endpoint unset>"
2843
+ });
2844
+ return new Observability({ configs: { serviceName: {
2845
+ serviceName,
2846
+ bridge: new OtelBridge(),
2847
+ requestContextKeys
2848
+ } } });
2849
+ }
2850
+
2851
+ //#endregion
2852
+ //#region packages/appkit-mastra/src/server.ts
2853
+ /**
2854
+ * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
2855
+ * stamps the per-request `RequestContext`, and a route-patch middleware
2856
+ * that lets the plugin's custom API routes (e.g. `historyRoute`) work
2857
+ * behind an Express mount point.
2858
+ */
2859
+ /**
2860
+ * `@mastra/express` subclass that stamps `RequestContext` with the
2861
+ * AppKit user, resource id, and a thread id backed by an HTTP-only
2862
+ * session cookie (`appkit_<plugin-name>_session_id`).
2863
+ */
2864
+ var MastraServer$1 = class extends MastraServer {
2865
+ log;
2866
+ constructor(config, ...args) {
2867
+ super(...args);
2868
+ this.config = config;
2869
+ this.log = logUtils.logger(config);
2870
+ }
2871
+ registerAuthMiddleware() {
2872
+ super.registerAuthMiddleware();
2873
+ this.app.use((req, res, next) => {
2874
+ const requestContext = res.locals.requestContext;
2875
+ this.configureRequestContextUser(requestContext);
2876
+ this.configureRequestContextThreadId(req, res, requestContext);
2877
+ this.configureRequestContextModelOverride(req, requestContext);
2878
+ this.configureRequestContextRequestId(req, res, requestContext);
2879
+ this.log.debug("auth:middleware", {
2880
+ method: req.method,
2881
+ path: req.path,
2882
+ requestId: requestContext.get(MASTRA_REQUEST_ID_KEY),
2883
+ threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
2884
+ resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
2885
+ userName: requestContext.get(MASTRA_USER_NAME_KEY),
2886
+ userEmail: requestContext.get(MASTRA_USER_EMAIL_KEY),
2887
+ modelOverride: requestContext.get("mastra__model_override")
2888
+ });
2889
+ next();
2890
+ });
2891
+ }
2892
+ configureRequestContextUser(requestContext) {
2893
+ if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
2894
+ const executionContext = getExecutionContext();
2895
+ const user = {
2896
+ id: "userId" in executionContext ? executionContext.userId : executionContext.serviceUserId,
2897
+ executionContext
2898
+ };
2899
+ requestContext.set(MASTRA_USER_KEY, user);
2900
+ requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
2901
+ if ("isUserContext" in executionContext) {
2902
+ if (executionContext.userName) requestContext.set(MASTRA_USER_NAME_KEY, executionContext.userName);
2903
+ if (executionContext.userEmail) requestContext.set(MASTRA_USER_EMAIL_KEY, executionContext.userEmail);
2904
+ }
2905
+ }
2906
+ /**
2907
+ * Stamp a per-request id and echo it on the response so an upstream
2908
+ * proxy / curl client / browser-side log line can pair its view of
2909
+ * the request with the matching trace span. Reuses `X-Request-Id`
2910
+ * when the upstream already supplies one so multi-hop traces stay
2911
+ * joined; otherwise mints a UUIDv4.
2912
+ *
2913
+ * The id is surfaced as `mastra__requestId` span metadata via
2914
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} and as the `X-Request-Id`
2915
+ * response header so dev tools can copy it from either side.
2916
+ */
2917
+ configureRequestContextRequestId(req, res, requestContext) {
2918
+ if (requestContext.get(MASTRA_REQUEST_ID_KEY)) return;
2919
+ const headerValue = req.headers["x-request-id"];
2920
+ const requestId = (Array.isArray(headerValue) ? headerValue[0] : headerValue)?.trim() || randomUUID();
2921
+ requestContext.set(MASTRA_REQUEST_ID_KEY, requestId);
2922
+ res.setHeader("X-Request-Id", requestId);
2923
+ }
2924
+ configureRequestContextThreadId(req, res, requestContext) {
2925
+ if (requestContext.get(MASTRA_THREAD_ID_KEY)) return;
2926
+ const cookies = httpUtils.parseCookies(req.headers.cookie);
2927
+ const cookieName = stringUtils.toIdentifierWithOptions({
2928
+ delimiter: "_",
2929
+ distinct: true
2930
+ }, "appkit", this.config.name, "sessionId");
2931
+ let sessionId = cookies[cookieName];
2932
+ if (!sessionId) {
2933
+ sessionId = randomUUID();
2934
+ res.cookie(cookieName, sessionId, {
2935
+ httpOnly: true,
2936
+ sameSite: "lax",
2937
+ secure: req.secure,
2938
+ path: "/"
2939
+ });
2940
+ }
2941
+ requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
2942
+ }
2943
+ configureRequestContextModelOverride(req, requestContext) {
2944
+ if (resolveServingConfig(this.config).allowOverride) {
2945
+ const override = extractModelOverride({
2946
+ headers: req.headers,
2947
+ query: req.query,
2948
+ body: req.body
2949
+ });
2950
+ if (override) requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
2951
+ }
2952
+ }
2953
+ };
2954
+ /**
2955
+ * Patches around `@mastra/express`'s custom-route dispatcher so the
2956
+ * plugin's custom API routes (e.g. `historyRoute`) work when
2957
+ * `MastraServer` is hosted on an Express subapp mounted under a parent
2958
+ * path (e.g. `/api/mastra`).
2959
+ *
2960
+ * The adapter's `registerCustomApiRoutes` matches against `req.path`
2961
+ * (mount-relative, correct) but dispatches to its internal Hono
2962
+ * mini-app using `req.originalUrl`, which still contains the parent
2963
+ * mount prefix. The Hono app registers the literal route paths
2964
+ * (for example `/route/history`), so the absolute URL never matches
2965
+ * until we overwrite `originalUrl` for `/route` and `/route/*` to the
2966
+ * mount-relative path.
2967
+ */
2968
+ function attachRoutePatchMiddleware(app) {
2969
+ app.use((req, _res, next) => {
2970
+ if (!(req.path === "/route" || req.path.startsWith("/route/"))) return next();
2971
+ req.originalUrl = req.path;
2972
+ next();
2973
+ });
2974
+ }
2975
+
2976
+ //#endregion
2977
+ //#region packages/appkit-mastra/src/plugin.ts
2978
+ /**
2979
+ * AppKit plugin that builds one or more Mastra `Agent` instances and
2980
+ * mounts the `@mastra/express` server. Clients drive the conversation
2981
+ * over the standard Mastra agent stream (`@mastra/client-js`'s
2982
+ * `getAgent(id).stream()`), so there's no bespoke chat transport to
2983
+ * keep in sync.
2984
+ *
2985
+ * - Agents: registered through `config.agents` at plugin creation
2986
+ * ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
2987
+ * either a plain record or a `(plugins) => tools` callback that gets
2988
+ * a typed sibling-plugin index ({@link MastraPlugins}). Omit
2989
+ * `config.agents` to get a single built-in `default` analyst.
2990
+ * - Model: each agent call resolves a `MastraModelConfig` via
2991
+ * {@link buildModel} from `./model.js`. Per-agent `model` overrides
2992
+ * (`AgentConfig["model"]` or a `modelId` string) flow through
2993
+ * {@link buildAgents}.
2994
+ * - Memory / storage: per-agent, built by {@link createMemoryBuilder}
2995
+ * from `./memory.js`. Both auto-default to `true` when the
2996
+ * `lakebase` plugin is registered (unless the caller passed
2997
+ * `false` or a custom config). Storage namespaces per agent via
2998
+ * `schemaName: "mastra_<agentId>"`; the vector store is a single
2999
+ * shared singleton across every agent.
3000
+ * - Server: the Express subapp wiring lives in `./server.js`.
3001
+ * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
3002
+ * Mastra agent routes, the plugin registers `/route/history`
3003
+ * (load + clear thread history), `/models`, `/suggestions`, and the
3004
+ * generic `/embed/:type/:id` resolver for inline chart / data
3005
+ * markers.
3006
+ * - MCP: opt in with `config.mcp` to expose the agents (and optionally
3007
+ * tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
3008
+ * instance via `mcpServers`, so `@mastra/express` serves the stock
3009
+ * MCP transport routes (`/mcp/<serverId>/...`) under the mount. See
3010
+ * `./mcp.js`.
3011
+ */
3012
+ const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
3013
+ const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
3014
+ /**
3015
+ * AppKit plugin (registered name: `mastra`) that hosts Mastra agents
3016
+ * with optional Lakebase-backed memory and AI SDK chat routes under
3017
+ * the plugin mount (typically `/api/mastra`).
3018
+ */
3019
+ var MastraPlugin = class MastraPlugin extends Plugin {
3020
+ static manifest = {
3021
+ name: "mastra",
3022
+ displayName: "Mastra",
3023
+ description: "Builds a Mastra Agent with user-scoped workspace auth (asUser) and optional Postgres-backed Mastra Memory via the `lakebase` plugin.",
3024
+ stability: "beta",
3025
+ resources: {
3026
+ required: [],
3027
+ optional: [...GENIE_MANIFEST.resources.required, ...LAKEBASE_MANIFEST.resources.required]
3028
+ }
3029
+ };
3030
+ /**
3031
+ * Tighten resource requirements based on which features are enabled.
3032
+ * AppKit calls this at registration time (config-aware) so disabled
3033
+ * features don't surface their resource asks to the host app.
3034
+ */
3035
+ static getResourceRequirements(config) {
3036
+ const resources = [];
3037
+ const enabledManifests = [];
3038
+ if (needsLakebase(config)) enabledManifests.push(LAKEBASE_MANIFEST);
3039
+ for (const m of enabledManifests) for (const resource of m.resources.required) resources.push({
3040
+ ...resource,
3041
+ required: true
3042
+ });
3043
+ return resources;
3044
+ }
3045
+ log = logUtils.logger(this);
3046
+ built = null;
3047
+ mastra = null;
3048
+ mastraApp = null;
3049
+ mastraServer = null;
3050
+ /**
3051
+ * The optional MCP server exposing this plugin's agents / tools, or
3052
+ * `null` when `config.mcp` is disabled (the default). Built in
3053
+ * {@link buildAgentAndServer} and registered on the Mastra instance.
3054
+ */
3055
+ mcp = null;
3056
+ /**
3057
+ * Dedicated service-principal Lakebase pool backing Mastra memory /
3058
+ * storage. Built once in {@link buildAgentAndServer} (outside any
3059
+ * `asUser` scope, so it never inherits a request's OBO identity) and
3060
+ * drained in {@link abortActiveOperations}. `null` until setup runs
3061
+ * or when Lakebase isn't needed.
3062
+ */
3063
+ servicePrincipalPool = null;
3064
+ async setup() {
3065
+ this.context?.onLifecycle("setup:complete", async () => {
3066
+ this.applyLakebaseAutoDefaults();
3067
+ this.log.info("setup:complete");
3068
+ await this.buildAgentAndServer();
3069
+ });
3070
+ }
3071
+ /**
3072
+ * When the `lakebase` plugin is registered, auto-enable `storage`
3073
+ * and `memory` unless the caller opted out explicitly (`false` or a
3074
+ * custom config object). Run after `setup:complete` so the lookup
3075
+ * is reliable: any plugin that registers itself synchronously is
3076
+ * already in the registry by the time this fires.
3077
+ */
3078
+ applyLakebaseAutoDefaults() {
3079
+ if (!(appkitUtils.instance(this.context, lakebase) !== void 0)) return;
3080
+ if (this.config.storage === void 0) this.config.storage = true;
3081
+ if (this.config.memory === void 0) this.config.memory = true;
3082
+ }
3083
+ /**
3084
+ * Drain the memory service-principal pool on shutdown. AppKit calls
3085
+ * this during teardown; the lakebase plugin closes its own SP / OBO
3086
+ * pools the same way. Fire-and-forget so shutdown isn't blocked on a
3087
+ * slow drain, and clear the handle so a re-`setup()` rebuilds it.
3088
+ */
3089
+ abortActiveOperations() {
3090
+ super.abortActiveOperations();
3091
+ if (this.servicePrincipalPool) {
3092
+ this.log.info("closing memory SP pool");
3093
+ const pool = this.servicePrincipalPool;
3094
+ this.servicePrincipalPool = null;
3095
+ pool.end().catch((err) => {
3096
+ this.log.error("error closing memory SP pool", { error: commonUtils.errorMessage(err) });
3097
+ });
3098
+ }
3099
+ }
3100
+ exports() {
3101
+ return {
3102
+ list: () => Object.keys(this.built?.agents ?? {}),
3103
+ get: (id) => this.built?.agents[id] ?? null,
3104
+ getDefault: () => (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
3105
+ getMastra: () => this.mastra,
3106
+ getMcp: () => this.mcp ? {
3107
+ serverId: this.mcp.serverId,
3108
+ http: `/api/${this.name}${this.mcp.httpPath}`,
3109
+ sse: `/api/${this.name}${this.mcp.ssePath}`,
3110
+ messages: `/api/${this.name}${this.mcp.messagePath}`
3111
+ } : null,
3112
+ getMastraServer: () => this.mastraServer,
3113
+ listModels: () => this.listModels(),
3114
+ clearModelsCache: (host) => clearServingEndpointsCache(host)
3115
+ };
3116
+ }
3117
+ clientConfig() {
3118
+ return {
3119
+ basePath: `/api/${this.name}`,
3120
+ defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
3121
+ agents: Object.keys(this.built?.agents ?? {})
3122
+ };
3123
+ }
3124
+ injectRoutes(router) {
3125
+ router.get(MASTRA_ROUTES.models, (req, res, next) => {
3126
+ this.userScopedSelf(req).listModels().then((endpoints) => res.json({ endpoints })).catch(next);
3127
+ });
3128
+ const embedResolvers = {
3129
+ chart: (req, id, signal) => {
3130
+ const timeoutMs = parseTimeoutMs(req.query["timeoutMs"]);
3131
+ return fetchChart(id, {
3132
+ ...timeoutMs !== void 0 ? { timeoutMs } : {},
3133
+ signal
3134
+ });
3135
+ },
3136
+ data: (req, id, signal) => {
3137
+ const limit = parseStatementLimit(req.query["limit"]);
3138
+ return this.userScopedSelf(req).fetchStatement(id, {
3139
+ ...limit !== void 0 ? { limit } : {},
3140
+ signal
3141
+ });
3142
+ }
3143
+ };
3144
+ router.get(`${MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
3145
+ const type = req.params["type"] ?? "";
3146
+ const id = req.params["id"];
3147
+ const resolve = embedResolvers[type];
3148
+ if (!resolve) {
3149
+ res.status(404).json({ error: `unsupported embed type: ${type}` });
3150
+ return;
3151
+ }
3152
+ if (!id) {
3153
+ res.status(400).json({ error: "id is required" });
3154
+ return;
3155
+ }
3156
+ const controller = new AbortController();
3157
+ req.on("close", () => controller.abort());
3158
+ resolve(req, id, controller.signal).then((entry) => {
3159
+ if (entry === void 0) {
3160
+ res.status(404).json({ error: `${type} not found` });
3161
+ return;
3162
+ }
3163
+ res.json(entry);
3164
+ }).catch(next);
3165
+ });
3166
+ const handleSuggestions = (req, res) => {
3167
+ const controller = new AbortController();
3168
+ req.on("close", () => controller.abort());
3169
+ this.userScopedSelf(req).fetchSuggestions(controller.signal).then((questions) => res.json({ questions })).catch((err) => {
3170
+ this.log.warn("suggestions:error", { error: commonUtils.errorMessage(err) });
3171
+ res.json({ questions: [] });
3172
+ });
3173
+ };
3174
+ router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
3175
+ router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
3176
+ router.use((req, res, next) => {
3177
+ if (!this.mastraApp) return res.status(503).end();
3178
+ return this.userScopedSelf(req).dispatchMastra(req, res, next);
3179
+ });
3180
+ }
3181
+ /**
3182
+ * Invoke the Mastra express sub-app. Exists as a method (instead of reading
3183
+ * `this.mastraApp` through the `asUser(req)` proxy at the call site) so the
3184
+ * proxy binds this plain method - whose `.bind` is `Function.prototype.bind`
3185
+ * - rather than the express app, whose `.bind` is the HTTP BIND route
3186
+ * registrar (see the note in `injectRoutes`). Runs inside the user scope so
3187
+ * `getExecutionContext()` returns the OBO client for the agent/model
3188
+ * resolvers.
3189
+ */
3190
+ dispatchMastra(req, res, next) {
3191
+ this.mastraApp(req, res, next);
3192
+ }
3193
+ /**
3194
+ * Implementation backing the `/suggestions` route. Runs inside the
3195
+ * AppKit user-context proxy so `getExecutionContext()` returns the
3196
+ * OBO-scoped client. Resolves the plugin's Genie spaces and merges
3197
+ * their curated `sample_questions` (see {@link collectSpaceSuggestions}).
3198
+ * Returns `[]` when no Genie space is configured so the client
3199
+ * shows a bare empty state instead of built-in example prompts.
3200
+ */
3201
+ async fetchSuggestions(signal) {
3202
+ const spaces = resolveGenieSpaces(this.config, this.context);
3203
+ if (Object.keys(spaces).length === 0) return [];
3204
+ const client = getExecutionContext().client;
3205
+ return collectSpaceSuggestions({
3206
+ spaces,
3207
+ client,
3208
+ ...signal ? { signal } : {}
3209
+ });
3210
+ }
3211
+ /**
3212
+ * Implementation backing the `data` embed resolver
3213
+ * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
3214
+ * `getExecutionContext()` returns the OBO-scoped workspace
3215
+ * client, then reuses the same `fetchStatementData` pipeline
3216
+ * the `get_statement` tool runs so the LLM and the UI see the
3217
+ * exact same shape for the same statement.
3218
+ *
3219
+ * Returns `undefined` for upstream 404s so the route can map
3220
+ * them to a clean HTTP 404; any other failure bubbles up.
3221
+ */
3222
+ async fetchStatement(statementId, options = {}) {
3223
+ const client = getExecutionContext().client;
3224
+ const limit = Math.min(options.limit ?? STATEMENT_ROW_CAP, STATEMENT_ROW_CAP);
3225
+ try {
3226
+ const data = await fetchStatementData(client, statementId, {
3227
+ limit,
3228
+ ...options.signal ? { signal: options.signal } : {}
3229
+ });
3230
+ return {
3231
+ columns: data.columns,
3232
+ rows: data.rows,
3233
+ rowCount: data.rowCount,
3234
+ truncated: data.rows.length < data.rowCount
3235
+ };
3236
+ } catch (err) {
3237
+ if (apiUtils.isNotFoundError(err)) return void 0;
3238
+ throw err;
3239
+ }
3240
+ }
3241
+ /**
3242
+ * Return `this.asUser(req)` when the request carries an OBO token,
3243
+ * otherwise return `this` directly. Prevents the noisy AppKit warn
3244
+ * (`asUser() called without user token in development mode. Skipping
3245
+ * user impersonation.`) on every request in local dev where the
3246
+ * browser never sends `x-forwarded-access-token`. Behavior is
3247
+ * unchanged in production: a missing token always means a real OBO
3248
+ * proxy call (and AppKit will throw upstream if that's wrong).
3249
+ */
3250
+ userScopedSelf(req) {
3251
+ return req.header("x-forwarded-access-token") ? this.asUser(req) : this;
3252
+ }
3253
+ /**
3254
+ * Implementation backing both the `/models` route and the
3255
+ * `listModels` export. Runs inside the AppKit user-context proxy so
3256
+ * `getExecutionContext()` returns the OBO-scoped client.
3257
+ */
3258
+ async listModels() {
3259
+ const client = getExecutionContext().client;
3260
+ return listServingEndpoints(client, (await client.config.getHost()).toString(), { ttlMs: resolveServingConfig(this.config).ttlMs });
3261
+ }
3262
+ async buildAgentAndServer() {
3263
+ if (needsLakebase(this.config)) this.servicePrincipalPool = await createServicePrincipalPool(appkitUtils.require(this.context, lakebase, this.config).exports().getPgConfig());
3264
+ const memoryBuilder = this.servicePrincipalPool ? createMemoryBuilder(this.config, this.servicePrincipalPool) : void 0;
3265
+ this.log.debug("build:start", {
3266
+ lakebase: memoryBuilder !== void 0,
3267
+ stripStaleCharts: this.config.stripStaleCharts !== false
3268
+ });
3269
+ this.built = await buildAgents({
3270
+ config: this.config,
3271
+ context: this.context,
3272
+ memoryBuilder,
3273
+ log: this.log
3274
+ });
3275
+ const instanceStorage = memoryBuilder?.instanceStorage();
3276
+ const observability = await buildObservability({ serviceName: this.name });
3277
+ this.mcp = buildMcpServer({
3278
+ config: this.config,
3279
+ pluginName: this.name,
3280
+ displayName: MastraPlugin.manifest.displayName,
3281
+ agents: this.built.agents,
3282
+ ambientTools: this.built.ambientTools
3283
+ });
3284
+ this.mastra = new Mastra({
3285
+ agents: this.built.agents,
3286
+ ...instanceStorage ? { storage: instanceStorage } : {},
3287
+ ...observability ? { observability } : {},
3288
+ ...this.mcp ? { mcpServers: { [this.mcp.serverId]: this.mcp.server } } : {}
3289
+ });
3290
+ this.mastraApp = express();
3291
+ attachRoutePatchMiddleware(this.mastraApp);
3292
+ this.mastraServer = new MastraServer$1(this.config, {
3293
+ app: this.mastraApp,
3294
+ mastra: this.mastra,
3295
+ prefix: "",
3296
+ customApiRoutes: [...historyRoute({
3297
+ path: MASTRA_ROUTES.history,
3298
+ agent: this.built.defaultAgentId
3299
+ }), ...historyRoute({ path: `${MASTRA_ROUTES.history}/:agentId` })]
3300
+ });
3301
+ await this.mastraServer.init();
3302
+ this.log.debug("build:done", {
3303
+ agents: Object.keys(this.built.agents),
3304
+ defaultAgent: this.built.defaultAgentId,
3305
+ routes: [
3306
+ "/route/history",
3307
+ "/models",
3308
+ "/suggestions",
3309
+ "/embed/:type/:id"
3310
+ ],
3311
+ instanceStorage: instanceStorage !== void 0,
3312
+ observability: observability !== void 0 ? "mlflow" : "off",
3313
+ mcp: this.mcp ? `/api/${this.name}${this.mcp.httpPath}` : "off"
3314
+ });
3315
+ }
3316
+ };
3317
+ /**
3318
+ * Parse the optional `?timeoutMs=<n>` query parameter from a
3319
+ * `GET /embed/chart/:id` request. Accepts a positive integer up
3320
+ * to 5 minutes (clamped) and rejects everything else as
3321
+ * `undefined` so {@link fetchChart} falls back to its default.
3322
+ * Express produces `string | string[] | undefined`; we normalize
3323
+ * to the first scalar before parsing.
3324
+ */
3325
+ function parseTimeoutMs(raw) {
3326
+ const v = Array.isArray(raw) ? raw[0] : raw;
3327
+ if (typeof v !== "string") return void 0;
3328
+ const n = Number(v);
3329
+ if (!Number.isFinite(n) || n <= 0) return void 0;
3330
+ return Math.min(Math.floor(n), 5 * 6e4);
3331
+ }
3332
+ /**
3333
+ * Parse the optional `?limit=<n>` query parameter from a
3334
+ * `GET /embed/data/:id` request. Accepts a non-negative
3335
+ * integer and lets the route clamp to `STATEMENT_ROW_CAP`;
3336
+ * rejects anything else as `undefined` so the route falls back
3337
+ * to the server-side cap.
3338
+ */
3339
+ function parseStatementLimit(raw) {
3340
+ const v = Array.isArray(raw) ? raw[0] : raw;
3341
+ if (typeof v !== "string") return void 0;
3342
+ const n = Number(v);
3343
+ if (!Number.isFinite(n) || n < 0) return void 0;
3344
+ return Math.floor(n);
3345
+ }
3346
+ const mastra = toPlugin(MastraPlugin);
3347
+
3348
+ //#endregion
3349
+ export { DEFAULT_AGENT_MAX_STEPS, DEFAULT_GENIE_ALIAS, DEFAULT_STYLE_INSTRUCTIONS, FALLBACK_AGENT_ID, GENIE_INSTRUCTIONS, MASTRA_MODEL_OVERRIDE_KEY, MASTRA_REQUEST_ID_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, MASTRA_USER_NAME_KEY, MastraPlugin, TRACE_REQUEST_CONTEXT_KEYS, buildAgents, buildGenieToolkitProvider, buildGenieTools, buildMcpServer, buildRenderDataTool, chartPlannerRequestSchema, collectSpaceSuggestions, createAgent, createTool, extractModelOverride, fetchChart, mastra, normalizeGenieSpaces, prepareChart, resolveGenieSpaces, tool };