@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/src/config.ts CHANGED
@@ -1,16 +1,24 @@
1
1
  /**
2
2
  * Plugin configuration types and shared `RequestContext` keys.
3
3
  *
4
+ * Owns the typed {@link MastraPluginConfig} (the plugin's slice of AppKit
5
+ * config) and {@link MASTRA_CONFIG_SCHEMA}, the JSON Schema the manifest
6
+ * publishes for it so scaffolding tools and agents can read the option set.
7
+ *
8
+ * Precedence per field is explicit plugin config, then the environment
9
+ * variable named on the field, then a built-in default.
10
+ *
4
11
  * Kept in a leaf module so `plugin.ts`, `server.ts`, `model.ts`, and
5
12
  * `memory.ts` can import them without creating a cycle.
6
13
  *
7
14
  * @module
8
15
  */
9
16
 
10
- import type { BasePluginConfig } from "@databricks/appkit";
17
+ import { getExecutionContext, type BasePluginConfig, type ConfigSchema } from "@databricks/appkit";
11
18
  import { appkit } from "@dbx-tools/appkit";
12
19
  import type { BrandContext } from "@dbx-tools/shared-core";
13
20
  import type { AgentConfig } from "@mastra/core/agent";
21
+ import type { RequestContext } from "@mastra/core/request-context";
14
22
  import { MASTRA_RESOURCE_ID_KEY, MASTRA_THREAD_ID_KEY } from "@mastra/core/request-context";
15
23
  import type { PgVectorConfig, PostgresStoreConfig } from "@mastra/pg";
16
24
 
@@ -83,6 +91,28 @@ export interface User {
83
91
  executionContext: appkit.ExecutionContextLike;
84
92
  }
85
93
 
94
+ /**
95
+ * Canonical identity for an AppKit execution context: the OBO user id on a
96
+ * user-scoped call, the service principal id otherwise.
97
+ */
98
+ export function executionContextUserId(context: appkit.ExecutionContextLike): string {
99
+ return "userId" in context ? context.userId : context.serviceUserId;
100
+ }
101
+
102
+ /**
103
+ * Identity every per-user cache entry is namespaced under, so an OBO result
104
+ * cannot be read back by another caller.
105
+ *
106
+ * Prefers the {@link User} that {@link MastraServer} stamps on the request
107
+ * context. The MCP transport routes do not thread that context into tool
108
+ * execution, so those fall back to the ambient execution context (the active
109
+ * OBO scope, or the service principal).
110
+ */
111
+ export function resolveUserKey(requestContext?: RequestContext): string {
112
+ const user = requestContext?.get(MASTRA_USER_KEY) as User | undefined;
113
+ return user?.id ?? executionContextUserId(getExecutionContext());
114
+ }
115
+
86
116
  /** PgVector config with an optional Mastra store id. */
87
117
  export type MastraMemoryConfig = PgVectorConfig & {
88
118
  id?: string;
@@ -129,7 +159,7 @@ export interface MastraMcpConfig {
129
159
 
130
160
  /** Configuration accepted by the Mastra AppKit plugin. */
131
161
  export interface MastraPluginConfig extends BasePluginConfig {
132
- /** Mastra OpenAI-compatible provider id. Defaults to `"databricks"`. */
162
+ /** Mastra OpenAI-compatible provider id. Defaults to `"databricks"`; no env fallback. */
133
163
  providerId?: string;
134
164
  /**
135
165
  * PostgresStore for Mastra threads/messages. `true` reuses the
@@ -213,34 +243,40 @@ export interface MastraPluginConfig extends BasePluginConfig {
213
243
  * or `providerId`.
214
244
  *
215
245
  * Resolution order per agent: `def.model` → `defaultModel` →
216
- * built-in `/serving-endpoints` resolver.
246
+ * `DATABRICKS_SERVING_ENDPOINT_NAME` → built-in `/serving-endpoints`
247
+ * resolver.
217
248
  */
218
249
  defaultModel?: AgentConfig["model"] | string;
219
250
  /**
220
- * Allow loose model names (`"claude sonnet"`) to be fuzzy-matched
221
- * against the workspace's Model Serving endpoints. Defaults to
222
- * `true`; set `false` to require exact endpoint names everywhere.
251
+ * Fuzzy-match loose model names (`"claude sonnet"`) against the workspace's
252
+ * Model Serving endpoints. Defaults to `true`; no env fallback.
253
+ *
254
+ * Set `false` to require exact endpoint names everywhere.
223
255
  */
224
256
  modelFuzzyMatch?: boolean;
225
257
  /**
226
- * Fuse.js score threshold for the fuzzy matcher (0 = exact match,
227
- * 1 = anything matches). Defaults to `0.4`. Lower values reject
228
- * loose matches; raise it if you have a sprawling endpoint
229
- * catalogue with similar-looking names.
258
+ * Fuse.js score threshold for the fuzzy matcher, 0 (exact) to 1 (anything).
259
+ * Defaults to `0.4`; no env fallback.
260
+ *
261
+ * Lower values reject loose matches; raise it if you have a sprawling
262
+ * endpoint catalogue with similar-looking names.
230
263
  */
231
264
  modelFuzzyThreshold?: number;
232
265
  /**
233
- * TTL for the in-memory serving-endpoints list cache, in
234
- * milliseconds. Defaults to 5 minutes. The cache is per workspace
235
- * host and shared across users; concurrent callers coalesce on a
236
- * single in-flight fetch.
266
+ * TTL for the in-memory serving-endpoints list cache, in milliseconds.
267
+ * Defaults to 5 minutes; no env fallback.
268
+ *
269
+ * The cache is per workspace host and shared across users; concurrent
270
+ * callers coalesce on a single in-flight fetch.
237
271
  */
238
272
  modelCacheTtlMs?: number;
239
273
  /**
240
- * Allow clients to override the active model per request via the
241
- * `X-Mastra-Model` header, `?model=` query string, or `model` body
242
- * field. Defaults to `true`. Disable when running multi-tenant
243
- * where untrusted clients shouldn't pick the backing endpoint.
274
+ * Let clients pick the backing endpoint per request. Defaults to `true`;
275
+ * no env fallback.
276
+ *
277
+ * Reads the `X-Mastra-Model` header, the `?model=` query string, or a
278
+ * `model` body field, in that order. Disable when running multi-tenant
279
+ * where untrusted clients shouldn't choose the endpoint.
244
280
  */
245
281
  modelOverride?: boolean;
246
282
  /**
@@ -459,3 +495,119 @@ export interface MastraPluginConfig extends BasePluginConfig {
459
495
  */
460
496
  brand?: BrandContext;
461
497
  }
498
+
499
+ /**
500
+ * JSON Schema published on the manifest's `config.schema`, mirroring the
501
+ * documented defaults and environment fallbacks of {@link MastraPluginConfig}.
502
+ *
503
+ * Covers the JSON-expressible options only. `agents`, `tools`, and a
504
+ * `defaultModel` passed as a Mastra `DynamicArgument` are code-defined
505
+ * (functions / class instances), so they carry no schema entry; the
506
+ * `defaultModel` property below describes its string form.
507
+ */
508
+ export const MASTRA_CONFIG_SCHEMA: ConfigSchema = {
509
+ type: "object",
510
+ properties: {
511
+ providerId: {
512
+ type: "string",
513
+ description: 'Mastra OpenAI-compatible provider id. Defaults to "databricks".',
514
+ },
515
+ storage: {
516
+ type: ["boolean", "object"],
517
+ description:
518
+ "PostgresStore for Mastra threads / messages. `true` reuses the `lakebase` plugin's pool, an object opens a dedicated store. Auto-enabled when the `lakebase` plugin is registered.",
519
+ },
520
+ memory: {
521
+ type: ["boolean", "object"],
522
+ description:
523
+ "PgVector store for Mastra semantic recall. `true` reuses the `lakebase` plugin's pool, an object opens a dedicated store. Auto-enabled when the `lakebase` plugin is registered.",
524
+ },
525
+ defaultAgent: {
526
+ type: "string",
527
+ description:
528
+ "Agent id used when the client names none. Defaults to the first registered agent, else the built-in `default`.",
529
+ },
530
+ defaultModel: {
531
+ type: "string",
532
+ description:
533
+ "Serving endpoint applied to every agent that omits its own model. Falls back to DATABRICKS_SERVING_ENDPOINT_NAME, then the auto-resolved catalogue.",
534
+ },
535
+ defaultModelFallbacks: {
536
+ type: "array",
537
+ items: { type: "string" },
538
+ description:
539
+ "Priority-ordered endpoint names tried before the score-classified catalogue when nothing else pins a model.",
540
+ },
541
+ modelFuzzyMatch: {
542
+ type: "boolean",
543
+ description:
544
+ "Fuzzy-match loose model names against the workspace catalogue. Defaults to true.",
545
+ },
546
+ modelFuzzyThreshold: {
547
+ type: "number",
548
+ description:
549
+ "Fuse.js score threshold for the fuzzy matcher, 0 (exact) to 1 (anything). Defaults to 0.4.",
550
+ },
551
+ modelCacheTtlMs: {
552
+ type: "number",
553
+ description:
554
+ "TTL in ms for the serving-endpoints list cache, per workspace host. Defaults to 5 minutes.",
555
+ },
556
+ modelOverride: {
557
+ type: "boolean",
558
+ description:
559
+ "Honor a per-request model override from the X-Mastra-Model header, ?model= query, or a `model` body field. Defaults to true.",
560
+ },
561
+ genieSpaces: {
562
+ type: "object",
563
+ additionalProperties: { type: ["string", "object"] },
564
+ description:
565
+ "Genie spaces the agents can delegate to, keyed by alias (the tool-name suffix). Each value is a space id or `{ spaceId, hint }`. Falls back to the `genie` plugin's own `spaces` config, then DATABRICKS_GENIE_SPACE_ID under the `default` alias.",
566
+ },
567
+ genieSpaceCacheTtlMs: {
568
+ type: "number",
569
+ description: "TTL in ms for the Genie space metadata cache. Defaults to 5 minutes.",
570
+ },
571
+ agentMaxSteps: {
572
+ type: "number",
573
+ description:
574
+ "Maximum LLM steps each agent gets per turn (a tool call and the final reply each consume one). Defaults to 25.",
575
+ },
576
+ stripStaleCharts: {
577
+ type: "boolean",
578
+ description:
579
+ "Strip chartIds from recalled assistant tool results so the model cannot reuse a turn-scoped chart marker. Defaults to true.",
580
+ },
581
+ styleInstructions: {
582
+ type: ["string", "boolean"],
583
+ description:
584
+ "Style guardrails appended to every agent's instructions. A string replaces the built-in block, `false` disables it.",
585
+ },
586
+ observability: {
587
+ type: "boolean",
588
+ description:
589
+ "Bridge Mastra spans into AppKit's OTel pipeline. Defaults to on only when OTEL_EXPORTER_OTLP_ENDPOINT or OTEL_EXPORTER_OTLP_TRACES_ENDPOINT is set.",
590
+ },
591
+ feedback: {
592
+ type: "boolean",
593
+ description:
594
+ "Log thumbs / comment assessments to MLflow and surface the feedback controls. Defaults to on only when an OTLP endpoint and MLFLOW_EXPERIMENT_ID or MLFLOW_EXPERIMENT_NAME are set.",
595
+ },
596
+ mcp: {
597
+ type: ["boolean", "object"],
598
+ description:
599
+ "Expose the registered agents over MCP. Defaults to agents-only; an object tunes the server id, advertised metadata, and whether ambient tools are exposed.",
600
+ },
601
+ apiAccess: {
602
+ type: "string",
603
+ enum: ["scoped", "full"],
604
+ description:
605
+ 'How much of the stock @mastra/express API is reachable through the mount. "scoped" (default) allows agent inference, read-only agent metadata, this plugin\'s own routes, and MCP; "full" dispatches the entire management surface.',
606
+ },
607
+ brand: {
608
+ type: "object",
609
+ description:
610
+ "Portable brand context applied to generated charts (series palette from colors.primary / colors.accent, base font from typography.sans).",
611
+ },
612
+ },
613
+ };
@@ -0,0 +1,133 @@
1
+ /**
2
+ * Interceptor defaults for every outbound call the plugin's own routes make
3
+ * through `Plugin.execute()`.
4
+ *
5
+ * One constant per call site so the cache / retry / timeout decision lives
6
+ * beside its reasoning instead of at the call site. The interceptor chain runs
7
+ * telemetry, timeout, retry, cache, so a disabled interceptor is a deliberate
8
+ * choice and is commented as such.
9
+ *
10
+ * The cache interceptor is inert without a `cache.cacheKey`, which is
11
+ * request-specific (a statement id, an agent id). Call sites therefore spread
12
+ * the constant and add the key; the identity namespace comes from the
13
+ * execution context's user id, which `execute()` resolves on its own.
14
+ *
15
+ * Cancellation is not expressed here: `execute()` derives a signal from the
16
+ * `timeout` setting only, so a caller's own signal is combined with it at the
17
+ * call site instead.
18
+ *
19
+ * @module
20
+ */
21
+
22
+ /**
23
+ * Per-call interceptor settings. Structural stand-in for AppKit's
24
+ * `PluginExecuteConfig`: the barrel exports `StreamExecutionSettings` but not
25
+ * the non-streaming config, so the nominal type is unimportable here. Declared
26
+ * as a type alias (not an interface) so it keeps the implicit index signature
27
+ * AppKit's own config carries.
28
+ */
29
+ type ExecuteConfig = {
30
+ cache?: { enabled?: boolean; ttl?: number; cacheKey?: (string | number | object)[] };
31
+ retry?: { enabled?: boolean; attempts?: number; initialDelay?: number; maxDelay?: number };
32
+ timeout?: number;
33
+ };
34
+
35
+ /** Default plus optional user-scoped settings, as `execute()` accepts them. */
36
+ type ExecuteSettings = {
37
+ default: ExecuteConfig;
38
+ user?: ExecuteConfig;
39
+ };
40
+
41
+ /** Ceiling on a single Model Serving / Genie / MLflow REST round-trip. */
42
+ const REST_TIMEOUT_MS = 30_000;
43
+
44
+ /** Ceiling on one Statement Execution fetch, which can page a large result. */
45
+ const STATEMENT_TIMEOUT_MS = 60_000;
46
+
47
+ /** TTL for a cached statement result set, in seconds. */
48
+ const STATEMENT_CACHE_TTL_SEC = 5 * 60;
49
+
50
+ /**
51
+ * `GET /models`: the workspace's Model Serving catalogue.
52
+ */
53
+ export const modelCatalogueDefaults: ExecuteSettings = {
54
+ default: {
55
+ // Cache disabled here because `listServingEndpoints` already memoizes the
56
+ // catalogue through the same `CacheManager`, keyed by workspace host with
57
+ // `config.modelCacheTtlMs`. That entry is shared across identities; a
58
+ // second interceptor cache would duplicate it once per user.
59
+ cache: { enabled: false },
60
+ // Retry enabled: listing endpoints is an idempotent GET, and the listing
61
+ // is on the chat UI's load path, so a transient 5xx should not blank the
62
+ // model picker.
63
+ retry: { enabled: true, attempts: 3 },
64
+ timeout: REST_TIMEOUT_MS,
65
+ },
66
+ };
67
+
68
+ /**
69
+ * `GET /suggestions`: the curated `sample_questions` on each Genie space.
70
+ */
71
+ export const genieSuggestionDefaults: ExecuteSettings = {
72
+ default: {
73
+ // Cache disabled here because `collectSpaceSuggestions` keeps its own
74
+ // space-id-keyed entry; the questions are authored config, identical for
75
+ // every caller, so caching them per identity would only fan out copies.
76
+ cache: { enabled: false },
77
+ // Retry enabled: reading space metadata is idempotent, and the route
78
+ // degrades to an empty starter list on failure, so one retry is cheaper
79
+ // than an empty chat state.
80
+ retry: { enabled: true, attempts: 2 },
81
+ timeout: REST_TIMEOUT_MS,
82
+ },
83
+ };
84
+
85
+ /**
86
+ * `GET /embed/data/:id`: one Statement Execution result set.
87
+ */
88
+ export const statementDataDefaults: ExecuteSettings = {
89
+ default: {
90
+ // Cache enabled: a completed statement's result set is immutable, and the
91
+ // same id is re-fetched every time the transcript re-renders a
92
+ // `[data:<statement_id>]` marker. The entry is namespaced by the caller's
93
+ // identity, so an OBO result never crosses users.
94
+ cache: { enabled: true, ttl: STATEMENT_CACHE_TTL_SEC },
95
+ // Retry enabled: fetching a statement by id is idempotent.
96
+ retry: { enabled: true, attempts: 3 },
97
+ timeout: STATEMENT_TIMEOUT_MS,
98
+ },
99
+ };
100
+
101
+ /**
102
+ * `GET /embed/chart/:id`: the cached chart entry behind a `[chart:<id>]`
103
+ * marker.
104
+ */
105
+ export const chartFetchDefaults: ExecuteSettings = {
106
+ default: {
107
+ // Cache disabled: the read long-polls a cache entry that is expected to
108
+ // change from processing to settled, so a cached answer would pin the
109
+ // client to the pre-planner state.
110
+ cache: { enabled: false },
111
+ // Retry disabled: the helper already polls to its own deadline, so a retry
112
+ // would only restart a wait the caller is already inside.
113
+ retry: { enabled: false },
114
+ // No timeout: the poll budget is per request (`?timeoutMs=`, capped by the
115
+ // route) and cancellation rides the connection's abort signal, so a second
116
+ // ceiling here could cut a legitimate long-poll short.
117
+ },
118
+ };
119
+
120
+ /**
121
+ * `POST /route/feedback`: an MLflow trace assessment.
122
+ */
123
+ export const feedbackWriteDefaults: ExecuteSettings = {
124
+ default: {
125
+ // Cache disabled: a write, and each submission is a distinct assessment.
126
+ cache: { enabled: false },
127
+ // Retry disabled: posting an assessment is not idempotent (a retry records
128
+ // a duplicate), and `logFeedback` already retries the one recoverable case
129
+ // itself, the trace not having finished exporting to MLflow yet.
130
+ retry: { enabled: false },
131
+ timeout: REST_TIMEOUT_MS,
132
+ },
133
+ };
@@ -14,9 +14,20 @@
14
14
  */
15
15
 
16
16
  import { posix as path } from "node:path";
17
- import { getExecutionContext } from "@databricks/appkit";
17
+ import { ExecutionError, getExecutionContext, ValidationError } from "@databricks/appkit";
18
18
  import { WorkspaceClient } from "@databricks/sdk-experimental";
19
19
  import { error, functionModule, hash, log } from "@dbx-tools/shared-core";
20
+ import {
21
+ DirectoryNotEmptyError,
22
+ DirectoryNotFoundError,
23
+ FileExistsError,
24
+ FileNotFoundError,
25
+ IsDirectoryError,
26
+ MastraFilesystem,
27
+ NotDirectoryError,
28
+ PermissionError,
29
+ WorkspaceReadOnlyError,
30
+ } from "@mastra/core/workspace";
20
31
  import type {
21
32
  CopyOptions,
22
33
  FileContent,
@@ -24,20 +35,11 @@ import type {
24
35
  FileStat,
25
36
  FilesystemInfo,
26
37
  ListOptions,
38
+ MastraFilesystemOptions,
27
39
  ProviderStatus,
28
40
  ReadOptions,
29
41
  RemoveOptions,
30
42
  WriteOptions,
31
- DirectoryNotEmptyError,
32
- DirectoryNotFoundError,
33
- FileExistsError,
34
- FileNotFoundError,
35
- IsDirectoryError,
36
- MastraFilesystem,
37
- NotDirectoryError,
38
- PermissionError,
39
- WorkspaceReadOnlyError,
40
- type MastraFilesystemOptions,
41
43
  } from "@mastra/core/workspace";
42
44
 
43
45
  /* ------------------------------ constants ------------------------------ */
@@ -99,7 +101,11 @@ export interface DatabricksWorkspaceFilesystemOptions extends MastraFilesystemOp
99
101
  export function normalizeDatabricksBasePath(basePath: string): string {
100
102
  const trimmed = basePath.trim();
101
103
  if (!trimmed.startsWith("/")) {
102
- throw new Error(`Databricks base path must be absolute: ${basePath}`);
104
+ throw ValidationError.invalidValue(
105
+ "basePath",
106
+ basePath,
107
+ "an absolute Databricks path, e.g. /Volumes/catalog/schema/volume",
108
+ );
103
109
  }
104
110
  return trimmed.replace(/\/+$/, "") || "/";
105
111
  }
@@ -295,8 +301,16 @@ export class DatabricksWorkspaceFilesystem extends MastraFilesystem {
295
301
  if (ErrorType) {
296
302
  throw new ErrorType(workspacePath);
297
303
  }
298
- const message = error.errorMessage(err);
299
- throw new Error(`Databricks filesystem ${workspacePath}: ${message}`);
304
+ // The upstream message is logged rather than raised: it reaches the model
305
+ // (and from there the chat transcript) as the tool's failure text.
306
+ logger.warn("operation-failed", {
307
+ path: workspacePath,
308
+ error: error.errorMessage(err),
309
+ });
310
+ throw new ExecutionError(`Databricks filesystem operation failed for ${workspacePath}`, {
311
+ cause: error.toError(err),
312
+ context: { path: workspacePath },
313
+ });
300
314
  }
301
315
 
302
316
  /* --- lifecycle --- */
@@ -516,7 +530,7 @@ export class DatabricksWorkspaceFilesystem extends MastraFilesystem {
516
530
  const created = await this.client.dbfs.create({ path: absolutePath, overwrite });
517
531
  const handle = created.handle;
518
532
  if (handle === undefined) {
519
- throw new Error(`DBFS create did not return a handle for ${absolutePath}`);
533
+ throw ExecutionError.missingData("DBFS upload handle");
520
534
  }
521
535
  for (let offset = 0; offset < buffer.length; offset += DBFS_PUT_MAX_BYTES) {
522
536
  const slice = buffer.subarray(offset, offset + DBFS_PUT_MAX_BYTES);