@dbx-tools/appkit-mastra 0.1.12 → 0.1.13

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/package.json CHANGED
@@ -9,17 +9,19 @@
9
9
  }
10
10
  },
11
11
  "name": "@dbx-tools/appkit-mastra",
12
- "version": "0.1.12",
12
+ "version": "0.1.13",
13
13
  "type": "module",
14
14
  "module": "index.ts",
15
15
  "dependencies": {
16
- "@dbx-tools/appkit-mastra-shared": "0.1.12",
17
- "@dbx-tools/appkit-shared": "0.1.12",
16
+ "@dbx-tools/appkit-mastra-shared": "0.1.13",
17
+ "@dbx-tools/appkit-shared": "0.1.13",
18
18
  "@mastra/ai-sdk": "^1.3",
19
19
  "@mastra/core": "^1.32",
20
20
  "@mastra/express": "^1.3",
21
21
  "@mastra/fastembed": "^1.0",
22
22
  "@mastra/memory": "^1.17",
23
+ "@mastra/observability": "^0.0.0-graph-crash-v3-20260528202217",
24
+ "@mastra/otel-exporter": "^0.0.0-graph-crash-v3-20260528202217",
23
25
  "@mastra/pg": "^1.10",
24
26
  "fuse.js": "^7.0.0",
25
27
  "zod": "^4.3.6"
package/src/memory.ts CHANGED
@@ -14,6 +14,14 @@
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.
@@ -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;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Mastra observability wiring for the `@dbx-tools/appkit-phoenix`
3
+ * sibling plugin.
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.
11
+ *
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.
19
+ */
20
+
21
+ import type { pluginUtils } from "@dbx-tools/appkit-shared";
22
+ import { Observability } from "@mastra/observability";
23
+ import { OtelExporter } from "@mastra/otel-exporter";
24
+
25
+ /** Plugin name the phoenix plugin registers under (matches `phoenix()`). */
26
+ const PHOENIX_PLUGIN_NAME = "phoenix";
27
+
28
+ /** Structural shape of the bits of `phoenix().exports()` we touch. */
29
+ interface PhoenixExportsLike {
30
+ collectorEndpoint?(): string | undefined;
31
+ }
32
+
33
+ /** Structural shape of an AppKit plugin instance with `exports()`. */
34
+ interface PluginWithExports {
35
+ exports?(): unknown;
36
+ }
37
+
38
+ /**
39
+ * If the sibling `phoenix` plugin is registered AND has booted with a
40
+ * usable collector URL, return a Mastra `Observability` configured to
41
+ * stream traces + logs there. Otherwise return `undefined` so the
42
+ * caller can omit the field on the `new Mastra({...})` constructor.
43
+ *
44
+ * The exporter uses `provider.custom` with `http/protobuf`, which is
45
+ * what Phoenix's `/v1/traces` endpoint speaks natively. Switching
46
+ * Phoenix to gRPC would be a one-line `protocol: "grpc"` change and
47
+ * a different exported URL.
48
+ */
49
+ export function buildPhoenixObservability(
50
+ context: pluginUtils.PluginContextLike | undefined,
51
+ serviceName: string,
52
+ ): Observability | undefined {
53
+ const endpoint = readPhoenixEndpoint(context);
54
+ if (!endpoint) return undefined;
55
+
56
+ return new Observability({
57
+ configs: {
58
+ phoenix: {
59
+ serviceName,
60
+ exporters: [
61
+ new OtelExporter({
62
+ provider: {
63
+ custom: {
64
+ endpoint,
65
+ protocol: "http/protobuf",
66
+ },
67
+ },
68
+ }),
69
+ ],
70
+ },
71
+ },
72
+ });
73
+ }
74
+
75
+ /**
76
+ * Pull the OTLP collector URL out of the registered `phoenix` plugin.
77
+ * Tolerant of the plugin being absent (returns `undefined`) and of a
78
+ * future shape change in its exports (anything that's not a string
79
+ * is ignored). The lookup is keyed off the registered plugin *name*
80
+ * so this file does not depend on `@dbx-tools/appkit-phoenix`.
81
+ */
82
+ function readPhoenixEndpoint(
83
+ context: pluginUtils.PluginContextLike | undefined,
84
+ ): string | undefined {
85
+ if (!context) return undefined;
86
+ const plugin = context.getPlugins().get(PHOENIX_PLUGIN_NAME) as
87
+ | PluginWithExports
88
+ | undefined;
89
+ const exports_ = plugin?.exports?.() as PhoenixExportsLike | undefined;
90
+ const url = exports_?.collectorEndpoint?.();
91
+ return typeof url === "string" ? url : undefined;
92
+ }
package/src/plugin.ts CHANGED
@@ -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 { buildPhoenixObservability } from "./observability.js";
51
52
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
52
53
  import {
53
54
  clearServingEndpointsCache,
@@ -271,7 +272,26 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
271
272
  // dev server. Since we're hosting Mastra inside our own Express
272
273
  // subapp via `@mastra/express`, custom routes must be passed to
273
274
  // the `MastraServer` constructor directly.
274
- this.mastra = new Mastra({ agents: this.built.agents });
275
+ //
276
+ // `storage` here is *Mastra-instance-level* and persists workflow
277
+ // snapshots (where suspended `requireApproval` tool calls live).
278
+ // It's separate from each agent's `Memory.storage`, which only
279
+ // covers thread / message history. Without it,
280
+ // `agent.resumeStream()` errors with "could not find a suspended
281
+ // run" and the approval UI hangs after the user clicks Approve.
282
+ const instanceStorage = memoryBuilder?.instanceStorage();
283
+ // Auto-wire OTLP trace export to the sibling `phoenix` plugin if
284
+ // it's registered. Returns undefined when phoenix isn't around so
285
+ // the field stays off the constructor and Mastra keeps its noop
286
+ // observability default. The serviceName is the plugin's bound
287
+ // name so multiple mastra instances in one process stay
288
+ // distinguishable in Phoenix.
289
+ const observability = buildPhoenixObservability(this.context, this.name);
290
+ this.mastra = new Mastra({
291
+ agents: this.built.agents,
292
+ ...(instanceStorage ? { storage: instanceStorage } : {}),
293
+ ...(observability ? { observability } : {}),
294
+ });
275
295
  this.mastraApp = express();
276
296
  attachRoutePatchMiddleware(this.mastraApp);
277
297
  this.mastraServer = new MastraServer(this.config, {
@@ -290,6 +310,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
290
310
  agents: Object.keys(this.built.agents),
291
311
  defaultAgent: this.built.defaultAgentId,
292
312
  routes: ["/route/chat", "/route/history", "/models"],
313
+ instanceStorage: instanceStorage !== undefined,
314
+ observability: observability !== undefined ? "phoenix" : "off",
293
315
  });
294
316
  }
295
317
  }