@dbx-tools/appkit-mastra 0.3.29 → 0.3.31

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/rest.ts CHANGED
@@ -1,5 +1,3 @@
1
- import { appkit } from "@dbx-tools/appkit";
2
- import { json } from "@dbx-tools/shared-core";
3
1
  /**
4
2
  * Minimal authenticated Databricks REST helper. Pulls the workspace
5
3
  * host and a fresh bearer header off an OBO-scoped `WorkspaceClient`
@@ -8,8 +6,13 @@ import { json } from "@dbx-tools/shared-core";
8
6
  * method (e.g. the MLflow assessments API); returns the raw `Response`
9
7
  * so callers decide how to treat status codes. Also carries the
10
8
  * defensive `Response` body readers those callers share.
9
+ *
10
+ * @module
11
11
  */
12
12
 
13
+ import { appkit } from "@dbx-tools/appkit";
14
+ import { json } from "@dbx-tools/shared-core";
15
+
13
16
  /** Workspace client carried on an AppKit execution context. */
14
17
  type WorkspaceClient = appkit.WorkspaceClientLike;
15
18
 
package/src/server.ts CHANGED
@@ -20,6 +20,7 @@ import { trace } from "@opentelemetry/api";
20
20
  import type express from "express";
21
21
 
22
22
  import {
23
+ executionContextUserId,
23
24
  MASTRA_REQUEST_ID_KEY,
24
25
  MASTRA_SCOPES_KEY,
25
26
  MASTRA_USER_EMAIL_KEY,
@@ -94,7 +95,7 @@ export class MastraServer extends MastraServerExpress {
94
95
  if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
95
96
  const executionContext = getExecutionContext();
96
97
  const user: User = {
97
- id: "userId" in executionContext ? executionContext.userId : executionContext.serviceUserId,
98
+ id: executionContextUserId(executionContext),
98
99
  executionContext,
99
100
  };
100
101
  requestContext.set(MASTRA_USER_KEY, user);
@@ -1,3 +1,18 @@
1
+ /**
2
+ * Repairs Mastra / AI SDK message replays sent to Databricks Model
3
+ * Serving before they hit the OpenAI-compatible `/chat/completions`
4
+ * route.
5
+ *
6
+ * Exists because the transcript Mastra persists is not always a transcript the
7
+ * provider will accept back: Databricks-hosted Claude rejects replayed
8
+ * extended-thinking blocks and reads a trailing assistant message as a prefill
9
+ * request. Both repairs are provider quirks, not schema violations, so they are
10
+ * applied on the wire (see the `globalThis.fetch` wrapper in `model.ts`) rather
11
+ * than by changing what the agent stores or what the UI shows.
12
+ *
13
+ * @module
14
+ */
15
+
1
16
  import { json, string } from "@dbx-tools/shared-core";
2
17
  import {
3
18
  type ChatMessage,
@@ -5,11 +20,6 @@ import {
5
20
  openaiChat,
6
21
  openaiResponses,
7
22
  } from "@dbx-tools/shared-model";
8
- /**
9
- * Repairs Mastra / AI SDK message replays sent to Databricks Model
10
- * Serving before they hit the OpenAI-compatible `/chat/completions`
11
- * route.
12
- */
13
23
 
14
24
  /**
15
25
  * A chat message as it arrives on the serving wire, plus the extended-thinking
@@ -1,12 +1,15 @@
1
- import { string } from "@dbx-tools/shared-core";
2
1
  /**
3
2
  * Derive a Postgres-safe schema name for per-agent Mastra storage.
4
3
  *
5
4
  * Agent ids are often kebab-case route segments (`data-mesh-book-assistant`)
6
5
  * but {@link PostgresStore} validates `schemaName` with Mastra's
7
6
  * `parseSqlIdentifier` (letter/underscore start, `[A-Za-z0-9_]` only, max 63).
7
+ *
8
+ * @module
8
9
  */
9
10
 
11
+ import { string } from "@dbx-tools/shared-core";
12
+
10
13
  const SCHEMA_PREFIX = "mastra_";
11
14
  const MAX_PG_IDENTIFIER_LEN = 63;
12
15
 
package/src/threads.ts CHANGED
@@ -21,6 +21,7 @@
21
21
  * @module
22
22
  */
23
23
 
24
+ import { ValidationError } from "@databricks/appkit";
24
25
  import { log } from "@dbx-tools/shared-core";
25
26
  import {
26
27
  wire,
@@ -36,6 +37,7 @@ import type { ContextWithMastra } from "@mastra/core/server";
36
37
  import { registerApiRoute } from "@mastra/core/server";
37
38
 
38
39
  import { clampPerPage, parseIntParam } from "./pagination";
40
+ import { invalidFields } from "./validation";
39
41
 
40
42
  const logger = log.logger("mastra/threads");
41
43
 
@@ -43,6 +45,8 @@ const logger = log.logger("mastra/threads");
43
45
  const DEFAULT_PER_PAGE = 30;
44
46
  /** Hard cap so a misbehaving client can't fetch every thread at once. */
45
47
  const MAX_PER_PAGE = 200;
48
+ /** Stable client message for a rename body that fails schema validation. */
49
+ const INVALID_RENAME_MESSAGE = "Invalid thread rename request";
46
50
 
47
51
  /** Inputs accepted by {@link listThreads}. */
48
52
  export interface ListThreadsOptions {
@@ -220,8 +224,10 @@ export function threadsRoute(options: ThreadsRouteOptions) {
220
224
  const { path } = options;
221
225
  const fixedAgent = "agent" in options ? options.agent : undefined;
222
226
  if (!fixedAgent && !path.includes(":agentId")) {
223
- throw new Error(
224
- "threadsRoute path must include `:agentId` or `agent` must be passed explicitly",
227
+ throw ValidationError.invalidValue(
228
+ "threadsRoute.path",
229
+ path,
230
+ "a path containing `:agentId`, or an explicit `agent`",
225
231
  );
226
232
  }
227
233
  // Shared by GET / DELETE: resolve the active agent and the caller's
@@ -298,7 +304,8 @@ export function threadsRoute(options: ThreadsRouteOptions) {
298
304
  }
299
305
  const body = wire.MastraUpdateThreadRequestSchema.safeParse(await c.req.json());
300
306
  if (!body.success) {
301
- return c.json({ error: body.error.message }, 400);
307
+ logger.warn("rename:invalid", { error: body.error.message });
308
+ return c.json({ error: INVALID_RENAME_MESSAGE, fields: invalidFields(body.error) }, 400);
302
309
  }
303
310
  const thread = await renameThread({
304
311
  agent: ctx.agent,
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Request-body validation helpers shared by every route that parses a JSON
3
+ * body with a schema from `@dbx-tools/shared-mastra`.
4
+ *
5
+ * A schema library's own issue text quotes the received body back, so it is
6
+ * logged rather than returned. What a client gets instead is a stable message
7
+ * plus the field paths that failed, which is enough to highlight the offending
8
+ * inputs.
9
+ *
10
+ * @module
11
+ */
12
+
13
+ /** The subset of a failed parse result these helpers read. */
14
+ export interface SchemaIssues {
15
+ issues: ReadonlyArray<{ path: PropertyKey[] }>;
16
+ }
17
+
18
+ /** Distinct dot-joined field paths a failed parse flagged. */
19
+ export function invalidFields(error: SchemaIssues): string[] {
20
+ const fields = error.issues.map((issue) => issue.path.map(String).join(".")).filter(Boolean);
21
+ return [...new Set(fields)];
22
+ }
@@ -0,0 +1,64 @@
1
+ import assert from "node:assert/strict";
2
+ import { describe, it } from "node:test";
3
+ import { ConfigurationError } from "@databricks/appkit";
4
+
5
+ import { MASTRA_CONFIG_SCHEMA } from "../src/config";
6
+ import { normalizeGenieSpaces } from "../src/genie";
7
+ import { invalidFields } from "../src/validation";
8
+
9
+ describe("mastra config schema", () => {
10
+ it("describes every published property", () => {
11
+ const properties = Object.entries(MASTRA_CONFIG_SCHEMA.properties ?? {});
12
+ assert.ok(properties.length > 0);
13
+ for (const [name, schema] of properties) {
14
+ assert.equal(typeof schema, "object", `${name} must be a schema object`);
15
+ const description = (schema as { description?: unknown }).description;
16
+ assert.equal(typeof description, "string", `${name} must carry a description`);
17
+ assert.ok((description as string).length > 0, `${name} description must be non-empty`);
18
+ }
19
+ });
20
+ });
21
+
22
+ describe("genie space normalization", () => {
23
+ it("wraps bare space ids and passes objects through", () => {
24
+ assert.deepEqual(normalizeGenieSpaces({ default: "01ef", sales: { spaceId: "02ef" } }), {
25
+ default: { spaceId: "01ef" },
26
+ sales: { spaceId: "02ef" },
27
+ });
28
+ });
29
+
30
+ it("treats no config as no spaces", () => {
31
+ assert.deepEqual(normalizeGenieSpaces(undefined), {});
32
+ assert.deepEqual(normalizeGenieSpaces({}), {});
33
+ });
34
+
35
+ it("fails loudly on an alias with no space id", () => {
36
+ for (const spaces of [
37
+ { default: undefined },
38
+ { default: "" },
39
+ { sales: { spaceId: "" } },
40
+ ] as Parameters<typeof normalizeGenieSpaces>[0][]) {
41
+ assert.throws(() => normalizeGenieSpaces(spaces), ConfigurationError);
42
+ }
43
+ });
44
+
45
+ it("names the env var only for the default alias", () => {
46
+ assert.throws(() => normalizeGenieSpaces({ default: undefined }), /DATABRICKS_GENIE_SPACE_ID/);
47
+ assert.throws(() => normalizeGenieSpaces({ sales: undefined }), /genieSpaces\.sales/);
48
+ });
49
+ });
50
+
51
+ describe("request body validation", () => {
52
+ it("reports distinct dot-joined field paths", () => {
53
+ assert.deepEqual(
54
+ invalidFields({
55
+ issues: [{ path: ["traceId"] }, { path: ["value", 0] }, { path: ["traceId"] }],
56
+ }),
57
+ ["traceId", "value.0"],
58
+ );
59
+ });
60
+
61
+ it("drops the root path, which names no field", () => {
62
+ assert.deepEqual(invalidFields({ issues: [{ path: [] }] }), []);
63
+ });
64
+ });