@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.
- package/README.md +145 -10
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/src/agents.d.ts +1 -1
- package/dist/src/agents.js +43 -19
- package/dist/src/chart.d.ts +170 -0
- package/dist/src/chart.js +491 -0
- package/dist/src/config.d.ts +13 -0
- package/dist/src/genie.d.ts +36 -14
- package/dist/src/genie.js +434 -75
- package/dist/src/history.d.ts +67 -0
- package/dist/src/history.js +172 -0
- package/dist/src/memory.js +15 -2
- package/dist/src/model.js +18 -14
- package/dist/src/plugin.d.ts +11 -1
- package/dist/src/plugin.js +28 -2
- package/dist/src/processors/strip-stale-charts.d.ts +29 -0
- package/dist/src/processors/strip-stale-charts.js +96 -0
- package/dist/src/server.d.ts +4 -0
- package/dist/src/server.js +59 -45
- package/dist/src/serving.js +19 -2
- package/dist/src/tools/email.d.ts +74 -0
- package/dist/src/tools/email.js +122 -0
- package/dist/tsconfig.build.tsbuildinfo +1 -0
- package/index.ts +2 -0
- package/package.json +21 -25
- package/src/agents.ts +46 -21
- package/src/chart.ts +593 -0
- package/src/config.ts +13 -0
- package/src/genie.ts +499 -102
- package/src/history.ts +210 -0
- package/src/memory.ts +19 -2
- package/src/model.ts +18 -13
- package/src/plugin.ts +30 -2
- package/src/processors/strip-stale-charts.ts +105 -0
- package/src/server.ts +76 -51
- package/src/serving.ts +21 -2
- package/src/tools/email.ts +147 -0
package/src/history.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
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
|
+
|
|
19
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
20
|
+
import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
|
|
21
|
+
import type { Agent } from "@mastra/core/agent";
|
|
22
|
+
import type { MastraDBMessage } from "@mastra/core/agent/message-list";
|
|
23
|
+
import {
|
|
24
|
+
MASTRA_RESOURCE_ID_KEY,
|
|
25
|
+
MASTRA_THREAD_ID_KEY,
|
|
26
|
+
} from "@mastra/core/request-context";
|
|
27
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
28
|
+
import type { ContextWithMastra } from "@mastra/core/server";
|
|
29
|
+
import type {
|
|
30
|
+
MastraHistoryResponse,
|
|
31
|
+
MastraHistoryUIMessage,
|
|
32
|
+
} from "@dbx-tools/appkit-mastra-shared";
|
|
33
|
+
|
|
34
|
+
const log = logUtils.logger("mastra/history");
|
|
35
|
+
|
|
36
|
+
/** Default history page size; matches the Mastra storage default. */
|
|
37
|
+
const DEFAULT_PER_PAGE = 20;
|
|
38
|
+
/** Hard cap so a misbehaving client can't fetch the whole thread at once. */
|
|
39
|
+
const MAX_PER_PAGE = 200;
|
|
40
|
+
|
|
41
|
+
/** Inputs accepted by {@link loadHistory}. */
|
|
42
|
+
export interface LoadHistoryOptions {
|
|
43
|
+
agent: Agent;
|
|
44
|
+
threadId: string;
|
|
45
|
+
resourceId?: string;
|
|
46
|
+
page?: number;
|
|
47
|
+
perPage?: number;
|
|
48
|
+
/** When true, returns the *oldest* page first (chronological). */
|
|
49
|
+
ascending?: boolean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Fetch a page of UI-formatted messages for a thread.
|
|
54
|
+
*
|
|
55
|
+
* Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
|
|
56
|
+
* storage namespaces (`mastra_<agentId>` schemas) and any future
|
|
57
|
+
* memory-side filters apply automatically. When the agent has no
|
|
58
|
+
* memory configured the response is a successful empty page so
|
|
59
|
+
* callers don't have to special-case stateless agents.
|
|
60
|
+
*
|
|
61
|
+
* Pagination is descending-by-default: page 0 is the most recent
|
|
62
|
+
* page, page 1 the page before that, etc. The returned `uiMessages`
|
|
63
|
+
* are always re-sorted into chronological order (oldest -> newest)
|
|
64
|
+
* so the client can prepend them above the existing transcript
|
|
65
|
+
* without sorting locally.
|
|
66
|
+
*/
|
|
67
|
+
export async function loadHistory(
|
|
68
|
+
opts: LoadHistoryOptions,
|
|
69
|
+
): Promise<MastraHistoryResponse> {
|
|
70
|
+
const perPage = clampPerPage(opts.perPage);
|
|
71
|
+
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
72
|
+
const memory = await opts.agent.getMemory();
|
|
73
|
+
if (!memory) {
|
|
74
|
+
log.debug("recall:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
|
|
75
|
+
return { uiMessages: [], page, perPage, total: 0, hasMore: false };
|
|
76
|
+
}
|
|
77
|
+
const startedAt = Date.now();
|
|
78
|
+
const result = await memory.recall({
|
|
79
|
+
threadId: opts.threadId,
|
|
80
|
+
...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
|
|
81
|
+
page,
|
|
82
|
+
perPage,
|
|
83
|
+
orderBy: {
|
|
84
|
+
field: "createdAt",
|
|
85
|
+
direction: opts.ascending ? "ASC" : "DESC",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
const chronological = sortChronological(result.messages);
|
|
89
|
+
const uiMessages = toAISdkV5Messages(
|
|
90
|
+
chronological,
|
|
91
|
+
) as unknown as MastraHistoryUIMessage[];
|
|
92
|
+
log.debug("recall:done", {
|
|
93
|
+
agentId: opts.agent.id,
|
|
94
|
+
threadId: opts.threadId,
|
|
95
|
+
page,
|
|
96
|
+
perPage,
|
|
97
|
+
returned: uiMessages.length,
|
|
98
|
+
total: result.total,
|
|
99
|
+
hasMore: result.hasMore,
|
|
100
|
+
elapsedMs: Date.now() - startedAt,
|
|
101
|
+
});
|
|
102
|
+
return {
|
|
103
|
+
uiMessages,
|
|
104
|
+
page,
|
|
105
|
+
perPage,
|
|
106
|
+
total: result.total,
|
|
107
|
+
hasMore: result.hasMore,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Options accepted by {@link historyRoute}. */
|
|
112
|
+
export type HistoryRouteOptions =
|
|
113
|
+
| { path: `${string}:agentId${string}`; agent?: never }
|
|
114
|
+
| { path: string; agent: string };
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Register a `GET <path>` Mastra custom API route that returns a page
|
|
118
|
+
* of AI SDK V5 `UIMessage`s for the caller's current thread.
|
|
119
|
+
*
|
|
120
|
+
* Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
|
|
121
|
+
* fixed-agent mount, or include `:agentId` in the path for dynamic
|
|
122
|
+
* routing. Pairs cleanly with the AppKit Mastra plugin's chat route
|
|
123
|
+
* layout (`/route/chat` + `/route/chat/:agentId`).
|
|
124
|
+
*
|
|
125
|
+
* The handler reads `threadId` and `resourceId` from `RequestContext`
|
|
126
|
+
* (populated upstream by `MastraServer.registerAuthMiddleware`), so
|
|
127
|
+
* no cookie or user lookups happen here.
|
|
128
|
+
*/
|
|
129
|
+
export function historyRoute(options: HistoryRouteOptions) {
|
|
130
|
+
const { path } = options;
|
|
131
|
+
const fixedAgent = "agent" in options ? options.agent : undefined;
|
|
132
|
+
if (!fixedAgent && !path.includes(":agentId")) {
|
|
133
|
+
throw new Error(
|
|
134
|
+
"historyRoute path must include `:agentId` or `agent` must be passed explicitly",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return registerApiRoute(path, {
|
|
138
|
+
method: "GET",
|
|
139
|
+
handler: async (c: ContextWithMastra) => {
|
|
140
|
+
const mastra = c.get("mastra");
|
|
141
|
+
const requestContext = c.get("requestContext");
|
|
142
|
+
const agentId = fixedAgent ?? c.req.param("agentId");
|
|
143
|
+
if (!agentId) {
|
|
144
|
+
return c.json({ error: "agentId is required" }, 400);
|
|
145
|
+
}
|
|
146
|
+
const agent = mastra.getAgentById(agentId);
|
|
147
|
+
if (!agent) {
|
|
148
|
+
return c.json({ error: `Unknown agent "${agentId}"` }, 404);
|
|
149
|
+
}
|
|
150
|
+
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
|
|
151
|
+
if (!threadId) {
|
|
152
|
+
return c.json({ error: "thread id missing from request context" }, 400);
|
|
153
|
+
}
|
|
154
|
+
const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY) as
|
|
155
|
+
| string
|
|
156
|
+
| undefined;
|
|
157
|
+
const payload = await loadHistory({
|
|
158
|
+
agent,
|
|
159
|
+
threadId,
|
|
160
|
+
...(resourceId ? { resourceId } : {}),
|
|
161
|
+
page: parseIntParam(c.req.query("page")),
|
|
162
|
+
perPage: parseIntParam(c.req.query("perPage")),
|
|
163
|
+
});
|
|
164
|
+
return c.json(payload);
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
170
|
+
function clampPerPage(value: number | undefined): number {
|
|
171
|
+
if (value === undefined || Number.isNaN(value)) return DEFAULT_PER_PAGE;
|
|
172
|
+
const n = Math.trunc(value);
|
|
173
|
+
if (n <= 0) return DEFAULT_PER_PAGE;
|
|
174
|
+
return Math.min(n, MAX_PER_PAGE);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Sort messages oldest-first by `createdAt`, falling back to whatever
|
|
179
|
+
* order the storage returned them in. The native `recall` call honors
|
|
180
|
+
* `orderBy` but doesn't guarantee a stable secondary sort, so we
|
|
181
|
+
* normalize here before handing the page to the AI SDK converter.
|
|
182
|
+
*/
|
|
183
|
+
function sortChronological(messages: MastraDBMessage[]): MastraDBMessage[] {
|
|
184
|
+
return [...messages].sort((a, b) => {
|
|
185
|
+
const ta = toEpoch(a.createdAt);
|
|
186
|
+
const tb = toEpoch(b.createdAt);
|
|
187
|
+
return ta - tb;
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function toEpoch(value: unknown): number {
|
|
192
|
+
if (value instanceof Date) return value.getTime();
|
|
193
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
194
|
+
const parsed = new Date(value).getTime();
|
|
195
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
196
|
+
}
|
|
197
|
+
return 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Coerce a Hono query value into a non-negative integer. Returns
|
|
202
|
+
* `undefined` for empty / non-numeric / negative inputs so
|
|
203
|
+
* {@link loadHistory} can apply its built-in defaults.
|
|
204
|
+
*/
|
|
205
|
+
function parseIntParam(value: string | undefined): number | undefined {
|
|
206
|
+
if (!value) return undefined;
|
|
207
|
+
const n = Number(value);
|
|
208
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
209
|
+
return Math.trunc(n);
|
|
210
|
+
}
|
package/src/memory.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
22
|
import { lakebase } from "@databricks/appkit";
|
|
23
|
-
import { pluginUtils } from "@dbx-tools/appkit-shared";
|
|
23
|
+
import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
|
|
24
24
|
import { fastembed } from "@mastra/fastembed";
|
|
25
25
|
import { Memory } from "@mastra/memory";
|
|
26
26
|
import { PgVector, PostgresStore } from "@mastra/pg";
|
|
@@ -33,6 +33,8 @@ import type {
|
|
|
33
33
|
} from "./agents.js";
|
|
34
34
|
import type { MastraPluginConfig } from "./config.js";
|
|
35
35
|
|
|
36
|
+
const log = logUtils.logger("mastra/memory");
|
|
37
|
+
|
|
36
38
|
/** Pool handle returned by the AppKit `lakebase` plugin `exports().pool`. */
|
|
37
39
|
export type LakebasePool = ReturnType<
|
|
38
40
|
InstanceType<ReturnType<typeof lakebase>["plugin"]>["exports"]
|
|
@@ -109,7 +111,22 @@ export class MemoryBuilder {
|
|
|
109
111
|
|
|
110
112
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
111
113
|
const vector = this.buildVector(memorySetting);
|
|
112
|
-
if (!storage && !vector)
|
|
114
|
+
if (!storage && !vector) {
|
|
115
|
+
log.debug("agent:stateless", { agentId });
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
log.debug("agent:configured", {
|
|
120
|
+
agentId,
|
|
121
|
+
storage: storage !== undefined,
|
|
122
|
+
vector: vector !== undefined,
|
|
123
|
+
vectorMode:
|
|
124
|
+
vector === undefined
|
|
125
|
+
? "off"
|
|
126
|
+
: typeof memorySetting === "object"
|
|
127
|
+
? "dedicated"
|
|
128
|
+
: "shared",
|
|
129
|
+
});
|
|
113
130
|
|
|
114
131
|
return new Memory({
|
|
115
132
|
...(storage ? { storage } : {}),
|
package/src/model.ts
CHANGED
|
@@ -306,6 +306,7 @@ async function pickModelId(
|
|
|
306
306
|
user: User,
|
|
307
307
|
host: string,
|
|
308
308
|
): Promise<string> {
|
|
309
|
+
const log = logUtils.logger(config);
|
|
309
310
|
const serving = resolveServingConfig(config, FALLBACK_MODEL_IDS);
|
|
310
311
|
const override = serving.allowOverride
|
|
311
312
|
? (requestContext.get(MASTRA_MODEL_OVERRIDE_KEY) as string | undefined)
|
|
@@ -315,7 +316,10 @@ async function pickModelId(
|
|
|
315
316
|
|
|
316
317
|
// Cheap exit: when the caller named a specific model and fuzzy
|
|
317
318
|
// matching is off, there's no reason to touch the catalogue at all.
|
|
318
|
-
if (explicit !== undefined && !serving.fuzzy)
|
|
319
|
+
if (explicit !== undefined && !serving.fuzzy) {
|
|
320
|
+
log.debug("model selected", { modelId: explicit, source: "explicit" });
|
|
321
|
+
return explicit;
|
|
322
|
+
}
|
|
319
323
|
|
|
320
324
|
const endpoints = await listServingEndpoints(user.executionContext.client, host, {
|
|
321
325
|
ttlMs: serving.ttlMs,
|
|
@@ -324,7 +328,11 @@ async function pickModelId(
|
|
|
324
328
|
explicit !== undefined
|
|
325
329
|
? resolveModelId(explicit, endpoints, { threshold: serving.threshold }).modelId
|
|
326
330
|
: pickFirstAvailable(serving.fallbacks, endpoints);
|
|
327
|
-
|
|
331
|
+
log.debug("model selected", {
|
|
332
|
+
modelId,
|
|
333
|
+
source: explicit !== undefined ? "fuzzy-match" : "fallback",
|
|
334
|
+
requestedExplicit: explicit,
|
|
335
|
+
});
|
|
328
336
|
return modelId;
|
|
329
337
|
}
|
|
330
338
|
|
|
@@ -369,9 +377,9 @@ interface ChatMessage {
|
|
|
369
377
|
* 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
|
|
370
378
|
* stream-replay quirks that Databricks-hosted Claude rejects (see
|
|
371
379
|
* {@link sanitizeServingMessages}).
|
|
372
|
-
* 2.
|
|
373
|
-
*
|
|
374
|
-
*
|
|
380
|
+
* 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
|
|
381
|
+
* 4xx debugging doesn't have to fight AI SDK's `[Array]`
|
|
382
|
+
* formatter.
|
|
375
383
|
*
|
|
376
384
|
* Safe to call from any hot path: {@link commonUtils.memoize} ensures
|
|
377
385
|
* the wrapper is installed at most once per process, so subsequent
|
|
@@ -379,7 +387,7 @@ interface ChatMessage {
|
|
|
379
387
|
* {@link buildModel} fires on every agent step.
|
|
380
388
|
*/
|
|
381
389
|
const setupFetchInterceptor = commonUtils.memoize((): void => {
|
|
382
|
-
const
|
|
390
|
+
const log = logUtils.logger("mastra/llm");
|
|
383
391
|
const original = globalThis.fetch.bind(globalThis);
|
|
384
392
|
globalThis.fetch = (async (input, init) => {
|
|
385
393
|
const url = httpUtils.toURL(input);
|
|
@@ -394,13 +402,10 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
|
|
|
394
402
|
if (rewritten !== init.body) {
|
|
395
403
|
init = { ...init, body: rewritten };
|
|
396
404
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
} catch {
|
|
402
|
-
console.error("[mastra:llm-debug] -> POST", url.toString(), "(non-JSON body)");
|
|
403
|
-
}
|
|
405
|
+
try {
|
|
406
|
+
log.debug("POST", { url: url.toString(), body: JSON.parse(rewritten) });
|
|
407
|
+
} catch {
|
|
408
|
+
log.debug("POST", { url: url.toString(), bodyType: "non-JSON" });
|
|
404
409
|
}
|
|
405
410
|
return original(input, init);
|
|
406
411
|
}) as typeof globalThis.fetch;
|
package/src/plugin.ts
CHANGED
|
@@ -46,6 +46,7 @@ import express from "express";
|
|
|
46
46
|
import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
|
|
47
47
|
import type { MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
|
|
48
48
|
import type { MastraPluginConfig } from "./config.js";
|
|
49
|
+
import { historyRoute } from "./history.js";
|
|
49
50
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
50
51
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
51
52
|
import {
|
|
@@ -187,6 +188,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
187
188
|
chatPath: `${basePath}/route/chat`,
|
|
188
189
|
chatPathTemplate: `${basePath}/route/chat/:agentId`,
|
|
189
190
|
modelsPath: `${basePath}/models`,
|
|
191
|
+
historyPath: `${basePath}/route/history`,
|
|
192
|
+
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
190
193
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
191
194
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
192
195
|
};
|
|
@@ -200,7 +203,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
200
203
|
// the Mastra subapp. Errors propagate to Express's default error
|
|
201
204
|
// handler via `next(err)` so callers see the real SDK message.
|
|
202
205
|
router.get("/models", (req, res, next) => {
|
|
203
|
-
this.
|
|
206
|
+
this.userScopedSelf(req)
|
|
204
207
|
.listModels()
|
|
205
208
|
.then((endpoints) => res.json({ endpoints }))
|
|
206
209
|
.catch(next);
|
|
@@ -208,10 +211,23 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
208
211
|
|
|
209
212
|
router.use("", (req, res, next) => {
|
|
210
213
|
if (!this.mastraApp) return res.status(503).end();
|
|
211
|
-
return this.
|
|
214
|
+
return this.userScopedSelf(req).mastraApp!(req, res, next);
|
|
212
215
|
});
|
|
213
216
|
}
|
|
214
217
|
|
|
218
|
+
/**
|
|
219
|
+
* Return `this.asUser(req)` when the request carries an OBO token,
|
|
220
|
+
* otherwise return `this` directly. Prevents the noisy AppKit warn
|
|
221
|
+
* (`asUser() called without user token in development mode. Skipping
|
|
222
|
+
* user impersonation.`) on every request in local dev where the
|
|
223
|
+
* browser never sends `x-forwarded-access-token`. Behavior is
|
|
224
|
+
* unchanged in production: a missing token always means a real OBO
|
|
225
|
+
* proxy call (and AppKit will throw upstream if that's wrong).
|
|
226
|
+
*/
|
|
227
|
+
private userScopedSelf(req: express.Request): this {
|
|
228
|
+
return req.header("x-forwarded-access-token") ? (this.asUser(req) as this) : this;
|
|
229
|
+
}
|
|
230
|
+
|
|
215
231
|
/**
|
|
216
232
|
* Implementation backing both the `/models` route and the
|
|
217
233
|
* `listModels` export. Runs inside the AppKit user-context proxy so
|
|
@@ -233,6 +249,11 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
233
249
|
? createMemoryBuilder(this.config, this.context)
|
|
234
250
|
: undefined;
|
|
235
251
|
|
|
252
|
+
this.log.debug("build:start", {
|
|
253
|
+
lakebase: memoryBuilder !== undefined,
|
|
254
|
+
stripStaleCharts: this.config.stripStaleCharts !== false,
|
|
255
|
+
});
|
|
256
|
+
|
|
236
257
|
// Build every agent declared in `config.agents` (or the built-in
|
|
237
258
|
// fallback when none are declared). Each agent's `model` resolves
|
|
238
259
|
// workspace URL + bearer at call time so concurrent requests get
|
|
@@ -260,9 +281,16 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
260
281
|
customApiRoutes: [
|
|
261
282
|
chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
|
|
262
283
|
chatRoute({ path: "/route/chat/:agentId" }),
|
|
284
|
+
historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
|
|
285
|
+
historyRoute({ path: "/route/history/:agentId" }),
|
|
263
286
|
],
|
|
264
287
|
});
|
|
265
288
|
await this.mastraServer.init();
|
|
289
|
+
this.log.debug("build:done", {
|
|
290
|
+
agents: Object.keys(this.built.agents),
|
|
291
|
+
defaultAgent: this.built.defaultAgentId,
|
|
292
|
+
routes: ["/route/chat", "/route/history", "/models"],
|
|
293
|
+
});
|
|
266
294
|
}
|
|
267
295
|
}
|
|
268
296
|
|
|
@@ -0,0 +1,105 @@
|
|
|
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
|
+
|
|
23
|
+
import { logUtils } from "@dbx-tools/appkit-shared";
|
|
24
|
+
import type {
|
|
25
|
+
InputProcessor,
|
|
26
|
+
ProcessInputArgs,
|
|
27
|
+
} 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 (
|
|
77
|
+
(part as { type?: unknown }).type !== "tool-invocation"
|
|
78
|
+
) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const inv = (part as { toolInvocation?: { result?: unknown } })
|
|
82
|
+
.toolInvocation;
|
|
83
|
+
if (!inv || inv.result === undefined) continue;
|
|
84
|
+
const before = inv.result;
|
|
85
|
+
const after = stripChartIds(before);
|
|
86
|
+
// Cheap structural check via JSON length - the actual
|
|
87
|
+
// strip writes a fresh object only when chartId keys
|
|
88
|
+
// existed, so different stringification length is a
|
|
89
|
+
// reliable signal that something was removed.
|
|
90
|
+
if (
|
|
91
|
+
typeof before === "object" &&
|
|
92
|
+
before !== null &&
|
|
93
|
+
JSON.stringify(before).length !== JSON.stringify(after).length
|
|
94
|
+
) {
|
|
95
|
+
inv.result = after;
|
|
96
|
+
stripped += 1;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (stripped > 0) {
|
|
101
|
+
log.debug("stripped", { results: stripped });
|
|
102
|
+
}
|
|
103
|
+
return args.messages;
|
|
104
|
+
},
|
|
105
|
+
};
|
package/src/server.ts
CHANGED
|
@@ -42,61 +42,86 @@ export class MastraServer extends MastraServerExpress {
|
|
|
42
42
|
override registerAuthMiddleware(): void {
|
|
43
43
|
super.registerAuthMiddleware();
|
|
44
44
|
this.app.use((req, res, next) => {
|
|
45
|
-
const executionContext = getExecutionContext();
|
|
46
|
-
const user: User = {
|
|
47
|
-
id:
|
|
48
|
-
"userId" in executionContext
|
|
49
|
-
? executionContext.userId
|
|
50
|
-
: executionContext.serviceUserId,
|
|
51
|
-
executionContext,
|
|
52
|
-
};
|
|
53
45
|
const requestContext = res.locals.requestContext! as RequestContext;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
sessionId = randomUUID();
|
|
69
|
-
res.cookie(cookieName, sessionId, {
|
|
70
|
-
httpOnly: true,
|
|
71
|
-
sameSite: "lax",
|
|
72
|
-
secure: req.secure,
|
|
73
|
-
path: "/",
|
|
74
|
-
});
|
|
75
|
-
}
|
|
76
|
-
res.locals.sessionId = sessionId;
|
|
77
|
-
if (!requestContext.get(MASTRA_THREAD_ID_KEY)) {
|
|
78
|
-
this.log.debug(`Setting thread id: ${sessionId}`);
|
|
79
|
-
requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
|
|
80
|
-
}
|
|
81
|
-
// Per-request model override: only honored when the plugin
|
|
82
|
-
// opts in (default). Sources, in priority order, are
|
|
83
|
-
// `X-Mastra-Model` header, `?model=` query, and `model` /
|
|
84
|
-
// `modelId` body field; see `serving.ts`.
|
|
85
|
-
const serving = resolveServingConfig(this.config);
|
|
86
|
-
if (serving.allowOverride) {
|
|
87
|
-
const override = extractModelOverride({
|
|
88
|
-
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
89
|
-
query: req.query as Record<string, unknown>,
|
|
90
|
-
body: req.body,
|
|
91
|
-
});
|
|
92
|
-
if (override) {
|
|
93
|
-
this.log.debug(`Model override: ${override}`);
|
|
94
|
-
requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
46
|
+
this.configureRequestContextUser(requestContext);
|
|
47
|
+
this.configureRequestContextThreadId(req, res, requestContext);
|
|
48
|
+
this.configureRequestContextModelOverride(req, requestContext);
|
|
49
|
+
this.log.debug("auth:middleware", {
|
|
50
|
+
method: req.method,
|
|
51
|
+
path: req.path,
|
|
52
|
+
threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
|
|
53
|
+
resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
|
|
54
|
+
modelOverride: requestContext.get(
|
|
55
|
+
// imported below; logged so a misrouted request shows
|
|
56
|
+
// up alongside its model selection in `LOG_LEVEL=debug`.
|
|
57
|
+
"mastra__model_override",
|
|
58
|
+
),
|
|
59
|
+
});
|
|
97
60
|
next();
|
|
98
61
|
});
|
|
99
62
|
}
|
|
63
|
+
|
|
64
|
+
configureRequestContextUser(requestContext: RequestContext) {
|
|
65
|
+
if (
|
|
66
|
+
[MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))
|
|
67
|
+
)
|
|
68
|
+
return;
|
|
69
|
+
const executionContext = getExecutionContext();
|
|
70
|
+
const user: User = {
|
|
71
|
+
id:
|
|
72
|
+
"userId" in executionContext
|
|
73
|
+
? executionContext.userId
|
|
74
|
+
: executionContext.serviceUserId,
|
|
75
|
+
executionContext,
|
|
76
|
+
};
|
|
77
|
+
requestContext.set(MASTRA_USER_KEY, user);
|
|
78
|
+
requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
configureRequestContextThreadId(
|
|
82
|
+
req: express.Request,
|
|
83
|
+
res: express.Response,
|
|
84
|
+
requestContext: RequestContext,
|
|
85
|
+
) {
|
|
86
|
+
if (requestContext.get(MASTRA_THREAD_ID_KEY)) return;
|
|
87
|
+
const cookies = httpUtils.parseCookies(req.headers.cookie);
|
|
88
|
+
const cookieName = stringUtils.toIdentifierWithOptions(
|
|
89
|
+
{ delimiter: "_", distinct: true },
|
|
90
|
+
"appkit",
|
|
91
|
+
this.config.name!,
|
|
92
|
+
"sessionId",
|
|
93
|
+
);
|
|
94
|
+
let sessionId = cookies[cookieName];
|
|
95
|
+
if (!sessionId) {
|
|
96
|
+
sessionId = randomUUID();
|
|
97
|
+
res.cookie(cookieName, sessionId, {
|
|
98
|
+
httpOnly: true,
|
|
99
|
+
sameSite: "lax",
|
|
100
|
+
secure: req.secure,
|
|
101
|
+
path: "/",
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
configureRequestContextModelOverride(
|
|
108
|
+
req: express.Request,
|
|
109
|
+
requestContext: RequestContext,
|
|
110
|
+
) {
|
|
111
|
+
// Per-request model override: only honored when the plugin
|
|
112
|
+
// opts in (default). Sources, in priority order, are
|
|
113
|
+
// `X-Mastra-Model` header, `?model=` query, and `model` /
|
|
114
|
+
// `modelId` body field; see `serving.ts`.
|
|
115
|
+
const serving = resolveServingConfig(this.config);
|
|
116
|
+
if (serving.allowOverride) {
|
|
117
|
+
const override = extractModelOverride({
|
|
118
|
+
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
119
|
+
query: req.query as Record<string, unknown>,
|
|
120
|
+
body: req.body,
|
|
121
|
+
});
|
|
122
|
+
if (override) requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
100
125
|
}
|
|
101
126
|
|
|
102
127
|
/**
|