@dbx-tools/appkit-mastra 0.1.91 → 0.1.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -576,7 +576,7 @@ declare class MemoryBuilder {
576
576
  * the `requireApproval` flow) will not be available.
577
577
  *
578
578
  * The store lives in a dedicated `mastra_instance` schema so it
579
- * never collides with per-agent `mastra_<agentId>` namespaces.
579
+ * never collides with per-agent {@link agentStorageSchemaName} namespaces.
580
580
  * Workflow snapshots are not per-agent state; they belong to the
581
581
  * `Mastra` instance that owns the workflow execution.
582
582
  */
@@ -817,7 +817,7 @@ interface MastraAgentDefinition {
817
817
  *
818
818
  * - `undefined` (default): inherit `config.storage`. When that's
819
819
  * enabled, the agent gets its **own per-agent `PostgresStore`**
820
- * keyed by `schemaName: "mastra_<agentId>"` so threads and
820
+ * keyed by {@link agentStorageSchemaName} so threads and
821
821
  * messages stay isolated between agents in the same database.
822
822
  * - `false`: disable storage for this agent only (purely in-memory).
823
823
  * - `true`: enable with the per-agent default schema.
package/dist/index.js CHANGED
@@ -76,6 +76,127 @@ const TRACE_REQUEST_CONTEXT_KEYS = [
76
76
  "mastra__model_override"
77
77
  ];
78
78
 
79
+ //#endregion
80
+ //#region packages/appkit-mastra/src/serving-sanitize.ts
81
+ /**
82
+ * Repairs Mastra / AI SDK message replays sent to Databricks Model
83
+ * Serving before they hit the OpenAI-compatible `/chat/completions`
84
+ * route.
85
+ */
86
+ const REASONING_PART_TYPES = new Set([
87
+ "reasoning",
88
+ "thinking",
89
+ "redacted_thinking"
90
+ ]);
91
+ /**
92
+ * Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
93
+ * body. Returns the original string verbatim when the body is not
94
+ * JSON, has no `messages`, or no rewrite was needed.
95
+ */
96
+ function rewriteServingBody(body) {
97
+ let parsed;
98
+ try {
99
+ parsed = JSON.parse(body);
100
+ } catch {
101
+ return body;
102
+ }
103
+ if (!Array.isArray(parsed.messages)) return body;
104
+ const messages = parsed.messages;
105
+ return stripReasoningFromServingMessages(messages) || repairAssistantPrefill(messages) ? JSON.stringify(parsed) : body;
106
+ }
107
+ /**
108
+ * Drop extended-thinking / reasoning blocks from a replayed transcript.
109
+ *
110
+ * Hybrid Claude endpoints (e.g. Sonnet 4.5+) may emit `reasoning` content
111
+ * parts on the first turn. Mastra persists and replays them on the next
112
+ * agent step, but Databricks-hosted Claude rejects those blocks on
113
+ * multi-turn tool continuations. The UI already captured reasoning for
114
+ * display; stripping here keeps provider replay compatible without
115
+ * changing what users see in the chat bubble.
116
+ */
117
+ function stripReasoningFromServingMessages(messages) {
118
+ let changed = false;
119
+ for (let i = messages.length - 1; i >= 0; i--) {
120
+ const msg = messages[i];
121
+ if (!msg) continue;
122
+ if (msg.role === "reasoning") {
123
+ messages.splice(i, 1);
124
+ changed = true;
125
+ continue;
126
+ }
127
+ if (msg.reasoning !== void 0) {
128
+ delete msg.reasoning;
129
+ changed = true;
130
+ }
131
+ if (msg.reasoning_content !== void 0) {
132
+ delete msg.reasoning_content;
133
+ changed = true;
134
+ }
135
+ if (!Array.isArray(msg.content)) continue;
136
+ const filtered = msg.content.filter((part) => {
137
+ const type = part?.type;
138
+ if (typeof type === "string" && REASONING_PART_TYPES.has(type)) {
139
+ changed = true;
140
+ return false;
141
+ }
142
+ return true;
143
+ });
144
+ if (filtered.length !== msg.content.length) msg.content = filtered;
145
+ const hasToolCalls = Array.isArray(msg.tool_calls) && msg.tool_calls.length > 0;
146
+ if (msg.role === "assistant" && !hasToolCalls && isEmptyServingContent(msg.content)) {
147
+ messages.splice(i, 1);
148
+ changed = true;
149
+ }
150
+ }
151
+ return changed;
152
+ }
153
+ /**
154
+ * Repair a Mastra/AI SDK message replay that Databricks-hosted Claude
155
+ * rejects with `"This model does not support assistant message
156
+ * prefill. The conversation must end with a user message."`.
157
+ *
158
+ * The bug pattern: when an assistant turn streams text *and* a
159
+ * `tool_call`, the AI SDK persists them as two separate assistant
160
+ * entries (text-only and tool-call-only). On the next agent step the
161
+ * tool-call entry is replayed *before* the tool result and the
162
+ * text entry is replayed *after* it, so the conversation ends with a
163
+ * trailing assistant text message. Anthropic interprets that as a
164
+ * prefill request and rejects it on Databricks (the upstream Bedrock
165
+ * route disallows prefill).
166
+ *
167
+ * Fix: when the last message is an assistant text with no `tool_calls`
168
+ * and the chain immediately before it is `assistant(tool_calls=...)`
169
+ * followed only by `tool(...)` results, fold the trailing text back
170
+ * into the `content` of that opening assistant and drop the duplicate.
171
+ */
172
+ function repairAssistantPrefill(messages) {
173
+ if (messages.length < 2) return false;
174
+ const last = messages[messages.length - 1];
175
+ if (!last || last.role !== "assistant" || last.tool_calls && last.tool_calls.length > 0) return false;
176
+ let i = messages.length - 2;
177
+ while (i >= 0 && messages[i]?.role === "tool") i--;
178
+ if (i < 0) return false;
179
+ const opener = messages[i];
180
+ if (!opener || opener.role !== "assistant" || !opener.tool_calls || opener.tool_calls.length === 0) return false;
181
+ opener.content = [stringUtils.trimToNull(textFromServingContent(opener.content)), stringUtils.trimToNull(textFromServingContent(last.content))].filter((s) => s !== null).join("\n\n");
182
+ messages.pop();
183
+ return true;
184
+ }
185
+ function textFromServingContent(content) {
186
+ if (typeof content === "string") return content;
187
+ if (!Array.isArray(content)) return "";
188
+ return content.filter((part) => part?.type === "text" && typeof part.text === "string").map((part) => part.text).join("\n\n");
189
+ }
190
+ function isEmptyServingContent(content) {
191
+ if (content === void 0) return true;
192
+ if (typeof content === "string") return content.trim().length === 0;
193
+ if (!Array.isArray(content)) return true;
194
+ return content.every((part) => {
195
+ if (part?.type === "text") return typeof part.text !== "string" || part.text.trim().length === 0;
196
+ return false;
197
+ });
198
+ }
199
+
79
200
  //#endregion
80
201
  //#region packages/appkit-mastra/src/serving.ts
81
202
  /**
@@ -219,7 +340,7 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
219
340
  *
220
341
  * 1. Rewrites the outgoing `messages` array to repair Mastra/AI SDK
221
342
  * stream-replay quirks that Databricks-hosted Claude rejects (see
222
- * {@link sanitizeServingMessages}).
343
+ * {@link rewriteServingBody} in `./serving-sanitize.js`).
223
344
  * 2. At `LOG_LEVEL=debug`, dumps the (post-sanitize) JSON body so
224
345
  * 4xx debugging doesn't have to fight AI SDK's `[Array]`
225
346
  * formatter.
@@ -254,61 +375,6 @@ const setupFetchInterceptor = commonUtils.memoize(() => {
254
375
  return original(input, init);
255
376
  });
256
377
  });
257
- /**
258
- * Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
259
- * body. Returns the original string verbatim when the body is not
260
- * JSON, has no `messages`, or no rewrite was needed; this lets the
261
- * caller skip the allocation of a new `init` object in the common
262
- * pass-through case.
263
- */
264
- function rewriteServingBody(body) {
265
- let parsed;
266
- try {
267
- parsed = JSON.parse(body);
268
- } catch {
269
- return body;
270
- }
271
- if (!Array.isArray(parsed.messages)) return body;
272
- return sanitizeServingMessages(parsed.messages) ? JSON.stringify(parsed) : body;
273
- }
274
- /**
275
- * Repair a Mastra/AI SDK message replay that Databricks-hosted Claude
276
- * rejects with `"This model does not support assistant message
277
- * prefill. The conversation must end with a user message."`.
278
- *
279
- * The bug pattern: when an assistant turn streams text *and* a
280
- * `tool_call`, the AI SDK persists them as two separate assistant
281
- * entries (text-only and tool-call-only). On the next agent step the
282
- * tool-call entry is replayed *before* the tool result and the
283
- * text entry is replayed *after* it, so the conversation ends with a
284
- * trailing assistant text message. Anthropic interprets that as a
285
- * prefill request and rejects it on Databricks (the upstream Bedrock
286
- * route disallows prefill).
287
- *
288
- * Fix: when the last message is an assistant text with no `tool_calls`
289
- * and the chain immediately before it is `assistant(tool_calls=...)`
290
- * followed only by `tool(...)` results, fold the trailing text back
291
- * into the `content` of that opening assistant and drop the duplicate.
292
- * The result is the canonical OpenAI shape
293
- * `[..., user, assistant(text + tool_calls), tool(...)]` which both
294
- * Databricks Claude and every other endpoint accept.
295
- *
296
- * Mutates `messages` in place; returns `true` when something changed
297
- * so the caller knows whether to re-serialize.
298
- */
299
- function sanitizeServingMessages(messages) {
300
- if (messages.length < 2) return false;
301
- const last = messages[messages.length - 1];
302
- if (!last || last.role !== "assistant" || last.tool_calls && last.tool_calls.length > 0) return false;
303
- let i = messages.length - 2;
304
- while (i >= 0 && messages[i]?.role === "tool") i--;
305
- if (i < 0) return false;
306
- const opener = messages[i];
307
- if (!opener || opener.role !== "assistant" || !opener.tool_calls || opener.tool_calls.length === 0) return false;
308
- opener.content = [stringUtils.trimToNull(opener.content), stringUtils.trimToNull(last.content)].filter((s) => s !== null).join("\n\n");
309
- messages.pop();
310
- return true;
311
- }
312
378
 
313
379
  //#endregion
314
380
  //#region packages/appkit-mastra/src/chart.ts
@@ -2758,6 +2824,36 @@ function toEpoch(value) {
2758
2824
  return 0;
2759
2825
  }
2760
2826
 
2827
+ //#endregion
2828
+ //#region packages/appkit-mastra/src/storage-schema.ts
2829
+ /**
2830
+ * Derive a Postgres-safe schema name for per-agent Mastra storage.
2831
+ *
2832
+ * Agent ids are often kebab-case route segments (`data-mesh-book-assistant`)
2833
+ * but {@link PostgresStore} validates `schemaName` with Mastra's
2834
+ * `parseSqlIdentifier` (letter/underscore start, `[A-Za-z0-9_]` only, max 63).
2835
+ */
2836
+ const SCHEMA_PREFIX = "mastra_";
2837
+ const MAX_PG_IDENTIFIER_LEN = 63;
2838
+ /**
2839
+ * Default Lakebase schema for one agent's thread/message store:
2840
+ * `mastra_<sanitized-agent-id>`.
2841
+ */
2842
+ function agentStorageSchemaName(agentId) {
2843
+ const maxSlugLen = MAX_PG_IDENTIFIER_LEN - 7;
2844
+ return `${SCHEMA_PREFIX}${stringUtils.toIdentifierWithOptions({
2845
+ delimiter: "_",
2846
+ maxLength: maxSlugLen,
2847
+ truncateStrategy: "hash",
2848
+ truncateHashLength: 6
2849
+ }, agentId) || stringUtils.toIdentifierWithOptions({
2850
+ delimiter: "_",
2851
+ maxLength: maxSlugLen,
2852
+ truncateStrategy: "hash",
2853
+ truncateHashLength: 6
2854
+ }, "agent", agentId)}`;
2855
+ }
2856
+
2761
2857
  //#endregion
2762
2858
  //#region packages/appkit-mastra/src/memory.ts
2763
2859
  /**
@@ -2767,7 +2863,7 @@ function toEpoch(value) {
2767
2863
  * with two independent knobs:
2768
2864
  *
2769
2865
  * - **Storage** (threads / messages via `PostgresStore`): defaults to
2770
- * **per-agent** namespacing via `schemaName: "mastra_<agentId>"` so
2866
+ * **per-agent** namespacing via {@link agentStorageSchemaName} so
2771
2867
  * conversation history stays isolated between agents in the same
2772
2868
  * database. `PostgresStore` auto-creates the schema with
2773
2869
  * `CREATE SCHEMA IF NOT EXISTS` on init.
@@ -2853,7 +2949,7 @@ var MemoryBuilder = class {
2853
2949
  * the `requireApproval` flow) will not be available.
2854
2950
  *
2855
2951
  * The store lives in a dedicated `mastra_instance` schema so it
2856
- * never collides with per-agent `mastra_<agentId>` namespaces.
2952
+ * never collides with per-agent {@link agentStorageSchemaName} namespaces.
2857
2953
  * Workflow snapshots are not per-agent state; they belong to the
2858
2954
  * `Mastra` instance that owns the workflow execution.
2859
2955
  */
@@ -2911,7 +3007,7 @@ var MemoryBuilder = class {
2911
3007
  if (!setting) return void 0;
2912
3008
  if (typeof setting === "boolean") return new PostgresStore({
2913
3009
  id: `mastra-store__${agentId}`,
2914
- schemaName: `mastra_${agentId}`,
3010
+ schemaName: agentStorageSchemaName(agentId),
2915
3011
  pool: this.servicePrincipalPool
2916
3012
  });
2917
3013
  return new PostgresStore(withId(setting, `mastra-store__${agentId}`));
@@ -3744,7 +3840,7 @@ function toIso(value) {
3744
3840
  * from `./memory.js`. Both auto-default to `true` when the
3745
3841
  * `lakebase` plugin is registered (unless the caller passed
3746
3842
  * `false` or a custom config). Storage namespaces per agent via
3747
- * `schemaName: "mastra_<agentId>"`; the vector store is a single
3843
+ * {@link agentStorageSchemaName} per agent; the vector store is a single
3748
3844
  * shared singleton across every agent.
3749
3845
  * - Server: the Express subapp wiring lives in `./server.js`.
3750
3846
  * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dbx-tools/appkit-mastra",
3
- "version": "0.1.91",
3
+ "version": "0.1.93",
4
4
  "dependencies": {
5
5
  "@databricks/sdk-experimental": "^0.17",
6
- "@dbx-tools/appkit-mastra-shared": "0.1.91",
7
- "@dbx-tools/genie": "0.1.91",
8
- "@dbx-tools/genie-shared": "0.1.91",
9
- "@dbx-tools/model": "0.1.91",
10
- "@dbx-tools/shared": "0.1.91",
6
+ "@dbx-tools/appkit-mastra-shared": "0.1.93",
7
+ "@dbx-tools/genie": "0.1.93",
8
+ "@dbx-tools/genie-shared": "0.1.93",
9
+ "@dbx-tools/model": "0.1.93",
10
+ "@dbx-tools/shared": "0.1.93",
11
11
  "@mastra/ai-sdk": "^1",
12
12
  "@mastra/core": "^1",
13
13
  "@mastra/express": "^1",