@dbx-tools/appkit-mastra 0.1.3 → 0.1.5
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/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/src/agents.d.ts +1 -1
- package/dist/src/agents.js +25 -11
- package/dist/src/chart.d.ts +104 -0
- package/dist/src/chart.js +375 -0
- package/dist/src/genie.d.ts +20 -13
- package/dist/src/genie.js +393 -70
- package/dist/src/history.d.ts +67 -0
- package/dist/src/history.js +158 -0
- package/dist/src/plugin.d.ts +10 -0
- package/dist/src/plugin.js +22 -2
- package/dist/src/render-chart-route.d.ts +33 -0
- package/dist/src/render-chart-route.js +120 -0
- package/dist/src/server.d.ts +4 -0
- package/dist/src/server.js +49 -45
- package/index.ts +1 -0
- package/package.json +4 -4
- package/src/agents.ts +27 -15
- package/src/chart.ts +425 -0
- package/src/genie.ts +431 -97
- package/src/history.ts +198 -0
- package/src/plugin.ts +23 -2
- package/src/render-chart-route.ts +141 -0
- package/src/server.ts +65 -51
- package/README.md +0 -593
package/src/history.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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 { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
|
|
20
|
+
import type { Agent } from "@mastra/core/agent";
|
|
21
|
+
import type {
|
|
22
|
+
MastraDBMessage,
|
|
23
|
+
} from "@mastra/core/agent/message-list";
|
|
24
|
+
import {
|
|
25
|
+
MASTRA_RESOURCE_ID_KEY,
|
|
26
|
+
MASTRA_THREAD_ID_KEY,
|
|
27
|
+
} from "@mastra/core/request-context";
|
|
28
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
29
|
+
import type {
|
|
30
|
+
MastraHistoryResponse,
|
|
31
|
+
MastraHistoryUIMessage,
|
|
32
|
+
} from "@dbx-tools/appkit-mastra-shared";
|
|
33
|
+
|
|
34
|
+
/** Default history page size; matches the Mastra storage default. */
|
|
35
|
+
const DEFAULT_PER_PAGE = 20;
|
|
36
|
+
/** Hard cap so a misbehaving client can't fetch the whole thread at once. */
|
|
37
|
+
const MAX_PER_PAGE = 200;
|
|
38
|
+
|
|
39
|
+
/** Inputs accepted by {@link loadHistory}. */
|
|
40
|
+
export interface LoadHistoryOptions {
|
|
41
|
+
agent: Agent;
|
|
42
|
+
threadId: string;
|
|
43
|
+
resourceId?: string;
|
|
44
|
+
page?: number;
|
|
45
|
+
perPage?: number;
|
|
46
|
+
/** When true, returns the *oldest* page first (chronological). */
|
|
47
|
+
ascending?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fetch a page of UI-formatted messages for a thread.
|
|
52
|
+
*
|
|
53
|
+
* Uses the agent's resolved `Memory` (`getMemory()`) so per-agent
|
|
54
|
+
* storage namespaces (`mastra_<agentId>` schemas) and any future
|
|
55
|
+
* memory-side filters apply automatically. When the agent has no
|
|
56
|
+
* memory configured the response is a successful empty page so
|
|
57
|
+
* callers don't have to special-case stateless agents.
|
|
58
|
+
*
|
|
59
|
+
* Pagination is descending-by-default: page 0 is the most recent
|
|
60
|
+
* page, page 1 the page before that, etc. The returned `uiMessages`
|
|
61
|
+
* are always re-sorted into chronological order (oldest -> newest)
|
|
62
|
+
* so the client can prepend them above the existing transcript
|
|
63
|
+
* without sorting locally.
|
|
64
|
+
*/
|
|
65
|
+
export async function loadHistory(
|
|
66
|
+
opts: LoadHistoryOptions,
|
|
67
|
+
): Promise<MastraHistoryResponse> {
|
|
68
|
+
const perPage = clampPerPage(opts.perPage);
|
|
69
|
+
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
70
|
+
const memory = await opts.agent.getMemory();
|
|
71
|
+
if (!memory) {
|
|
72
|
+
return { uiMessages: [], page, perPage, total: 0, hasMore: false };
|
|
73
|
+
}
|
|
74
|
+
const result = await memory.recall({
|
|
75
|
+
threadId: opts.threadId,
|
|
76
|
+
...(opts.resourceId ? { resourceId: opts.resourceId } : {}),
|
|
77
|
+
page,
|
|
78
|
+
perPage,
|
|
79
|
+
orderBy: {
|
|
80
|
+
field: "createdAt",
|
|
81
|
+
direction: opts.ascending ? "ASC" : "DESC",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
const chronological = sortChronological(result.messages);
|
|
85
|
+
const uiMessages = toAISdkV5Messages(
|
|
86
|
+
chronological,
|
|
87
|
+
) as unknown as MastraHistoryUIMessage[];
|
|
88
|
+
return {
|
|
89
|
+
uiMessages,
|
|
90
|
+
page,
|
|
91
|
+
perPage,
|
|
92
|
+
total: result.total,
|
|
93
|
+
hasMore: result.hasMore,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Options accepted by {@link historyRoute}. */
|
|
98
|
+
export type HistoryRouteOptions =
|
|
99
|
+
| { path: `${string}:agentId${string}`; agent?: never }
|
|
100
|
+
| { path: string; agent: string };
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Register a `GET <path>` Mastra custom API route that returns a page
|
|
104
|
+
* of AI SDK V5 `UIMessage`s for the caller's current thread.
|
|
105
|
+
*
|
|
106
|
+
* Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
|
|
107
|
+
* fixed-agent mount, or include `:agentId` in the path for dynamic
|
|
108
|
+
* routing. Pairs cleanly with the AppKit Mastra plugin's chat route
|
|
109
|
+
* layout (`/route/chat` + `/route/chat/:agentId`).
|
|
110
|
+
*
|
|
111
|
+
* The handler reads `threadId` and `resourceId` from `RequestContext`
|
|
112
|
+
* (populated upstream by `MastraServer.registerAuthMiddleware`), so
|
|
113
|
+
* no cookie or user lookups happen here.
|
|
114
|
+
*/
|
|
115
|
+
export function historyRoute(options: HistoryRouteOptions) {
|
|
116
|
+
const { path } = options;
|
|
117
|
+
const fixedAgent = "agent" in options ? options.agent : undefined;
|
|
118
|
+
if (!fixedAgent && !path.includes(":agentId")) {
|
|
119
|
+
throw new Error(
|
|
120
|
+
"historyRoute path must include `:agentId` or `agent` must be passed explicitly",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
return registerApiRoute(path, {
|
|
124
|
+
method: "GET",
|
|
125
|
+
handler: async (c) => {
|
|
126
|
+
const mastra = c.get("mastra");
|
|
127
|
+
const requestContext = c.get("requestContext");
|
|
128
|
+
const agentId = fixedAgent ?? c.req.param("agentId");
|
|
129
|
+
if (!agentId) {
|
|
130
|
+
return c.json({ error: "agentId is required" }, 400);
|
|
131
|
+
}
|
|
132
|
+
const agent = mastra.getAgentById(agentId);
|
|
133
|
+
if (!agent) {
|
|
134
|
+
return c.json({ error: `Unknown agent "${agentId}"` }, 404);
|
|
135
|
+
}
|
|
136
|
+
const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as
|
|
137
|
+
| string
|
|
138
|
+
| undefined;
|
|
139
|
+
if (!threadId) {
|
|
140
|
+
return c.json({ error: "thread id missing from request context" }, 400);
|
|
141
|
+
}
|
|
142
|
+
const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY) as
|
|
143
|
+
| string
|
|
144
|
+
| undefined;
|
|
145
|
+
const payload = await loadHistory({
|
|
146
|
+
agent,
|
|
147
|
+
threadId,
|
|
148
|
+
...(resourceId ? { resourceId } : {}),
|
|
149
|
+
page: parseIntParam(c.req.query("page")),
|
|
150
|
+
perPage: parseIntParam(c.req.query("perPage")),
|
|
151
|
+
});
|
|
152
|
+
return c.json(payload);
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
158
|
+
function clampPerPage(value: number | undefined): number {
|
|
159
|
+
if (value === undefined || Number.isNaN(value)) return DEFAULT_PER_PAGE;
|
|
160
|
+
const n = Math.trunc(value);
|
|
161
|
+
if (n <= 0) return DEFAULT_PER_PAGE;
|
|
162
|
+
return Math.min(n, MAX_PER_PAGE);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Sort messages oldest-first by `createdAt`, falling back to whatever
|
|
167
|
+
* order the storage returned them in. The native `recall` call honors
|
|
168
|
+
* `orderBy` but doesn't guarantee a stable secondary sort, so we
|
|
169
|
+
* normalize here before handing the page to the AI SDK converter.
|
|
170
|
+
*/
|
|
171
|
+
function sortChronological(messages: MastraDBMessage[]): MastraDBMessage[] {
|
|
172
|
+
return [...messages].sort((a, b) => {
|
|
173
|
+
const ta = toEpoch(a.createdAt);
|
|
174
|
+
const tb = toEpoch(b.createdAt);
|
|
175
|
+
return ta - tb;
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function toEpoch(value: unknown): number {
|
|
180
|
+
if (value instanceof Date) return value.getTime();
|
|
181
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
182
|
+
const parsed = new Date(value).getTime();
|
|
183
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
184
|
+
}
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Coerce a Hono query value into a non-negative integer. Returns
|
|
190
|
+
* `undefined` for empty / non-numeric / negative inputs so
|
|
191
|
+
* {@link loadHistory} can apply its built-in defaults.
|
|
192
|
+
*/
|
|
193
|
+
function parseIntParam(value: string | undefined): number | undefined {
|
|
194
|
+
if (!value) return undefined;
|
|
195
|
+
const n = Number(value);
|
|
196
|
+
if (!Number.isFinite(n) || n < 0) return undefined;
|
|
197
|
+
return Math.trunc(n);
|
|
198
|
+
}
|
package/src/plugin.ts
CHANGED
|
@@ -46,6 +46,8 @@ 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";
|
|
50
|
+
import { renderChartRoute } from "./render-chart-route.js";
|
|
49
51
|
import { createMemoryBuilder, needsLakebase } from "./memory.js";
|
|
50
52
|
import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
|
|
51
53
|
import {
|
|
@@ -187,6 +189,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
187
189
|
chatPath: `${basePath}/route/chat`,
|
|
188
190
|
chatPathTemplate: `${basePath}/route/chat/:agentId`,
|
|
189
191
|
modelsPath: `${basePath}/models`,
|
|
192
|
+
historyPath: `${basePath}/route/history`,
|
|
193
|
+
historyPathTemplate: `${basePath}/route/history/:agentId`,
|
|
194
|
+
renderChartPath: `${basePath}/route/render-chart`,
|
|
190
195
|
defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
|
|
191
196
|
agents: Object.keys(this.built?.agents ?? {}),
|
|
192
197
|
};
|
|
@@ -200,7 +205,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
200
205
|
// the Mastra subapp. Errors propagate to Express's default error
|
|
201
206
|
// handler via `next(err)` so callers see the real SDK message.
|
|
202
207
|
router.get("/models", (req, res, next) => {
|
|
203
|
-
this.
|
|
208
|
+
this.userScopedSelf(req)
|
|
204
209
|
.listModels()
|
|
205
210
|
.then((endpoints) => res.json({ endpoints }))
|
|
206
211
|
.catch(next);
|
|
@@ -208,10 +213,23 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
208
213
|
|
|
209
214
|
router.use("", (req, res, next) => {
|
|
210
215
|
if (!this.mastraApp) return res.status(503).end();
|
|
211
|
-
return this.
|
|
216
|
+
return this.userScopedSelf(req).mastraApp!(req, res, next);
|
|
212
217
|
});
|
|
213
218
|
}
|
|
214
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Return `this.asUser(req)` when the request carries an OBO token,
|
|
222
|
+
* otherwise return `this` directly. Prevents the noisy AppKit warn
|
|
223
|
+
* (`asUser() called without user token in development mode. Skipping
|
|
224
|
+
* user impersonation.`) on every request in local dev where the
|
|
225
|
+
* browser never sends `x-forwarded-access-token`. Behavior is
|
|
226
|
+
* unchanged in production: a missing token always means a real OBO
|
|
227
|
+
* proxy call (and AppKit will throw upstream if that's wrong).
|
|
228
|
+
*/
|
|
229
|
+
private userScopedSelf(req: express.Request): this {
|
|
230
|
+
return req.header("x-forwarded-access-token") ? (this.asUser(req) as this) : this;
|
|
231
|
+
}
|
|
232
|
+
|
|
215
233
|
/**
|
|
216
234
|
* Implementation backing both the `/models` route and the
|
|
217
235
|
* `listModels` export. Runs inside the AppKit user-context proxy so
|
|
@@ -260,6 +278,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
260
278
|
customApiRoutes: [
|
|
261
279
|
chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
|
|
262
280
|
chatRoute({ path: "/route/chat/:agentId" }),
|
|
281
|
+
historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
|
|
282
|
+
historyRoute({ path: "/route/history/:agentId" }),
|
|
283
|
+
renderChartRoute({ path: "/route/render-chart", config: this.config }),
|
|
263
284
|
],
|
|
264
285
|
});
|
|
265
286
|
await this.mastraServer.init();
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart-render HTTP endpoint for the Mastra plugin.
|
|
3
|
+
*
|
|
4
|
+
* The `render_data` tool returns immediately with a `chartId` and
|
|
5
|
+
* emits the dataset over `ctx.writer`; the client then POSTs that
|
|
6
|
+
* dataset to this endpoint to actually run the chart-planner
|
|
7
|
+
* agent and get back an Echarts `EChartsOption` JSON. Planning
|
|
8
|
+
* happens out-of-band so the calling agent's response stream
|
|
9
|
+
* doesn't sit idle waiting for it - the model can finish the
|
|
10
|
+
* report while the client is still rendering charts.
|
|
11
|
+
*
|
|
12
|
+
* Auth flows through the standard Mastra middleware: the route
|
|
13
|
+
* sits in the same dispatcher pipeline as `chatRoute` /
|
|
14
|
+
* `historyRoute`, so by the time the handler runs the
|
|
15
|
+
* `RequestContext` is populated with the workspace user and
|
|
16
|
+
* the chart-planner's model resolver has the OBO token it
|
|
17
|
+
* needs.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type {
|
|
21
|
+
RenderChartRequest,
|
|
22
|
+
RenderChartResponse,
|
|
23
|
+
} from "@dbx-tools/appkit-mastra-shared";
|
|
24
|
+
import { registerApiRoute } from "@mastra/core/server";
|
|
25
|
+
|
|
26
|
+
import { runChartPlanner } from "./chart.js";
|
|
27
|
+
import type { MastraPluginConfig } from "./config.js";
|
|
28
|
+
|
|
29
|
+
/** Hard cap so a misbehaving client can't hand us a million-row payload. */
|
|
30
|
+
const MAX_ROWS = 5_000;
|
|
31
|
+
/**
|
|
32
|
+
* Hard cap on the JSON body the route accepts (in bytes). Mirrors
|
|
33
|
+
* the same intent as {@link MAX_ROWS}: bound the chart-planner's
|
|
34
|
+
* prompt size and protect against accidental denial-of-service
|
|
35
|
+
* from a runaway tool that ships an enormous payload.
|
|
36
|
+
*/
|
|
37
|
+
const MAX_BODY_BYTES = 2 * 1024 * 1024;
|
|
38
|
+
|
|
39
|
+
/** Options accepted by {@link renderChartRoute}. */
|
|
40
|
+
export interface RenderChartRouteOptions {
|
|
41
|
+
path: string;
|
|
42
|
+
config: MastraPluginConfig;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Register a `POST <path>` Mastra custom API route that runs the
|
|
47
|
+
* chart-planner agent against a dataset and returns an Echarts
|
|
48
|
+
* `EChartsOption` JSON.
|
|
49
|
+
*
|
|
50
|
+
* Body shape: {@link RenderChartRequest}; response:
|
|
51
|
+
* {@link RenderChartResponse}.
|
|
52
|
+
*/
|
|
53
|
+
export function renderChartRoute(options: RenderChartRouteOptions) {
|
|
54
|
+
const { path, config } = options;
|
|
55
|
+
return registerApiRoute(path, {
|
|
56
|
+
method: "POST",
|
|
57
|
+
handler: async (c) => {
|
|
58
|
+
const requestContext = c.get("requestContext");
|
|
59
|
+
|
|
60
|
+
// Hono parses the body as JSON; we still validate shape /
|
|
61
|
+
// size since the tool's structured output is a contract,
|
|
62
|
+
// not a guarantee, and the route is publicly mountable.
|
|
63
|
+
const raw = (await c.req.json().catch(() => null)) as unknown;
|
|
64
|
+
const validation = validateBody(raw);
|
|
65
|
+
if ("error" in validation) {
|
|
66
|
+
return c.json({ error: validation.error }, 400);
|
|
67
|
+
}
|
|
68
|
+
const { title, description, data } = validation.body;
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const result = await runChartPlanner({
|
|
72
|
+
config,
|
|
73
|
+
...(requestContext ? { requestContext } : {}),
|
|
74
|
+
title,
|
|
75
|
+
...(description ? { description } : {}),
|
|
76
|
+
data,
|
|
77
|
+
});
|
|
78
|
+
const payload: RenderChartResponse = {
|
|
79
|
+
option: result.option,
|
|
80
|
+
chartType: result.chartType,
|
|
81
|
+
};
|
|
82
|
+
return c.json(payload);
|
|
83
|
+
} catch (err) {
|
|
84
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
85
|
+
return c.json({ error: message }, 500);
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
type ValidationResult =
|
|
92
|
+
| { body: RenderChartRequest }
|
|
93
|
+
| { error: string };
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Best-effort body validation. Surfaces a 400 for malformed input
|
|
97
|
+
* instead of letting a downstream `.map` / `.length` blow up
|
|
98
|
+
* inside the planner agent. Field-level shape mirrors
|
|
99
|
+
* {@link RenderChartRequest}.
|
|
100
|
+
*/
|
|
101
|
+
function validateBody(raw: unknown): ValidationResult {
|
|
102
|
+
if (!raw || typeof raw !== "object") {
|
|
103
|
+
return { error: "request body must be a JSON object" };
|
|
104
|
+
}
|
|
105
|
+
const r = raw as Record<string, unknown>;
|
|
106
|
+
const title = r.title;
|
|
107
|
+
if (typeof title !== "string" || title.length === 0) {
|
|
108
|
+
return { error: "`title` must be a non-empty string" };
|
|
109
|
+
}
|
|
110
|
+
if (r.description !== undefined && typeof r.description !== "string") {
|
|
111
|
+
return { error: "`description` must be a string when provided" };
|
|
112
|
+
}
|
|
113
|
+
if (!Array.isArray(r.data)) {
|
|
114
|
+
return { error: "`data` must be an array of row objects" };
|
|
115
|
+
}
|
|
116
|
+
if (r.data.length === 0) {
|
|
117
|
+
return { error: "`data` must contain at least one row" };
|
|
118
|
+
}
|
|
119
|
+
if (r.data.length > MAX_ROWS) {
|
|
120
|
+
return { error: `\`data\` exceeds the per-request limit of ${MAX_ROWS} rows` };
|
|
121
|
+
}
|
|
122
|
+
for (const [i, row] of r.data.entries()) {
|
|
123
|
+
if (!row || typeof row !== "object" || Array.isArray(row)) {
|
|
124
|
+
return { error: `data[${i}] must be a plain object` };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
// Approximate body-size check; spares us pulling Buffer in.
|
|
128
|
+
const approximateBytes = JSON.stringify(r.data).length;
|
|
129
|
+
if (approximateBytes > MAX_BODY_BYTES) {
|
|
130
|
+
return {
|
|
131
|
+
error: `\`data\` exceeds the per-request size limit of ${MAX_BODY_BYTES} bytes`,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
body: {
|
|
136
|
+
title,
|
|
137
|
+
...(typeof r.description === "string" ? { description: r.description } : {}),
|
|
138
|
+
data: r.data as Array<Record<string, unknown>>,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -42,61 +42,75 @@ 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
|
-
requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
|
|
58
|
-
}
|
|
59
|
-
const cookies = httpUtils.parseCookies(req.headers.cookie);
|
|
60
|
-
const cookieName = stringUtils.toIdentifierWithOptions(
|
|
61
|
-
{ delimiter: "_", distinct: true },
|
|
62
|
-
"appkit",
|
|
63
|
-
this.config.name!,
|
|
64
|
-
"sessionId",
|
|
65
|
-
);
|
|
66
|
-
let sessionId = cookies[cookieName];
|
|
67
|
-
if (!sessionId) {
|
|
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);
|
|
97
49
|
next();
|
|
98
50
|
});
|
|
99
51
|
}
|
|
52
|
+
|
|
53
|
+
configureRequestContextUser(requestContext: RequestContext) {
|
|
54
|
+
if (
|
|
55
|
+
[MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))
|
|
56
|
+
)
|
|
57
|
+
return;
|
|
58
|
+
const executionContext = getExecutionContext();
|
|
59
|
+
const user: User = {
|
|
60
|
+
id:
|
|
61
|
+
"userId" in executionContext
|
|
62
|
+
? executionContext.userId
|
|
63
|
+
: executionContext.serviceUserId,
|
|
64
|
+
executionContext,
|
|
65
|
+
};
|
|
66
|
+
requestContext.set(MASTRA_USER_KEY, user);
|
|
67
|
+
requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
configureRequestContextThreadId(
|
|
71
|
+
req: express.Request,
|
|
72
|
+
res: express.Response,
|
|
73
|
+
requestContext: RequestContext,
|
|
74
|
+
) {
|
|
75
|
+
if (requestContext.get(MASTRA_THREAD_ID_KEY)) return;
|
|
76
|
+
const cookies = httpUtils.parseCookies(req.headers.cookie);
|
|
77
|
+
const cookieName = stringUtils.toIdentifierWithOptions(
|
|
78
|
+
{ delimiter: "_", distinct: true },
|
|
79
|
+
"appkit",
|
|
80
|
+
this.config.name!,
|
|
81
|
+
"sessionId",
|
|
82
|
+
);
|
|
83
|
+
let sessionId = cookies[cookieName];
|
|
84
|
+
if (!sessionId) {
|
|
85
|
+
sessionId = randomUUID();
|
|
86
|
+
res.cookie(cookieName, sessionId, {
|
|
87
|
+
httpOnly: true,
|
|
88
|
+
sameSite: "lax",
|
|
89
|
+
secure: req.secure,
|
|
90
|
+
path: "/",
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
configureRequestContextModelOverride(
|
|
97
|
+
req: express.Request,
|
|
98
|
+
requestContext: RequestContext,
|
|
99
|
+
) {
|
|
100
|
+
// Per-request model override: only honored when the plugin
|
|
101
|
+
// opts in (default). Sources, in priority order, are
|
|
102
|
+
// `X-Mastra-Model` header, `?model=` query, and `model` /
|
|
103
|
+
// `modelId` body field; see `serving.ts`.
|
|
104
|
+
const serving = resolveServingConfig(this.config);
|
|
105
|
+
if (serving.allowOverride) {
|
|
106
|
+
const override = extractModelOverride({
|
|
107
|
+
headers: req.headers as Record<string, string | string[] | undefined>,
|
|
108
|
+
query: req.query as Record<string, unknown>,
|
|
109
|
+
body: req.body,
|
|
110
|
+
});
|
|
111
|
+
if (override) requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
100
114
|
}
|
|
101
115
|
|
|
102
116
|
/**
|