@dbx-tools/appkit-mastra 0.1.27 → 0.1.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  /**
2
- * AppKit Mastra integration: {@link MastraPlugin} / {@link mastra},
3
- * plugin config types, agent registration helpers, Genie tool
4
- * builders, and dynamic Model Serving endpoint resolution.
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.
5
6
  *
6
7
  * Client-side consumers should import URL helpers and the
7
8
  * {@link MastraClientConfig} type from `@dbx-tools/appkit-mastra-shared`
package/package.json CHANGED
@@ -9,13 +9,13 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.27",
12
+ "version": "0.1.29",
13
13
  "dependencies": {
14
14
  "@databricks/sdk-experimental": "^0.17",
15
- "@dbx-tools/appkit-mastra-shared": "0.1.27",
16
- "@dbx-tools/genie": "0.1.27",
17
- "@dbx-tools/genie-shared": "0.1.27",
18
- "@dbx-tools/shared": "0.1.27",
15
+ "@dbx-tools/appkit-mastra-shared": "0.1.29",
16
+ "@dbx-tools/genie": "0.1.29",
17
+ "@dbx-tools/genie-shared": "0.1.29",
18
+ "@dbx-tools/shared": "0.1.29",
19
19
  "@mastra/ai-sdk": "^1",
20
20
  "@mastra/core": "^1",
21
21
  "@mastra/express": "^1",
package/src/agents.ts CHANGED
@@ -26,6 +26,7 @@ import type { MemoryBuilder } from "./memory.js";
26
26
  import { buildModel, FALLBACK_MODEL_IDS } from "./model.js";
27
27
  import { stripStaleChartsProcessor } from "./processors/strip-stale-charts.js";
28
28
  import { buildRenderDataTool } from "./chart.js";
29
+ import { ResultProcessor } from "./processor.js";
29
30
 
30
31
  /**
31
32
  * Tool record accepted by every Mastra `Agent.tools` field and by the
@@ -433,6 +434,7 @@ export async function buildAgents(opts: {
433
434
  // chartIds from prior assistant tool results into the new
434
435
  // turn's `[chart:<id>]` markers. Opt out per-plugin via
435
436
  // `config.stripStaleCharts: false`.
437
+ const outputProcessors = [new ResultProcessor()];
436
438
  const inputProcessors =
437
439
  config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
438
440
  const agents: Record<string, Agent> = {};
@@ -451,7 +453,8 @@ export async function buildAgents(opts: {
451
453
  },
452
454
  tools,
453
455
  ...(memory ? { memory } : {}),
454
- ...(inputProcessors.length > 0 ? { inputProcessors } : {}),
456
+ inputProcessors,
457
+ outputProcessors,
455
458
  });
456
459
  // Surface the effective default model per agent so operators can
457
460
  // see at a glance which endpoint each agent points at without
package/src/chart.ts CHANGED
@@ -20,18 +20,9 @@
20
20
  * dataset, custom API). The module has no knowledge of Genie or
21
21
  * statement ids; those concerns live in the tools that wrap it.
22
22
  *
23
- * Wire-format schemas (`ChartSchema`, `ChartResultSchema`,
24
- * {@link ChartTypeSchema}) live in
25
- * `@dbx-tools/appkit-mastra-shared` so the demo client and any
26
- * other UI consumer share the exact same shape this module reads
27
- * and writes.
28
- *
29
- * Public surface (everything else is module-private):
30
- * - {@link chartPlannerRequestSchema} / {@link ChartPlannerRequest}
31
- * - {@link prepareChart} / {@link PrepareChartOptions}
32
- * - {@link fetchChart} / {@link FetchChartOptions}
33
- * - {@link buildRenderDataTool} (the `render_data` Mastra tool
34
- * auto-wired on every agent in `agents.ts`)
23
+ * Wire-format schemas live in `@dbx-tools/appkit-mastra-shared` so
24
+ * the demo client and any other UI consumer share the exact same
25
+ * shape this module reads and writes.
35
26
  */
36
27
 
37
28
  import { CacheManager } from "@databricks/appkit";
package/src/genie.ts CHANGED
@@ -1,41 +1,19 @@
1
1
  /**
2
2
  * Genie tools for Mastra.
3
3
  *
4
- * Each configured Genie space is surfaced as a small set of flat
5
- * Mastra tools the calling agent can drive directly - no inner
6
- * orchestrator agent. The central agent decomposes user questions,
7
- * picks which Genie space to ask, and composes the final reply.
8
- *
9
- * Per-space tools (suffixed with `_<alias>` for non-default
10
- * aliases):
11
- *
12
- * - `ask_genie`: drives one `genieEventChat` turn and
13
- * forwards every wire event (status, thinking, sql, rows)
14
- * through `ctx.writer` for streaming UI updates. Returns
15
- * the terminal `GenieMessage` only - rows are NOT fetched
16
- * eagerly.
17
- * - `get_space_description`: cheap title / description /
18
- * warehouse id lookup for grounding.
19
- * - `get_space_serialized`: full `GenieSpace` JSON for
20
- * column-level grounding when the description isn't enough.
21
- *
22
- * Space-agnostic shared tools (registered once, regardless of
23
- * how many spaces are wired):
24
- *
25
- * - `get_statement`: opt-in lookup of a Genie statement's rows
26
- * by `statement_id` (with a row `limit`). The agent calls
27
- * this only when it needs to read values to reason about
28
- * them; if the data is just being displayed, it embeds a
29
- * `[data:<statement_id>]` marker in prose instead and lets
30
- * the host UI resolve it.
31
- * - `prepare_chart`: mints a short `chartId`, kicks off a
32
- * background task that fetches the statement's rows and
33
- * runs the chart-planner, and caches the resolved Echarts
34
- * spec under the id (1h TTL). Returns the `chartId`
35
- * synchronously so the agent embeds `[chart:<chartId>]`
36
- * markers in prose without blocking on chart generation;
37
- * the host UI fetches the cached chart by id once it's
38
- * ready (see {@link fetchChart}).
4
+ * Surfaces each configured Genie space as a small set of flat Mastra
5
+ * tools the calling agent drives directly - no inner orchestrator
6
+ * agent. The central agent decomposes user questions, picks which
7
+ * space to ask, streams the per-turn wire events (status, thinking,
8
+ * sql, rows) through `ctx.writer`, and composes the final reply.
9
+ * Rows are never fetched eagerly: the agent reads a statement's
10
+ * values only when it needs to reason about them, otherwise it embeds
11
+ * a `[data:<statement_id>]` marker in prose and lets the host UI
12
+ * resolve the data. Charts are minted asynchronously and referenced
13
+ * by `[chart:<chartId>]` markers so prose isn't blocked on chart
14
+ * generation; the host UI fetches the cached spec by id once ready.
15
+ * Space description and serialized-space lookups are available for
16
+ * grounding when the agent needs schema context.
39
17
  *
40
18
  * Each tool's `execute` pulls the per-request
41
19
  * {@link WorkspaceClient} off `ctx.requestContext` (stamped by
package/src/model.ts CHANGED
@@ -25,12 +25,7 @@
25
25
  * the input verbatim and let Databricks return the canonical error.
26
26
  */
27
27
 
28
- import {
29
- commonUtils,
30
- logUtils,
31
- netUtils,
32
- stringUtils,
33
- } from "@dbx-tools/shared";
28
+ import { commonUtils, logUtils, netUtils, stringUtils } from "@dbx-tools/shared";
34
29
  import type { MastraModelConfig } from "@mastra/core/llm";
35
30
  import type { RequestContext } from "@mastra/core/request-context";
36
31
 
@@ -390,7 +385,7 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
390
385
  const log = logUtils.logger("mastra/llm");
391
386
  const original = globalThis.fetch.bind(globalThis);
392
387
  globalThis.fetch = (async (input, init) => {
393
- const url = netUtils.parseUrl(input);
388
+ const url = netUtils.urlBuilder(input);
394
389
  if (
395
390
  !url ||
396
391
  !url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) ||
package/src/plugin.ts CHANGED
@@ -54,10 +54,6 @@ import { historyRoute } from "./history.js";
54
54
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
55
55
  import { buildObservability } from "./observability.js";
56
56
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
57
- import {
58
- installStreamEventInterceptor,
59
- type StreamFrameInterceptor,
60
- } from "./intercept.js";
61
57
  import {
62
58
  clearServingEndpointsCache,
63
59
  listServingEndpoints,
@@ -69,6 +65,7 @@ import {
69
65
  isStatementNotFoundError,
70
66
  STATEMENT_ROW_CAP,
71
67
  } from "./statement.js";
68
+ import { ResultProcessor } from "./processor.js";
72
69
 
73
70
  const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
74
71
  const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
@@ -311,11 +308,6 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
311
308
 
312
309
  router.use("", (req, res, next) => {
313
310
  if (!this.mastraApp) return res.status(503).end();
314
- // Run each Mastra `/stream` SSE frame through the interceptor
315
- // (keep / rewrite / drop). Only engages on a
316
- // `200 text/event-stream` body, so the JSON routes above and any
317
- // error response are untouched.
318
- installStreamEventInterceptor(res, interceptStreamFrame);
319
311
  return this.userScopedSelf(req).mastraApp!(req, res, next);
320
312
  });
321
313
  }
@@ -502,36 +494,4 @@ function parseStatementLimit(raw: unknown): number | undefined {
502
494
  return Math.floor(n);
503
495
  }
504
496
 
505
- /**
506
- * {@link StreamFrameInterceptor} for Mastra `/stream` responses.
507
- *
508
- * Removes large, redundant payloads from terminal `step-finish`,
509
- * `finish`, and `tool-result` frames before they reach the browser.
510
- * These frames often repeat information already delivered incrementally
511
- * via streamed text and tool events, including full tool outputs,
512
- * accumulated responses, message history, SQL results, chart data, and
513
- * other large result payloads.
514
- *
515
- * The interceptor deletes heavyweight payload properties
516
- * (`output`, `messages`, `response`, and `result`) while preserving the
517
- * event envelope and lifecycle metadata required by the client.
518
- *
519
- * Deletion is key based, so streams that do not contain these
520
- * fields are passed through unchanged.
521
- */
522
- const interceptStreamFrame: StreamFrameInterceptor = (chunk) => {
523
- if (!commonUtils.isRecord(chunk)) return true;
524
- if (!["step-finish", "finish", "tool-result"].find((type) => type === chunk.type))
525
- return true;
526
- const payload = chunk.payload;
527
- if (!commonUtils.isRecord(payload)) return true;
528
- const trimmedPayload = commonUtils.deleteKeys(payload, [
529
- "output",
530
- "messages",
531
- "response",
532
- "result",
533
- ]);
534
- return trimmedPayload ? { replace: chunk } : true;
535
- };
536
-
537
497
  export const mastra = toPlugin(MastraPlugin);
@@ -0,0 +1,52 @@
1
+ import type {
2
+ Processor,
3
+ ProcessAPIErrorArgs,
4
+ ProcessAPIErrorResult,
5
+ ProcessOutputStreamArgs,
6
+ } from "@mastra/core/processors";
7
+ import type { ChunkType } from "@mastra/core/workflows";
8
+
9
+ export class ResultProcessor implements Processor {
10
+ id = "result-processor";
11
+
12
+ // Tell Mastra to also route tool/data parts to this processor method
13
+ processDataParts = true;
14
+
15
+ async processOutputStream({ part }: { part: any }): Promise<any | null> {
16
+ // 1. Guard clause: Ensure the chunk is a valid object
17
+ if (!part || typeof part !== "object") {
18
+ return part;
19
+ }
20
+
21
+ // 2. Filter for the targeted frame types
22
+ const targetedTypes = ["step-finish", "finish", "tool-result", "data-tool-agent"];
23
+ if (!targetedTypes.includes(part.type)) {
24
+ return part; // Return unchanged to pass-through
25
+ }
26
+
27
+ // 3. Check for the presence of a payload object
28
+ const payload = part.payload;
29
+ if (!payload || typeof payload !== "object") {
30
+ return part;
31
+ }
32
+
33
+ // 4. Safely delete the unwanted keys from the payload reference
34
+ const keysToDelete = ["output", "messages", "response", "result"];
35
+ for (const key of keysToDelete) {
36
+ if (key in payload) {
37
+ const value = payload[key];
38
+ if (typeof value === "object") {
39
+ payload[key] = {};
40
+ } else if (Array.isArray(value)) {
41
+ payload[key] = [];
42
+ } else {
43
+ delete payload[key];
44
+ }
45
+ }
46
+ }
47
+
48
+ // 5. Return the modified part object. Mastra handles re-serialization
49
+ // for the outbound SSE client stream automatically.
50
+ return part;
51
+ }
52
+ }
package/src/serving.ts CHANGED
@@ -1,23 +1,16 @@
1
1
  /**
2
2
  * Dynamic model resolution against Databricks Model Serving.
3
3
  *
4
- * Three concerns live here:
5
- *
6
- * 1. **Listing** - {@link listServingEndpoints} pulls the workspace's
7
- * `/serving-endpoints` via the SDK and caches the result per host
8
- * with a TTL. Concurrent callers share one in-flight promise (the
9
- * same coalescing pattern as Python's `cachetools-async`).
10
- * 2. **Fuzzy matching** - {@link resolveModelId} runs the user's input
11
- * through `fuse.js` extended search so loose tokens like
12
- * `"claude sonnet"` snap to `databricks-claude-sonnet-4-6` even
13
- * when typed without the full endpoint name.
14
- * 3. **Per-request override** - {@link extractModelOverride} pulls a
15
- * model name from the `X-Mastra-Model` header, `?model=` query
16
- * string, or `model` body field so the same agent can be exercised
17
- * against different endpoints without redeploying.
18
- *
19
- * `model.ts` glues these together inside the per-step model resolver;
20
- * `plugin.ts` exposes the cached list at `GET /models`.
4
+ * Turns loose, human-typed model names into real serving-endpoint ids
5
+ * so the same agent can target different endpoints without code
6
+ * changes. The workspace's `/serving-endpoints` list is fetched once
7
+ * per host and cached with a TTL, with concurrent callers sharing one
8
+ * in-flight promise (the coalescing pattern of Python's
9
+ * `cachetools-async`). Names are matched through `fuse.js` extended
10
+ * search so tokens like `"claude sonnet"` snap to
11
+ * `databricks-claude-sonnet-4-6`, and a per-request override can be
12
+ * pulled from the request header, query string, or body so a model
13
+ * can be swapped per call without redeploying.
21
14
  */
22
15
 
23
16
  import { CacheManager, type getExecutionContext } from "@databricks/appkit";
package/src/statement.ts CHANGED
@@ -1,24 +1,17 @@
1
1
  /**
2
2
  * Databricks Statement Execution helpers for the Mastra plugin.
3
3
  *
4
- * Wraps `client.statementExecution.getStatement` with the shape
5
- * + size + error handling the plugin's tools and the
6
- * `/embed/data/:id` route both need:
7
- *
8
- * - {@link fetchStatementData}: low-level fetch that returns the
9
- * raw `{columns, rows, rowCount}` shape used by the
10
- * `get_statement` tool's output, the `prepare_chart` tool's
11
- * dataset resolver, and the route's response body. Coerces
12
- * numeric strings to numbers so downstream charts /
13
- * aggregations don't have to.
14
- * - {@link STATEMENT_ROW_CAP}: hard cap callers (notably the
15
- * `/embed/data/:id` route) clamp `limit` to so a
16
- * runaway result set can't hose a response.
17
- * - {@link isStatementNotFoundError}: structural detector that
18
- * normalizes the SDK's two error classes plus the loose
19
- * `does not exist` / `not found` message shapes into a single
20
- * boolean - lets the route map upstream 404s to a clean
21
- * HTTP 404 without coupling to SDK error-class identity.
4
+ * Wraps `client.statementExecution.getStatement` with the shape,
5
+ * size, and error handling the plugin's tools and the
6
+ * `/embed/data/:id` route both need: a low-level fetch that returns a
7
+ * raw `{columns, rows, rowCount}` shape and coerces numeric strings
8
+ * to numbers so downstream charts and aggregations don't have to, a
9
+ * hard row cap callers clamp `limit` to so a runaway result set can't
10
+ * hose a response, and a structural not-found detector that
11
+ * normalizes the SDK's error classes and loose `does not exist` /
12
+ * `not found` message shapes into one boolean so the route can map
13
+ * upstream 404s to a clean HTTP 404 without coupling to SDK
14
+ * error-class identity.
22
15
  *
23
16
  * Not Genie-specific: a Databricks `statement_id` is workspace
24
17
  * scoped and lives in the Statement Execution API regardless of