@dbx-tools/appkit-mastra 0.3.28 → 0.3.30
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/README.md +86 -15
- package/index.ts +3 -0
- package/package.json +10 -11
- package/src/agents.ts +12 -11
- package/src/chart.ts +69 -38
- package/src/config.ts +170 -18
- package/src/defaults.ts +133 -0
- package/src/filesystems.ts +29 -15
- package/src/genie.ts +81 -39
- package/src/history.ts +5 -2
- package/src/memory.ts +3 -4
- package/src/model.ts +15 -6
- package/src/plugin.ts +469 -194
- package/src/rest.ts +6 -7
- package/src/server.ts +5 -5
- package/src/serving-sanitize.ts +22 -9
- package/src/storage-schema.ts +4 -1
- package/src/threads.ts +10 -3
- package/src/validation.ts +22 -0
- package/test/config.test.ts +64 -0
package/src/rest.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { appkit } from "@dbx-tools/appkit";
|
|
2
1
|
/**
|
|
3
2
|
* Minimal authenticated Databricks REST helper. Pulls the workspace
|
|
4
3
|
* host and a fresh bearer header off an OBO-scoped `WorkspaceClient`
|
|
@@ -7,8 +6,13 @@ import { appkit } from "@dbx-tools/appkit";
|
|
|
7
6
|
* method (e.g. the MLflow assessments API); returns the raw `Response`
|
|
8
7
|
* so callers decide how to treat status codes. Also carries the
|
|
9
8
|
* defensive `Response` body readers those callers share.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
10
11
|
*/
|
|
11
12
|
|
|
13
|
+
import { appkit } from "@dbx-tools/appkit";
|
|
14
|
+
import { json } from "@dbx-tools/shared-core";
|
|
15
|
+
|
|
12
16
|
/** Workspace client carried on an AppKit execution context. */
|
|
13
17
|
type WorkspaceClient = appkit.WorkspaceClientLike;
|
|
14
18
|
|
|
@@ -58,10 +62,5 @@ export async function readResponseText(res: Response): Promise<string> {
|
|
|
58
62
|
/** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
|
|
59
63
|
export async function readResponseJson(res: Response): Promise<unknown> {
|
|
60
64
|
const text = await readResponseText(res);
|
|
61
|
-
|
|
62
|
-
try {
|
|
63
|
-
return JSON.parse(text);
|
|
64
|
-
} catch {
|
|
65
|
-
return {};
|
|
66
|
-
}
|
|
65
|
+
return json.parse<unknown>(text, {});
|
|
67
66
|
}
|
package/src/server.ts
CHANGED
|
@@ -7,9 +7,8 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import { randomUUID } from "node:crypto";
|
|
11
10
|
import { getExecutionContext } from "@databricks/appkit";
|
|
12
|
-
import { http, log, object, string, token } from "@dbx-tools/shared-core";
|
|
11
|
+
import { hash, http, log, object, string, token } from "@dbx-tools/shared-core";
|
|
13
12
|
import { feedback, thread } from "@dbx-tools/shared-mastra";
|
|
14
13
|
import {
|
|
15
14
|
MASTRA_RESOURCE_ID_KEY,
|
|
@@ -21,6 +20,7 @@ import { trace } from "@opentelemetry/api";
|
|
|
21
20
|
import type express from "express";
|
|
22
21
|
|
|
23
22
|
import {
|
|
23
|
+
executionContextUserId,
|
|
24
24
|
MASTRA_REQUEST_ID_KEY,
|
|
25
25
|
MASTRA_SCOPES_KEY,
|
|
26
26
|
MASTRA_USER_EMAIL_KEY,
|
|
@@ -95,7 +95,7 @@ export class MastraServer extends MastraServerExpress {
|
|
|
95
95
|
if ([MASTRA_USER_KEY, MASTRA_RESOURCE_ID_KEY].every((key) => requestContext.get(key))) return;
|
|
96
96
|
const executionContext = getExecutionContext();
|
|
97
97
|
const user: User = {
|
|
98
|
-
id:
|
|
98
|
+
id: executionContextUserId(executionContext),
|
|
99
99
|
executionContext,
|
|
100
100
|
};
|
|
101
101
|
requestContext.set(MASTRA_USER_KEY, user);
|
|
@@ -142,7 +142,7 @@ export class MastraServer extends MastraServerExpress {
|
|
|
142
142
|
if (requestContext.get(MASTRA_REQUEST_ID_KEY)) return;
|
|
143
143
|
const headerValue = req.headers["x-request-id"];
|
|
144
144
|
const upstream = Array.isArray(headerValue) ? headerValue[0] : headerValue;
|
|
145
|
-
const requestId = upstream?.trim() ||
|
|
145
|
+
const requestId = upstream?.trim() || hash.id();
|
|
146
146
|
requestContext.set(MASTRA_REQUEST_ID_KEY, requestId);
|
|
147
147
|
res.setHeader("X-Request-Id", requestId);
|
|
148
148
|
}
|
|
@@ -218,7 +218,7 @@ export class MastraServer extends MastraServerExpress {
|
|
|
218
218
|
);
|
|
219
219
|
let sessionId = cookies[cookieName];
|
|
220
220
|
if (!sessionId) {
|
|
221
|
-
sessionId =
|
|
221
|
+
sessionId = hash.id();
|
|
222
222
|
res.cookie(cookieName, sessionId, {
|
|
223
223
|
httpOnly: true,
|
|
224
224
|
sameSite: "lax",
|
package/src/serving-sanitize.ts
CHANGED
|
@@ -1,11 +1,26 @@
|
|
|
1
|
-
import { string } from "@dbx-tools/shared-core";
|
|
2
|
-
import { type ChatMessage, type ChatRole, openaiChat } from "@dbx-tools/shared-model";
|
|
3
1
|
/**
|
|
4
2
|
* Repairs Mastra / AI SDK message replays sent to Databricks Model
|
|
5
3
|
* Serving before they hit the OpenAI-compatible `/chat/completions`
|
|
6
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
|
|
7
14
|
*/
|
|
8
15
|
|
|
16
|
+
import { json, string } from "@dbx-tools/shared-core";
|
|
17
|
+
import {
|
|
18
|
+
type ChatMessage,
|
|
19
|
+
type ChatRole,
|
|
20
|
+
openaiChat,
|
|
21
|
+
openaiResponses,
|
|
22
|
+
} from "@dbx-tools/shared-model";
|
|
23
|
+
|
|
9
24
|
/**
|
|
10
25
|
* A chat message as it arrives on the serving wire, plus the extended-thinking
|
|
11
26
|
* fields Databricks-hosted Claude adds. The OpenAI-standard part of the shape
|
|
@@ -19,7 +34,9 @@ export interface ServingChatMessage extends ChatMessage {
|
|
|
19
34
|
reasoning_content?: unknown;
|
|
20
35
|
}
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
// Shared with the Responses sanitize path so both wire surfaces agree on what
|
|
38
|
+
// counts as a signed reasoning block.
|
|
39
|
+
const REASONING_PART_TYPES = openaiResponses.REASONING_TYPES;
|
|
23
40
|
|
|
24
41
|
/**
|
|
25
42
|
* Parse, sanitize, and re-serialize a `/serving-endpoints/...` POST
|
|
@@ -27,12 +44,8 @@ const REASONING_PART_TYPES = new Set(["reasoning", "thinking", "redacted_thinkin
|
|
|
27
44
|
* JSON or no rewrite was needed.
|
|
28
45
|
*/
|
|
29
46
|
export function rewriteServingBody(body: string): string {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
parsed = JSON.parse(body) as Record<string, unknown>;
|
|
33
|
-
} catch {
|
|
34
|
-
return body;
|
|
35
|
-
}
|
|
47
|
+
const parsed = json.parseRecord(body);
|
|
48
|
+
if (!parsed) return body;
|
|
36
49
|
|
|
37
50
|
// Runs regardless of `messages`: Databricks refuses to parse a body carrying
|
|
38
51
|
// an unknown top-level field, so this failure is not specific to a transcript.
|
package/src/storage-schema.ts
CHANGED
|
@@ -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
|
|
224
|
-
"threadsRoute
|
|
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
|
-
|
|
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
|
+
});
|