@dbx-tools/appkit-mastra 0.1.41 → 0.1.42

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/plugin.ts CHANGED
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * AppKit plugin that builds one or more Mastra `Agent` instances and
3
- * mounts the `@mastra/express` server plus `@mastra/ai-sdk` `chatRoute`
4
- * handlers. The UI message stream matches what `chatRoute()` emits, so
5
- * the client can use `useChat()` from `@ai-sdk/react` without custom
6
- * parsing.
3
+ * mounts the `@mastra/express` server. Clients drive the conversation
4
+ * over the standard Mastra agent stream (`@mastra/client-js`'s
5
+ * `getAgent(id).stream()`), so there's no bespoke chat transport to
6
+ * keep in sync.
7
7
  *
8
8
  * - Agents: registered through `config.agents` at plugin creation
9
9
  * ({@link MastraAgentDefinition}). Each entry's `tools` field accepts
@@ -21,10 +21,11 @@
21
21
  * `schemaName: "mastra_<agentId>"`; the vector store is a single
22
22
  * shared singleton across every agent.
23
23
  * - Server: the Express subapp wiring lives in `./server.js`.
24
- * - HTTP: AppKit mounts this plugin under `/api/mastra`. `chatRoute`
25
- * is registered at `/route/chat` (bound to `config.defaultAgent` or
26
- * the first registered id) and `/route/chat/:agentId`, so the
27
- * AI SDK transport URL is `/api/mastra/route/chat/<agentId>`.
24
+ * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
25
+ * Mastra agent routes, the plugin registers `/route/history`
26
+ * (load + clear thread history), `/models`, `/suggestions`, and the
27
+ * generic `/embed/:type/:id` resolver for inline chart / data
28
+ * markers.
28
29
  */
29
30
 
30
31
  import {
@@ -38,16 +39,21 @@ import {
38
39
  type ResourceRequirement,
39
40
  } from "@databricks/appkit";
40
41
  import { appkitUtils, commonUtils, logUtils } from "@dbx-tools/shared";
41
- import { chatRoute } from "@mastra/ai-sdk";
42
42
  import type { Agent } from "@mastra/core/agent";
43
43
  import { Mastra } from "@mastra/core/mastra";
44
44
  import express from "express";
45
45
  import type { Pool } from "pg";
46
46
 
47
- import type {
48
- MastraClientConfig,
49
- StatementData,
47
+ import {
48
+ MASTRA_ROUTES,
49
+ type MastraClientConfig,
50
+ type StatementData,
50
51
  } from "@dbx-tools/appkit-mastra-shared";
52
+ import {
53
+ clearServingEndpointsCache,
54
+ listServingEndpoints,
55
+ type ServingEndpointSummary,
56
+ } from "@dbx-tools/model";
51
57
  import { buildAgents, FALLBACK_AGENT_ID, type BuiltAgents } from "./agents.js";
52
58
  import { fetchChart } from "./chart.js";
53
59
  import type { MastraPluginConfig } from "./config.js";
@@ -60,12 +66,7 @@ import {
60
66
  } from "./memory.js";
61
67
  import { buildObservability } from "./observability.js";
62
68
  import { attachRoutePatchMiddleware, MastraServer } from "./server.js";
63
- import {
64
- clearServingEndpointsCache,
65
- listServingEndpoints,
66
- resolveServingConfig,
67
- type ServingEndpointSummary,
68
- } from "./serving.js";
69
+ import { resolveServingConfig } from "./serving.js";
69
70
  import {
70
71
  fetchStatementData,
71
72
  isStatementNotFoundError,
@@ -198,9 +199,9 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
198
199
  */
199
200
  get: (id: string): Agent | null => this.built?.agents[id] ?? null,
200
201
  /**
201
- * The agent `chatRoute` binds to when the client doesn't name
202
- * one. Resolves to `config.defaultAgent`, the first registered
203
- * id, or the built-in `default` fallback.
202
+ * The agent the client converses with when it doesn't name one.
203
+ * Resolves to `config.defaultAgent`, the first registered id, or
204
+ * the built-in `default` fallback.
204
205
  */
205
206
  getDefault: (): Agent | null =>
206
207
  (this.built && this.built.agents[this.built.defaultAgentId]) ?? null,
@@ -229,22 +230,16 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
229
230
 
230
231
  override clientConfig(): Record<string, unknown> {
231
232
  // AppKit mounts every plugin at `/api/<plugin.name>`. `this.name`
232
- // honors `config.name` overrides, so the published paths stay
233
- // accurate if someone remounts the plugin under a custom id.
233
+ // honors `config.name` overrides, so publishing `basePath` is
234
+ // enough for the client to stay correct under a custom mount id -
235
+ // the per-route segments are fixed (`MASTRA_ROUTES`) and the
236
+ // client (`MastraPluginClient`) derives every endpoint from
237
+ // `basePath`.
234
238
  // Return widens to `Record<string, unknown>` to satisfy the
235
239
  // base-class signature; consumers read it through the typed
236
240
  // `MastraClientConfig` shape via `usePluginClientConfig<...>(...)`.
237
- const basePath = `/api/${this.name}`;
238
241
  const config: MastraClientConfig = {
239
- basePath,
240
- chatPath: `${basePath}/route/chat`,
241
- chatPathTemplate: `${basePath}/route/chat/:agentId`,
242
- modelsPath: `${basePath}/models`,
243
- historyPath: `${basePath}/route/history`,
244
- historyPathTemplate: `${basePath}/route/history/:agentId`,
245
- embedPathTemplate: `${basePath}/embed/:type/:id`,
246
- suggestionsPath: `${basePath}/suggestions`,
247
- suggestionsPathTemplate: `${basePath}/suggestions/:agentId`,
242
+ basePath: `/api/${this.name}`,
248
243
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
249
244
  agents: Object.keys(this.built?.agents ?? {}),
250
245
  };
@@ -257,7 +252,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
257
252
  // be registered before the catch-all that forwards everything to
258
253
  // the Mastra subapp. Errors propagate to Express's default error
259
254
  // handler via `next(err)` so callers see the real SDK message.
260
- router.get("/models", (req, res, next) => {
255
+ router.get(MASTRA_ROUTES.models, (req, res, next) => {
261
256
  this.userScopedSelf(req)
262
257
  .listModels()
263
258
  .then((endpoints) => res.json({ endpoints }))
@@ -311,7 +306,7 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
311
306
  },
312
307
  };
313
308
 
314
- router.get("/embed/:type/:id", (req, res, next) => {
309
+ router.get(`${MASTRA_ROUTES.embed}/:type/:id`, (req, res, next) => {
315
310
  const type = req.params["type"] ?? "";
316
311
  const id = req.params["id"];
317
312
  const resolve = embedResolvers[type];
@@ -367,8 +362,8 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
367
362
  res.json({ questions: [] });
368
363
  });
369
364
  };
370
- router.get("/suggestions", handleSuggestions);
371
- router.get("/suggestions/:agentId", handleSuggestions);
365
+ router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
366
+ router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
372
367
 
373
368
  router.use((req, res, next) => {
374
369
  if (!this.mastraApp) return res.status(503).end();
@@ -553,20 +548,28 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
553
548
  mastra: this.mastra,
554
549
  prefix: "",
555
550
  customApiRoutes: [
556
- chatRoute({ path: "/route/chat", agent: this.built.defaultAgentId }),
557
- chatRoute({ path: "/route/chat/:agentId" }),
558
551
  // `historyRoute` registers both GET (load) and DELETE
559
552
  // (clear) on the same path, so it returns an array we
560
553
  // splice in.
561
- ...historyRoute({ path: "/route/history", agent: this.built.defaultAgentId }),
562
- ...historyRoute({ path: "/route/history/:agentId" }),
554
+ ...historyRoute({
555
+ path: MASTRA_ROUTES.history,
556
+ agent: this.built.defaultAgentId,
557
+ }),
558
+ // Assert the `:agentId` template type: the per-package build's
559
+ // NodeNext resolution widens the imported `MASTRA_ROUTES.history`
560
+ // to `string` (the source/bundler typecheck keeps it a literal),
561
+ // which would otherwise drop this out of the dynamic-agent
562
+ // overload and demand a fixed `agent`.
563
+ ...historyRoute({
564
+ path: `${MASTRA_ROUTES.history}/:agentId` as `${string}:agentId`,
565
+ }),
563
566
  ],
564
567
  });
565
568
  await this.mastraServer.init();
566
569
  this.log.debug("build:done", {
567
570
  agents: Object.keys(this.built.agents),
568
571
  defaultAgent: this.built.defaultAgentId,
569
- routes: ["/route/chat", "/route/history", "/models"],
572
+ routes: ["/route/history", "/models", "/suggestions", "/embed/:type/:id"],
570
573
  instanceStorage: instanceStorage !== undefined,
571
574
  observability: observability !== undefined ? "mlflow" : "off",
572
575
  });
package/src/server.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  /**
2
2
  * Express-layer plumbing for the Mastra plugin: a `MastraServer` that
3
3
  * stamps the per-request `RequestContext`, and a route-patch middleware
4
- * that lets `@mastra/ai-sdk` `chatRoute` work behind an Express mount
5
- * point.
4
+ * that lets the plugin's custom API routes (e.g. `historyRoute`) work
5
+ * behind an Express mount point.
6
6
  */
7
7
 
8
8
  import { getExecutionContext } from "@databricks/appkit";
@@ -172,30 +172,23 @@ export class MastraServer extends MastraServerExpress {
172
172
  }
173
173
 
174
174
  /**
175
- * Patches around `@mastra/express`'s custom-route dispatcher so
176
- * `chatRoute` works when `MastraServer` is hosted on an Express subapp
177
- * mounted under a parent path (e.g. `/api/mastra`).
175
+ * Patches around `@mastra/express`'s custom-route dispatcher so the
176
+ * plugin's custom API routes (e.g. `historyRoute`) work when
177
+ * `MastraServer` is hosted on an Express subapp mounted under a parent
178
+ * path (e.g. `/api/mastra`).
178
179
  *
179
- * Two concerns:
180
- *
181
- * 1. The adapter's `registerCustomApiRoutes` matches against `req.path`
182
- * (mount-relative, correct) but dispatches to its internal Hono
183
- * mini-app using `req.originalUrl`, which still contains the parent
184
- * mount prefix. The Hono app registers the literal `chatRoute` paths
185
- * (for example `/route/chat`), so the absolute URL never matches
186
- * until we overwrite `originalUrl` for `/route` and `/route/*` to
187
- * the mount-relative path.
188
- *
189
- * 2. `memory.resource` must be the authenticated user, not whatever the
190
- * client posts. The custom-route forwarder re-serializes `req.body`
191
- * into the Request body it hands Hono, so mutating the parsed body
192
- * here would propagate into `handleChatStream`'s params (kept for
193
- * future use; `express.json()` runs first so `req.body` is parsed).
180
+ * The adapter's `registerCustomApiRoutes` matches against `req.path`
181
+ * (mount-relative, correct) but dispatches to its internal Hono
182
+ * mini-app using `req.originalUrl`, which still contains the parent
183
+ * mount prefix. The Hono app registers the literal route paths
184
+ * (for example `/route/history`), so the absolute URL never matches
185
+ * until we overwrite `originalUrl` for `/route` and `/route/*` to the
186
+ * mount-relative path.
194
187
  */
195
188
  export function attachRoutePatchMiddleware(app: express.Express): void {
196
189
  app.use((req, _res, next) => {
197
- const isChat = req.path === "/route" || req.path.startsWith("/route/");
198
- if (!isChat) return next();
190
+ const isCustomRoute = req.path === "/route" || req.path.startsWith("/route/");
191
+ if (!isCustomRoute) return next();
199
192
  req.originalUrl = req.path;
200
193
  next();
201
194
  });
package/src/serving.ts CHANGED
@@ -1,37 +1,19 @@
1
1
  /**
2
- * Dynamic model resolution against Databricks Model Serving.
2
+ * Mastra-specific glue over the generic `@dbx-tools/model` toolkit.
3
3
  *
4
- * Turns loose, human-typed model names into real serving-endpoint ids
5
- * so the same agent can target different endpoints without code
6
- * changes. The workspace's `/serving-endpoints` list is fetched once
7
- * per host and cached with a TTL, with concurrent callers sharing one
8
- * in-flight promise (the coalescing pattern of Python's
9
- * `cachetools-async`). Names are matched through `fuse.js` extended
10
- * search so tokens like `"claude sonnet"` snap to
11
- * `databricks-claude-sonnet-4-6`, and a per-request override can be
12
- * pulled from the request header, query string, or body so a model
13
- * can be swapped per call without redeploying.
4
+ * The live `/serving-endpoints` catalogue access, fuzzy name
5
+ * resolution, and tier/fallback selection all live in
6
+ * `@dbx-tools/model`; this module only adds what is specific to the
7
+ * Mastra plugin: pulling a per-request model override off an HTTP
8
+ * request (header / query / body) and projecting the plugin config
9
+ * onto the knobs `buildModel` and the `/models` route share.
14
10
  */
15
11
 
16
- import { CacheManager, type getExecutionContext } from "@databricks/appkit";
17
- import { logUtils, stringUtils } from "@dbx-tools/shared";
18
- import Fuse from "fuse.js";
12
+ import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS } from "@dbx-tools/model";
13
+ import { stringUtils } from "@dbx-tools/shared";
19
14
 
20
- import type { ServingEndpointSummary } from "@dbx-tools/appkit-mastra-shared";
21
15
  import type { MastraPluginConfig } from "./config.js";
22
16
 
23
- export type { ServingEndpointSummary };
24
-
25
- const log = logUtils.logger("mastra/serving");
26
-
27
- /**
28
- * Structural type for the Databricks workspace client. Derived from
29
- * AppKit's `ExecutionContext` so this module doesn't take a direct
30
- * dependency on `@databricks/sdk-experimental`; the dep flows in
31
- * transitively through `@databricks/appkit`.
32
- */
33
- type WorkspaceClientLike = ReturnType<typeof getExecutionContext>["client"];
34
-
35
17
  /**
36
18
  * `RequestContext` key under which {@link MastraServer} stores the
37
19
  * per-request model override (header / query / body). `model.ts`
@@ -48,187 +30,6 @@ export const MODEL_OVERRIDE_QUERY = "model";
48
30
  /** Body fields (in priority order) inspected for a per-request model override. */
49
31
  export const MODEL_OVERRIDE_BODY_FIELDS = ["model", "modelId"] as const;
50
32
 
51
- /** Default TTL for the in-memory endpoint cache. Matches the Databricks SDK's session lifetime budget. */
52
- const DEFAULT_TTL_MS = 5 * 60 * 1000;
53
-
54
- /** Default Fuse.js score threshold below which a fuzzy match is accepted. */
55
- const DEFAULT_FUZZY_THRESHOLD = 0.4;
56
-
57
- /** Cache key parts under which endpoint listings are stored. */
58
- const CACHE_KEY_NAMESPACE = "mastra:serving-endpoints";
59
-
60
- /**
61
- * Stable `userKey` arg for AppKit's `CacheManager.getOrExecute`.
62
- * Endpoint visibility is effectively workspace-scoped (we cache by
63
- * host in the key parts), so a single shared key lets every user of
64
- * the same workspace share one cached fetch and coalesce on the
65
- * in-flight promise. Permissions can differ in theory, but the
66
- * Foundation Model API catalogue is the same view for every caller.
67
- */
68
- const SHARED_USER_KEY = "mastra-shared";
69
-
70
- /**
71
- * List Model Serving endpoints for the workspace owning `client`,
72
- * routed through AppKit's `CacheManager`. The manager gives us
73
- * everything `cachetools.TTLCache` provides plus what
74
- * `cachetools-async` adds on top: per-entry TTL, in-flight request
75
- * coalescing (concurrent callers share one fetch via the manager's
76
- * internal `inFlightRequests` map), bounded size, telemetry spans
77
- * (`cache.getOrExecute`), and optional Lakebase persistence so the
78
- * catalogue survives restarts when the lakebase plugin is wired up.
79
- *
80
- * Returns plain {@link ServingEndpointSummary} objects (a stable
81
- * subset of the SDK type) so cache hits never expose stale SDK
82
- * internals. Errors from `CacheManager` or the SDK fetch propagate
83
- * to the caller - we don't swallow them so users see the real
84
- * auth / network issue.
85
- *
86
- * @param host - Workspace host used as the cache key. Pass the value
87
- * resolved from `client.config.getHost()` so multi-host apps share
88
- * one entry per workspace.
89
- * @param opts.ttlMs - Override the default TTL just for this call.
90
- * Forwarded to `CacheManager` as seconds.
91
- */
92
- export async function listServingEndpoints(
93
- client: WorkspaceClientLike,
94
- host: string,
95
- opts: { ttlMs?: number } = {},
96
- ): Promise<ServingEndpointSummary[]> {
97
- const ttlSec = Math.max(1, Math.round((opts.ttlMs ?? DEFAULT_TTL_MS) / 1000));
98
- return CacheManager.getInstanceSync().getOrExecute(
99
- [CACHE_KEY_NAMESPACE, host],
100
- () => fetchEndpoints(client),
101
- SHARED_USER_KEY,
102
- { ttl: ttlSec },
103
- );
104
- }
105
-
106
- async function fetchEndpoints(
107
- client: WorkspaceClientLike,
108
- ): Promise<ServingEndpointSummary[]> {
109
- const startedAt = Date.now();
110
- const out: ServingEndpointSummary[] = [];
111
- for await (const ep of client.servingEndpoints.list()) {
112
- if (!ep.name) continue;
113
- out.push({
114
- name: ep.name,
115
- ...(ep.task !== undefined ? { task: ep.task } : {}),
116
- ...(ep.state?.ready !== undefined ? { state: String(ep.state.ready) } : {}),
117
- ...(ep.description !== undefined ? { description: ep.description } : {}),
118
- });
119
- }
120
- log.debug("listed", { count: out.length, elapsedMs: Date.now() - startedAt });
121
- return out;
122
- }
123
-
124
- /**
125
- * Force-evict cached endpoint listings via AppKit's `CacheManager`.
126
- * With a `host` deletes that one workspace's entry; without one
127
- * clears every cache entry on the manager (since `CacheManager`
128
- * doesn't expose a namespace-scoped clear, this is the brute-force
129
- * path - fine for tests, avoid in steady-state code).
130
- */
131
- export async function clearServingEndpointsCache(host?: string): Promise<void> {
132
- const cache = CacheManager.getInstanceSync();
133
- if (host) {
134
- const key = cache.generateKey([CACHE_KEY_NAMESPACE, host], SHARED_USER_KEY);
135
- await cache.delete(key);
136
- } else {
137
- await cache.clear();
138
- }
139
- }
140
-
141
- /**
142
- * Result of fuzzy-resolving a user-supplied model name against the
143
- * live endpoint list. `score` is Fuse.js's distance (`0` is exact,
144
- * `1` is no match); `matched` is `false` when the score exceeds the
145
- * configured threshold so callers can fall back to the original
146
- * input (Databricks will then return a clean 404).
147
- */
148
- export interface ResolvedModel {
149
- modelId: string;
150
- matched: boolean;
151
- score?: number;
152
- }
153
-
154
- /** Options accepted by {@link resolveModelId}. */
155
- export interface ResolveModelOptions {
156
- /** Fuse.js threshold (0 = exact, 1 = anything). Default `0.4`. */
157
- threshold?: number;
158
- }
159
-
160
- /**
161
- * Snap a user-supplied model name to the closest configured serving
162
- * endpoint:
163
- *
164
- * 1. Exact name match wins immediately (no fuzzy needed).
165
- * 2. Otherwise the input is tokenized (dashes / underscores / spaces
166
- * become separators) and fed through Fuse.js extended search,
167
- * which AND-s each token with fuzzy matching enabled. This is the
168
- * "tokenized fuzzy match" the user reaches for when they type
169
- * `"claude sonnet"` instead of the full endpoint name.
170
- * 3. If the best Fuse score is above `threshold`, return the input
171
- * unchanged and let the upstream call surface the 404. This keeps
172
- * deliberate model ids (e.g. brand new endpoints) from being
173
- * silently rewritten to a similar-looking neighbour.
174
- *
175
- * Pass an empty endpoint list to short-circuit fuzzy matching - the
176
- * input is returned verbatim. This is what {@link buildModel} does
177
- * when the workspace client can't be reached at resolve time.
178
- */
179
- export function resolveModelId(
180
- input: string,
181
- endpoints: readonly ServingEndpointSummary[],
182
- opts: ResolveModelOptions = {},
183
- ): ResolvedModel {
184
- if (endpoints.length === 0) {
185
- log.debug("resolve:no-endpoints", { input });
186
- return { modelId: input, matched: false };
187
- }
188
- for (const ep of endpoints) {
189
- if (ep.name === input) {
190
- log.debug("resolve:exact", { input });
191
- return { modelId: ep.name, matched: true, score: 0 };
192
- }
193
- }
194
- const threshold = opts.threshold ?? DEFAULT_FUZZY_THRESHOLD;
195
- const fuse = new Fuse(endpoints, {
196
- keys: ["name"],
197
- threshold,
198
- ignoreLocation: true,
199
- includeScore: true,
200
- useExtendedSearch: true,
201
- isCaseSensitive: false,
202
- });
203
- // Fuse 7.3 has no built-in tokenize hook; in extended search,
204
- // space-separated tokens are AND-ed with fuzzy matching enabled. We
205
- // lean on the shared tokenizer so the splitting rules stay
206
- // consistent with the rest of the toolkit.
207
- const query = Array.from(
208
- stringUtils.tokenizeWithOptions({ lowerCase: true, camelCase: false }, input),
209
- ).join(" ");
210
- if (!query) {
211
- log.debug("resolve:empty-tokens", { input });
212
- return { modelId: input, matched: false };
213
- }
214
- const results = fuse.search(query);
215
- const best = results[0];
216
- if (best?.item.name && (best.score ?? 0) <= threshold) {
217
- log.debug("resolve:fuzzy-match", {
218
- input,
219
- modelId: best.item.name,
220
- score: best.score,
221
- });
222
- return { modelId: best.item.name, matched: true, score: best.score };
223
- }
224
- log.debug("resolve:no-match", {
225
- input,
226
- bestScore: best?.score,
227
- threshold,
228
- });
229
- return { modelId: input, matched: false };
230
- }
231
-
232
33
  /**
233
34
  * Minimal Express-ish request shape used by {@link extractModelOverride}.
234
35
  * Keeps this module independent of `express` so the helper can be
@@ -278,18 +79,15 @@ export function extractModelOverride(req: ModelOverrideRequest): string | null {
278
79
 
279
80
  /**
280
81
  * Read the fuzzy-resolution config knobs off the plugin config with
281
- * defaults applied. Kept here so `buildModel` and the `/models` route
282
- * agree on what "enabled" means.
82
+ * `@dbx-tools/model` defaults applied. Kept here so `buildModel` and
83
+ * the `/models` route agree on what "enabled" means.
283
84
  *
284
- * `fallbacks` is the priority-ordered list `pickModelId` walks when
285
- * nothing explicit is set; defaults live in `model.ts`
286
- * (`FALLBACK_MODEL_IDS`) and are passed in by callers to avoid a
287
- * circular import between `serving.ts` and `model.ts`.
85
+ * `fallbacks` is the priority-ordered list `resolveModel` walks first
86
+ * when nothing explicit is set; defaults to an empty list so the
87
+ * generic resolver falls through to the live catalogue and its own
88
+ * `FALLBACK_MODEL_IDS` floor.
288
89
  */
289
- export function resolveServingConfig(
290
- config: MastraPluginConfig,
291
- defaultFallbacks: readonly string[] = [],
292
- ): {
90
+ export function resolveServingConfig(config: MastraPluginConfig): {
293
91
  ttlMs: number;
294
92
  threshold: number;
295
93
  fuzzy: boolean;
@@ -297,10 +95,10 @@ export function resolveServingConfig(
297
95
  fallbacks: readonly string[];
298
96
  } {
299
97
  return {
300
- ttlMs: config.modelCacheTtlMs ?? DEFAULT_TTL_MS,
98
+ ttlMs: config.modelCacheTtlMs ?? DEFAULT_MODEL_CACHE_TTL_MS,
301
99
  threshold: config.modelFuzzyThreshold ?? DEFAULT_FUZZY_THRESHOLD,
302
100
  fuzzy: config.modelFuzzyMatch !== false,
303
101
  allowOverride: config.modelOverride !== false,
304
- fallbacks: config.defaultModelFallbacks ?? defaultFallbacks,
102
+ fallbacks: config.defaultModelFallbacks ?? [],
305
103
  };
306
104
  }
package/src/writer.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  * away can't crash a tool mid-flight.
11
11
  */
12
12
 
13
- import type { MinimalWriter } from "@dbx-tools/appkit-mastra-shared";
13
+ import type { MastraWriter } from "@dbx-tools/appkit-mastra-shared";
14
14
  import { commonUtils, type logUtils } from "@dbx-tools/shared";
15
15
 
16
16
  /**
@@ -24,7 +24,7 @@ import { commonUtils, type logUtils } from "@dbx-tools/shared";
24
24
  */
25
25
  export async function safeWrite(
26
26
  log: logUtils.Logger,
27
- writer: MinimalWriter | undefined,
27
+ writer: MastraWriter | undefined,
28
28
  chunk: unknown,
29
29
  context: Record<string, unknown> = {},
30
30
  ): Promise<void> {
@@ -1,48 +0,0 @@
1
- /**
2
- * Server-side Server-Sent-Events frame interceptor for the native
3
- * Mastra agent `/stream` endpoint.
4
- *
5
- * The Mastra stream (`@mastra/express`) emits one `data: <json>\n\n`
6
- * SSE frame per chunk, where the decoded JSON carries a
7
- * discriminating `type` (`text-delta`, `reasoning-delta`,
8
- * `tool-call`, `step-finish`, ...) and is terminated by a
9
- * `data: [DONE]` sentinel. {@link installStreamEventInterceptor} wraps
10
- * an Express response and runs a caller-supplied
11
- * {@link StreamFrameInterceptor} over each decoded frame so the caller
12
- * can keep, rewrite, or drop it; every non-`data:` byte (SSE
13
- * keep-alive comments, the `[DONE]` sentinel, blank padding) passes
14
- * through verbatim.
15
- *
16
- * The wrap is inert unless the response turns out to be a streamed
17
- * `200 text/event-stream` body, so non-streaming JSON routes and
18
- * error responses are never touched.
19
- */
20
- import type express from "express";
21
- /**
22
- * Per-frame interceptor invoked with the `JSON.parse`d object of every
23
- * `data: {json}` frame on the Mastra `/stream` response. The return
24
- * value decides the frame's fate:
25
- *
26
- * - `true`: forward the frame's original bytes unchanged (no
27
- * re-serialization).
28
- * - `false`: drop the frame entirely.
29
- * - `{ replace }`: re-emit the frame with the original `data:` prefix
30
- * and trailing whitespace preserved and only the JSON body replaced
31
- * by `JSON.stringify(replace)`.
32
- */
33
- export type StreamFrameInterceptor = (chunk: unknown) => {
34
- replace: unknown;
35
- } | true | false;
36
- /**
37
- * Wrap `res.write` / `res.end` so each SSE frame on a streamed Mastra
38
- * response is run through `interceptor` (keep / rewrite / drop).
39
- *
40
- * Interception only engages once the response is confirmed to be a
41
- * `200 text/event-stream` body (decided lazily on the first write,
42
- * after the handler has set status + content-type); any other
43
- * response streams through unchanged. A {@link StringDecoder}
44
- * reassembles multi-byte UTF-8 sequences that straddle chunk
45
- * boundaries, and a running buffer holds the trailing partial frame
46
- * until its `\n\n` arrives.
47
- */
48
- export declare function installStreamEventInterceptor(res: express.Response, interceptor: StreamFrameInterceptor): void;