@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
|
@@ -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
|
+
}
|
package/dist/src/memory.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
309
|
-
*
|
|
310
|
-
*
|
|
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
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
});
|
package/dist/src/plugin.d.ts
CHANGED
|
@@ -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
|
package/dist/src/plugin.js
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
+
};
|
package/dist/src/server.d.ts
CHANGED
|
@@ -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
|