@dbx-tools/appkit-mastra 0.3.21 → 0.3.22

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
@@ -28,22 +28,22 @@
28
28
  "express": "^5.1.0",
29
29
  "pg": "^8.22.0",
30
30
  "zod": "^4.3.6",
31
- "@dbx-tools/core": "0.3.21",
32
- "@dbx-tools/appkit": "0.3.21",
33
- "@dbx-tools/databricks": "0.3.21",
34
- "@dbx-tools/shared-genie": "0.3.21",
35
- "@dbx-tools/model": "0.3.21",
36
- "@dbx-tools/shared-core": "0.3.21",
37
- "@dbx-tools/genie": "0.3.21",
38
- "@dbx-tools/shared-mastra": "0.3.21",
39
- "@dbx-tools/shared-model": "0.3.21"
31
+ "@dbx-tools/appkit": "0.3.22",
32
+ "@dbx-tools/databricks": "0.3.22",
33
+ "@dbx-tools/genie": "0.3.22",
34
+ "@dbx-tools/model": "0.3.22",
35
+ "@dbx-tools/shared-core": "0.3.22",
36
+ "@dbx-tools/shared-mastra": "0.3.22",
37
+ "@dbx-tools/shared-genie": "0.3.22",
38
+ "@dbx-tools/core": "0.3.22",
39
+ "@dbx-tools/shared-model": "0.3.22"
40
40
  },
41
41
  "main": "index.ts",
42
42
  "license": "UNLICENSED",
43
43
  "publishConfig": {
44
44
  "access": "public"
45
45
  },
46
- "version": "0.3.21",
46
+ "version": "0.3.22",
47
47
  "types": "index.ts",
48
48
  "type": "module",
49
49
  "exports": {
package/src/plugin.ts CHANGED
@@ -51,11 +51,9 @@ import {
51
51
  type PluginManifest,
52
52
  type ResourceRequirement,
53
53
  } from "@databricks/appkit";
54
- import type { Agent } from "@mastra/core/agent";
55
- import { Mastra } from "@mastra/core/mastra";
56
- import express from "express";
57
- import type { Pool } from "pg";
58
-
54
+ import { plugin } from "@dbx-tools/appkit";
55
+ import { serving as nodeServing } from "@dbx-tools/model";
56
+ import { error, log, string } from "@dbx-tools/shared-core";
59
57
  import {
60
58
  feedback,
61
59
  routes,
@@ -63,8 +61,12 @@ import {
63
61
  type MastraFeedbackRequest,
64
62
  type StatementData,
65
63
  } from "@dbx-tools/shared-mastra";
66
- import { serving as nodeServing } from "@dbx-tools/model";
67
64
  import { display, type ServingEndpointSummary } from "@dbx-tools/shared-model";
65
+ import type { Agent } from "@mastra/core/agent";
66
+ import { Mastra } from "@mastra/core/mastra";
67
+ import express from "express";
68
+ import type { Pool } from "pg";
69
+
68
70
  import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents";
69
71
  import { fetchChart } from "./chart";
70
72
  import type { MastraPluginConfig } from "./config";
@@ -78,8 +80,6 @@ import { attachRoutePatchMiddleware, isMastraRequestAllowed, MastraServer } from
78
80
  import { resolveServingConfig } from "./serving";
79
81
  import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement";
80
82
  import { threadsRoute } from "./threads";
81
- import { error, log, string } from "@dbx-tools/shared-core";
82
- import { plugin } from "@dbx-tools/appkit";
83
83
 
84
84
  const GENIE_MANIFEST = plugin.data(genie).plugin.manifest;
85
85
  const LAKEBASE_MANIFEST = plugin.data(lakebase).plugin.manifest;
@@ -330,7 +330,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
330
330
  // Reads only in-memory build state, so it's synchronous and needs no OBO
331
331
  // scoping. Registered before the catch-all, same as `/models`.
332
332
  const handleDefaultModel = (req: express.Request, res: express.Response): void => {
333
- const requested = string.firstNonEmpty(req.params["agentId"]);
333
+ const requested = string.firstNonEmpty(req.params.agentId);
334
334
  const agentId = requested ?? this.built?.defaultAgentId ?? FALLBACK_AGENT_ID;
335
335
  const raw = this.built?.defaultModels[agentId];
336
336
  // `"<dynamic>"` (a call-time function) has no fixed id to advertise.
@@ -378,14 +378,14 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
378
378
  // clean 404; thrown errors bubble through `next(err)`.
379
379
  const embedResolvers: Record<string, EmbedResolver> = {
380
380
  chart: (req, id, signal) => {
381
- const timeoutMs = parseTimeoutMs(req.query["timeoutMs"]);
381
+ const timeoutMs = parseTimeoutMs(req.query.timeoutMs);
382
382
  return fetchChart(id, {
383
383
  ...(timeoutMs !== undefined ? { timeoutMs } : {}),
384
384
  signal,
385
385
  });
386
386
  },
387
387
  data: (req, id, signal) => {
388
- const limit = parseStatementLimit(req.query["limit"]);
388
+ const limit = parseStatementLimit(req.query.limit);
389
389
  return this.userScopedSelf(req).fetchStatement(id, {
390
390
  ...(limit !== undefined ? { limit } : {}),
391
391
  signal,
@@ -394,8 +394,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
394
394
  };
395
395
 
396
396
  router.get(`${routes.MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
397
- const type = req.params["type"] ?? "";
398
- const id = req.params["id"];
397
+ const type = req.params.type ?? "";
398
+ const id = req.params.id;
399
399
  const resolve = embedResolvers[type];
400
400
  if (!resolve) {
401
401
  res.status(404).json({ error: `unsupported embed type: ${type}` });
package/src/server.ts CHANGED
@@ -7,7 +7,9 @@
7
7
  * @module
8
8
  */
9
9
 
10
+ import { randomUUID } from "node:crypto";
10
11
  import { getExecutionContext } from "@databricks/appkit";
12
+ import { http, log, object, string, token } from "@dbx-tools/shared-core";
11
13
  import { feedback, thread } from "@dbx-tools/shared-mastra";
12
14
  import {
13
15
  MASTRA_RESOURCE_ID_KEY,
@@ -17,9 +19,6 @@ import {
17
19
  import { MastraServer as MastraServerExpress } from "@mastra/express";
18
20
  import { trace } from "@opentelemetry/api";
19
21
  import type express from "express";
20
- import { randomUUID } from "node:crypto";
21
-
22
- import { resolveFeedbackEnabled } from "./mlflow";
23
22
 
24
23
  import {
25
24
  MASTRA_REQUEST_ID_KEY,
@@ -30,7 +29,8 @@ import {
30
29
  type MastraPluginConfig,
31
30
  type User,
32
31
  } from "./config";
33
- import { http, log, object, string, token } from "@dbx-tools/shared-core";
32
+ import { resolveFeedbackEnabled } from "./mlflow";
33
+
34
34
  import { extractModelOverride, MASTRA_MODEL_OVERRIDE_KEY, resolveServingConfig } from "./serving";
35
35
  /**
36
36
  * OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
@@ -154,10 +154,7 @@ export class MastraServer extends MastraServerExpress {
154
154
  configureRequestContextScopes(req: express.Request, requestContext: RequestContext) {
155
155
  const scopes = [
156
156
  ...object
157
- .sequence(
158
- token.getAccessTokenScopes(req),
159
- token.getAccessTokenScopes(req, "authorization"),
160
- )
157
+ .sequence(token.getAccessTokenScopes(req), token.getAccessTokenScopes(req, "authorization"))
161
158
  .distinct(),
162
159
  ];
163
160
  requestContext.set(MASTRA_SCOPES_KEY, scopes);
@@ -1,4 +1,5 @@
1
1
  import { string } from "@dbx-tools/shared-core";
2
+ import { type ChatMessage, type ChatRole, openaiChat } from "@dbx-tools/shared-model";
2
3
  /**
3
4
  * Repairs Mastra / AI SDK message replays sent to Databricks Model
4
5
  * Serving before they hit the OpenAI-compatible `/chat/completions`
@@ -6,43 +7,46 @@ import { string } from "@dbx-tools/shared-core";
6
7
  */
7
8
 
8
9
  /**
9
- * OpenAI-flavoured chat message shape we need to mutate. We do not
10
- * import the OpenAI / AI SDK types because both packages keep these
11
- * fields under internal namespaces; the wire payload is the contract
12
- * here and it's stable enough to inline.
10
+ * A chat message as it arrives on the serving wire, plus the extended-thinking
11
+ * fields Databricks-hosted Claude adds. The OpenAI-standard part of the shape
12
+ * is {@link ChatMessage} from `@dbx-tools/shared-model`; only the provider
13
+ * extensions this module strips are declared here.
13
14
  */
14
- export interface ServingChatMessage {
15
- role: "system" | "user" | "assistant" | "tool" | "reasoning";
16
- content?: string | ServingContentPart[];
17
- tool_calls?: Array<{ id: string; type: string; function: unknown }>;
18
- tool_call_id?: string;
15
+ export interface ServingChatMessage extends ChatMessage {
16
+ /** Narrowed to the roles this repair pass reasons about, plus Claude's `reasoning` turn. */
17
+ role: ChatRole | "reasoning";
19
18
  reasoning?: unknown;
20
19
  reasoning_content?: unknown;
21
20
  }
22
21
 
23
- type ServingContentPart = {
24
- type?: string;
25
- text?: string;
26
- [key: string]: unknown;
27
- };
28
-
29
22
  const REASONING_PART_TYPES = new Set(["reasoning", "thinking", "redacted_thinking"]);
30
23
 
31
24
  /**
32
25
  * Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
33
26
  * body. Returns the original string verbatim when the body is not
34
- * JSON, has no `messages`, or no rewrite was needed.
27
+ * JSON or no rewrite was needed.
35
28
  */
36
29
  export function rewriteServingBody(body: string): string {
37
- let parsed: { messages?: unknown };
30
+ let parsed: Record<string, unknown>;
38
31
  try {
39
- parsed = JSON.parse(body);
32
+ parsed = JSON.parse(body) as Record<string, unknown>;
40
33
  } catch {
41
34
  return body;
42
35
  }
43
- if (!Array.isArray(parsed.messages)) return body;
44
- const messages = parsed.messages as ServingChatMessage[];
45
- const changed = stripReasoningFromServingMessages(messages) || repairAssistantPrefill(messages);
36
+
37
+ // Runs regardless of `messages`: Databricks refuses to parse a body carrying
38
+ // an unknown top-level field, so this failure is not specific to a transcript.
39
+ let changed = openaiChat.stripUnsupportedChatFields(parsed).length > 0;
40
+
41
+ if (Array.isArray(parsed.messages)) {
42
+ const messages = parsed.messages as ServingChatMessage[];
43
+ // Evaluated eagerly, not short-circuited: one transcript can need both
44
+ // reasoning blocks stripped AND a trailing assistant prefill folded back.
45
+ const stripped = stripReasoningFromServingMessages(messages);
46
+ const repaired = repairAssistantPrefill(messages);
47
+ changed = changed || stripped || repaired;
48
+ }
49
+
46
50
  return changed ? JSON.stringify(parsed) : body;
47
51
  }
48
52
 
@@ -145,15 +149,19 @@ export function repairAssistantPrefill(messages: ServingChatMessage[]): boolean
145
149
  return true;
146
150
  }
147
151
 
152
+ /**
153
+ * Flatten content to the prose a human would read: text parts only, separated
154
+ * by blank lines, since the result is spliced into another turn's message body.
155
+ */
148
156
  function textFromServingContent(content: ServingChatMessage["content"]): string {
149
- if (typeof content === "string") return content;
150
- if (!Array.isArray(content)) return "";
151
- return content
152
- .filter((part) => part?.type === "text" && typeof part.text === "string")
153
- .map((part) => part.text as string)
154
- .join("\n\n");
157
+ return openaiChat.chatContentToText(content, { separator: "\n\n", types: ["text"] });
155
158
  }
156
159
 
160
+ /**
161
+ * Whether a turn carries nothing worth replaying. Deliberately stricter than
162
+ * "has no text": a part of any other type (an image, a cache marker) counts as
163
+ * content, so a message holding one is kept even though it has no prose.
164
+ */
157
165
  function isEmptyServingContent(content: ServingChatMessage["content"]): boolean {
158
166
  if (content === undefined) return true;
159
167
  if (typeof content === "string") return content.trim().length === 0;