@dbx-tools/appkit-mastra 0.1.12 → 0.1.18

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/src/history.ts CHANGED
@@ -16,7 +16,7 @@
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
18
 
19
- import { logUtils } from "@dbx-tools/appkit-shared";
19
+ import { logUtils } from "@dbx-tools/shared";
20
20
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
21
21
  import type { Agent } from "@mastra/core/agent";
22
22
  import type { MastraDBMessage } from "@mastra/core/agent/message-list";
@@ -27,6 +27,7 @@ import {
27
27
  import { registerApiRoute } from "@mastra/core/server";
28
28
  import type { ContextWithMastra } from "@mastra/core/server";
29
29
  import type {
30
+ MastraClearHistoryResponse,
30
31
  MastraHistoryResponse,
31
32
  MastraHistoryUIMessage,
32
33
  } from "@dbx-tools/appkit-mastra-shared";
@@ -108,14 +109,93 @@ export async function loadHistory(
108
109
  };
109
110
  }
110
111
 
112
+ /** Inputs accepted by {@link clearHistory}. */
113
+ export interface ClearHistoryOptions {
114
+ agent: Agent;
115
+ threadId: string;
116
+ }
117
+
118
+ /**
119
+ * Wipe every persisted message tied to a thread. Returns the count
120
+ * of messages that were on the thread at delete time so the caller
121
+ * can render a "cleared N messages" affordance without an
122
+ * additional round-trip.
123
+ *
124
+ * Agents without a configured `Memory` resolve to a no-op (count
125
+ * 0), matching {@link loadHistory}'s "stateless agents return an
126
+ * empty page" stance so callers don't have to special-case them.
127
+ * Threads that don't exist yet are also a successful no-op - the
128
+ * operation is idempotent so the UI can fire-and-forget without
129
+ * tracking thread existence.
130
+ */
131
+ export async function clearHistory(
132
+ opts: ClearHistoryOptions,
133
+ ): Promise<{ cleared: number }> {
134
+ const memory = await opts.agent.getMemory();
135
+ if (!memory) {
136
+ log.debug("clear:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
137
+ return { cleared: 0 };
138
+ }
139
+ // Mastra's `deleteThread` cascades to the message table, so we
140
+ // can't ask for a count after the fact. Read it pre-delete with a
141
+ // one-page recall sized to fit common threads in a single round
142
+ // trip; the value is for telemetry / UI, not correctness.
143
+ let cleared = 0;
144
+ try {
145
+ const probe = await memory.recall({
146
+ threadId: opts.threadId,
147
+ page: 0,
148
+ perPage: 1,
149
+ });
150
+ cleared = probe.total;
151
+ } catch (err) {
152
+ // A missing-thread error is the happy-path "nothing to count";
153
+ // every other error is logged but doesn't block the delete.
154
+ log.debug("clear:probe-failed", {
155
+ agentId: opts.agent.id,
156
+ threadId: opts.threadId,
157
+ error: err instanceof Error ? err.message : String(err),
158
+ });
159
+ }
160
+
161
+ const startedAt = Date.now();
162
+ try {
163
+ await memory.deleteThread(opts.threadId);
164
+ } catch (err) {
165
+ // Mastra's `deleteThread` raises when the thread row was never
166
+ // created (e.g. clearing an empty session). Surface as a soft
167
+ // warn and treat as success - the user-facing semantic is
168
+ // "history is now empty" which is already true.
169
+ log.warn("clear:delete-soft-failed", {
170
+ agentId: opts.agent.id,
171
+ threadId: opts.threadId,
172
+ error: err instanceof Error ? err.message : String(err),
173
+ });
174
+ }
175
+ log.info("clear:done", {
176
+ agentId: opts.agent.id,
177
+ threadId: opts.threadId,
178
+ cleared,
179
+ elapsedMs: Date.now() - startedAt,
180
+ });
181
+ return { cleared };
182
+ }
183
+
111
184
  /** Options accepted by {@link historyRoute}. */
112
185
  export type HistoryRouteOptions =
113
186
  | { path: `${string}:agentId${string}`; agent?: never }
114
187
  | { path: string; agent: string };
115
188
 
116
189
  /**
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.
190
+ * Register the `<path>` Mastra custom API route. Handles two
191
+ * methods on the same mount:
192
+ *
193
+ * - `GET`: return a page of AI SDK V5 `UIMessage`s for the
194
+ * caller's current thread ({@link loadHistory}).
195
+ * - `DELETE`: wipe every persisted message on the caller's
196
+ * thread ({@link clearHistory}). The session cookie that
197
+ * anchors the thread id is left alone so the user keeps the
198
+ * same thread - only the contents go away.
119
199
  *
120
200
  * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
121
201
  * fixed-agent mount, or include `:agentId` in the path for dynamic
@@ -134,36 +214,70 @@ export function historyRoute(options: HistoryRouteOptions) {
134
214
  "historyRoute path must include `:agentId` or `agent` must be passed explicitly",
135
215
  );
136
216
  }
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
- });
217
+ // Tiny resolver shared by GET / DELETE: derive the active agent
218
+ // and thread id, returning a JSON error response when either is
219
+ // missing. Keeps both handlers thin and gives them identical
220
+ // validation behaviour.
221
+ const resolveContext = (c: ContextWithMastra) => {
222
+ const mastra = c.get("mastra");
223
+ const requestContext = c.get("requestContext");
224
+ const agentId = fixedAgent ?? c.req.param("agentId");
225
+ if (!agentId) {
226
+ return { error: c.json({ error: "agentId is required" }, 400) } as const;
227
+ }
228
+ const agent = mastra.getAgentById(agentId);
229
+ if (!agent) {
230
+ return {
231
+ error: c.json({ error: `Unknown agent "${agentId}"` }, 404),
232
+ } as const;
233
+ }
234
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY) as string | undefined;
235
+ if (!threadId) {
236
+ return {
237
+ error: c.json({ error: "thread id missing from request context" }, 400),
238
+ } as const;
239
+ }
240
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY) as
241
+ | string
242
+ | undefined;
243
+ return { agentId, agent, threadId, resourceId } as const;
244
+ };
245
+
246
+ return [
247
+ registerApiRoute(path, {
248
+ method: "GET",
249
+ handler: async (c: ContextWithMastra) => {
250
+ const ctx = resolveContext(c);
251
+ if ("error" in ctx) return ctx.error;
252
+ const payload = await loadHistory({
253
+ agent: ctx.agent,
254
+ threadId: ctx.threadId,
255
+ ...(ctx.resourceId ? { resourceId: ctx.resourceId } : {}),
256
+ page: parseIntParam(c.req.query("page")),
257
+ perPage: parseIntParam(c.req.query("perPage")),
258
+ });
259
+ return c.json(payload);
260
+ },
261
+ }),
262
+ registerApiRoute(path, {
263
+ method: "DELETE",
264
+ handler: async (c: ContextWithMastra) => {
265
+ const ctx = resolveContext(c);
266
+ if ("error" in ctx) return ctx.error;
267
+ const { cleared } = await clearHistory({
268
+ agent: ctx.agent,
269
+ threadId: ctx.threadId,
270
+ });
271
+ const payload: MastraClearHistoryResponse = {
272
+ ok: true,
273
+ agentId: ctx.agentId,
274
+ threadId: ctx.threadId,
275
+ cleared,
276
+ };
277
+ return c.json(payload);
278
+ },
279
+ }),
280
+ ];
167
281
  }
168
282
 
169
283
  /** Coerce / clamp `perPage`; falls back to the page-size default. */
package/src/memory.ts CHANGED
@@ -14,13 +14,21 @@
14
14
  * index is almost always what users want; opt into per-agent recall
15
15
  * by passing a {@link MastraMemoryConfigOverride} on the agent.
16
16
  *
17
+ * Additionally, {@link MemoryBuilder.instanceStorage} returns a
18
+ * **Mastra-instance-level** `PostgresStore` (schema `mastra_instance`)
19
+ * used for workflow snapshots - the persistence layer
20
+ * `agent.resumeStream()` reads from when waking a suspended
21
+ * `requireApproval` tool call. Per-agent stores are not enough for
22
+ * this: workflow runs are scoped to the Mastra instance, not an
23
+ * individual agent's `Memory`.
24
+ *
17
25
  * Plugin-level `config.storage` / `config.memory` act as the baseline
18
26
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
19
27
  * is registered); per-agent settings cascade on top of that.
20
28
  */
21
29
 
22
30
  import { lakebase } from "@databricks/appkit";
23
- import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
31
+ import { appkitUtils, logUtils } from "@dbx-tools/shared";
24
32
  import { fastembed } from "@mastra/fastembed";
25
33
  import { Memory } from "@mastra/memory";
26
34
  import { PgVector, PostgresStore } from "@mastra/pg";
@@ -67,10 +75,10 @@ export function needsLakebase(config: MastraPluginConfig): boolean {
67
75
  * condition we can recover from.
68
76
  */
69
77
  export function resolveLakebasePool(
70
- context: pluginUtils.PluginContextLike | undefined,
78
+ context: appkitUtils.PluginContextLike | undefined,
71
79
  caller: MastraPluginConfig,
72
80
  ): LakebasePool {
73
- return pluginUtils.require(context, lakebase, caller).exports().pool;
81
+ return appkitUtils.require(context, lakebase, caller).exports().pool;
74
82
  }
75
83
 
76
84
  /**
@@ -80,7 +88,7 @@ export function resolveLakebasePool(
80
88
  */
81
89
  export function createMemoryBuilder(
82
90
  config: MastraPluginConfig,
83
- context: pluginUtils.PluginContextLike | undefined,
91
+ context: appkitUtils.PluginContextLike | undefined,
84
92
  ): MemoryBuilder {
85
93
  return new MemoryBuilder(config, context);
86
94
  }
@@ -96,7 +104,7 @@ export class MemoryBuilder {
96
104
 
97
105
  constructor(
98
106
  private readonly config: MastraPluginConfig,
99
- private readonly context: pluginUtils.PluginContextLike | undefined,
107
+ private readonly context: appkitUtils.PluginContextLike | undefined,
100
108
  ) {}
101
109
 
102
110
  /**
@@ -105,6 +113,34 @@ export class MemoryBuilder {
105
113
  * vector store enabled - Mastra accepts a missing `memory` field
106
114
  * and treats the agent as stateless.
107
115
  */
116
+ /**
117
+ * Build the Mastra-instance-level storage used for workflow
118
+ * snapshots. Returns `undefined` when plugin-level `storage` is
119
+ * disabled, in which case `agent.resumeStream()` (and therefore
120
+ * the `requireApproval` flow) will not be available.
121
+ *
122
+ * The store lives in a dedicated `mastra_instance` schema so it
123
+ * never collides with per-agent `mastra_<agentId>` namespaces.
124
+ * Workflow snapshots are not per-agent state; they belong to the
125
+ * `Mastra` instance that owns the workflow execution.
126
+ */
127
+ instanceStorage(): PostgresStore | undefined {
128
+ const setting = this.config.storage;
129
+ if (!setting) return undefined;
130
+ if (typeof setting === "object") {
131
+ return new PostgresStore(
132
+ withId(setting, "mastra-store__instance") as ConstructorParameters<
133
+ typeof PostgresStore
134
+ >[0],
135
+ );
136
+ }
137
+ return new PostgresStore({
138
+ id: "mastra-store__instance",
139
+ schemaName: "mastra_instance",
140
+ pool: this.requirePool() as Pool,
141
+ });
142
+ }
143
+
108
144
  forAgent(agentId: string, def: MastraAgentDefinition): Memory | undefined {
109
145
  const storageSetting = def.storage ?? this.config.storage;
110
146
  const memorySetting = def.memory ?? this.config.memory;
package/src/model.ts CHANGED
@@ -27,10 +27,10 @@
27
27
 
28
28
  import {
29
29
  commonUtils,
30
- httpUtils,
31
30
  logUtils,
31
+ netUtils,
32
32
  stringUtils,
33
- } from "@dbx-tools/appkit-shared";
33
+ } from "@dbx-tools/shared";
34
34
  import type { MastraModelConfig } from "@mastra/core/llm";
35
35
  import type { RequestContext } from "@mastra/core/request-context";
36
36
 
@@ -390,7 +390,7 @@ const setupFetchInterceptor = commonUtils.memoize((): void => {
390
390
  const log = logUtils.logger("mastra/llm");
391
391
  const original = globalThis.fetch.bind(globalThis);
392
392
  globalThis.fetch = (async (input, init) => {
393
- const url = httpUtils.toURL(input);
393
+ const url = netUtils.parseUrl(input);
394
394
  if (
395
395
  !url ||
396
396
  !url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) ||
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Mastra observability wired through the same OTel pipeline AppKit's
3
+ * built-in plugins (e.g. `agents`) use, via `@mastra/otel-bridge`.
4
+ *
5
+ * How traces flow:
6
+ *
7
+ * 1. `@databricks/appkit` boots a global `NodeSDK` in
8
+ * `TelemetryManager.initialize()` (during `createApp`) when
9
+ * `OTEL_EXPORTER_OTLP_ENDPOINT` is set in the process env.
10
+ * 2. Every AppKit plugin span (e.g. the `agents` plugin's
11
+ * `executeStream`) is created via the global OTel tracer
12
+ * (`trace.getTracer(<plugin>)`), so it lands on that NodeSDK and
13
+ * is shipped through its OTLP exporter.
14
+ * 3. The Mastra `OtelBridge` ALSO creates real OTel spans on the same
15
+ * global tracer for every Mastra operation (agent runs, model
16
+ * calls, tool invocations, workflow steps). They inherit the
17
+ * ambient OTel context, so when Mastra is invoked from inside an
18
+ * AppKit HTTP span the trace stays connected.
19
+ *
20
+ * Net effect: Mastra spans get exactly the treatment AppKit's
21
+ * `agents` plugin gets. No custom OTLP pipeline lives in this
22
+ * package; the OTLP endpoint, headers, and resource attributes are
23
+ * driven by the standard OTel env vars
24
+ * (`OTEL_EXPORTER_OTLP_ENDPOINT`, `OTEL_EXPORTER_OTLP_HEADERS`,
25
+ * `OTEL_SERVICE_NAME`, `OTEL_RESOURCE_ATTRIBUTES`, ...) and consumed
26
+ * by AppKit's `TelemetryManager`. Set those once and both AppKit and
27
+ * Mastra spans end up at the same backend.
28
+ *
29
+ * When `OTEL_EXPORTER_OTLP_ENDPOINT` is unset the bridge's spans go
30
+ * to the global noop tracer, mirroring how the `agents` plugin
31
+ * silently no-ops in the same situation.
32
+ */
33
+
34
+ import { logUtils, projectUtils } from "@dbx-tools/shared";
35
+ import { Observability } from "@mastra/observability";
36
+ import { OtelBridge } from "@mastra/otel-bridge";
37
+
38
+ import { TRACE_REQUEST_CONTEXT_KEYS } from "./config.js";
39
+
40
+ const log = logUtils.logger("mastra/observability");
41
+
42
+ const DEFAULT_SERVICE_NAME = "mastra";
43
+
44
+ export interface BuildObservabilityOptions {
45
+ /**
46
+ * Service name attached to the Mastra `Observability` config. Used
47
+ * as the tracer scope name on bridged OTel spans (the `service.name`
48
+ * resource attribute is owned by AppKit's `TelemetryManager` instead
49
+ * - it reads `OTEL_SERVICE_NAME` / `DATABRICKS_APP_NAME` at
50
+ * `createApp` time).
51
+ *
52
+ * Defaults to project name then `"mastra"`.
53
+ */
54
+ serviceName?: string;
55
+ /**
56
+ * `RequestContext` keys to extract as span metadata on every Mastra
57
+ * trace. Defaults to {@link TRACE_REQUEST_CONTEXT_KEYS} (user id,
58
+ * thread id, request id, environment, model override, ...).
59
+ *
60
+ * Supports dot notation for nested values per the Mastra docs.
61
+ */
62
+ requestContextKeys?: readonly string[];
63
+ }
64
+
65
+ /**
66
+ * Build a Mastra `Observability` whose spans ride AppKit's global
67
+ * OTel pipeline via `@mastra/otel-bridge`.
68
+ *
69
+ * Returns `undefined` only if someone explicitly opts out in the
70
+ * future; today it always returns an `Observability` because the
71
+ * bridge degrades gracefully (no-op tracer) when no global OTel SDK
72
+ * is registered. Callers can spread `...(observability ? { observability } : {})`
73
+ * either way to stay forward-compatible.
74
+ */
75
+ export async function buildObservability(
76
+ options?: BuildObservabilityOptions,
77
+ ): Promise<Observability | undefined> {
78
+ const serviceName =
79
+ options?.serviceName ??
80
+ (await projectUtils.name()) ??
81
+ DEFAULT_SERVICE_NAME;
82
+ const requestContextKeys = [
83
+ ...(options?.requestContextKeys ?? TRACE_REQUEST_CONTEXT_KEYS),
84
+ ];
85
+
86
+ // The OTel HTTP exporter treats `OTEL_EXPORTER_OTLP_ENDPOINT` as a
87
+ // *base* URL and appends the signal path itself (e.g.
88
+ // `http://localhost:6006` -> `http://localhost:6006/v1/traces`). Log
89
+ // the resolved POST URL so misconfigurations (e.g. accidentally
90
+ // setting the base var to a `/v1/traces`-suffixed URL, which makes
91
+ // the SDK POST to `.../v1/traces/v1/traces` and Phoenix 404s) are
92
+ // obvious in startup output.
93
+ const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
94
+ const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
95
+ const resolvedTracesUrl = otelTracesOverride
96
+ ? otelTracesOverride
97
+ : otelBase
98
+ ? `${otelBase.replace(/\/+$/, "")}/v1/traces`
99
+ : undefined;
100
+ log.info("Mastra observability wired through OTel bridge", {
101
+ serviceName,
102
+ requestContextKeys,
103
+ otelBase: otelBase ?? "<unset>",
104
+ resolvedTracesUrl: resolvedTracesUrl ?? "<noop; OTLP endpoint unset>",
105
+ });
106
+
107
+ return new Observability({
108
+ configs: {
109
+ serviceName: {
110
+ serviceName,
111
+ bridge: new OtelBridge(),
112
+ requestContextKeys,
113
+ },
114
+ },
115
+ });
116
+ }
package/src/plugin.ts CHANGED
@@ -37,7 +37,7 @@ import {
37
37
  type PluginManifest,
38
38
  type ResourceRequirement,
39
39
  } from "@databricks/appkit";
40
- import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
40
+ import { appkitUtils, logUtils } from "@dbx-tools/shared";
41
41
  import { chatRoute } from "@mastra/ai-sdk";
42
42
  import type { Agent } from "@mastra/core/agent";
43
43
  import { Mastra } from "@mastra/core/mastra";
@@ -48,6 +48,7 @@ import type { MastraClientConfig } from "@dbx-tools/appkit-mastra-shared";
48
48
  import type { MastraPluginConfig } from "./config.js";
49
49
  import { historyRoute } from "./history.js";
50
50
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
51
+ import { buildObservability } from "./observability.js";
51
52
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
52
53
  import {
53
54
  clearServingEndpointsCache,
@@ -56,8 +57,8 @@ import {
56
57
  type ServingEndpointSummary,
57
58
  } from "./serving.js";
58
59
 
59
- const GENIE_MANIFEST = pluginUtils.data(genie).plugin.manifest;
60
- const LAKEBASE_MANIFEST = pluginUtils.data(lakebase).plugin.manifest;
60
+ const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
61
+ const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
61
62
 
62
63
  /**
63
64
  * AppKit plugin (registered name: `mastra`) that hosts Mastra agents
@@ -75,6 +76,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
75
76
  resources: {
76
77
  required: [],
77
78
  optional: [
79
+ // Surface the Genie resource binding (space id) declared by
80
+ // AppKit's `genie` plugin manifest. The Mastra plugin no
81
+ // longer uses the genie plugin's tools at runtime - the
82
+ // built-in Genie agent talks to Genie directly via
83
+ // `@dbx-tools/genie` - but reusing the manifest keeps the
84
+ // resource-binding shape identical to AppKit's so existing
85
+ // `app.yaml` configs and `genie({ spaces })` wiring keep
86
+ // working without change.
78
87
  ...GENIE_MANIFEST.resources.required,
79
88
  ...LAKEBASE_MANIFEST.resources.required,
80
89
  ],
@@ -125,7 +134,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
125
134
  * already in the registry by the time this fires.
126
135
  */
127
136
  private applyLakebaseAutoDefaults(): void {
128
- const hasLakebase = pluginUtils.instance(this.context, lakebase) !== undefined;
137
+ const hasLakebase = appkitUtils.instance(this.context, lakebase) !== undefined;
129
138
  if (!hasLakebase) return;
130
139
  if (this.config.storage === undefined) this.config.storage = true;
131
140
  if (this.config.memory === undefined) this.config.memory = true;
@@ -271,7 +280,25 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
271
280
  // dev server. Since we're hosting Mastra inside our own Express
272
281
  // subapp via `@mastra/express`, custom routes must be passed to
273
282
  // the `MastraServer` constructor directly.
274
- this.mastra = new Mastra({ agents: this.built.agents });
283
+ //
284
+ // `storage` here is *Mastra-instance-level* and persists workflow
285
+ // snapshots (where suspended `requireApproval` tool calls live).
286
+ // It's separate from each agent's `Memory.storage`, which only
287
+ // covers thread / message history. Without it,
288
+ // `agent.resumeStream()` errors with "could not find a suspended
289
+ // run" and the approval UI hangs after the user clicks Approve.
290
+ const instanceStorage = memoryBuilder?.instanceStorage();
291
+ // Wire Mastra's tracer into AppKit's global OTel pipeline via
292
+ // `@mastra/otel-bridge`. Mastra spans become native OTel spans on
293
+ // whatever tracer provider `TelemetryManager` registered during
294
+ // `createApp`, so the OTLP endpoint / headers / sampling are
295
+ // env-driven and shared with every other AppKit plugin.
296
+ const observability = await buildObservability({ serviceName: this.name });
297
+ this.mastra = new Mastra({
298
+ agents: this.built.agents,
299
+ ...(instanceStorage ? { storage: instanceStorage } : {}),
300
+ ...(observability ? { observability } : {}),
301
+ });
275
302
  this.mastraApp = express();
276
303
  attachRoutePatchMiddleware(this.mastraApp);
277
304
  this.mastraServer = new MastraServer(this.config, {
@@ -281,8 +308,11 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
281
308
  customApiRoutes: [
282
309
  chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
283
310
  chatRoute({ path: "/route/chat/:agentId" }),
284
- historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
285
- historyRoute({ path: "/route/history/:agentId" }),
311
+ // `historyRoute` registers both GET (load) and DELETE
312
+ // (clear) on the same path, so it returns an array we
313
+ // splice in.
314
+ ...historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
315
+ ...historyRoute({ path: "/route/history/:agentId" }),
286
316
  ],
287
317
  });
288
318
  await this.mastraServer.init();
@@ -290,6 +320,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
290
320
  agents: Object.keys(this.built.agents),
291
321
  defaultAgent: this.built.defaultAgentId,
292
322
  routes: ["/route/chat", "/route/history", "/models"],
323
+ instanceStorage: instanceStorage !== undefined,
324
+ observability: observability !== undefined ? "mlflow" : "off",
293
325
  });
294
326
  }
295
327
  }
@@ -20,7 +20,7 @@
20
20
  * uniformly without coupling to specific tool ids.
21
21
  */
22
22
 
23
- import { logUtils } from "@dbx-tools/appkit-shared";
23
+ import { logUtils } from "@dbx-tools/shared";
24
24
  import type {
25
25
  InputProcessor,
26
26
  ProcessInputArgs,
package/src/server.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { getExecutionContext } from "@databricks/appkit";
9
- import { httpUtils, logUtils, stringUtils } from "@dbx-tools/appkit-shared";
9
+ import { httpUtils, logUtils, stringUtils } from "@dbx-tools/shared";
10
10
  import {
11
11
  MASTRA_RESOURCE_ID_KEY,
12
12
  MASTRA_THREAD_ID_KEY,
@@ -16,7 +16,14 @@ import { MastraServer as MastraServerExpress } from "@mastra/express";
16
16
  import type express from "express";
17
17
  import { randomUUID } from "node:crypto";
18
18
 
19
- import { MASTRA_USER_KEY, type MastraPluginConfig, type User } from "./config.js";
19
+ import {
20
+ MASTRA_REQUEST_ID_KEY,
21
+ MASTRA_USER_EMAIL_KEY,
22
+ MASTRA_USER_KEY,
23
+ MASTRA_USER_NAME_KEY,
24
+ type MastraPluginConfig,
25
+ type User,
26
+ } from "./config.js";
20
27
  import {
21
28
  extractModelOverride,
22
29
  MASTRA_MODEL_OVERRIDE_KEY,
@@ -46,11 +53,15 @@ export class MastraServer extends MastraServerExpress {
46
53
  this.configureRequestContextUser(requestContext);
47
54
  this.configureRequestContextThreadId(req, res, requestContext);
48
55
  this.configureRequestContextModelOverride(req, requestContext);
56
+ this.configureRequestContextRequestId(req, res, requestContext);
49
57
  this.log.debug("auth:middleware", {
50
58
  method: req.method,
51
59
  path: req.path,
60
+ requestId: requestContext.get(MASTRA_REQUEST_ID_KEY),
52
61
  threadId: requestContext.get(MASTRA_THREAD_ID_KEY),
53
62
  resourceId: requestContext.get(MASTRA_RESOURCE_ID_KEY),
63
+ userName: requestContext.get(MASTRA_USER_NAME_KEY),
64
+ userEmail: requestContext.get(MASTRA_USER_EMAIL_KEY),
54
65
  modelOverride: requestContext.get(
55
66
  // imported below; logged so a misrouted request shows
56
67
  // up alongside its model selection in `LOG_LEVEL=debug`.
@@ -76,6 +87,42 @@ export class MastraServer extends MastraServerExpress {
76
87
  };
77
88
  requestContext.set(MASTRA_USER_KEY, user);
78
89
  requestContext.set(MASTRA_RESOURCE_ID_KEY, user.id);
90
+ // AppKit's `UserContext` surfaces display name / email only on
91
+ // OBO requests. Service-context calls (background tasks, server
92
+ // start-up) leave these undefined and we skip the stamp so
93
+ // downstream trace metadata stays absent rather than empty.
94
+ if ("isUserContext" in executionContext) {
95
+ if (executionContext.userName) {
96
+ requestContext.set(MASTRA_USER_NAME_KEY, executionContext.userName);
97
+ }
98
+ if (executionContext.userEmail) {
99
+ requestContext.set(MASTRA_USER_EMAIL_KEY, executionContext.userEmail);
100
+ }
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Stamp a per-request id and echo it on the response so an upstream
106
+ * proxy / curl client / browser-side log line can pair its view of
107
+ * the request with the matching trace span. Reuses `X-Request-Id`
108
+ * when the upstream already supplies one so multi-hop traces stay
109
+ * joined; otherwise mints a UUIDv4.
110
+ *
111
+ * The id is surfaced as `mastra__requestId` span metadata via
112
+ * {@link TRACE_REQUEST_CONTEXT_KEYS} and as the `X-Request-Id`
113
+ * response header so dev tools can copy it from either side.
114
+ */
115
+ configureRequestContextRequestId(
116
+ req: express.Request,
117
+ res: express.Response,
118
+ requestContext: RequestContext,
119
+ ) {
120
+ if (requestContext.get(MASTRA_REQUEST_ID_KEY)) return;
121
+ const headerValue = req.headers["x-request-id"];
122
+ const upstream = Array.isArray(headerValue) ? headerValue[0] : headerValue;
123
+ const requestId = upstream?.trim() || randomUUID();
124
+ requestContext.set(MASTRA_REQUEST_ID_KEY, requestId);
125
+ res.setHeader("X-Request-Id", requestId);
79
126
  }
80
127
 
81
128
  configureRequestContextThreadId(
package/src/serving.ts CHANGED
@@ -21,7 +21,7 @@
21
21
  */
22
22
 
23
23
  import { CacheManager, type getExecutionContext } from "@databricks/appkit";
24
- import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
24
+ import { logUtils, stringUtils } from "@dbx-tools/shared";
25
25
  import Fuse from "fuse.js";
26
26
 
27
27
  import type { ServingEndpointSummary } from "@dbx-tools/appkit-mastra-shared";
@@ -26,7 +26,7 @@
26
26
  * specific agents that should be able to draft emails.
27
27
  */
28
28
 
29
- import { logUtils, stringUtils } from "@dbx-tools/appkit-shared";
29
+ import { logUtils, stringUtils } from "@dbx-tools/shared";
30
30
  import { createTool } from "@mastra/core/tools";
31
31
  import { z } from "zod";
32
32