@dbx-tools/appkit-mastra 0.1.4 → 0.1.12

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Thread history loader exposed as a Mastra custom API route.
3
+ *
4
+ * Backed entirely by native Mastra: looks up the active agent by id,
5
+ * asks its `Memory` instance to `recall` a page of `MastraDBMessage`s,
6
+ * and converts the result to AI SDK V5 `UIMessage`s with the official
7
+ * {@link toAISdkV5Messages} helper from `@mastra/ai-sdk/ui`. No direct
8
+ * database reads.
9
+ *
10
+ * The route is registered through {@link historyRoute} as a Mastra
11
+ * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
+ * `chatRoute`. That means the `MastraServer` auth middleware (in
13
+ * `./server.ts`) has already populated `RequestContext` with
14
+ * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
+ * the handler runs - no cookie or user lookups happen here, and the
16
+ * session-cookie logic stays the single source of truth in `server.ts`.
17
+ */
18
+ import type { Agent } from "@mastra/core/agent";
19
+ import type { MastraHistoryResponse } from "@dbx-tools/appkit-mastra-shared";
20
+ /** Inputs accepted by {@link loadHistory}. */
21
+ export interface LoadHistoryOptions {
22
+ agent: Agent;
23
+ threadId: string;
24
+ resourceId?: string;
25
+ page?: number;
26
+ perPage?: number;
27
+ /** When true, returns the *oldest* page first (chronological). */
28
+ ascending?: boolean;
29
+ }
30
+ /**
31
+ * Fetch a page of UI-formatted messages for a thread.
32
+ *
33
+ * Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
34
+ * storage namespaces (`mastra_<agentId>` schemas) and any future
35
+ * memory-side filters apply automatically. When the agent has no
36
+ * memory configured the response is a successful empty page so
37
+ * callers don't have to special-case stateless agents.
38
+ *
39
+ * Pagination is descending-by-default: page 0 is the most recent
40
+ * page, page 1 the page before that, etc. The returned `uiMessages`
41
+ * are always re-sorted into chronological order (oldest -> newest)
42
+ * so the client can prepend them above the existing transcript
43
+ * without sorting locally.
44
+ */
45
+ export declare function loadHistory(opts: LoadHistoryOptions): Promise<MastraHistoryResponse>;
46
+ /** Options accepted by {@link historyRoute}. */
47
+ export type HistoryRouteOptions = {
48
+ path: `${string}:agentId${string}`;
49
+ agent?: never;
50
+ } | {
51
+ path: string;
52
+ agent: string;
53
+ };
54
+ /**
55
+ * Register a `GET <path>` Mastra custom API route that returns a page
56
+ * of AI SDK V5 `UIMessage`s for the caller's current thread.
57
+ *
58
+ * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
59
+ * fixed-agent mount, or include `:agentId` in the path for dynamic
60
+ * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
61
+ * layout (`/route/chat` + `/route/chat/:agentId`).
62
+ *
63
+ * The handler reads `threadId` and `resourceId` from `RequestContext`
64
+ * (populated upstream by `MastraServer.registerAuthMiddleware`), so
65
+ * no cookie or user lookups happen here.
66
+ */
67
+ export declare function historyRoute(options: HistoryRouteOptions): import("@mastra/core/server").ApiRoute;
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Thread history loader exposed as a Mastra custom API route.
3
+ *
4
+ * Backed entirely by native Mastra: looks up the active agent by id,
5
+ * asks its `Memory` instance to `recall` a page of `MastraDBMessage`s,
6
+ * and converts the result to AI SDK V5 `UIMessage`s with the official
7
+ * {@link toAISdkV5Messages} helper from `@mastra/ai-sdk/ui`. No direct
8
+ * database reads.
9
+ *
10
+ * The route is registered through {@link historyRoute} as a Mastra
11
+ * `registerApiRoute` so it sits in the same dispatcher pipeline as
12
+ * `chatRoute`. That means the `MastraServer` auth middleware (in
13
+ * `./server.ts`) has already populated `RequestContext` with
14
+ * `MASTRA_THREAD_ID_KEY` and `MASTRA_RESOURCE_ID_KEY` by the time
15
+ * the handler runs - no cookie or user lookups happen here, and the
16
+ * session-cookie logic stays the single source of truth in `server.ts`.
17
+ */
18
+ import { logUtils } from "@dbx-tools/appkit-shared";
19
+ import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
20
+ import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
21
+ import { registerApiRoute } from "@mastra/core/server";
22
+ const log = logUtils.logger("mastra/history");
23
+ /** Default history page size; matches the Mastra storage default. */
24
+ const DEFAULT_PER_PAGE = 20;
25
+ /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
26
+ const MAX_PER_PAGE = 200;
27
+ /**
28
+ * Fetch a page of UI-formatted messages for a thread.
29
+ *
30
+ * Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
31
+ * storage namespaces (`mastra_<agentId>` schemas) and any future
32
+ * memory-side filters apply automatically. When the agent has no
33
+ * memory configured the response is a successful empty page so
34
+ * callers don't have to special-case stateless agents.
35
+ *
36
+ * Pagination is descending-by-default: page 0 is the most recent
37
+ * page, page 1 the page before that, etc. The returned `uiMessages`
38
+ * are always re-sorted into chronological order (oldest -> newest)
39
+ * so the client can prepend them above the existing transcript
40
+ * without sorting locally.
41
+ */
42
+ export async function loadHistory(opts) {
43
+ const perPage = clampPerPage(opts.perPage);
44
+ const page = Math.max(0, Math.trunc(opts.page ?? 0));
45
+ const memory = await opts.agent.getMemory();
46
+ if (!memory) {
47
+ log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
48
+ return { uiMessages: [], page, perPage, total: 0, hasMore: false };
49
+ }
50
+ const startedAt = Date.now();
51
+ const result = await memory.recall({
52
+ threadId: opts.threadId,
53
+ ...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
54
+ page,
55
+ perPage,
56
+ orderBy: {
57
+ field: "createdAt",
58
+ direction: opts.ascending ? "ASC" : "DESC",
59
+ },
60
+ });
61
+ const chronological = sortChronological(result.messages);
62
+ const uiMessages = toAISdkV5Messages(chronological);
63
+ log.debug("recall:done", {
64
+ agentId: opts.agent.id,
65
+ threadId: opts.threadId,
66
+ page,
67
+ perPage,
68
+ returned: uiMessages.length,
69
+ total: result.total,
70
+ hasMore: result.hasMore,
71
+ elapsedMs: Date.now() - startedAt,
72
+ });
73
+ return {
74
+ uiMessages,
75
+ page,
76
+ perPage,
77
+ total: result.total,
78
+ hasMore: result.hasMore,
79
+ };
80
+ }
81
+ /**
82
+ * Register a `GET <path>` Mastra custom API route that returns a page
83
+ * of AI SDK V5 `UIMessage`s for the caller's current thread.
84
+ *
85
+ * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
86
+ * fixed-agent mount, or include `:agentId` in the path for dynamic
87
+ * routing. Pairs cleanly with the AppKit Mastra plugin's chat route
88
+ * layout (`/route/chat` + `/route/chat/:agentId`).
89
+ *
90
+ * The handler reads `threadId` and `resourceId` from `RequestContext`
91
+ * (populated upstream by `MastraServer.registerAuthMiddleware`), so
92
+ * no cookie or user lookups happen here.
93
+ */
94
+ export function historyRoute(options) {
95
+ const { path } = options;
96
+ const fixedAgent = "agent" in options ? options.agent : undefined;
97
+ if (!fixedAgent && !path.includes(":agentId")) {
98
+ throw new Error("historyRoute path must include `:agentId` or `agent` must be passed explicitly");
99
+ }
100
+ return registerApiRoute(path, {
101
+ method: "GET",
102
+ handler: async (c) => {
103
+ const mastra = c.get("mastra");
104
+ const requestContext = c.get("requestContext");
105
+ const agentId = fixedAgent ?? c.req.param("agentId");
106
+ if (!agentId) {
107
+ return c.json({ error: "agentId is required" }, 400);
108
+ }
109
+ const agent = mastra.getAgentById(agentId);
110
+ if (!agent) {
111
+ return c.json({ error: `Unknown agent "${agentId}"` }, 404);
112
+ }
113
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY);
114
+ if (!threadId) {
115
+ return c.json({ error: "thread id missing from request context" }, 400);
116
+ }
117
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY);
118
+ const payload = await loadHistory({
119
+ agent,
120
+ threadId,
121
+ ...(resourceId ? { resourceId } : {}),
122
+ page: parseIntParam(c.req.query("page")),
123
+ perPage: parseIntParam(c.req.query("perPage")),
124
+ });
125
+ return c.json(payload);
126
+ },
127
+ });
128
+ }
129
+ /** Coerce / clamp `perPage`; falls back to the page-size default. */
130
+ function clampPerPage(value) {
131
+ if (value === undefined || Number.isNaN(value))
132
+ return DEFAULT_PER_PAGE;
133
+ const n = Math.trunc(value);
134
+ if (n <= 0)
135
+ return DEFAULT_PER_PAGE;
136
+ return Math.min(n, MAX_PER_PAGE);
137
+ }
138
+ /**
139
+ * Sort messages oldest-first by `createdAt`, falling back to whatever
140
+ * order the storage returned them in. The native `recall` call honors
141
+ * `orderBy` but doesn't guarantee a stable secondary sort, so we
142
+ * normalize here before handing the page to the AI SDK converter.
143
+ */
144
+ function sortChronological(messages) {
145
+ return [...messages].sort((a, b) => {
146
+ const ta = toEpoch(a.createdAt);
147
+ const tb = toEpoch(b.createdAt);
148
+ return ta - tb;
149
+ });
150
+ }
151
+ function toEpoch(value) {
152
+ if (value instanceof Date)
153
+ return value.getTime();
154
+ if (typeof value === "string" || typeof value === "number") {
155
+ const parsed = new Date(value).getTime();
156
+ return Number.isNaN(parsed) ? 0 : parsed;
157
+ }
158
+ return 0;
159
+ }
160
+ /**
161
+ * Coerce a Hono query value into a non-negative integer. Returns
162
+ * `undefined` for empty / non-numeric / negative inputs so
163
+ * {@link loadHistory} can apply its built-in defaults.
164
+ */
165
+ function parseIntParam(value) {
166
+ if (!value)
167
+ return undefined;
168
+ const n = Number(value);
169
+ if (!Number.isFinite(n) || n < 0)
170
+ return undefined;
171
+ return Math.trunc(n);
172
+ }
@@ -19,11 +19,12 @@
19
19
  * is registered); per-agent settings cascade on top of that.
20
20
  */
21
21
  import { lakebase } from "@databricks/appkit";
22
- import { pluginUtils } from "@dbx-tools/appkit-shared";
22
+ import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
23
23
  import { fastembed } from "@mastra/fastembed";
24
24
  import { Memory } from "@mastra/memory";
25
25
  import { PgVector, PostgresStore } from "@mastra/pg";
26
26
  import { randomUUID } from "node:crypto";
27
+ const log = logUtils.logger("mastra/memory");
27
28
  /**
28
29
  * True when any plugin-level or per-agent setting could need the
29
30
  * Lakebase pool. Used by `plugin.ts` to gate pool acquisition; the
@@ -80,8 +81,20 @@ export class MemoryBuilder {
80
81
  const memorySetting = def.memory ?? this.config.memory;
81
82
  const storage = this.buildStorage(agentId, storageSetting);
82
83
  const vector = this.buildVector(memorySetting);
83
- if (!storage && !vector)
84
+ if (!storage && !vector) {
85
+ log.debug("agent:stateless", { agentId });
84
86
  return undefined;
87
+ }
88
+ log.debug("agent:configured", {
89
+ agentId,
90
+ storage: storage !== undefined,
91
+ vector: vector !== undefined,
92
+ vectorMode: vector === undefined
93
+ ? "off"
94
+ : typeof memorySetting === "object"
95
+ ? "dedicated"
96
+ : "shared",
97
+ });
85
98
  return new Memory({
86
99
  ...(storage ? { storage } : {}),
87
100
  ...(vector ? { vector, embedder: fastembed } : {}),
package/dist/src/model.js CHANGED
@@ -263,6 +263,7 @@ export async function buildModel(config, requestContext, overrides = {}) {
263
263
  * to the top of the priority list.
264
264
  */
265
265
  async function pickModelId(config, requestContext, overrides, user, host) {
266
+ const log = logUtils.logger(config);
266
267
  const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
267
268
  const override = serving.allowOverride
268
269
  ? requestContext.get(MASTRA_MODEL_OVERRIDE_KEY)
@@ -270,15 +271,21 @@ async function pickModelId(config, requestContext, overrides, user, host) {
270
271
  const explicit = override ?? overrides.modelId ?? process.env.DATABRICKS_SERVING_ENDPOINT_NAME;
271
272
  // Cheap exit: when the caller named a specific model and fuzzy
272
273
  // matching is off, there's no reason to touch the catalogue at all.
273
- if (explicit !== undefined && !serving.fuzzy)
274
+ if (explicit !== undefined && !serving.fuzzy) {
275
+ log.debug("model selected", { modelId: explicit, source: "explicit" });
274
276
  return explicit;
277
+ }
275
278
  const endpoints = await listServingEndpoints(user.executionContext.client, host, {
276
279
  ttlMs: serving.ttlMs,
277
280
  });
278
281
  const modelId = explicit !== undefined
279
282
  ? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
280
283
  : pickFirstAvailable(serving.fallbacks, endpoints);
281
- //logUtils.logger(config).debug(`model selected: ${modelId}`);
284
+ log.debug("model selected", {
285
+ modelId,
286
+ source: explicit !== undefined ? "fuzzy-match" : "fallback",
287
+ requestedExplicit: explicit,
288
+ });
282
289
  return modelId;
283
290
  }
284
291
  /**
@@ -305,9 +312,9 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
305
312
  * 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
306
313
  * stream-replay quirks that Databricks-hosted Claude rejects (see
307
314
  * {@link sanitizeServingMessages}).
308
- * 2. When `MASTRA_DEBUG_LLM=1`, dumps the (post-sanitize) JSON body
309
- * to stderr so 4xx debugging doesn't have to fight AI SDK's
310
- * `[Array]` formatter.
315
+ * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
316
+ * 4xx debugging doesn't have to fight AI SDK's `[Array]`
317
+ * formatter.
311
318
  *
312
319
  * Safe to call from any hot path: {@link commonUtils.memoize} ensures
313
320
  * the wrapper is installed at most once per process, so subsequent
@@ -315,7 +322,7 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
315
322
  * {@link buildModel} fires on every agent step.
316
323
  */
317
324
  const setupFetchInterceptor = commonUtils.memoize(() => {
318
- const debug = Boolean(process.env.MASTRA_DEBUG_LLM);
325
+ const log = logUtils.logger("mastra/llm");
319
326
  const original = globalThis.fetch.bind(globalThis);
320
327
  globalThis.fetch = (async (input, init) => {
321
328
  const url = httpUtils.toURL(input);
@@ -328,14 +335,11 @@ const setupFetchInterceptor = commonUtils.memoize(() => {
328
335
  if (rewritten !== init.body) {
329
336
  init = { ...init, body: rewritten };
330
337
  }
331
- if (debug) {
332
- try {
333
- console.error("[mastra:llm-debug] -> POST", url.toString());
334
- console.error(JSON.stringify(JSON.parse(rewritten), null, 2));
335
- }
336
- catch {
337
- console.error("[mastra:llm-debug] -> POST", url.toString(), "(non-JSON body)");
338
- }
338
+ try {
339
+ log.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
340
+ }
341
+ catch {
342
+ log.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
339
343
  }
340
344
  return original(input, init);
341
345
  });
@@ -88,7 +88,7 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
88
88
  */
89
89
  getDefault: () => Agent | null;
90
90
  /** Underlying Mastra instance for advanced use (custom routes etc.). */
91
- getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
91
+ getMastra: () => Mastra<Record<string, Agent<any, import("@mastra/core/agent").ToolsInput, undefined, unknown, import("@mastra/core/agent").AgentEditorConfig | undefined>>, Record<string, import("@mastra/core/workflows").AnyWorkflow>, Record<string, import("@mastra/core/vector").MastraVector<any>>, Record<string, import("@mastra/core/tts").MastraTTS>, import("@mastra/core/logger").IMastraLogger, Record<string, import("@mastra/core/mcp").MCPServerBase<any>>, Record<string, import("@mastra/core/evals").MastraScorer<any, any, any, any>>, Record<string, import("@mastra/core/tools").ToolAction<any, any, any, any, any, any, unknown>>, Record<string, import("@mastra/core/processors").Processor<any, unknown>>, Record<string, import("@mastra/core/memory").MastraMemory>, Record<string, import("@mastra/core/channels").ChannelProvider>> | null;
92
92
  /** Express subapp Mastra is mounted on; mostly for tests. */
93
93
  getMastraServer: () => MastraServer | null;
94
94
  /**
@@ -109,6 +109,16 @@ export declare class MastraPlugin extends Plugin<MastraPluginConfig> {
109
109
  };
110
110
  clientConfig(): Record<string, unknown>;
111
111
  injectRoutes(router: IAppRouter): void;
112
+ /**
113
+ * Return `this.asUser(req)` when the request carries an OBO token,
114
+ * otherwise return `this` directly. Prevents the noisy AppKit warn
115
+ * (`asUser() called without user token in development mode. Skipping
116
+ * user impersonation.`) on every request in local dev where the
117
+ * browser never sends `x-forwarded-access-token`. Behavior is
118
+ * unchanged in production: a missing token always means a real OBO
119
+ * proxy call (and AppKit will throw upstream if that's wrong).
120
+ */
121
+ private userScopedSelf;
112
122
  /**
113
123
  * Implementation backing both the `/models` route and the
114
124
  * `listModels` export. Runs inside the AppKit user-context proxy so
@@ -32,6 +32,7 @@ import { chatRoute } from "@mastra/ai-sdk";
32
32
  import { Mastra } from "@mastra/core/mastra";
33
33
  import express from "express";
34
34
  import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
35
+ import { historyRoute } from "./history.js";
35
36
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
36
37
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
37
38
  import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
@@ -159,6 +160,8 @@ export class MastraPlugin extends Plugin {
159
160
  chatPath: `${basePath}/route/chat`,
160
161
  chatPathTemplate: `${basePath}/route/chat/:agentId`,
161
162
  modelsPath: `${basePath}/models`,
163
+ historyPath: `${basePath}/route/history`,
164
+ historyPathTemplate: `${basePath}/route/history/:agentId`,
162
165
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
163
166
  agents: Object.keys(this.built?.agents ?? {}),
164
167
  };
@@ -171,7 +174,7 @@ export class MastraPlugin extends Plugin {
171
174
  // the Mastra subapp. Errors propagate to Express's default error
172
175
  // handler via `next(err)` so callers see the real SDK message.
173
176
  router.get("/models", (req, res, next) => {
174
- this.asUser(req)
177
+ this.userScopedSelf(req)
175
178
  .listModels()
176
179
  .then((endpoints) => res.json({ endpoints }))
177
180
  .catch(next);
@@ -179,9 +182,21 @@ export class MastraPlugin extends Plugin {
179
182
  router.use("", (req, res, next) => {
180
183
  if (!this.mastraApp)
181
184
  return res.status(503).end();
182
- return this.asUser(req).mastraApp(req, res, next);
185
+ return this.userScopedSelf(req).mastraApp(req, res, next);
183
186
  });
184
187
  }
188
+ /**
189
+ * Return `this.asUser(req)` when the request carries an OBO token,
190
+ * otherwise return `this` directly. Prevents the noisy AppKit warn
191
+ * (`asUser() called without user token in development mode. Skipping
192
+ * user impersonation.`) on every request in local dev where the
193
+ * browser never sends `x-forwarded-access-token`. Behavior is
194
+ * unchanged in production: a missing token always means a real OBO
195
+ * proxy call (and AppKit will throw upstream if that's wrong).
196
+ */
197
+ userScopedSelf(req) {
198
+ return req.header("x-forwarded-access-token") ? this.asUser(req) : this;
199
+ }
185
200
  /**
186
201
  * Implementation backing both the `/models` route and the
187
202
  * `listModels` export. Runs inside the AppKit user-context proxy so
@@ -201,6 +216,10 @@ export class MastraPlugin extends Plugin {
201
216
  const memoryBuilder = needsLakebase(this.config)
202
217
  ? createMemoryBuilder(this.config, this.context)
203
218
  : undefined;
219
+ this.log.debug("build:start", {
220
+ lakebase: memoryBuilder !== undefined,
221
+ stripStaleCharts: this.config.stripStaleCharts !== false,
222
+ });
204
223
  // Build every agent declared in `config.agents` (or the built-in
205
224
  // fallback when none are declared). Each agent's `model` resolves
206
225
  // workspace URL + bearer at call time so concurrent requests get
@@ -227,9 +246,16 @@ export class MastraPlugin extends Plugin {
227
246
  customApiRoutes: [
228
247
  chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
229
248
  chatRoute({ path: "/route/chat/:agentId" }),
249
+ historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
250
+ historyRoute({ path: "/route/history/:agentId" }),
230
251
  ],
231
252
  });
232
253
  await this.mastraServer.init();
254
+ this.log.debug("build:done", {
255
+ agents: Object.keys(this.built.agents),
256
+ defaultAgent: this.built.defaultAgentId,
257
+ routes: ["/route/chat", "/route/history", "/models"],
258
+ });
233
259
  }
234
260
  }
235
261
  export const mastra = toPlugin(MastraPlugin);
@@ -0,0 +1,29 @@
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 only meaningful within the assistant turn that
7
+ * minted them - the writer events backing them are gone after the
8
+ * stream closes. When the model sees old chartIds in memory recall
9
+ * (Mastra Memory persists tool results), it's tempted to type
10
+ * those ids into the new turn's `[[chart:<id>]]` markers, leaving
11
+ * the chat client's chart slots stuck with no matching event. This
12
+ * processor removes the temptation by deleting `chartId` keys from
13
+ * every assistant message's tool results before the prompt is
14
+ * built. The current turn's tool results don't exist yet at
15
+ * `processInput` time, so they pass through unmodified.
16
+ *
17
+ * The strip is recursive - any nested `chartId` field is removed,
18
+ * regardless of which tool produced the result. This covers Genie's
19
+ * `datasets[].chartId` and `render_data`'s top-level `chartId`
20
+ * uniformly without coupling to specific tool ids.
21
+ */
22
+ import type { InputProcessor } from "@mastra/core/processors";
23
+ /**
24
+ * Input processor that scrubs `chartId` from every tool-invocation
25
+ * result in the message list. Wired onto every agent by default
26
+ * via {@link buildAgents}; opt out with
27
+ * `MastraPluginConfig.stripStaleCharts: false`.
28
+ */
29
+ export declare const stripStaleChartsProcessor: InputProcessor;
@@ -0,0 +1,96 @@
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 only meaningful within the assistant turn that
7
+ * minted them - the writer events backing them are gone after the
8
+ * stream closes. When the model sees old chartIds in memory recall
9
+ * (Mastra Memory persists tool results), it's tempted to type
10
+ * those ids into the new turn's `[[chart:<id>]]` markers, leaving
11
+ * the chat client's chart slots stuck with no matching event. This
12
+ * processor removes the temptation by deleting `chartId` keys from
13
+ * every assistant message's tool results before the prompt is
14
+ * built. The current turn's tool results don't exist yet at
15
+ * `processInput` time, so they pass through unmodified.
16
+ *
17
+ * The strip is recursive - any nested `chartId` field is removed,
18
+ * regardless of which tool produced the result. This covers Genie's
19
+ * `datasets[].chartId` and `render_data`'s top-level `chartId`
20
+ * uniformly without coupling to specific tool ids.
21
+ */
22
+ import { logUtils } from "@dbx-tools/appkit-shared";
23
+ const log = logUtils.logger("mastra/processor/strip-stale-charts");
24
+ /**
25
+ * Recursively clone `value`, omitting any property whose key is
26
+ * `chartId`. Arrays are mapped element-wise; primitives are
27
+ * returned as-is. The result is structurally identical to the
28
+ * input minus chartIds, so downstream message-shape consumers
29
+ * keep working.
30
+ */
31
+ function stripChartIds(value) {
32
+ if (Array.isArray(value)) {
33
+ return value.map(stripChartIds);
34
+ }
35
+ if (value && typeof value === "object") {
36
+ const obj = value;
37
+ const out = {};
38
+ for (const [key, val] of Object.entries(obj)) {
39
+ if (key === "chartId")
40
+ continue;
41
+ out[key] = stripChartIds(val);
42
+ }
43
+ return out;
44
+ }
45
+ return value;
46
+ }
47
+ /**
48
+ * Input processor that scrubs `chartId` from every tool-invocation
49
+ * result in the message list. Wired onto every agent by default
50
+ * via {@link buildAgents}; opt out with
51
+ * `MastraPluginConfig.stripStaleCharts: false`.
52
+ */
53
+ export const stripStaleChartsProcessor = {
54
+ id: "strip-stale-charts",
55
+ description: "Removes chartId fields from prior tool-invocation results so the model can't reuse turn-scoped ids from memory.",
56
+ processInput(args) {
57
+ let stripped = 0;
58
+ for (const message of args.messages) {
59
+ if (message.role !== "assistant")
60
+ continue;
61
+ const parts = message.content?.parts;
62
+ if (!Array.isArray(parts))
63
+ continue;
64
+ for (const part of parts) {
65
+ // Tool-invocation parts hold the persisted tool result.
66
+ // We don't scrub the input args (`rawInput` / `args`) because
67
+ // the chartId there is the model's outgoing claim, not
68
+ // anything it could re-reference; only `result` carries
69
+ // ids that subsequent turns might copy.
70
+ if (part.type !== "tool-invocation") {
71
+ continue;
72
+ }
73
+ const inv = part
74
+ .toolInvocation;
75
+ if (!inv || inv.result === undefined)
76
+ continue;
77
+ const before = inv.result;
78
+ const after = stripChartIds(before);
79
+ // Cheap structural check via JSON length - the actual
80
+ // strip writes a fresh object only when chartId keys
81
+ // existed, so different stringification length is a
82
+ // reliable signal that something was removed.
83
+ if (typeof before === "object" &&
84
+ before !== null &&
85
+ JSON.stringify(before).length !== JSON.stringify(after).length) {
86
+ inv.result = after;
87
+ stripped += 1;
88
+ }
89
+ }
90
+ }
91
+ if (stripped > 0) {
92
+ log.debug("stripped", { results: stripped });
93
+ }
94
+ return args.messages;
95
+ },
96
+ };
@@ -4,6 +4,7 @@
4
4
  * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
5
  * point.
6
6
  */
7
+ import { type RequestContext } from "@mastra/core/request-context";
7
8
  import { MastraServer as MastraServerExpress } from "@mastra/express";
8
9
  import type express from "express";
9
10
  import { type MastraPluginConfig } from "./config.js";
@@ -17,6 +18,9 @@ export declare class MastraServer extends MastraServerExpress {
17
18
  private log;
18
19
  constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServerExpress>);
19
20
  registerAuthMiddleware(): void;
21
+ configureRequestContextUser(requestContext: RequestContext): void;
22
+ configureRequestContextThreadId(req: express.Request, res: express.Response, requestContext: RequestContext): void;
23
+ configureRequestContextModelOverride(req: express.Request, requestContext: RequestContext): void;
20
24
  }
21
25
  /**
22
26
  * Patches around `@mastra/express`'s custom-route dispatcher so