@dbx-tools/appkit-mastra 0.1.13 → 0.1.14

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.
Files changed (59) hide show
  1. package/README.md +304 -645
  2. package/index.ts +46 -38
  3. package/package.json +58 -45
  4. package/src/agents.ts +224 -66
  5. package/src/chart.ts +531 -429
  6. package/src/config.ts +270 -19
  7. package/src/filesystems.ts +1090 -0
  8. package/src/genie.ts +1000 -660
  9. package/src/history.ts +166 -79
  10. package/src/mcp.ts +105 -0
  11. package/src/memory.ts +94 -92
  12. package/src/mlflow.ts +149 -0
  13. package/src/model.ts +75 -408
  14. package/src/observability.ts +121 -69
  15. package/src/pagination.ts +34 -0
  16. package/src/plugin.ts +552 -67
  17. package/src/processors.ts +168 -0
  18. package/src/rest.ts +67 -0
  19. package/src/server.ts +232 -45
  20. package/src/serving-sanitize.ts +167 -0
  21. package/src/serving.ts +27 -243
  22. package/src/statement.ts +89 -0
  23. package/src/storage-schema.ts +41 -0
  24. package/src/summarize.ts +176 -0
  25. package/src/threads.ts +338 -0
  26. package/src/workspaces.ts +346 -0
  27. package/src/writer.ts +44 -0
  28. package/tsconfig.json +41 -0
  29. package/dist/index.d.ts +0 -20
  30. package/dist/index.js +0 -20
  31. package/dist/src/agents.d.ts +0 -306
  32. package/dist/src/agents.js +0 -403
  33. package/dist/src/chart.d.ts +0 -170
  34. package/dist/src/chart.js +0 -491
  35. package/dist/src/config.d.ts +0 -183
  36. package/dist/src/config.js +0 -12
  37. package/dist/src/genie.d.ts +0 -131
  38. package/dist/src/genie.js +0 -630
  39. package/dist/src/history.d.ts +0 -67
  40. package/dist/src/history.js +0 -172
  41. package/dist/src/memory.d.ts +0 -100
  42. package/dist/src/memory.js +0 -242
  43. package/dist/src/model.d.ts +0 -159
  44. package/dist/src/model.js +0 -427
  45. package/dist/src/observability.d.ts +0 -33
  46. package/dist/src/observability.js +0 -71
  47. package/dist/src/plugin.d.ts +0 -130
  48. package/dist/src/plugin.js +0 -283
  49. package/dist/src/processors/strip-stale-charts.d.ts +0 -29
  50. package/dist/src/processors/strip-stale-charts.js +0 -96
  51. package/dist/src/server.d.ts +0 -46
  52. package/dist/src/server.js +0 -123
  53. package/dist/src/serving.d.ts +0 -156
  54. package/dist/src/serving.js +0 -231
  55. package/dist/src/tools/email.d.ts +0 -74
  56. package/dist/src/tools/email.js +0 -122
  57. package/dist/tsconfig.build.tsbuildinfo +0 -1
  58. package/src/processors/strip-stale-charts.ts +0 -105
  59. package/src/tools/email.ts +0 -147
@@ -1,283 +0,0 @@
1
- /**
2
- * AppKit plugin that builds one or more Mastra `Agent` instances and
3
- * mounts the `@mastra/express` server plus `@mastra/ai-sdk` `chatRoute`
4
- * handlers. The UI message stream matches what `chatRoute()` emits, so
5
- * the client can use `useChat()` from `@ai-sdk/react` without custom
6
- * parsing.
7
- *
8
- * - Agents: registered through `config.agents` at plugin creation
9
- * ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
10
- * either a plain record or a `(plugins) => tools` callback that gets
11
- * a typed sibling-plugin index ({@link MastraPlugins}). Omit
12
- * `config.agents` to get a single built-in `default` analyst.
13
- * - Model: each agent call resolves a `MastraModelConfig` via
14
- * {@link buildModel} from `./model.js`. Per-agent `model` overrides
15
- * (`AgentConfig["model"]` or a `modelId` string) flow through
16
- * {@link buildAgents}.
17
- * - Memory / storage: per-agent, built by {@link createMemoryBuilder}
18
- * from `./memory.js`. Both auto-default to `true` when the
19
- * `lakebase` plugin is registered (unless the caller passed
20
- * `false` or a custom config). Storage namespaces per agent via
21
- * `schemaName: "mastra_<agentId>"`; the vector store is a single
22
- * shared singleton across every agent.
23
- * - Server: the Express subapp wiring lives in `./server.js`.
24
- * - HTTP: AppKit mounts this plugin under `/api/mastra`. `chatRoute`
25
- * is registered at `/route/chat` (bound to `config.defaultAgent` or
26
- * the first registered id) and `/route/chat/:agentId`, so the
27
- * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
28
- */
29
- import { genie, getExecutionContext, lakebase, Plugin, toPlugin, } from "@databricks/appkit";
30
- import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
31
- import { chatRoute } from "@mastra/ai-sdk";
32
- import { Mastra } from "@mastra/core/mastra";
33
- import express from "express";
34
- import { buildAgents, FALLBACK_AGENT_ID } from "./agents.js";
35
- import { historyRoute } from "./history.js";
36
- import { createMemoryBuilder, needsLakebase } from "./memory.js";
37
- import { buildPhoenixObservability } from "./observability.js";
38
- import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
39
- import { clearServingEndpointsCache, listServingEndpoints, resolveServingConfig, } from "./serving.js";
40
- const GENIE_MANIFEST = pluginUtils.data(genie).plugin.manifest;
41
- const LAKEBASE_MANIFEST = pluginUtils.data(lakebase).plugin.manifest;
42
- /**
43
- * AppKit plugin (registered name: `mastra`) that hosts Mastra agents
44
- * with optional Lakebase-backed memory and AI SDK chat routes under
45
- * the plugin mount (typically `/api/mastra`).
46
- */
47
- export class MastraPlugin extends Plugin {
48
- static manifest = {
49
- name: "mastra",
50
- displayName: "Mastra",
51
- description: "Builds a Mastra Agent with user-scoped workspace auth (asUser) " +
52
- "and optional Postgres-backed Mastra Memory via the `lakebase` plugin.",
53
- stability: "beta",
54
- resources: {
55
- required: [],
56
- optional: [
57
- ...GENIE_MANIFEST.resources.required,
58
- ...LAKEBASE_MANIFEST.resources.required,
59
- ],
60
- },
61
- };
62
- /**
63
- * Tighten resource requirements based on which features are enabled.
64
- * AppKit calls this at registration time (config-aware) so disabled
65
- * features don't surface their resource asks to the host app.
66
- */
67
- static getResourceRequirements(config) {
68
- const resources = [];
69
- const enabledManifests = [];
70
- if (needsLakebase(config)) {
71
- enabledManifests.push(LAKEBASE_MANIFEST);
72
- }
73
- for (const m of enabledManifests) {
74
- for (const resource of m.resources.required) {
75
- resources.push({ ...resource, required: true });
76
- }
77
- }
78
- return resources;
79
- }
80
- log = logUtils.logger(this);
81
- built = null;
82
- mastra = null;
83
- mastraApp = null;
84
- mastraServer = null;
85
- async setup() {
86
- // Wait until sibling plugins (e.g. `lakebase`) finish `setup()` so
87
- // the lakebase pool is valid when storage/memory are enabled.
88
- this.context?.onLifecycle("setup:complete", async () => {
89
- this.applyLakebaseAutoDefaults();
90
- this.log.info("setup:complete");
91
- await this.buildAgentAndServer();
92
- });
93
- }
94
- /**
95
- * When the `lakebase` plugin is registered, auto-enable `storage`
96
- * and `memory` unless the caller opted out explicitly (`false` or a
97
- * custom config object). Run after `setup:complete` so the lookup
98
- * is reliable: any plugin that registers itself synchronously is
99
- * already in the registry by the time this fires.
100
- */
101
- applyLakebaseAutoDefaults() {
102
- const hasLakebase = pluginUtils.instance(this.context, lakebase) !== undefined;
103
- if (!hasLakebase)
104
- return;
105
- if (this.config.storage === undefined)
106
- this.config.storage = true;
107
- if (this.config.memory === undefined)
108
- this.config.memory = true;
109
- }
110
- exports() {
111
- return {
112
- /**
113
- * Ids of every registered agent in registration order. Matches
114
- * AppKit `agents.list()` so callers can iterate the registry the
115
- * same way under both plugins.
116
- */
117
- list: () => Object.keys(this.built?.agents ?? {}),
118
- /**
119
- * Look up a registered agent by id. Returns `null` (not
120
- * undefined) when unknown so call sites can early-return without
121
- * a separate `in` check.
122
- */
123
- get: (id) => this.built?.agents[id] ?? null,
124
- /**
125
- * The agent `chatRoute` binds to when the client doesn't name
126
- * one. Resolves to `config.defaultAgent`, the first registered
127
- * id, or the built-in `default` fallback.
128
- */
129
- getDefault: () => (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
130
- /** Underlying Mastra instance for advanced use (custom routes etc.). */
131
- getMastra: () => this.mastra,
132
- /** Express subapp Mastra is mounted on; mostly for tests. */
133
- getMastraServer: () => this.mastraServer,
134
- /**
135
- * Fetch the workspace's Model Serving endpoints (cached). Same
136
- * payload the `GET /models` route returns; surfaced here so
137
- * other plugins / scripts can introspect the catalogue without
138
- * an HTTP round-trip. AppKit wraps this with `asUser(req)` for
139
- * OBO scoping automatically.
140
- */
141
- listModels: () => this.listModels(),
142
- /**
143
- * Force-evict cached endpoint listings via AppKit's
144
- * `CacheManager`. Useful in tests or right after an admin
145
- * deploys a new endpoint and doesn't want to wait for the TTL.
146
- * Returns the underlying `CacheManager.delete`/`clear` promise.
147
- */
148
- clearModelsCache: (host) => clearServingEndpointsCache(host),
149
- };
150
- }
151
- clientConfig() {
152
- // AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
153
- // honors `config.name` overrides, so the published paths stay
154
- // accurate if someone remounts the plugin under a custom id.
155
- // Return widens to `Record<string, unknown>` to satisfy the
156
- // base-class signature; consumers read it through the typed
157
- // `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
158
- const basePath = `/api/${this.name}`;
159
- const config = {
160
- basePath,
161
- chatPath: `${basePath}/route/chat`,
162
- chatPathTemplate: `${basePath}/route/chat/:agentId`,
163
- modelsPath: `${basePath}/models`,
164
- historyPath: `${basePath}/route/history`,
165
- historyPathTemplate: `${basePath}/route/history/:agentId`,
166
- defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
167
- agents: Object.keys(this.built?.agents ?? {}),
168
- };
169
- return config;
170
- }
171
- injectRoutes(router) {
172
- // `GET /models` exposes the cached endpoint list so clients can
173
- // populate model pickers, validate `?model=` choices, etc. Must
174
- // be registered before the catch-all that forwards everything to
175
- // the Mastra subapp. Errors propagate to Express's default error
176
- // handler via `next(err)` so callers see the real SDK message.
177
- router.get("/models", (req, res, next) => {
178
- this.userScopedSelf(req)
179
- .listModels()
180
- .then((endpoints) => res.json({ endpoints }))
181
- .catch(next);
182
- });
183
- router.use("", (req, res, next) => {
184
- if (!this.mastraApp)
185
- return res.status(503).end();
186
- return this.userScopedSelf(req).mastraApp(req, res, next);
187
- });
188
- }
189
- /**
190
- * Return `this.asUser(req)` when the request carries an OBO token,
191
- * otherwise return `this` directly. Prevents the noisy AppKit warn
192
- * (`asUser() called without user token in development mode. Skipping
193
- * user impersonation.`) on every request in local dev where the
194
- * browser never sends `x-forwarded-access-token`. Behavior is
195
- * unchanged in production: a missing token always means a real OBO
196
- * proxy call (and AppKit will throw upstream if that's wrong).
197
- */
198
- userScopedSelf(req) {
199
- return req.header("x-forwarded-access-token") ? this.asUser(req) : this;
200
- }
201
- /**
202
- * Implementation backing both the `/models` route and the
203
- * `listModels` export. Runs inside the AppKit user-context proxy so
204
- * `getExecutionContext()` returns the OBO-scoped client.
205
- */
206
- async listModels() {
207
- const client = getExecutionContext().client;
208
- const host = (await client.config.getHost()).toString();
209
- const serving = resolveServingConfig(this.config);
210
- return listServingEndpoints(client, host, { ttlMs: serving.ttlMs });
211
- }
212
- async buildAgentAndServer() {
213
- // Per-agent memory factory. The builder resolves the Lakebase pool
214
- // lazily (on first agent that actually needs storage / vector) and
215
- // caches both the pool and the shared `PgVector` singleton so
216
- // registering N agents stays cheap. See `./memory.js`.
217
- const memoryBuilder = needsLakebase(this.config)
218
- ? createMemoryBuilder(this.config, this.context)
219
- : undefined;
220
- this.log.debug("build:start", {
221
- lakebase: memoryBuilder !== undefined,
222
- stripStaleCharts: this.config.stripStaleCharts !== false,
223
- });
224
- // Build every agent declared in `config.agents` (or the built-in
225
- // fallback when none are declared). Each agent's `model` resolves
226
- // workspace URL + bearer at call time so concurrent requests get
227
- // distinct user identities; the `asUser(req)` scope around
228
- // `handleChat` is what lets `getExecutionContext()` return the
229
- // right user inside the resolver.
230
- this.built = await buildAgents({
231
- config: this.config,
232
- context: this.context,
233
- memoryBuilder,
234
- log: this.log,
235
- });
236
- // `mastra.server.apiRoutes` is only honored by Mastra's standalone
237
- // dev server. Since we're hosting Mastra inside our own Express
238
- // subapp via `@mastra/express`, custom routes must be passed to
239
- // the `MastraServer` constructor directly.
240
- //
241
- // `storage` here is *Mastra-instance-level* and persists workflow
242
- // snapshots (where suspended `requireApproval` tool calls live).
243
- // It's separate from each agent's `Memory.storage`, which only
244
- // covers thread / message history. Without it,
245
- // `agent.resumeStream()` errors with "could not find a suspended
246
- // run" and the approval UI hangs after the user clicks Approve.
247
- const instanceStorage = memoryBuilder?.instanceStorage();
248
- // Auto-wire OTLP trace export to the sibling `phoenix` plugin if
249
- // it's registered. Returns undefined when phoenix isn't around so
250
- // the field stays off the constructor and Mastra keeps its noop
251
- // observability default. The serviceName is the plugin's bound
252
- // name so multiple mastra instances in one process stay
253
- // distinguishable in Phoenix.
254
- const observability = buildPhoenixObservability(this.context, this.name);
255
- this.mastra = new Mastra({
256
- agents: this.built.agents,
257
- ...(instanceStorage ? { storage: instanceStorage } : {}),
258
- ...(observability ? { observability } : {}),
259
- });
260
- this.mastraApp = express();
261
- attachRoutePatchMiddleware(this.mastraApp);
262
- this.mastraServer = new MastraServer(this.config, {
263
- app: this.mastraApp,
264
- mastra: this.mastra,
265
- prefix: "",
266
- customApiRoutes: [
267
- chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
268
- chatRoute({ path: "/route/chat/:agentId" }),
269
- historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
270
- historyRoute({ path: "/route/history/:agentId" }),
271
- ],
272
- });
273
- await this.mastraServer.init();
274
- this.log.debug("build:done", {
275
- agents: Object.keys(this.built.agents),
276
- defaultAgent: this.built.defaultAgentId,
277
- routes: ["/route/chat", "/route/history", "/models"],
278
- instanceStorage: instanceStorage !== undefined,
279
- observability: observability !== undefined ? "phoenix" : "off",
280
- });
281
- }
282
- }
283
- export const mastra = toPlugin(MastraPlugin);
@@ -1,29 +0,0 @@
1
- /**
2
- * Mastra input processor that strips `chartId` fields from every
3
- * tool-invocation result in prior assistant messages before they
4
- * reach the model.
5
- *
6
- * Why: chartIds are 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;
@@ -1,96 +0,0 @@
1
- /**
2
- * Mastra input processor that strips `chartId` fields from every
3
- * tool-invocation result in prior assistant messages before they
4
- * reach the model.
5
- *
6
- * Why: chartIds are 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
- };
@@ -1,46 +0,0 @@
1
- /**
2
- * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
- * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
- * point.
6
- */
7
- import { type RequestContext } from "@mastra/core/request-context";
8
- import { MastraServer as MastraServerExpress } from "@mastra/express";
9
- import type express from "express";
10
- import { type MastraPluginConfig } from "./config.js";
11
- /**
12
- * `@mastra/express` subclass that stamps `RequestContext` with the
13
- * AppKit user, resource id, and a thread id backed by an HTTP-only
14
- * session cookie (`appkit_<plugin-name>_session_id`).
15
- */
16
- export declare class MastraServer extends MastraServerExpress {
17
- private config;
18
- private log;
19
- constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServerExpress>);
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;
24
- }
25
- /**
26
- * Patches around `@mastra/express`'s custom-route dispatcher so
27
- * `chatRoute` works when `MastraServer` is hosted on an Express subapp
28
- * mounted under a parent path (e.g. `/api/mastra`).
29
- *
30
- * Two concerns:
31
- *
32
- * 1. The adapter's `registerCustomApiRoutes` matches against `req.path`
33
- * (mount-relative, correct) but dispatches to its internal Hono
34
- * mini-app using `req.originalUrl`, which still contains the parent
35
- * mount prefix. The Hono app registers the literal `chatRoute` paths
36
- * (for example `/route/chat`), so the absolute URL never matches
37
- * until we overwrite `originalUrl` for `/route` and `/route/*` to
38
- * the mount-relative path.
39
- *
40
- * 2. `memory.resource` must be the authenticated user, not whatever the
41
- * client posts. The custom-route forwarder re-serializes `req.body`
42
- * into the Request body it hands Hono, so mutating the parsed body
43
- * here would propagate into `handleChatStream`'s params (kept for
44
- * future use; `express.json()` runs first so `req.body` is parsed).
45
- */
46
- export declare function attachRoutePatchMiddleware(app: express.Express): void;
@@ -1,123 +0,0 @@
1
- /**
2
- * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
- * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
- * point.
6
- */
7
- import { getExecutionContext } from "@databricks/appkit";
8
- import { httpUtils, logUtils, stringUtils } from "@dbx-tools/appkit-shared";
9
- import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
10
- import { MastraServer as MastraServerExpress } from "@mastra/express";
11
- import { randomUUID } from "node:crypto";
12
- import { MASTRA_USER_KEY } from "./config.js";
13
- import { extractModelOverride, MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig, } from "./serving.js";
14
- /**
15
- * `@mastra/express` subclass that stamps `RequestContext` with the
16
- * AppKit user, resource id, and a thread id backed by an HTTP-only
17
- * session cookie (`appkit_<plugin-name>_session_id`).
18
- */
19
- export class MastraServer extends MastraServerExpress {
20
- config;
21
- log;
22
- constructor(config, ...args) {
23
- super(...args);
24
- this.config = config;
25
- this.log = logUtils.logger(config);
26
- }
27
- registerAuthMiddleware() {
28
- super.registerAuthMiddleware();
29
- this.app.use((req, res, next) => {
30
- const requestContext = res.locals.requestContext;
31
- this.configureRequestContextUser(requestContext);
32
- this.configureRequestContextThreadId(req, res, requestContext);
33
- this.configureRequestContextModelOverride(req, requestContext);
34
- this.log.debug("auth:middleware", {
35
- method: req.method,
36
- path: req.path,
37
- threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
38
- resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
39
- modelOverride: requestContext.get(
40
- // imported below; logged so a misrouted request shows
41
- // up alongside its model selection in `LOG_LEVEL=debug`.
42
- "mastra__model_override"),
43
- });
44
- next();
45
- });
46
- }
47
- configureRequestContextUser(requestContext) {
48
- if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key)))
49
- return;
50
- const executionContext = getExecutionContext();
51
- const user = {
52
- id: "userId" in executionContext
53
- ? executionContext.userId
54
- : executionContext.serviceUserId,
55
- executionContext,
56
- };
57
- requestContext.set(MASTRA_USER_KEY, user);
58
- requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
59
- }
60
- configureRequestContextThreadId(req, res, requestContext) {
61
- if (requestContext.get(MASTRA_THREAD_ID_KEY))
62
- return;
63
- const cookies = httpUtils.parseCookies(req.headers.cookie);
64
- const cookieName = stringUtils.toIdentifierWithOptions({ delimiter: "_", distinct: true }, "appkit", this.config.name, "sessionId");
65
- let sessionId = cookies[cookieName];
66
- if (!sessionId) {
67
- sessionId = randomUUID();
68
- res.cookie(cookieName, sessionId, {
69
- httpOnly: true,
70
- sameSite: "lax",
71
- secure: req.secure,
72
- path: "/",
73
- });
74
- }
75
- requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
76
- }
77
- configureRequestContextModelOverride(req, requestContext) {
78
- // Per-request model override: only honored when the plugin
79
- // opts in (default). Sources, in priority order, are
80
- // `X-Mastra-Model` header, `?model=` query, and `model` /
81
- // `modelId` body field; see `serving.ts`.
82
- const serving = resolveServingConfig(this.config);
83
- if (serving.allowOverride) {
84
- const override = extractModelOverride({
85
- headers: req.headers,
86
- query: req.query,
87
- body: req.body,
88
- });
89
- if (override)
90
- requestContext.set(MASTRA_MODEL_OVERRIDE_KEY, override);
91
- }
92
- }
93
- }
94
- /**
95
- * Patches around `@mastra/express`'s custom-route dispatcher so
96
- * `chatRoute` works when `MastraServer` is hosted on an Express subapp
97
- * mounted under a parent path (e.g. `/api/mastra`).
98
- *
99
- * Two concerns:
100
- *
101
- * 1. The adapter's `registerCustomApiRoutes` matches against `req.path`
102
- * (mount-relative, correct) but dispatches to its internal Hono
103
- * mini-app using `req.originalUrl`, which still contains the parent
104
- * mount prefix. The Hono app registers the literal `chatRoute` paths
105
- * (for example `/route/chat`), so the absolute URL never matches
106
- * until we overwrite `originalUrl` for `/route` and `/route/*` to
107
- * the mount-relative path.
108
- *
109
- * 2. `memory.resource` must be the authenticated user, not whatever the
110
- * client posts. The custom-route forwarder re-serializes `req.body`
111
- * into the Request body it hands Hono, so mutating the parsed body
112
- * here would propagate into `handleChatStream`'s params (kept for
113
- * future use; `express.json()` runs first so `req.body` is parsed).
114
- */
115
- export function attachRoutePatchMiddleware(app) {
116
- app.use((req, _res, next) => {
117
- const isChat = req.path === "/route" || req.path.startsWith("/route/");
118
- if (!isChat)
119
- return next();
120
- req.originalUrl = req.path;
121
- next();
122
- });
123
- }