@dbx-tools/appkit-mastra 0.1.67 → 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.
package/src/processor.ts DELETED
@@ -1,46 +0,0 @@
1
- import type { Processor } from "@mastra/core/processors";
2
-
3
- export class ResultProcessor implements Processor {
4
- id = "result-processor";
5
-
6
- // Tell Mastra to also route tool/data parts to this processor method
7
- processDataParts = true;
8
-
9
- async processOutputStream({ part }: { part: any }): Promise<any | null> {
10
- // 1. Guard clause: Ensure the chunk is a valid object
11
- if (!part || typeof part !== "object") {
12
- return part;
13
- }
14
-
15
- // 2. Filter for the targeted frame types
16
- const targetedTypes = ["step-finish", "finish", "tool-result", "data-tool-agent"];
17
- if (!targetedTypes.includes(part.type)) {
18
- return part; // Return unchanged to pass-through
19
- }
20
-
21
- // 3. Check for the presence of a payload object
22
- const payload = part.payload;
23
- if (!payload || typeof payload !== "object") {
24
- return part;
25
- }
26
-
27
- // 4. Safely delete the unwanted keys from the payload reference
28
- const keysToDelete = ["output", "messages", "response", "result"];
29
- for (const key of keysToDelete) {
30
- if (key in payload) {
31
- const value = payload[key];
32
- if (typeof value === "object") {
33
- payload[key] = {};
34
- } else if (Array.isArray(value)) {
35
- payload[key] = [];
36
- } else {
37
- delete payload[key];
38
- }
39
- }
40
- }
41
-
42
- // 5. Return the modified part object. Mastra handles re-serialization
43
- // for the outbound SSE client stream automatically.
44
- return part;
45
- }
46
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * Mastra input processor that strips `chartId` fields from every
3
- * tool-invocation result in prior assistant messages before they
4
- * reach the model.
5
- *
6
- * Why: chartIds are turn-scoped from the model's point of view -
7
- * each `prepare_chart` / `render_data` call mints a fresh id and
8
- * the host UI binds it to that turn's reply. Mastra Memory
9
- * replays prior tool results into the prompt; if old chartIds
10
- * leak through, the model is tempted to copy them verbatim into
11
- * the new turn's `[chart:<id>]` markers and the host UI ends up
12
- * rendering an unrelated chart from the chart cache (or
13
- * a 404 once the 1h TTL elapsed). This processor removes the
14
- * temptation by deleting `chartId` keys from every assistant
15
- * message's tool results before the prompt is built. The current
16
- * turn's tool results don't exist yet at `processInput` time, so
17
- * they pass through unmodified.
18
- *
19
- * The strip is recursive - any nested `chartId` field is removed,
20
- * regardless of which tool produced the result. This covers
21
- * `prepare_chart` / `render_data` top-level chartIds and any
22
- * legacy `datasets[].chartId` payloads uniformly without coupling
23
- * to specific tool ids.
24
- */
25
-
26
- import { logUtils } from "@dbx-tools/shared";
27
- import type { InputProcessor, ProcessInputArgs } from "@mastra/core/processors";
28
-
29
- const log = logUtils.logger("mastra/processor/strip-stale-charts");
30
-
31
- /**
32
- * Recursively clone `value`, omitting any property whose key is
33
- * `chartId`. Arrays are mapped element-wise; primitives are
34
- * returned as-is. The result is structurally identical to the
35
- * input minus chartIds, so downstream message-shape consumers
36
- * keep working.
37
- */
38
- function stripChartIds(value: unknown): unknown {
39
- if (Array.isArray(value)) {
40
- return value.map(stripChartIds);
41
- }
42
- if (value && typeof value === "object") {
43
- const obj = value as Record<string, unknown>;
44
- const out: Record<string, unknown> = {};
45
- for (const [key, val] of Object.entries(obj)) {
46
- if (key === "chartId") continue;
47
- out[key] = stripChartIds(val);
48
- }
49
- return out;
50
- }
51
- return value;
52
- }
53
-
54
- /**
55
- * Input processor that scrubs `chartId` from every tool-invocation
56
- * result in the message list. Wired onto every agent by default
57
- * via {@link buildAgents}; opt out with
58
- * `MastraPluginConfig.stripStaleCharts: false`.
59
- */
60
- export const stripStaleChartsProcessor: InputProcessor = {
61
- id: "strip-stale-charts",
62
- description:
63
- "Removes chartId fields from prior tool-invocation results so the model can't reuse turn-scoped ids from memory.",
64
- processInput(args: ProcessInputArgs) {
65
- let stripped = 0;
66
- for (const message of args.messages) {
67
- if (message.role !== "assistant") continue;
68
- const parts = message.content?.parts;
69
- if (!Array.isArray(parts)) continue;
70
- for (const part of parts) {
71
- // Tool-invocation parts hold the persisted tool result.
72
- // We don't scrub the input args (`rawInput` / `args`) because
73
- // the chartId there is the model's outgoing claim, not
74
- // anything it could re-reference; only `result` carries
75
- // ids that subsequent turns might copy.
76
- if ((part as { type?: unknown }).type !== "tool-invocation") {
77
- continue;
78
- }
79
- const inv = (part as { toolInvocation?: { result?: unknown } }).toolInvocation;
80
- if (!inv || inv.result === undefined) continue;
81
- const before = inv.result;
82
- const after = stripChartIds(before);
83
- // Cheap structural check via JSON length - the actual
84
- // strip writes a fresh object only when chartId keys
85
- // existed, so different stringification length is a
86
- // reliable signal that something was removed.
87
- if (
88
- typeof before === "object" &&
89
- before !== null &&
90
- JSON.stringify(before).length !== JSON.stringify(after).length
91
- ) {
92
- inv.result = after;
93
- stripped += 1;
94
- }
95
- }
96
- }
97
- if (stripped > 0) {
98
- log.debug("stripped", { results: stripped });
99
- }
100
- return args.messages;
101
- },
102
- };
package/src/server.ts DELETED
@@ -1,195 +0,0 @@
1
- /**
2
- * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
- * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets the plugin's custom API routes (e.g. `historyRoute`) work
5
- * behind an Express mount point.
6
- */
7
-
8
- import { getExecutionContext } from "@databricks/appkit";
9
- import { httpUtils, logUtils, stringUtils } from "@dbx-tools/shared";
10
- import {
11
- MASTRA_RESOURCE_ID_KEY,
12
- MASTRA_THREAD_ID_KEY,
13
- type RequestContext,
14
- } from "@mastra/core/request-context";
15
- import { MastraServer as MastraServerExpress } from "@mastra/express";
16
- import type express from "express";
17
- import { randomUUID } from "node:crypto";
18
-
19
- import {
20
- MASTRA_REQUEST_ID_KEY,
21
- MASTRA_USER_EMAIL_KEY,
22
- MASTRA_USER_KEY,
23
- MASTRA_USER_NAME_KEY,
24
- type MastraPluginConfig,
25
- type User,
26
- } from "./config.js";
27
- import {
28
- extractModelOverride,
29
- MASTRA_MODEL_OVERRIDE_KEY,
30
- resolveServingConfig,
31
- } from "./serving.js";
32
-
33
- /**
34
- * `@mastra/express` subclass that stamps `RequestContext` with the
35
- * AppKit user, resource id, and a thread id backed by an HTTP-only
36
- * session cookie (`appkit_<plugin-name>_session_id`).
37
- */
38
- export class MastraServer extends MastraServerExpress {
39
- private log: logUtils.Logger;
40
-
41
- constructor(
42
- private config: MastraPluginConfig,
43
- ...args: ConstructorParameters<typeof MastraServerExpress>
44
- ) {
45
- super(...args);
46
- this.log = logUtils.logger(config);
47
- }
48
-
49
- override registerAuthMiddleware(): void {
50
- super.registerAuthMiddleware();
51
- this.app.use((req, res, next) => {
52
- const requestContext = res.locals.requestContext! as RequestContext;
53
- this.configureRequestContextUser(requestContext);
54
- this.configureRequestContextThreadId(req, res, requestContext);
55
- this.configureRequestContextModelOverride(req, requestContext);
56
- this.configureRequestContextRequestId(req, res, requestContext);
57
- this.log.debug("auth:middleware", {
58
- method: req.method,
59
- path: req.path,
60
- requestId: requestContext.get(MASTRA_REQUEST_ID_KEY),
61
- threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
62
- resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
63
- userName: requestContext.get(MASTRA_USER_NAME_KEY),
64
- userEmail: requestContext.get(MASTRA_USER_EMAIL_KEY),
65
- modelOverride: requestContext.get(
66
- // imported below; logged so a misrouted request shows
67
- // up alongside its model selection in `LOG_LEVEL=debug`.
68
- "mastra__model_override",
69
- ),
70
- });
71
- next();
72
- });
73
- }
74
-
75
- configureRequestContextUser(requestContext: RequestContext) {
76
- if (
77
- [MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))
78
- )
79
- return;
80
- const executionContext = getExecutionContext();
81
- const user: User = {
82
- id:
83
- "userId" in executionContext
84
- ? executionContext.userId
85
- : executionContext.serviceUserId,
86
- executionContext,
87
- };
88
- requestContext.set(MASTRA_USER_KEY, user);
89
- requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
90
- // AppKit's `UserContext` surfaces display name / email only on
91
- // OBO requests. Service-context calls (background tasks, server
92
- // start-up) leave these undefined and we skip the stamp so
93
- // downstream trace metadata stays absent rather than empty.
94
- if ("isUserContext" in executionContext) {
95
- if (executionContext.userName) {
96
- requestContext.set(MASTRA_USER_NAME_KEY, executionContext.userName);
97
- }
98
- if (executionContext.userEmail) {
99
- requestContext.set(MASTRA_USER_EMAIL_KEY, executionContext.userEmail);
100
- }
101
- }
102
- }
103
-
104
- /**
105
- * Stamp a per-request id and echo it on the response so an upstream
106
- * proxy / curl client / browser-side log line can pair its view of
107
- * the request with the matching trace span. Reuses `X-Request-Id`
108
- * when the upstream already supplies one so multi-hop traces stay
109
- * joined; otherwise mints a UUIDv4.
110
- *
111
- * The id is surfaced as `mastra__requestId` span metadata via
112
- * {@link TRACE_REQUEST_CONTEXT_KEYS} and as the `X-Request-Id`
113
- * response header so dev tools can copy it from either side.
114
- */
115
- configureRequestContextRequestId(
116
- req: express.Request,
117
- res: express.Response,
118
- requestContext: RequestContext,
119
- ) {
120
- if (requestContext.get(MASTRA_REQUEST_ID_KEY)) return;
121
- const headerValue = req.headers["x-request-id"];
122
- const upstream = Array.isArray(headerValue) ? headerValue[0] : headerValue;
123
- const requestId = upstream?.trim() || randomUUID();
124
- requestContext.set(MASTRA_REQUEST_ID_KEY, requestId);
125
- res.setHeader("X-Request-Id", requestId);
126
- }
127
-
128
- configureRequestContextThreadId(
129
- req: express.Request,
130
- res: express.Response,
131
- requestContext: RequestContext,
132
- ) {
133
- if (requestContext.get(MASTRA_THREAD_ID_KEY)) return;
134
- const cookies = httpUtils.parseCookies(req.headers.cookie);
135
- const cookieName = stringUtils.toIdentifierWithOptions(
136
- { delimiter: "_", distinct: true },
137
- "appkit",
138
- this.config.name!,
139
- "sessionId",
140
- );
141
- let sessionId = cookies[cookieName];
142
- if (!sessionId) {
143
- sessionId = randomUUID();
144
- res.cookie(cookieName, sessionId, {
145
- httpOnly: true,
146
- sameSite: "lax",
147
- secure: req.secure,
148
- path: "/",
149
- });
150
- }
151
- requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
152
- }
153
-
154
- configureRequestContextModelOverride(
155
- req: express.Request,
156
- requestContext: RequestContext,
157
- ) {
158
- // Per-request model override: only honored when the plugin
159
- // opts in (default). Sources, in priority order, are
160
- // `X-Mastra-Model` header, `?model=` query, and `model` /
161
- // `modelId` body field; see `serving.ts`.
162
- const serving = resolveServingConfig(this.config);
163
- if (serving.allowOverride) {
164
- const override = extractModelOverride({
165
- headers: req.headers as Record<string, string | string[] | undefined>,
166
- query: req.query as Record<string, unknown>,
167
- body: req.body,
168
- });
169
- if (override) requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
170
- }
171
- }
172
- }
173
-
174
- /**
175
- * Patches around `@mastra/express`'s custom-route dispatcher so the
176
- * plugin's custom API routes (e.g. `historyRoute`) work when
177
- * `MastraServer` is hosted on an Express subapp mounted under a parent
178
- * path (e.g. `/api/mastra`).
179
- *
180
- * The adapter's `registerCustomApiRoutes` matches against `req.path`
181
- * (mount-relative, correct) but dispatches to its internal Hono
182
- * mini-app using `req.originalUrl`, which still contains the parent
183
- * mount prefix. The Hono app registers the literal route paths
184
- * (for example `/route/history`), so the absolute URL never matches
185
- * until we overwrite `originalUrl` for `/route` and `/route/*` to the
186
- * mount-relative path.
187
- */
188
- export function attachRoutePatchMiddleware(app: express.Express): void {
189
- app.use((req, _res, next) => {
190
- const isCustomRoute = req.path === "/route" || req.path.startsWith("/route/");
191
- if (!isCustomRoute) return next();
192
- req.originalUrl = req.path;
193
- next();
194
- });
195
- }
package/src/serving.ts DELETED
@@ -1,100 +0,0 @@
1
- /**
2
- * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
3
- *
4
- * The live `/serving-endpoints` catalogue access, fuzzy name
5
- * resolution, and class/fallback selection all live in
6
- * `@dbx-tools/model`; this module only adds what is specific to the
7
- * Mastra plugin: pulling a per-request model override off an HTTP
8
- * request (header / query / body) and projecting the plugin config
9
- * onto the knobs `buildModel` and the `/models` route share.
10
- */
11
-
12
- import {
13
- MODEL_OVERRIDE_BODY_FIELDS,
14
- MODEL_OVERRIDE_HEADER,
15
- MODEL_OVERRIDE_QUERY,
16
- } from "@dbx-tools/appkit-mastra-shared";
17
- import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS } from "@dbx-tools/model";
18
- import { stringUtils } from "@dbx-tools/shared";
19
-
20
- import type { MastraPluginConfig } from "./config.js";
21
-
22
- /**
23
- * `RequestContext` key under which {@link MastraServer} stores the
24
- * per-request model override (header / query / body). `model.ts`
25
- * reads it before falling back to the agent / plugin default.
26
- */
27
- export const MASTRA_MODEL_OVERRIDE_KEY = "mastra__model_override";
28
-
29
- /**
30
- * Minimal Express-ish request shape used by {@link extractModelOverride}.
31
- * Keeps this module independent of `express` so the helper can be
32
- * reused from non-Express adapters.
33
- */
34
- export interface ModelOverrideRequest {
35
- headers?: Record<string, string | string[] | undefined>;
36
- query?: Record<string, unknown> | undefined;
37
- body?: unknown;
38
- }
39
-
40
- /**
41
- * Pull a model override out of a single HTTP request, checking
42
- * sources in priority order:
43
- *
44
- * 1. `X-Mastra-Model` header
45
- * 2. `?model=` query string parameter
46
- * 3. Body field (`model` or `modelId`, in that order)
47
- *
48
- * Returns `null` when nothing is set, so callers can wrap with
49
- * `if (override) ...` without juggling empty strings. Body inspection
50
- * is lenient - any plain object with one of the configured keys
51
- * counts, mirroring how AI SDK chat clients pass arbitrary metadata
52
- * alongside `messages`.
53
- */
54
- export function extractModelOverride(req: ModelOverrideRequest): string | null {
55
- const headers = req.headers;
56
- if (headers) {
57
- const headerVal = stringUtils.firstNonEmpty(
58
- headers[MODEL_OVERRIDE_HEADER] ?? headers[MODEL_OVERRIDE_HEADER.toLowerCase()],
59
- );
60
- if (headerVal) return headerVal;
61
- }
62
- if (req.query) {
63
- const queryVal = stringUtils.firstNonEmpty(req.query[MODEL_OVERRIDE_QUERY]);
64
- if (queryVal) return queryVal;
65
- }
66
- if (req.body && typeof req.body === "object") {
67
- const record = req.body as Record<string, unknown>;
68
- for (const field of MODEL_OVERRIDE_BODY_FIELDS) {
69
- const bodyVal = stringUtils.firstNonEmpty(record[field]);
70
- if (bodyVal) return bodyVal;
71
- }
72
- }
73
- return null;
74
- }
75
-
76
- /**
77
- * Read the fuzzy-resolution config knobs off the plugin config with
78
- * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
79
- * the `/models` route agree on what "enabled" means.
80
- *
81
- * `fallbacks` is the priority-ordered list `resolveModel` walks first
82
- * when nothing explicit is set; defaults to an empty list so the
83
- * generic resolver falls through to the live catalogue and its own
84
- * `FALLBACK_MODEL_IDS` floor.
85
- */
86
- export function resolveServingConfig(config: MastraPluginConfig): {
87
- ttlMs: number;
88
- threshold: number;
89
- fuzzy: boolean;
90
- allowOverride: boolean;
91
- fallbacks: readonly string[];
92
- } {
93
- return {
94
- ttlMs: config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
95
- threshold: config.modelFuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD,
96
- fuzzy: config.modelFuzzyMatch !== false,
97
- allowOverride: config.modelOverride !== false,
98
- fallbacks: config.defaultModelFallbacks ?? [],
99
- };
100
- }
package/src/statement.ts DELETED
@@ -1,92 +0,0 @@
1
- /**
2
- * Databricks Statement Execution helpers for the Mastra plugin.
3
- *
4
- * Wraps `client.statementExecution.getStatement` with the shape and
5
- * size handling the plugin's tools and the `/embed/data/:id` route
6
- * both need: a low-level fetch that returns a raw
7
- * `{columns, rows, rowCount}` shape and coerces numeric strings to
8
- * numbers so downstream charts and aggregations don't have to, plus a
9
- * hard row cap callers clamp `limit` to so a runaway result set can't
10
- * hose a response. Upstream 404s are detected via
11
- * `apiUtils.isNotFoundError` at the call sites.
12
- *
13
- * Not Genie-specific: a Databricks `statement_id` is workspace
14
- * scoped and lives in the Statement Execution API regardless of
15
- * which producer (Genie, a tool, a notebook, etc.) submitted the
16
- * query. Co-located here so consumers can fetch / cap / handle
17
- * 404s without reaching into the Genie tool module.
18
- */
19
-
20
- import { WorkspaceClient } from "@databricks/sdk-experimental";
21
- import type { GenieDatasetData } from "@dbx-tools/appkit-mastra-shared";
22
- import { apiUtils } from "@dbx-tools/shared";
23
-
24
- /**
25
- * Hard server-side cap on rows returned by the
26
- * `/embed/data/:id` route. Sized to keep responses small
27
- * enough for inline tables to render snappily; the route surfaces
28
- * a `truncated` flag whenever the upstream `rowCount` exceeds
29
- * this so end users know they're seeing a sample.
30
- */
31
- export const STATEMENT_ROW_CAP = 500;
32
-
33
- /**
34
- * Best-effort numeric coercion for the Statement Execution API's
35
- * all-strings cells. Leaves non-numeric strings (and explicit
36
- * `null`s) intact; everything else flows through `Number`.
37
- */
38
- function coerceCell(cell: string | null): unknown {
39
- if (cell === null) return null;
40
- if (/^-?\d+(\.\d+)?$/.test(cell)) {
41
- const n = Number(cell);
42
- if (Number.isFinite(n)) return n;
43
- }
44
- return cell;
45
- }
46
-
47
- /**
48
- * Fetch a single statement's rows via the Statement Execution API
49
- * and reshape into the shared {@link GenieDatasetData} shape
50
- * (column array + row records).
51
- *
52
- * Optional `limit` slices the returned `rows` client-side so the
53
- * agent can scan a small sample without paging the full result
54
- * set into context. `rowCount` always reflects the upstream total
55
- * so callers know when the slice truncated.
56
- *
57
- * Exported because every consumer in the plugin (the
58
- * `get_statement` tool, the `prepare_chart` dataset resolver, and
59
- * the `/embed/data/:id` route) needs the exact same
60
- * fetch + coercion pipeline so LLM-side `get_statement` output
61
- * and UI-side `[data:<id>]` rendering stay shape-identical for
62
- * the same `statement_id`.
63
- */
64
- export async function fetchStatementData(
65
- client: WorkspaceClient,
66
- statementId: string,
67
- options?: { limit?: number; signal?: AbortSignal },
68
- ): Promise<GenieDatasetData> {
69
- const ctx = options?.signal ? apiUtils.toContext(options.signal) : undefined;
70
- const r = await client.statementExecution.getStatement(
71
- { statement_id: statementId },
72
- ctx,
73
- );
74
- const columns = (r.manifest?.schema?.columns ?? []).map((c) => c.name ?? "");
75
- const dataArray = (r.result?.data_array ?? []) as Array<Array<string | null>>;
76
- const sliced =
77
- options?.limit !== undefined && options.limit >= 0
78
- ? dataArray.slice(0, options.limit)
79
- : dataArray;
80
- const rows = sliced.map((row) => {
81
- const obj: Record<string, unknown> = {};
82
- columns.forEach((col, i) => {
83
- obj[col] = coerceCell(row[i] ?? null);
84
- });
85
- return obj;
86
- });
87
- return {
88
- columns,
89
- rows,
90
- rowCount: r.manifest?.total_row_count ?? dataArray.length,
91
- };
92
- }
package/src/writer.ts DELETED
@@ -1,44 +0,0 @@
1
- /**
2
- * Shared helper for publishing events through Mastra's
3
- * `ctx.writer`. Centralizes the "the downstream stream may already
4
- * be closed, don't take the whole tool down" pattern that the
5
- * Genie agent and chart tool both need.
6
- *
7
- * Failures are logged at `warn` (a persistently-closed writer is
8
- * the most likely culprit when events go missing client-side) but
9
- * swallowed so a cancelled request or a client that navigated
10
- * away can't crash a tool mid-flight.
11
- */
12
-
13
- import type { MastraWriter } from "@dbx-tools/appkit-mastra-shared";
14
- import { commonUtils, type logUtils } from "@dbx-tools/shared";
15
-
16
- /**
17
- * Best-effort `writer.write`. No-op when `writer` is undefined;
18
- * caught errors are logged via `log.warn("writer:error", ...)`
19
- * along with any caller-supplied `context` fields (e.g. a
20
- * `chartId` or `messageId`) so the warning is greppable per
21
- * resource.
22
- *
23
- * Returns when the write resolves or rejects; never throws.
24
- */
25
- export async function safeWrite(
26
- log: logUtils.Logger,
27
- writer: MastraWriter | undefined,
28
- chunk: unknown,
29
- context: Record<string, unknown> = {},
30
- ): Promise<void> {
31
- if (!writer) {
32
- log.debug("writer:no-writer", context);
33
- return;
34
- }
35
- try {
36
- await writer.write(chunk);
37
- log.debug("writer:ok", context);
38
- } catch (err) {
39
- log.warn("writer:error", {
40
- ...context,
41
- error: commonUtils.errorMessage(err),
42
- });
43
- }
44
- }