@dbx-tools/appkit-mastra 0.1.13 → 0.1.19

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.
@@ -43,6 +43,27 @@ export interface LoadHistoryOptions {
43
43
  * without sorting locally.
44
44
  */
45
45
  export declare function loadHistory(opts: LoadHistoryOptions): Promise<MastraHistoryResponse>;
46
+ /** Inputs accepted by {@link clearHistory}. */
47
+ export interface ClearHistoryOptions {
48
+ agent: Agent;
49
+ threadId: string;
50
+ }
51
+ /**
52
+ * Wipe every persisted message tied to a thread. Returns the count
53
+ * of messages that were on the thread at delete time so the caller
54
+ * can render a "cleared N messages" affordance without an
55
+ * additional round-trip.
56
+ *
57
+ * Agents without a configured `Memory` resolve to a no-op (count
58
+ * 0), matching {@link loadHistory}'s "stateless agents return an
59
+ * empty page" stance so callers don't have to special-case them.
60
+ * Threads that don't exist yet are also a successful no-op - the
61
+ * operation is idempotent so the UI can fire-and-forget without
62
+ * tracking thread existence.
63
+ */
64
+ export declare function clearHistory(opts: ClearHistoryOptions): Promise<{
65
+ cleared: number;
66
+ }>;
46
67
  /** Options accepted by {@link historyRoute}. */
47
68
  export type HistoryRouteOptions = {
48
69
  path: `${string}:agentId${string}`;
@@ -52,8 +73,15 @@ export type HistoryRouteOptions = {
52
73
  agent: string;
53
74
  };
54
75
  /**
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.
76
+ * Register the `<path>` Mastra custom API route. Handles two
77
+ * methods on the same mount:
78
+ *
79
+ * - `GET`: return a page of AI SDK V5 `UIMessage`s for the
80
+ * caller's current thread ({@link loadHistory}).
81
+ * - `DELETE`: wipe every persisted message on the caller's
82
+ * thread ({@link clearHistory}). The session cookie that
83
+ * anchors the thread id is left alone so the user keeps the
84
+ * same thread - only the contents go away.
57
85
  *
58
86
  * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
59
87
  * fixed-agent mount, or include `:agentId` in the path for dynamic
@@ -64,4 +92,4 @@ export type HistoryRouteOptions = {
64
92
  * (populated upstream by `MastraServer.registerAuthMiddleware`), so
65
93
  * no cookie or user lookups happen here.
66
94
  */
67
- export declare function historyRoute(options: HistoryRouteOptions): import("@mastra/core/server").ApiRoute;
95
+ export declare function historyRoute(options: HistoryRouteOptions): import("@mastra/core/server").ApiRoute[];
@@ -15,7 +15,7 @@
15
15
  * the handler runs - no cookie or user lookups happen here, and the
16
16
  * session-cookie logic stays the single source of truth in `server.ts`.
17
17
  */
18
- import { logUtils } from "@dbx-tools/appkit-shared";
18
+ import { commonUtils, logUtils } from "@dbx-tools/shared";
19
19
  import { toAISdkV5Messages } from "@mastra/ai-sdk/ui";
20
20
  import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY, } from "@mastra/core/request-context";
21
21
  import { registerApiRoute } from "@mastra/core/server";
@@ -79,8 +79,79 @@ export async function loadHistory(opts) {
79
79
  };
80
80
  }
81
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.
82
+ * Wipe every persisted message tied to a thread. Returns the count
83
+ * of messages that were on the thread at delete time so the caller
84
+ * can render a "cleared N messages" affordance without an
85
+ * additional round-trip.
86
+ *
87
+ * Agents without a configured `Memory` resolve to a no-op (count
88
+ * 0), matching {@link loadHistory}'s "stateless agents return an
89
+ * empty page" stance so callers don't have to special-case them.
90
+ * Threads that don't exist yet are also a successful no-op - the
91
+ * operation is idempotent so the UI can fire-and-forget without
92
+ * tracking thread existence.
93
+ */
94
+ export async function clearHistory(opts) {
95
+ const memory = await opts.agent.getMemory();
96
+ if (!memory) {
97
+ log.debug("clear:no-memory", { agentId: opts.agent.id, threadId: opts.threadId });
98
+ return { cleared: 0 };
99
+ }
100
+ // Mastra's `deleteThread` cascades to the message table, so we
101
+ // can't ask for a count after the fact. Read it pre-delete with a
102
+ // one-page recall sized to fit common threads in a single round
103
+ // trip; the value is for telemetry / UI, not correctness.
104
+ let cleared = 0;
105
+ try {
106
+ const probe = await memory.recall({
107
+ threadId: opts.threadId,
108
+ page: 0,
109
+ perPage: 1,
110
+ });
111
+ cleared = probe.total;
112
+ }
113
+ catch (err) {
114
+ // A missing-thread error is the happy-path "nothing to count";
115
+ // every other error is logged but doesn't block the delete.
116
+ log.debug("clear:probe-failed", {
117
+ agentId: opts.agent.id,
118
+ threadId: opts.threadId,
119
+ error: commonUtils.errorMessage(err),
120
+ });
121
+ }
122
+ const startedAt = Date.now();
123
+ try {
124
+ await memory.deleteThread(opts.threadId);
125
+ }
126
+ catch (err) {
127
+ // Mastra's `deleteThread` raises when the thread row was never
128
+ // created (e.g. clearing an empty session). Surface as a soft
129
+ // warn and treat as success - the user-facing semantic is
130
+ // "history is now empty" which is already true.
131
+ log.warn("clear:delete-soft-failed", {
132
+ agentId: opts.agent.id,
133
+ threadId: opts.threadId,
134
+ error: commonUtils.errorMessage(err),
135
+ });
136
+ }
137
+ log.info("clear:done", {
138
+ agentId: opts.agent.id,
139
+ threadId: opts.threadId,
140
+ cleared,
141
+ elapsedMs: Date.now() - startedAt,
142
+ });
143
+ return { cleared };
144
+ }
145
+ /**
146
+ * Register the `<path>` Mastra custom API route. Handles two
147
+ * methods on the same mount:
148
+ *
149
+ * - `GET`: return a page of AI SDK V5 `UIMessage`s for the
150
+ * caller's current thread ({@link loadHistory}).
151
+ * - `DELETE`: wipe every persisted message on the caller's
152
+ * thread ({@link clearHistory}). The session cookie that
153
+ * anchors the thread id is left alone so the user keeps the
154
+ * same thread - only the contents go away.
84
155
  *
85
156
  * Modeled after `chatRoute` from `@mastra/ai-sdk`: pass `agent` for a
86
157
  * fixed-agent mount, or include `:agentId` in the path for dynamic
@@ -97,34 +168,69 @@ export function historyRoute(options) {
97
168
  if (!fixedAgent && !path.includes(":agentId")) {
98
169
  throw new Error("historyRoute path must include `:agentId` or `agent` must be passed explicitly");
99
170
  }
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
- });
171
+ // Tiny resolver shared by GET / DELETE: derive the active agent
172
+ // and thread id, returning a JSON error response when either is
173
+ // missing. Keeps both handlers thin and gives them identical
174
+ // validation behaviour.
175
+ const resolveContext = (c) => {
176
+ const mastra = c.get("mastra");
177
+ const requestContext = c.get("requestContext");
178
+ const agentId = fixedAgent ?? c.req.param("agentId");
179
+ if (!agentId) {
180
+ return { error: c.json({ error: "agentId is required" }, 400) };
181
+ }
182
+ const agent = mastra.getAgentById(agentId);
183
+ if (!agent) {
184
+ return {
185
+ error: c.json({ error: `Unknown agent "${agentId}"` }, 404),
186
+ };
187
+ }
188
+ const threadId = requestContext.get(MASTRA_THREAD_ID_KEY);
189
+ if (!threadId) {
190
+ return {
191
+ error: c.json({ error: "thread id missing from request context" }, 400),
192
+ };
193
+ }
194
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY);
195
+ return { agentId, agent, threadId, resourceId };
196
+ };
197
+ return [
198
+ registerApiRoute(path, {
199
+ method: "GET",
200
+ handler: async (c) => {
201
+ const ctx = resolveContext(c);
202
+ if ("error" in ctx)
203
+ return ctx.error;
204
+ const payload = await loadHistory({
205
+ agent: ctx.agent,
206
+ threadId: ctx.threadId,
207
+ ...(ctx.resourceId ? { resourceId: ctx.resourceId } : {}),
208
+ page: parseIntParam(c.req.query("page")),
209
+ perPage: parseIntParam(c.req.query("perPage")),
210
+ });
211
+ return c.json(payload);
212
+ },
213
+ }),
214
+ registerApiRoute(path, {
215
+ method: "DELETE",
216
+ handler: async (c) => {
217
+ const ctx = resolveContext(c);
218
+ if ("error" in ctx)
219
+ return ctx.error;
220
+ const { cleared } = await clearHistory({
221
+ agent: ctx.agent,
222
+ threadId: ctx.threadId,
223
+ });
224
+ const payload = {
225
+ ok: true,
226
+ agentId: ctx.agentId,
227
+ threadId: ctx.threadId,
228
+ cleared,
229
+ };
230
+ return c.json(payload);
231
+ },
232
+ }),
233
+ ];
128
234
  }
129
235
  /** Coerce / clamp `perPage`; falls back to the page-size default. */
130
236
  function clampPerPage(value) {
@@ -27,7 +27,7 @@
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
29
  import { lakebase } from "@databricks/appkit";
30
- import { pluginUtils } from "@dbx-tools/appkit-shared";
30
+ import { appkitUtils } from "@dbx-tools/shared";
31
31
  import { Memory } from "@mastra/memory";
32
32
  import { PostgresStore } from "@mastra/pg";
33
33
  import type { MastraAgentDefinition } from "./agents.js";
@@ -47,13 +47,13 @@ export declare function needsLakebase(config: MastraPluginConfig): boolean;
47
47
  * `storage` / `memory` without lakebase is a wiring bug, not a runtime
48
48
  * condition we can recover from.
49
49
  */
50
- export declare function resolveLakebasePool(context: pluginUtils.PluginContextLike | undefined, caller: MastraPluginConfig): LakebasePool;
50
+ export declare function resolveLakebasePool(context: appkitUtils.PluginContextLike | undefined, caller: MastraPluginConfig): LakebasePool;
51
51
  /**
52
52
  * Construct a per-agent {@link Memory} factory. Caches the shared
53
53
  * `PgVector` singleton (built on first need) and the lazily-resolved
54
54
  * Lakebase pool so each agent build is O(1) after the first.
55
55
  */
56
- export declare function createMemoryBuilder(config: MastraPluginConfig, context: pluginUtils.PluginContextLike | undefined): MemoryBuilder;
56
+ export declare function createMemoryBuilder(config: MastraPluginConfig, context: appkitUtils.PluginContextLike | undefined): MemoryBuilder;
57
57
  /**
58
58
  * Builds one `Memory` per agent. Per-instance state keeps the shared
59
59
  * `PgVector` and the resolved Lakebase pool alive across calls so
@@ -64,7 +64,7 @@ export declare class MemoryBuilder {
64
64
  private readonly context;
65
65
  private sharedVector;
66
66
  private pool;
67
- constructor(config: MastraPluginConfig, context: pluginUtils.PluginContextLike | undefined);
67
+ constructor(config: MastraPluginConfig, context: appkitUtils.PluginContextLike | undefined);
68
68
  /**
69
69
  * Build a `Memory` for `agentId` after the plugin/agent cascade.
70
70
  * Returns `undefined` when the agent has neither storage nor a
@@ -27,7 +27,7 @@
27
27
  * is registered); per-agent settings cascade on top of that.
28
28
  */
29
29
  import { lakebase } from "@databricks/appkit";
30
- import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
30
+ import { appkitUtils, logUtils } from "@dbx-tools/shared";
31
31
  import { fastembed } from "@mastra/fastembed";
32
32
  import { Memory } from "@mastra/memory";
33
33
  import { PgVector, PostgresStore } from "@mastra/pg";
@@ -54,7 +54,7 @@ export function needsLakebase(config) {
54
54
  * condition we can recover from.
55
55
  */
56
56
  export function resolveLakebasePool(context, caller) {
57
- return pluginUtils.require(context, lakebase, caller).exports().pool;
57
+ return appkitUtils.require(context, lakebase, caller).exports().pool;
58
58
  }
59
59
  /**
60
60
  * Construct a per-agent {@link Memory} factory. Caches the shared
package/dist/src/model.js CHANGED
@@ -24,7 +24,7 @@
24
24
  * (network blip, expired token at cache-fill time) we fall back to
25
25
  * the input verbatim and let Databricks return the canonical error.
26
26
  */
27
- import { commonUtils, httpUtils, logUtils, stringUtils, } from "@dbx-tools/appkit-shared";
27
+ import { commonUtils, logUtils, netUtils, stringUtils, } from "@dbx-tools/shared";
28
28
  import { MASTRA_USER_KEY } from "./config.js";
29
29
  import { listServingEndpoints, MASTRA_MODEL_OVERRIDE_KEY, resolveModelId, resolveServingConfig, } from "./serving.js";
30
30
  /**
@@ -325,7 +325,7 @@ const setupFetchInterceptor = commonUtils.memoize(() => {
325
325
  const log = logUtils.logger("mastra/llm");
326
326
  const original = globalThis.fetch.bind(globalThis);
327
327
  globalThis.fetch = (async (input, init) => {
328
- const url = httpUtils.toURL(input);
328
+ const url = netUtils.parseUrl(input);
329
329
  if (!url ||
330
330
  !url.pathname.startsWith(SERVING_ENDPOINTS_PATH_PREFIX) ||
331
331
  typeof init?.body !== "string") {
@@ -1,33 +1,64 @@
1
1
  /**
2
- * Mastra observability wiring for the `@dbx-tools/appkit-phoenix`
3
- * sibling plugin.
2
+ * Mastra observability wired through the same OTel pipeline AppKit's
3
+ * built-in plugins (e.g. `agents`) use, via `@mastra/otel-bridge`.
4
4
  *
5
- * Mastra's `Observability` registry accepts any
6
- * `@mastra/observability` `BaseExporter`. We use `OtelExporter` from
7
- * `@mastra/otel-exporter` (Mastra's first-party OTLP shim) with the
8
- * `custom` provider pointed at Phoenix's local collector URL. No
9
- * Arize-specific wrapper is needed - Phoenix is a vanilla
10
- * OpenInference-compatible OTLP/HTTP receiver.
5
+ * How traces flow:
11
6
  *
12
- * Discovery is structural so this module doesn't depend on
13
- * `@dbx-tools/appkit-phoenix` at compile time: we look up the
14
- * registered plugin by its registered name (`"phoenix"`) and read its
15
- * `exports().collectorEndpoint()` if it is shaped like the phoenix
16
- * plugin. The phoenix package is therefore an *optional* sibling -
17
- * apps that don't install it just get an undefined observability
18
- * config and Mastra runs without OTLP export.
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.
19
32
  */
20
- import type { pluginUtils } from "@dbx-tools/appkit-shared";
21
33
  import { Observability } from "@mastra/observability";
34
+ export interface BuildObservabilityOptions {
35
+ /**
36
+ * Service name attached to the Mastra `Observability` config. Used
37
+ * as the tracer scope name on bridged OTel spans (the `service.name`
38
+ * resource attribute is owned by AppKit's `TelemetryManager` instead
39
+ * - it reads `OTEL_SERVICE_NAME` / `DATABRICKS_APP_NAME` at
40
+ * `createApp` time).
41
+ *
42
+ * Defaults to project name then `"mastra"`.
43
+ */
44
+ serviceName?: string;
45
+ /**
46
+ * `RequestContext` keys to extract as span metadata on every Mastra
47
+ * trace. Defaults to {@link TRACE_REQUEST_CONTEXT_KEYS} (user id,
48
+ * thread id, request id, environment, model override, ...).
49
+ *
50
+ * Supports dot notation for nested values per the Mastra docs.
51
+ */
52
+ requestContextKeys?: readonly string[];
53
+ }
22
54
  /**
23
- * If the sibling `phoenix` plugin is registered AND has booted with a
24
- * usable collector URL, return a Mastra `Observability` configured to
25
- * stream traces + logs there. Otherwise return `undefined` so the
26
- * caller can omit the field on the `new Mastra({...})` constructor.
55
+ * Build a Mastra `Observability` whose spans ride AppKit's global
56
+ * OTel pipeline via `@mastra/otel-bridge`.
27
57
  *
28
- * The exporter uses `provider.custom` with `http/protobuf`, which is
29
- * what Phoenix's `/v1/traces` endpoint speaks natively. Switching
30
- * Phoenix to gRPC would be a one-line `protocol: "grpc"` change and
31
- * a different exported URL.
58
+ * Returns `undefined` only if someone explicitly opts out in the
59
+ * future; today it always returns an `Observability` because the
60
+ * bridge degrades gracefully (no-op tracer) when no global OTel SDK
61
+ * is registered. Callers can spread `...(observability ? { observability } : {})`
62
+ * either way to stay forward-compatible.
32
63
  */
33
- export declare function buildPhoenixObservability(context: pluginUtils.PluginContextLike | undefined, serviceName: string): Observability | undefined;
64
+ export declare function buildObservability(options?: BuildObservabilityOptions): Promise<Observability | undefined>;
@@ -1,71 +1,85 @@
1
1
  /**
2
- * Mastra observability wiring for the `@dbx-tools/appkit-phoenix`
3
- * sibling plugin.
2
+ * Mastra observability wired through the same OTel pipeline AppKit's
3
+ * built-in plugins (e.g. `agents`) use, via `@mastra/otel-bridge`.
4
4
  *
5
- * Mastra's `Observability` registry accepts any
6
- * `@mastra/observability` `BaseExporter`. We use `OtelExporter` from
7
- * `@mastra/otel-exporter` (Mastra's first-party OTLP shim) with the
8
- * `custom` provider pointed at Phoenix's local collector URL. No
9
- * Arize-specific wrapper is needed - Phoenix is a vanilla
10
- * OpenInference-compatible OTLP/HTTP receiver.
5
+ * How traces flow:
11
6
  *
12
- * Discovery is structural so this module doesn't depend on
13
- * `@dbx-tools/appkit-phoenix` at compile time: we look up the
14
- * registered plugin by its registered name (`"phoenix"`) and read its
15
- * `exports().collectorEndpoint()` if it is shaped like the phoenix
16
- * plugin. The phoenix package is therefore an *optional* sibling -
17
- * apps that don't install it just get an undefined observability
18
- * config and Mastra runs without OTLP export.
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.
19
32
  */
33
+ import { logUtils, projectUtils } from "@dbx-tools/shared";
20
34
  import { Observability } from "@mastra/observability";
21
- import { OtelExporter } from "@mastra/otel-exporter";
22
- /** Plugin name the phoenix plugin registers under (matches `phoenix()`). */
23
- const PHOENIX_PLUGIN_NAME = "phoenix";
35
+ import { OtelBridge } from "@mastra/otel-bridge";
36
+ import { TRACE_REQUEST_CONTEXT_KEYS } from "./config.js";
37
+ const log = logUtils.logger("mastra/observability");
38
+ const DEFAULT_SERVICE_NAME = "mastra";
24
39
  /**
25
- * If the sibling `phoenix` plugin is registered AND has booted with a
26
- * usable collector URL, return a Mastra `Observability` configured to
27
- * stream traces + logs there. Otherwise return `undefined` so the
28
- * caller can omit the field on the `new Mastra({...})` constructor.
40
+ * Build a Mastra `Observability` whose spans ride AppKit's global
41
+ * OTel pipeline via `@mastra/otel-bridge`.
29
42
  *
30
- * The exporter uses `provider.custom` with `http/protobuf`, which is
31
- * what Phoenix's `/v1/traces` endpoint speaks natively. Switching
32
- * Phoenix to gRPC would be a one-line `protocol: "grpc"` change and
33
- * a different exported URL.
43
+ * Returns `undefined` only if someone explicitly opts out in the
44
+ * future; today it always returns an `Observability` because the
45
+ * bridge degrades gracefully (no-op tracer) when no global OTel SDK
46
+ * is registered. Callers can spread `...(observability ? { observability } : {})`
47
+ * either way to stay forward-compatible.
34
48
  */
35
- export function buildPhoenixObservability(context, serviceName) {
36
- const endpoint = readPhoenixEndpoint(context);
37
- if (!endpoint)
38
- return undefined;
49
+ export async function buildObservability(options) {
50
+ const serviceName = options?.serviceName ??
51
+ (await projectUtils.name()) ??
52
+ DEFAULT_SERVICE_NAME;
53
+ const requestContextKeys = [
54
+ ...(options?.requestContextKeys ?? TRACE_REQUEST_CONTEXT_KEYS),
55
+ ];
56
+ // The OTel HTTP exporter treats `OTEL_EXPORTER_OTLP_ENDPOINT` as a
57
+ // *base* URL and appends the signal path itself (e.g.
58
+ // `http://localhost:6006` -> `http://localhost:6006/v1/traces`). Log
59
+ // the resolved POST URL so misconfigurations (e.g. accidentally
60
+ // setting the base var to a `/v1/traces`-suffixed URL, which makes
61
+ // the SDK POST to `.../v1/traces/v1/traces` and Phoenix 404s) are
62
+ // obvious in startup output.
63
+ const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
64
+ const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
65
+ const resolvedTracesUrl = otelTracesOverride
66
+ ? otelTracesOverride
67
+ : otelBase
68
+ ? `${otelBase.replace(/\/+$/, "")}/v1/traces`
69
+ : undefined;
70
+ log.info("Mastra observability wired through OTel bridge", {
71
+ serviceName,
72
+ requestContextKeys,
73
+ otelBase: otelBase ?? "<unset>",
74
+ resolvedTracesUrl: resolvedTracesUrl ?? "<noop; OTLP endpoint unset>",
75
+ });
39
76
  return new Observability({
40
77
  configs: {
41
- phoenix: {
78
+ serviceName: {
42
79
  serviceName,
43
- exporters: [
44
- new OtelExporter({
45
- provider: {
46
- custom: {
47
- endpoint,
48
- protocol: "http/protobuf",
49
- },
50
- },
51
- }),
52
- ],
80
+ bridge: new OtelBridge(),
81
+ requestContextKeys,
53
82
  },
54
83
  },
55
84
  });
56
85
  }
57
- /**
58
- * Pull the OTLP collector URL out of the registered `phoenix` plugin.
59
- * Tolerant of the plugin being absent (returns `undefined`) and of a
60
- * future shape change in its exports (anything that's not a string
61
- * is ignored). The lookup is keyed off the registered plugin *name*
62
- * so this file does not depend on `@dbx-tools/appkit-phoenix`.
63
- */
64
- function readPhoenixEndpoint(context) {
65
- if (!context)
66
- return undefined;
67
- const plugin = context.getPlugins().get(PHOENIX_PLUGIN_NAME);
68
- const exports_ = plugin?.exports?.();
69
- const url = exports_?.collectorEndpoint?.();
70
- return typeof url === "string" ? url : undefined;
71
- }
@@ -27,18 +27,18 @@
27
27
  * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
28
28
  */
29
29
  import { genie, getExecutionContext, lakebase, Plugin, toPlugin, } from "@databricks/appkit";
30
- import { logUtils, pluginUtils } from "@dbx-tools/appkit-shared";
30
+ import { appkitUtils, logUtils } from "@dbx-tools/shared";
31
31
  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
35
  import { historyRoute } from "./history.js";
36
36
  import { createMemoryBuilder, needsLakebase } from "./memory.js";
37
- import { buildPhoenixObservability } from "./observability.js";
37
+ import { buildObservability } from "./observability.js";
38
38
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
39
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;
40
+ const GENIE_MANIFEST = appkitUtils.data(genie).plugin.manifest;
41
+ const LAKEBASE_MANIFEST = appkitUtils.data(lakebase).plugin.manifest;
42
42
  /**
43
43
  * AppKit plugin (registered name: `mastra`) that hosts Mastra agents
44
44
  * with optional Lakebase-backed memory and AI SDK chat routes under
@@ -54,6 +54,14 @@ export class MastraPlugin extends Plugin {
54
54
  resources: {
55
55
  required: [],
56
56
  optional: [
57
+ // Surface the Genie resource binding (space id) declared by
58
+ // AppKit's `genie` plugin manifest. The Mastra plugin no
59
+ // longer uses the genie plugin's tools at runtime - the
60
+ // built-in Genie agent talks to Genie directly via
61
+ // `@dbx-tools/genie` - but reusing the manifest keeps the
62
+ // resource-binding shape identical to AppKit's so existing
63
+ // `app.yaml` configs and `genie({ spaces })` wiring keep
64
+ // working without change.
57
65
  ...GENIE_MANIFEST.resources.required,
58
66
  ...LAKEBASE_MANIFEST.resources.required,
59
67
  ],
@@ -99,7 +107,7 @@ export class MastraPlugin extends Plugin {
99
107
  * already in the registry by the time this fires.
100
108
  */
101
109
  applyLakebaseAutoDefaults() {
102
- const hasLakebase = pluginUtils.instance(this.context, lakebase) !== undefined;
110
+ const hasLakebase = appkitUtils.instance(this.context, lakebase) !== undefined;
103
111
  if (!hasLakebase)
104
112
  return;
105
113
  if (this.config.storage === undefined)
@@ -245,13 +253,12 @@ export class MastraPlugin extends Plugin {
245
253
  // `agent.resumeStream()` errors with "could not find a suspended
246
254
  // run" and the approval UI hangs after the user clicks Approve.
247
255
  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);
256
+ // Wire Mastra's tracer into AppKit's global OTel pipeline via
257
+ // `@mastra/otel-bridge`. Mastra spans become native OTel spans on
258
+ // whatever tracer provider `TelemetryManager` registered during
259
+ // `createApp`, so the OTLP endpoint / headers / sampling are
260
+ // env-driven and shared with every other AppKit plugin.
261
+ const observability = await buildObservability({ serviceName: this.name });
255
262
  this.mastra = new Mastra({
256
263
  agents: this.built.agents,
257
264
  ...(instanceStorage ? { storage: instanceStorage } : {}),
@@ -266,8 +273,11 @@ export class MastraPlugin extends Plugin {
266
273
  customApiRoutes: [
267
274
  chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
268
275
  chatRoute({ path: "/route/chat/:agentId" }),
269
- historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
270
- historyRoute({ path: "/route/history/:agentId" }),
276
+ // `historyRoute` registers both GET (load) and DELETE
277
+ // (clear) on the same path, so it returns an array we
278
+ // splice in.
279
+ ...historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
280
+ ...historyRoute({ path: "/route/history/:agentId" }),
271
281
  ],
272
282
  });
273
283
  await this.mastraServer.init();
@@ -276,7 +286,7 @@ export class MastraPlugin extends Plugin {
276
286
  defaultAgent: this.built.defaultAgentId,
277
287
  routes: ["/route/chat", "/route/history", "/models"],
278
288
  instanceStorage: instanceStorage !== undefined,
279
- observability: observability !== undefined ? "phoenix" : "off",
289
+ observability: observability !== undefined ? "mlflow" : "off",
280
290
  });
281
291
  }
282
292
  }
@@ -19,7 +19,7 @@
19
19
  * `datasets[].chartId` and `render_data`'s top-level `chartId`
20
20
  * uniformly without coupling to specific tool ids.
21
21
  */
22
- import { logUtils } from "@dbx-tools/appkit-shared";
22
+ import { logUtils } from "@dbx-tools/shared";
23
23
  const log = logUtils.logger("mastra/processor/strip-stale-charts");
24
24
  /**
25
25
  * Recursively clone `value`, omitting any property whose key is