@dbx-tools/appkit-mastra 0.1.73 → 0.1.75

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 CHANGED
@@ -13,10 +13,14 @@ is a one-line import change.
13
13
 
14
14
  The implementation lives under `src/`: agent registration in
15
15
  [`agents.ts`](src/agents.ts), the plugin + routes in
16
- [`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), Model
17
- Serving resolution in [`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts),
18
- Genie tooling in [`genie.ts`](src/genie.ts), and the chart pipeline in
19
- [`chart.ts`](src/chart.ts).
16
+ [`plugin.ts`](src/plugin.ts) / [`server.ts`](src/server.ts), thread
17
+ history / listing in [`history.ts`](src/history.ts) /
18
+ [`threads.ts`](src/threads.ts), Model Serving resolution in
19
+ [`model.ts`](src/model.ts) / [`serving.ts`](src/serving.ts), Genie
20
+ tooling in [`genie.ts`](src/genie.ts), the chart pipeline in
21
+ [`chart.ts`](src/chart.ts), and MLflow feedback logging in
22
+ [`mlflow.ts`](src/mlflow.ts) (over the shared REST helper in
23
+ [`rest.ts`](src/rest.ts)).
20
24
 
21
25
  ## Quick start
22
26
 
@@ -267,6 +271,35 @@ entry settles. Cache entries carry a 1h TTL; after expiry the route
267
271
  placement contract are in [`chart.ts`](src/chart.ts); the wire shapes
268
272
  are in [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
269
273
 
274
+ ## Feedback (MLflow)
275
+
276
+ When MLflow tracing is wired for the deployment, the plugin lets users
277
+ rate an assistant turn (thumbs up / down) and leave a comment, logged as
278
+ a HUMAN [assessment](https://mlflow.org/docs/latest/genai/assessments/)
279
+ on that turn's trace and attributed to the signed-in (OBO) user.
280
+
281
+ Enablement is auto-detected: it turns on when an OTLP exporter endpoint
282
+ (`OTEL_EXPORTER_OTLP_ENDPOINT` / `..._TRACES_ENDPOINT`) **and** an MLflow
283
+ experiment (`MLFLOW_EXPERIMENT_ID` / `MLFLOW_EXPERIMENT_NAME`) are
284
+ configured - the two signals that traces actually materialize in MLflow.
285
+ Set `config.feedback` to `true` / `false` to force it on or off
286
+ regardless of the env.
287
+
288
+ ```ts
289
+ mastra({ agents: support, feedback: true }); // force-enable
290
+ ```
291
+
292
+ Under the hood the server derives MLflow's trace id from the active
293
+ OpenTelemetry span (`tr-<hex(otelTraceId)>`) and stamps it on each
294
+ turn's response (`MLFLOW_TRACE_ID_HEADER`); the client sends it back to
295
+ `POST ${basePath}/route/feedback`, which posts the assessment to the
296
+ Databricks MLflow REST API. Trace export is asynchronous, so a
297
+ just-finished trace may not exist yet - the log call retries briefly on
298
+ "not found" and otherwise fails softly (the chat stays usable). The
299
+ current state is published to the client as
300
+ `clientConfig().feedbackEnabled`; see [`mlflow.ts`](src/mlflow.ts) and
301
+ the UI wiring in [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui).
302
+
270
303
  ## Model resolution
271
304
 
272
305
  Each agent call resolves a model lazily (so concurrent requests keep
@@ -338,6 +371,36 @@ resolves its model and tools as the calling user. The resolved endpoint
338
371
  paths are available in-process via
339
372
  `appkitUtils.require(this.context, mastra).exports().getMcp()`.
340
373
 
374
+ ## Client API surface (`apiAccess`)
375
+
376
+ `@mastra/express` registers its full management route table under the
377
+ plugin mount - agent inference *plus* admin / mutating routes (direct
378
+ tool execution, workflow control, raw memory read/write, telemetry,
379
+ logs, scores). AppKit authenticates every request as the OBO user, but
380
+ does not restrict *which* of those a browser client may call.
381
+
382
+ `config.apiAccess` gates that surface at the plugin's dispatch point:
383
+
384
+ - `"scoped"` (default): only the routes the chat client needs are
385
+ dispatched to Mastra - agent inference (`stream` / `generate` /
386
+ `network`), read-only agent metadata (`GET /agents`,
387
+ `GET /agents/:id`), this plugin's own OBO- and resource-scoped
388
+ `/route/*` routes (history / threads / feedback), and, when `mcp` is
389
+ enabled, the MCP transport. Everything else is refused with `403`
390
+ before it reaches Mastra.
391
+ - `"full"`: dispatch the entire stock Mastra API. Use only for a trusted
392
+ first-party console that genuinely needs the management surface.
393
+
394
+ ```ts
395
+ mastra({ agents: support }); // scoped by default
396
+ mastra({ agents: support, apiAccess: "full" }); // opt into the full API
397
+ ```
398
+
399
+ The gate is a pure allowlist (`isMastraRequestAllowed` in
400
+ [`server.ts`](src/server.ts)); the browser chat client only ever uses
401
+ the agent stream and the `/route/*` routes, so the default breaks
402
+ nothing.
403
+
341
404
  ## License
342
405
 
343
406
  Apache-2.0
package/dist/index.d.ts CHANGED
@@ -599,6 +599,24 @@ interface MastraPluginConfig extends BasePluginConfig {
599
599
  * to drill deep into a dataset within a single turn.
600
600
  */
601
601
  agentMaxSteps?: number;
602
+ /**
603
+ * Log user feedback (thumbs up/down + freeform comments) to MLflow as
604
+ * trace assessments, and surface the feedback controls in the chat UI.
605
+ *
606
+ * - `undefined` (default, auto): enabled only when MLflow tracing is
607
+ * wired - an OTLP exporter endpoint is set and an MLflow experiment
608
+ * is named (the same signals the observability pipeline needs to
609
+ * ship traces to MLflow). Otherwise off, since there'd be no trace
610
+ * to attach feedback to.
611
+ * - `true`: force on. Feedback controls show and writes are attempted
612
+ * regardless of env detection (use when the env is configured in a
613
+ * way the auto-probe doesn't recognize).
614
+ * - `false`: force off. No trace-id header, no feedback route, no UI.
615
+ *
616
+ * Feedback attaches to a turn's MLflow trace via the OpenTelemetry
617
+ * trace id the server stamps on each response; see `mlflow.ts`.
618
+ */
619
+ feedback?: boolean;
602
620
  /**
603
621
  * Expose the plugin's agents (and optionally tools) as a Mastra MCP
604
622
  * server so external MCP clients - Claude Desktop, Cursor, the Mastra
@@ -624,6 +642,28 @@ interface MastraPluginConfig extends BasePluginConfig {
624
642
  * calling user.
625
643
  */
626
644
  mcp?: boolean | MastraMcpConfig;
645
+ /**
646
+ * How much of the stock `@mastra/express` management API is reachable
647
+ * through the plugin mount. `@mastra/express` registers its full route
648
+ * table (agent inference plus admin / mutating routes: direct tool
649
+ * execution, workflow control, raw memory read/write, telemetry, logs,
650
+ * scores). AppKit already authenticates every request as the OBO user,
651
+ * but nothing there restricts *which* of those operations the browser
652
+ * client may invoke.
653
+ *
654
+ * - `"scoped"` (default): only the routes the chat client legitimately
655
+ * needs are dispatched to Mastra - agent inference
656
+ * (`stream` / `generate` / `network`), read-only agent metadata, this
657
+ * plugin's own OBO- and resource-scoped `/route/*` routes (history /
658
+ * threads), and, when {@link mcp} is enabled, the MCP transport.
659
+ * Everything else (tool execution, workflow control, raw memory,
660
+ * telemetry, logs, scores, and other mutations) is rejected with
661
+ * `403` before it reaches Mastra.
662
+ * - `"full"`: dispatch the entire stock Mastra API. Use only for a
663
+ * trusted first-party console that genuinely needs the management
664
+ * surface.
665
+ */
666
+ apiAccess?: "scoped" | "full";
627
667
  }
628
668
  //#endregion
629
669
  //#region packages/appkit-mastra/src/memory.d.ts
@@ -1300,6 +1340,11 @@ declare function buildMcpServer(opts: {
1300
1340
  declare class MastraServer$1 extends MastraServer {
1301
1341
  private config;
1302
1342
  private log;
1343
+ /**
1344
+ * Whether to stamp the MLflow trace-id header on responses. Shares the
1345
+ * plugin's feedback gate via {@link resolveFeedbackEnabled}.
1346
+ */
1347
+ private feedbackEnabled;
1303
1348
  constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServer>);
1304
1349
  registerAuthMiddleware(): void;
1305
1350
  configureRequestContextUser(requestContext: RequestContext): void;
@@ -1315,6 +1360,20 @@ declare class MastraServer$1 extends MastraServer {
1315
1360
  * response header so dev tools can copy it from either side.
1316
1361
  */
1317
1362
  configureRequestContextRequestId(req: express.Request, res: express.Response, requestContext: RequestContext): void;
1363
+ /**
1364
+ * Stamp the turn's MLflow trace id on the response so the chat client
1365
+ * can attach thumbs / comment feedback to it later. MLflow derives
1366
+ * its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
1367
+ * Mastra span for this request inherits the ambient OTel context (see
1368
+ * `observability.ts`), so the active span's trace id here is the id
1369
+ * MLflow will record for the turn.
1370
+ *
1371
+ * No-op unless feedback is enabled, and when no live OTel span is
1372
+ * active (e.g. the OTLP SDK isn't registered): in that case the header
1373
+ * is simply absent and the client hides feedback for that message,
1374
+ * degrading gracefully rather than emitting a bogus trace id.
1375
+ */
1376
+ configureMlflowTraceId(res: express.Response): void;
1318
1377
  /**
1319
1378
  * Resolve the thread id this request targets and pin it on
1320
1379
  * `RequestContext` (consumed by the agent stream for persistence and
@@ -1452,6 +1511,12 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
1452
1511
  clearModelsCache: (host?: string) => Promise<void>;
1453
1512
  };
1454
1513
  clientConfig(): Record<string, unknown>;
1514
+ /**
1515
+ * Whether user feedback can be logged to MLflow. Delegates to
1516
+ * {@link resolveFeedbackEnabled} so the client-config flag and the
1517
+ * feedback route share the same gate as the server's trace-id header.
1518
+ */
1519
+ private feedbackEnabled;
1455
1520
  injectRoutes(router: IAppRouter): void;
1456
1521
  /**
1457
1522
  * Invoke the Mastra express sub-app. Exists as a method (instead of reading
@@ -1472,6 +1537,15 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
1472
1537
  * shows a bare empty state instead of built-in example prompts.
1473
1538
  */
1474
1539
  private fetchSuggestions;
1540
+ /**
1541
+ * Implementation backing the `/route/feedback` route. Runs inside the
1542
+ * AppKit user-context proxy so `getExecutionContext()` returns the
1543
+ * OBO-scoped client and the assessment is attributed to the signed-in
1544
+ * user (their email / id as the assessment source). Returns the
1545
+ * created assessment id on success, or `undefined` on a soft failure
1546
+ * (see {@link logFeedback} in `./mlflow.js`).
1547
+ */
1548
+ private logFeedback;
1475
1549
  /**
1476
1550
  * Implementation backing the `data` embed resolver
1477
1551
  * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { ChartSchema, ChartTypeSchema, MASTRA_ROUTES, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
1
+ import { ChartSchema, ChartTypeSchema, DEFAULT_COMMENT_NAME, DEFAULT_FEEDBACK_NAME, MASTRA_ROUTES, MLFLOW_TRACE_ID_HEADER, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY, MastraFeedbackRequestSchema, THREAD_ID_HEADER, THREAD_ID_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
2
  import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, ModelClass as ModelClass$1, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
3
3
  import { apiUtils, appkitUtils, commonUtils, httpUtils, logUtils, netUtils, projectUtils, stringUtils } from "@dbx-tools/shared";
4
4
  import { Agent } from "@mastra/core/agent";
@@ -22,6 +22,7 @@ import { Pool } from "pg";
22
22
  import { Observability } from "@mastra/observability";
23
23
  import { OtelBridge } from "@mastra/otel-bridge";
24
24
  import { MastraServer } from "@mastra/express";
25
+ import { trace } from "@opentelemetry/api";
25
26
 
26
27
  export * from "@dbx-tools/appkit-mastra-shared"
27
28
 
@@ -337,7 +338,7 @@ function sanitizeServingMessages(messages) {
337
338
  * the demo client and any other UI consumer share the exact same
338
339
  * shape this module reads and writes.
339
340
  */
340
- const log$8 = logUtils.logger("mastra/chart");
341
+ const log$9 = logUtils.logger("mastra/chart");
341
342
  /**
342
343
  * TTL for cached chart entries. One hour balances "long enough for
343
344
  * the host UI to fetch the chart well after the model finished
@@ -593,7 +594,7 @@ async function writeChart(entry) {
593
594
  const key = await chartCacheKey(entry.chartId);
594
595
  await CacheManager.getInstanceSync().set(key, entry, { ttl: CHART_CACHE_TTL_SEC });
595
596
  } catch (err) {
596
- log$8.warn("write-error", {
597
+ log$9.warn("write-error", {
597
598
  chartId: entry.chartId,
598
599
  error: commonUtils.errorMessage(err)
599
600
  });
@@ -608,7 +609,7 @@ async function readChart(chartId) {
608
609
  const key = await chartCacheKey(chartId);
609
610
  return await CacheManager.getInstanceSync().get(key) ?? void 0;
610
611
  } catch (err) {
611
- log$8.warn("read-error", {
612
+ log$9.warn("read-error", {
612
613
  chartId,
613
614
  error: commonUtils.errorMessage(err)
614
615
  });
@@ -633,7 +634,7 @@ async function readChart(chartId) {
633
634
  async function prepareChart(opts) {
634
635
  const chartId = commonUtils.id();
635
636
  await writeChart({ chartId });
636
- log$8.debug("queued", { chartId });
637
+ log$9.debug("queued", { chartId });
637
638
  runPrepareChart(chartId, opts);
638
639
  return { chartId };
639
640
  }
@@ -654,14 +655,14 @@ async function runPrepareChart(chartId, opts) {
654
655
  chartId,
655
656
  result
656
657
  });
657
- log$8.info("done", {
658
+ log$9.info("done", {
658
659
  chartId,
659
660
  chartType: result.chartType,
660
661
  elapsedMs: Date.now() - startedAt
661
662
  });
662
663
  } catch (err) {
663
664
  const error = commonUtils.errorMessage(err);
664
- log$8.warn("error", {
665
+ log$9.warn("error", {
665
666
  chartId,
666
667
  error
667
668
  });
@@ -848,8 +849,63 @@ function buildRenderDataTool(config) {
848
849
  });
849
850
  }
850
851
 
852
+ //#endregion
853
+ //#region packages/appkit-mastra/src/rest.ts
854
+ /**
855
+ * Resolve the workspace host + an authenticated header set off the
856
+ * client and issue a `fetch` against `path` (mounted on the host).
857
+ * Runs as whatever identity the client carries - the per-request OBO
858
+ * user when called from a request scope, the service principal
859
+ * otherwise.
860
+ */
861
+ async function databricksFetch(client, path, init) {
862
+ const host = (await client.config.getHost()).toString();
863
+ const headers = new Headers({
864
+ "Content-Type": "application/json",
865
+ ...init.headers
866
+ });
867
+ await client.config.authenticate(headers);
868
+ const url = new URL(path, host).toString();
869
+ return fetch(url, {
870
+ method: init.method,
871
+ headers,
872
+ ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
873
+ ...init.signal ? { signal: init.signal } : {}
874
+ });
875
+ }
876
+ /** Read a response body as text, swallowing read errors (returns `""`). */
877
+ async function readResponseText(res) {
878
+ try {
879
+ return await res.text();
880
+ } catch {
881
+ return "";
882
+ }
883
+ }
884
+ /** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
885
+ async function readResponseJson(res) {
886
+ const text = await readResponseText(res);
887
+ if (!text) return {};
888
+ try {
889
+ return JSON.parse(text);
890
+ } catch {
891
+ return {};
892
+ }
893
+ }
894
+
851
895
  //#endregion
852
896
  //#region packages/appkit-mastra/src/connectors/managed-memory/client.ts
897
+ /**
898
+ * Thin REST client over the Databricks Unity Catalog Managed Agent
899
+ * Memory API (Beta). There is no TS SDK for memory-stores yet, so this
900
+ * issues authenticated requests through the shared {@link databricksFetch}
901
+ * primitive (`rest.ts`) as whatever identity the caller's client carries
902
+ * - the per-request OBO user for tools / recall, the service principal
903
+ * for setup-time probes.
904
+ *
905
+ * Responses are parsed defensively: the Beta wire shape may still drift,
906
+ * so the search parser tolerates missing / renamed entry fields and the
907
+ * store probe maps a 404 to `null` rather than throwing.
908
+ */
853
909
  /** Base path for the Unity Catalog memory-stores REST surface. */
854
910
  const MEMORY_STORES_PATH = "/api/2.1/unity-catalog/memory-stores";
855
911
  /**
@@ -882,32 +938,14 @@ function parseStoreName(fullName) {
882
938
  };
883
939
  }
884
940
  /**
885
- * Resolve the workspace host and an authenticated header set off the
886
- * client, then issue a `fetch`. Returns the raw `Response` so callers
887
- * decide how to handle status codes (the store probe wants the 404, the
888
- * mutating calls want a throw).
889
- */
890
- async function rawFetch(client, path, init) {
891
- const host = (await client.config.getHost()).toString();
892
- const headers = new Headers({ "Content-Type": "application/json" });
893
- await client.config.authenticate(headers);
894
- const url = new URL(path, host).toString();
895
- return fetch(url, {
896
- method: init.method,
897
- headers,
898
- ...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
899
- ...init.signal ? { signal: init.signal } : {}
900
- });
901
- }
902
- /**
903
941
  * Issue a request and parse the JSON body, throwing
904
942
  * {@link ManagedMemoryError} on any non-2xx status. Used by the calls
905
943
  * where anything but success is a real error (create / write / search).
906
944
  */
907
945
  async function requestJson(client, path, init) {
908
- const res = await rawFetch(client, path, init);
909
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText(res));
910
- return safeJson(res);
946
+ const res = await databricksFetch(client, path, init);
947
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
948
+ return readResponseJson(res);
911
949
  }
912
950
  /**
913
951
  * Fetch a store by full name. Returns the raw store payload when it
@@ -916,13 +954,13 @@ async function requestJson(client, path, init) {
916
954
  * Any other non-2xx status throws.
917
955
  */
918
956
  async function getStore(client, fullName, signal) {
919
- const res = await rawFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
957
+ const res = await databricksFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
920
958
  method: "GET",
921
959
  ...signal ? { signal } : {}
922
960
  });
923
961
  if (res.status === 404) return null;
924
- if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText(res));
925
- return safeJson(res);
962
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
963
+ return readResponseJson(res);
926
964
  }
927
965
  /**
928
966
  * Probe for the store and create it when missing. Returns `true` when
@@ -989,10 +1027,14 @@ function parseEntries(body) {
989
1027
  for (const raw of list) {
990
1028
  if (!raw || typeof raw !== "object") continue;
991
1029
  const obj = raw;
992
- const contents = firstString(obj["contents"], obj["content"], obj["text"]);
1030
+ const contents = stringUtils.firstNonEmpty([
1031
+ obj["contents"],
1032
+ obj["content"],
1033
+ obj["text"]
1034
+ ]);
993
1035
  if (!contents) continue;
994
- const path = firstString(obj["path"]);
995
- const description = firstString(obj["description"], obj["summary"]);
1036
+ const path = stringUtils.firstNonEmpty(obj["path"]);
1037
+ const description = stringUtils.firstNonEmpty([obj["description"], obj["summary"]]);
996
1038
  const score = typeof obj["score"] === "number" ? obj["score"] : void 0;
997
1039
  out.push({
998
1040
  contents,
@@ -1003,28 +1045,6 @@ function parseEntries(body) {
1003
1045
  }
1004
1046
  return out;
1005
1047
  }
1006
- /** Return the first argument that is a non-empty string, else undefined. */
1007
- function firstString(...values) {
1008
- for (const v of values) if (typeof v === "string" && v.trim() !== "") return v;
1009
- }
1010
- /** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
1011
- async function safeJson(res) {
1012
- const text = await safeText(res);
1013
- if (!text) return {};
1014
- try {
1015
- return JSON.parse(text);
1016
- } catch {
1017
- return {};
1018
- }
1019
- }
1020
- /** Read a response body as text, swallowing read errors. */
1021
- async function safeText(res) {
1022
- try {
1023
- return await res.text();
1024
- } catch {
1025
- return "";
1026
- }
1027
- }
1028
1048
 
1029
1049
  //#endregion
1030
1050
  //#region packages/appkit-mastra/src/connectors/managed-memory/context.ts
@@ -1084,7 +1104,7 @@ function resolveMemoryContext(requestContext) {
1084
1104
  * failure degrades to a short error string rather than aborting the
1085
1105
  * turn.
1086
1106
  */
1087
- const log$7 = logUtils.logger("mastra/managed-memory/tools");
1107
+ const log$8 = logUtils.logger("mastra/managed-memory/tools");
1088
1108
  const saveMemoryInput = z.object({
1089
1109
  content: z.string().describe("The durable fact or preference to remember."),
1090
1110
  summary: z.string().optional().describe("Optional one-line summary used to label the memory.")
@@ -1121,10 +1141,10 @@ function buildManagedMemoryTools(runtime) {
1121
1141
  contents: content,
1122
1142
  ...summary ? { description: summary } : {}
1123
1143
  });
1124
- log$7.debug("saved", { scope: ctx.scope });
1144
+ log$8.debug("saved", { scope: ctx.scope });
1125
1145
  return "Saved to memory.";
1126
1146
  } catch (err) {
1127
- log$7.warn("save:error", { error: commonUtils.errorMessage(err) });
1147
+ log$8.warn("save:error", { error: commonUtils.errorMessage(err) });
1128
1148
  return "Could not save to memory right now.";
1129
1149
  }
1130
1150
  }
@@ -1145,7 +1165,7 @@ function buildManagedMemoryTools(runtime) {
1145
1165
  try {
1146
1166
  return { memories: (await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK)).map(renderEntry) };
1147
1167
  } catch (err) {
1148
- log$7.warn("search:error", { error: commonUtils.errorMessage(err) });
1168
+ log$8.warn("search:error", { error: commonUtils.errorMessage(err) });
1149
1169
  return { memories: [] };
1150
1170
  }
1151
1171
  }
@@ -1283,7 +1303,7 @@ async function safeWrite(log, writer, chunk, context = {}) {
1283
1303
  * `instructions` when you want the canonical "how to drive the
1284
1304
  * Genie tools" guidance.
1285
1305
  */
1286
- const log$6 = logUtils.logger("mastra/genie");
1306
+ const log$7 = logUtils.logger("mastra/genie");
1287
1307
  /** Default alias used when a single unnamed Genie space is wired up. */
1288
1308
  const DEFAULT_GENIE_ALIAS = "default";
1289
1309
  /**
@@ -1398,7 +1418,7 @@ async function readCachedConversationId(cacheKey) {
1398
1418
  try {
1399
1419
  return await CacheManager.getInstanceSync().get(cacheKey) ?? void 0;
1400
1420
  } catch (err) {
1401
- log$6.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1421
+ log$7.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1402
1422
  return;
1403
1423
  }
1404
1424
  }
@@ -1414,7 +1434,7 @@ async function saveCachedConversationId(cacheKey, conversationId) {
1414
1434
  try {
1415
1435
  await CacheManager.getInstanceSync().set(cacheKey, conversationId, { ttl: CONVERSATION_TTL_SEC });
1416
1436
  } catch (err) {
1417
- log$6.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1437
+ log$7.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1418
1438
  }
1419
1439
  }
1420
1440
  /** Force-evict a cached conversation id. Used on the stale-id recovery path. */
@@ -1423,7 +1443,7 @@ async function evictCachedConversationId(cacheKey) {
1423
1443
  try {
1424
1444
  await CacheManager.getInstanceSync().delete(cacheKey);
1425
1445
  } catch (err) {
1426
- log$6.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1446
+ log$7.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1427
1447
  }
1428
1448
  }
1429
1449
  /**
@@ -1537,7 +1557,7 @@ function buildAskGenieTool(opts) {
1537
1557
  if (trimmed.length === 0 || PLACEHOLDER_QUESTIONS.has(trimmed.toLowerCase())) throw new Error(`${toolId}: refusing placeholder question "${question}" - call ${toolId} only with a real natural-language question, or skip the call entirely`);
1538
1558
  const cacheKey = await conversationCacheKey(spaceId, threadId);
1539
1559
  await ensureConversationSeeded(requestContext, spaceId, cacheKey);
1540
- await safeWrite(log$6, writer, {
1560
+ await safeWrite(log$7, writer, {
1541
1561
  type: "started",
1542
1562
  spaceId,
1543
1563
  content: question
@@ -1550,7 +1570,7 @@ function buildAskGenieTool(opts) {
1550
1570
  ...seedConversationId ? { conversationId: seedConversationId } : {},
1551
1571
  ...signal ? { context: signal } : {}
1552
1572
  })) {
1553
- if (event.type !== "message") await safeWrite(log$6, writer, event);
1573
+ if (event.type !== "message") await safeWrite(log$7, writer, event);
1554
1574
  const eventConversationId = event.conversation_id;
1555
1575
  if (eventConversationId) writeContextConversationId(requestContext, spaceId, eventConversationId);
1556
1576
  if (event.type === "result") finalMessage = event.message;
@@ -1564,7 +1584,7 @@ function buildAskGenieTool(opts) {
1564
1584
  } catch (err) {
1565
1585
  const seeded = readContextConversationId(requestContext, spaceId);
1566
1586
  if (seeded && apiUtils.isNotFoundError(err)) {
1567
- log$6.warn("conversation-cache:stale, resetting", {
1587
+ log$7.warn("conversation-cache:stale, resetting", {
1568
1588
  spaceId,
1569
1589
  conversationId: seeded,
1570
1590
  error: commonUtils.errorMessage(err)
@@ -2049,7 +2069,7 @@ async function collectSpaceSuggestions(opts) {
2049
2069
  }));
2050
2070
  writeSuggestionCache(spaceId, questions);
2051
2071
  } catch (err) {
2052
- log$6.warn("suggestions:fetch-error", {
2072
+ log$7.warn("suggestions:fetch-error", {
2053
2073
  spaceId,
2054
2074
  error: commonUtils.errorMessage(err)
2055
2075
  });
@@ -2110,7 +2130,7 @@ var ResultProcessor = class {
2110
2130
  * query, or any REST failure the processor passes the messages through
2111
2131
  * unchanged so a recall hiccup never blocks the conversation.
2112
2132
  */
2113
- const log$5 = logUtils.logger("mastra/processor/managed-memory-recall");
2133
+ const log$6 = logUtils.logger("mastra/processor/managed-memory-recall");
2114
2134
  /**
2115
2135
  * Build the recall processor bound to a managed-memory runtime. Created
2116
2136
  * once at setup (when managed memory is active) and wired onto every
@@ -2129,7 +2149,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
2129
2149
  const entries = await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK, args.abortSignal);
2130
2150
  if (entries.length === 0) return args.messages;
2131
2151
  const block = ["Relevant memories about the user (from long-term memory):", ...entries.map((e) => `- ${renderEntry(e)}`)].join("\n");
2132
- log$5.debug("recalled", {
2152
+ log$6.debug("recalled", {
2133
2153
  scope: ctx.scope,
2134
2154
  entries: entries.length
2135
2155
  });
@@ -2141,7 +2161,7 @@ function buildManagedMemoryRecallProcessor(runtime) {
2141
2161
  }]
2142
2162
  };
2143
2163
  } catch (err) {
2144
- log$5.warn("recall:error", { error: commonUtils.errorMessage(err) });
2164
+ log$6.warn("recall:error", { error: commonUtils.errorMessage(err) });
2145
2165
  return args.messages;
2146
2166
  }
2147
2167
  }
@@ -2191,7 +2211,7 @@ function latestUserText(messages) {
2191
2211
  * legacy `datasets[].chartId` payloads uniformly without coupling
2192
2212
  * to specific tool ids.
2193
2213
  */
2194
- const log$4 = logUtils.logger("mastra/processor/strip-stale-charts");
2214
+ const log$5 = logUtils.logger("mastra/processor/strip-stale-charts");
2195
2215
  /**
2196
2216
  * Recursively clone `value`, omitting any property whose key is
2197
2217
  * `chartId`. Arrays are mapped element-wise; primitives are
@@ -2239,7 +2259,7 @@ const stripStaleChartsProcessor = {
2239
2259
  }
2240
2260
  }
2241
2261
  }
2242
- if (stripped > 0) log$4.debug("stripped", { results: stripped });
2262
+ if (stripped > 0) log$5.debug("stripped", { results: stripped });
2243
2263
  return args.messages;
2244
2264
  }
2245
2265
  };
@@ -2897,9 +2917,30 @@ function buildMcpServer(opts) {
2897
2917
  };
2898
2918
  }
2899
2919
 
2920
+ //#endregion
2921
+ //#region packages/appkit-mastra/src/pagination.ts
2922
+ /** Coerce / clamp a `perPage` value, falling back to `bounds.fallback`. */
2923
+ function clampPerPage(value, bounds) {
2924
+ if (value === void 0 || Number.isNaN(value)) return bounds.fallback;
2925
+ const n = Math.trunc(value);
2926
+ if (n <= 0) return bounds.fallback;
2927
+ return Math.min(n, bounds.max);
2928
+ }
2929
+ /**
2930
+ * Coerce a Hono query value into a non-negative integer. Returns
2931
+ * `undefined` for empty / non-numeric / negative inputs so the caller
2932
+ * can apply its built-in defaults.
2933
+ */
2934
+ function parseIntParam(value) {
2935
+ if (!value) return void 0;
2936
+ const n = Number(value);
2937
+ if (!Number.isFinite(n) || n < 0) return void 0;
2938
+ return Math.trunc(n);
2939
+ }
2940
+
2900
2941
  //#endregion
2901
2942
  //#region packages/appkit-mastra/src/history.ts
2902
- const log$3 = logUtils.logger("mastra/history");
2943
+ const log$4 = logUtils.logger("mastra/history");
2903
2944
  /** Default history page size; matches the Mastra storage default. */
2904
2945
  const DEFAULT_PER_PAGE$1 = 20;
2905
2946
  /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
@@ -2920,11 +2961,14 @@ const MAX_PER_PAGE$1 = 200;
2920
2961
  * without sorting locally.
2921
2962
  */
2922
2963
  async function loadHistory(opts) {
2923
- const perPage = clampPerPage$1(opts.perPage);
2964
+ const perPage = clampPerPage(opts.perPage, {
2965
+ fallback: DEFAULT_PER_PAGE$1,
2966
+ max: MAX_PER_PAGE$1
2967
+ });
2924
2968
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
2925
2969
  const memory = await opts.agent.getMemory();
2926
2970
  if (!memory) {
2927
- log$3.debug("recall:no-memory", {
2971
+ log$4.debug("recall:no-memory", {
2928
2972
  agentId: opts.agent.id,
2929
2973
  threadId: opts.threadId
2930
2974
  });
@@ -2948,7 +2992,7 @@ async function loadHistory(opts) {
2948
2992
  }
2949
2993
  });
2950
2994
  const uiMessages = toAISdkV5Messages(sortChronological(result.messages));
2951
- log$3.debug("recall:done", {
2995
+ log$4.debug("recall:done", {
2952
2996
  agentId: opts.agent.id,
2953
2997
  threadId: opts.threadId,
2954
2998
  page,
@@ -2982,7 +3026,7 @@ async function loadHistory(opts) {
2982
3026
  async function clearHistory(opts) {
2983
3027
  const memory = await opts.agent.getMemory();
2984
3028
  if (!memory) {
2985
- log$3.debug("clear:no-memory", {
3029
+ log$4.debug("clear:no-memory", {
2986
3030
  agentId: opts.agent.id,
2987
3031
  threadId: opts.threadId
2988
3032
  });
@@ -2996,7 +3040,7 @@ async function clearHistory(opts) {
2996
3040
  perPage: 1
2997
3041
  })).total;
2998
3042
  } catch (err) {
2999
- log$3.debug("clear:probe-failed", {
3043
+ log$4.debug("clear:probe-failed", {
3000
3044
  agentId: opts.agent.id,
3001
3045
  threadId: opts.threadId,
3002
3046
  error: commonUtils.errorMessage(err)
@@ -3006,13 +3050,13 @@ async function clearHistory(opts) {
3006
3050
  try {
3007
3051
  await memory.deleteThread(opts.threadId);
3008
3052
  } catch (err) {
3009
- log$3.warn("clear:delete-soft-failed", {
3053
+ log$4.warn("clear:delete-soft-failed", {
3010
3054
  agentId: opts.agent.id,
3011
3055
  threadId: opts.threadId,
3012
3056
  error: commonUtils.errorMessage(err)
3013
3057
  });
3014
3058
  }
3015
- log$3.info("clear:done", {
3059
+ log$4.info("clear:done", {
3016
3060
  agentId: opts.agent.id,
3017
3061
  threadId: opts.threadId,
3018
3062
  cleared,
@@ -3069,8 +3113,8 @@ function historyRoute(options) {
3069
3113
  agent: ctx.agent,
3070
3114
  threadId: ctx.threadId,
3071
3115
  ...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
3072
- page: parseIntParam$1(c.req.query("page")),
3073
- perPage: parseIntParam$1(c.req.query("perPage"))
3116
+ page: parseIntParam(c.req.query("page")),
3117
+ perPage: parseIntParam(c.req.query("perPage"))
3074
3118
  });
3075
3119
  return c.json(payload);
3076
3120
  }
@@ -3093,13 +3137,6 @@ function historyRoute(options) {
3093
3137
  }
3094
3138
  })];
3095
3139
  }
3096
- /** Coerce / clamp `perPage`; falls back to the page-size default. */
3097
- function clampPerPage$1(value) {
3098
- if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE$1;
3099
- const n = Math.trunc(value);
3100
- if (n <= 0) return DEFAULT_PER_PAGE$1;
3101
- return Math.min(n, MAX_PER_PAGE$1);
3102
- }
3103
3140
  /**
3104
3141
  * Sort messages oldest-first by `createdAt`, falling back to whatever
3105
3142
  * order the storage returned them in. The native `recall` call honors
@@ -3119,17 +3156,6 @@ function toEpoch(value) {
3119
3156
  }
3120
3157
  return 0;
3121
3158
  }
3122
- /**
3123
- * Coerce a Hono query value into a non-negative integer. Returns
3124
- * `undefined` for empty / non-numeric / negative inputs so
3125
- * {@link loadHistory} can apply its built-in defaults.
3126
- */
3127
- function parseIntParam$1(value) {
3128
- if (!value) return void 0;
3129
- const n = Number(value);
3130
- if (!Number.isFinite(n) || n < 0) return void 0;
3131
- return Math.trunc(n);
3132
- }
3133
3159
 
3134
3160
  //#endregion
3135
3161
  //#region packages/appkit-mastra/src/memory.ts
@@ -3161,7 +3187,7 @@ function parseIntParam$1(value) {
3161
3187
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
3162
3188
  * is registered); per-agent settings cascade on top of that.
3163
3189
  */
3164
- const log$2 = logUtils.logger("mastra/memory");
3190
+ const log$3 = logUtils.logger("mastra/memory");
3165
3191
  /**
3166
3192
  * Build a dedicated **service-principal** Lakebase pool for Mastra
3167
3193
  * memory from the lakebase plugin's resolved SP pg config.
@@ -3253,10 +3279,10 @@ var MemoryBuilder = class {
3253
3279
  const storage = this.buildStorage(agentId, storageSetting);
3254
3280
  const vector = this.buildVector(memorySetting);
3255
3281
  if (!storage && !vector) {
3256
- log$2.debug("agent:stateless", { agentId });
3282
+ log$3.debug("agent:stateless", { agentId });
3257
3283
  return;
3258
3284
  }
3259
- log$2.debug("agent:configured", {
3285
+ log$3.debug("agent:configured", {
3260
3286
  agentId,
3261
3287
  storage: storage !== void 0,
3262
3288
  vector: vector !== void 0,
@@ -3368,6 +3394,110 @@ function withId(value, fallback) {
3368
3394
  };
3369
3395
  }
3370
3396
 
3397
+ //#endregion
3398
+ //#region packages/appkit-mastra/src/mlflow.ts
3399
+ /**
3400
+ * MLflow user-feedback logging: detect whether MLflow tracing is wired
3401
+ * for this deployment, and log a thumbs / comment as a trace
3402
+ * *assessment* via the Databricks MLflow REST API.
3403
+ *
3404
+ * Feedback attaches to a trace, and the plugin's spans reach MLflow
3405
+ * through the same OTel pipeline as every other AppKit span (see
3406
+ * `observability.ts`). MLflow derives its trace id from the OpenTelemetry
3407
+ * trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
3408
+ * trace id on each turn's response and the client sends it back here.
3409
+ *
3410
+ * There is no MLflow JS SDK, so this posts to the assessments REST
3411
+ * endpoint directly using the OBO-scoped workspace client (the feedback
3412
+ * is thus attributed to the signed-in user). Trace export is
3413
+ * asynchronous, so the just-finished trace may not exist in MLflow yet
3414
+ * when the user reacts; the log call retries briefly on "not found"
3415
+ * before giving up softly.
3416
+ */
3417
+ const log$2 = logUtils.logger("mastra/mlflow");
3418
+ /** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
3419
+ const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
3420
+ /** Number of times to retry a "trace not found" response before giving up. */
3421
+ const NOT_FOUND_RETRIES = 3;
3422
+ /** Base backoff between "trace not found" retries, in ms (grows linearly). */
3423
+ const NOT_FOUND_BACKOFF_MS = 1200;
3424
+ /**
3425
+ * Whether MLflow feedback logging is available for this deployment.
3426
+ *
3427
+ * Enabled when an OTLP exporter endpoint is configured (traces are
3428
+ * actually shipped somewhere) AND an MLflow experiment is named - the
3429
+ * two signals that the OTLP backend is MLflow and traces will
3430
+ * materialize there. Both are standard env vars, so no plugin config is
3431
+ * required; a deployment opts in simply by wiring MLflow tracing.
3432
+ */
3433
+ function mlflowEnabled() {
3434
+ const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
3435
+ const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
3436
+ return hasExporter && hasExperiment;
3437
+ }
3438
+ /**
3439
+ * Resolve whether user feedback is enabled from the plugin's optional
3440
+ * `config.feedback` override: the explicit boolean wins, otherwise fall
3441
+ * back to auto-detecting MLflow tracing ({@link mlflowEnabled}). Shared
3442
+ * by the plugin's client-config gate and the server's trace-id header so
3443
+ * the two never disagree.
3444
+ */
3445
+ function resolveFeedbackEnabled(explicit) {
3446
+ return explicit ?? mlflowEnabled();
3447
+ }
3448
+ /**
3449
+ * Log a HUMAN feedback assessment to a trace. Returns the created
3450
+ * assessment id on success, or `undefined` when the trace can't be
3451
+ * found (even after retrying for export lag) or the request otherwise
3452
+ * fails - callers surface that as a soft "not recorded" rather than an
3453
+ * error, keeping the chat usable.
3454
+ */
3455
+ async function logFeedback(client, params) {
3456
+ const hasValue = params.value !== void 0;
3457
+ const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
3458
+ const value = hasValue ? params.value : params.comment;
3459
+ const body = { assessment: {
3460
+ trace_id: params.traceId,
3461
+ assessment_name: name,
3462
+ source: {
3463
+ source_type: "HUMAN",
3464
+ source_id: params.sourceId?.trim() || "user"
3465
+ },
3466
+ feedback: { value },
3467
+ ...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
3468
+ } };
3469
+ for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
3470
+ let res;
3471
+ try {
3472
+ res = await databricksFetch(client, assessmentsPath(params.traceId), {
3473
+ method: "POST",
3474
+ body
3475
+ });
3476
+ } catch (err) {
3477
+ log$2.warn("feedback request failed", {
3478
+ traceId: params.traceId,
3479
+ error: commonUtils.errorMessage(err)
3480
+ });
3481
+ return;
3482
+ }
3483
+ if (res.ok) {
3484
+ const parsed = await readResponseJson(res);
3485
+ const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
3486
+ return typeof assessmentId === "string" ? assessmentId : "";
3487
+ }
3488
+ if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
3489
+ await commonUtils.sleep(NOT_FOUND_BACKOFF_MS * (attempt + 1));
3490
+ continue;
3491
+ }
3492
+ log$2.warn("feedback not recorded", {
3493
+ traceId: params.traceId,
3494
+ status: res.status,
3495
+ body: await readResponseText(res)
3496
+ });
3497
+ return;
3498
+ }
3499
+ }
3500
+
3371
3501
  //#endregion
3372
3502
  //#region packages/appkit-mastra/src/observability.ts
3373
3503
  /**
@@ -3442,16 +3572,29 @@ async function buildObservability(options) {
3442
3572
  * behind an Express mount point.
3443
3573
  */
3444
3574
  /**
3575
+ * OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
3576
+ * `trace.getActiveSpan()` returns a non-recording span with this id
3577
+ * when no SDK is registered, which must never be surfaced as a trace to
3578
+ * attach feedback to.
3579
+ */
3580
+ const INVALID_TRACE_ID = "0".repeat(32);
3581
+ /**
3445
3582
  * `@mastra/express` subclass that stamps `RequestContext` with the
3446
3583
  * AppKit user, resource id, and a thread id backed by an HTTP-only
3447
3584
  * session cookie (`appkit_<plugin-name>_session_id`).
3448
3585
  */
3449
3586
  var MastraServer$1 = class extends MastraServer {
3450
3587
  log;
3588
+ /**
3589
+ * Whether to stamp the MLflow trace-id header on responses. Shares the
3590
+ * plugin's feedback gate via {@link resolveFeedbackEnabled}.
3591
+ */
3592
+ feedbackEnabled;
3451
3593
  constructor(config, ...args) {
3452
3594
  super(...args);
3453
3595
  this.config = config;
3454
3596
  this.log = logUtils.logger(config);
3597
+ this.feedbackEnabled = resolveFeedbackEnabled(config.feedback);
3455
3598
  }
3456
3599
  registerAuthMiddleware() {
3457
3600
  super.registerAuthMiddleware();
@@ -3461,6 +3604,7 @@ var MastraServer$1 = class extends MastraServer {
3461
3604
  this.configureRequestContextThreadId(req, res, requestContext);
3462
3605
  this.configureRequestContextModelOverride(req, requestContext);
3463
3606
  this.configureRequestContextRequestId(req, res, requestContext);
3607
+ this.configureMlflowTraceId(res);
3464
3608
  this.log.debug("auth:middleware", {
3465
3609
  method: req.method,
3466
3610
  path: req.path,
@@ -3507,6 +3651,25 @@ var MastraServer$1 = class extends MastraServer {
3507
3651
  res.setHeader("X-Request-Id", requestId);
3508
3652
  }
3509
3653
  /**
3654
+ * Stamp the turn's MLflow trace id on the response so the chat client
3655
+ * can attach thumbs / comment feedback to it later. MLflow derives
3656
+ * its trace id from the OpenTelemetry trace id (`tr-<hex>`), and every
3657
+ * Mastra span for this request inherits the ambient OTel context (see
3658
+ * `observability.ts`), so the active span's trace id here is the id
3659
+ * MLflow will record for the turn.
3660
+ *
3661
+ * No-op unless feedback is enabled, and when no live OTel span is
3662
+ * active (e.g. the OTLP SDK isn't registered): in that case the header
3663
+ * is simply absent and the client hides feedback for that message,
3664
+ * degrading gracefully rather than emitting a bogus trace id.
3665
+ */
3666
+ configureMlflowTraceId(res) {
3667
+ if (!this.feedbackEnabled || res.headersSent) return;
3668
+ const traceId = trace.getActiveSpan()?.spanContext().traceId;
3669
+ if (!traceId || traceId === INVALID_TRACE_ID) return;
3670
+ res.setHeader(MLFLOW_TRACE_ID_HEADER, `tr-${traceId}`);
3671
+ }
3672
+ /**
3510
3673
  * Resolve the thread id this request targets and pin it on
3511
3674
  * `RequestContext` (consumed by the agent stream for persistence and
3512
3675
  * by the history / threads routes). Resolution order:
@@ -3571,6 +3734,37 @@ var MastraServer$1 = class extends MastraServer {
3571
3734
  }
3572
3735
  };
3573
3736
  /**
3737
+ * Mount-relative agent inference paths (`/agents/:id/<verb>...`). Matched
3738
+ * by prefix on the verb so future variants (`streamVNext`, `stream/vnext`,
3739
+ * `generateVNext`) stay covered without a code change. These are the only
3740
+ * *writes* the browser client is allowed to make against stock Mastra.
3741
+ */
3742
+ const AGENT_INFERENCE = /^\/agents\/[^/]+\/(stream|generate|network)/i;
3743
+ /** Mount-relative read-only agent metadata (`/agents`, `/agents/:id`). */
3744
+ const AGENT_METADATA = /^\/agents(\/[^/]+)?$/;
3745
+ /**
3746
+ * Whether a request to the stock `@mastra/express` sub-app should be
3747
+ * dispatched, given the configured {@link MastraApiGateOptions.access}.
3748
+ *
3749
+ * `path` is mount-relative (what the plugin's catch-all sees, e.g.
3750
+ * `/agents/x/stream`, `/route/history/x`, `/mcp/...`). In `"scoped"`
3751
+ * mode the allowlist is deliberately tight - the chat client only ever
3752
+ * needs agent inference, read-only agent metadata, this plugin's own
3753
+ * OBO/resource-scoped `/route/*` routes, and (when enabled) MCP - so the
3754
+ * whole admin / mutating / bulk-export surface Mastra also exposes is
3755
+ * denied by default rather than enumerated.
3756
+ */
3757
+ function isMastraRequestAllowed(method, path, opts) {
3758
+ if (opts.access === "full") return true;
3759
+ const p = path.startsWith("/") ? path : `/${path}`;
3760
+ if (p === "/route" || p.startsWith("/route/")) return true;
3761
+ if (opts.mcpEnabled && (p === "/mcp" || p.startsWith("/mcp/"))) return true;
3762
+ const m = method.toUpperCase();
3763
+ if (m === "POST" && AGENT_INFERENCE.test(p)) return true;
3764
+ if (m === "GET" && AGENT_METADATA.test(p)) return true;
3765
+ return false;
3766
+ }
3767
+ /**
3574
3768
  * Patches around `@mastra/express`'s custom-route dispatcher so the
3575
3769
  * plugin's custom API routes (e.g. `historyRoute`) work when
3576
3770
  * `MastraServer` is hosted on an Express subapp mounted under a parent
@@ -3609,7 +3803,10 @@ const MAX_PER_PAGE = 200;
3609
3803
  * empty page so callers don't have to special-case stateless agents.
3610
3804
  */
3611
3805
  async function listThreads(opts) {
3612
- const perPage = clampPerPage(opts.perPage);
3806
+ const perPage = clampPerPage(opts.perPage, {
3807
+ fallback: DEFAULT_PER_PAGE,
3808
+ max: MAX_PER_PAGE
3809
+ });
3613
3810
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
3614
3811
  const memory = await opts.agent.getMemory();
3615
3812
  if (!memory) {
@@ -3773,24 +3970,6 @@ function toIso(value) {
3773
3970
  const parsed = new Date(value);
3774
3971
  return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date(0)).toISOString() : parsed.toISOString();
3775
3972
  }
3776
- /** Coerce / clamp `perPage`; falls back to the page-size default. */
3777
- function clampPerPage(value) {
3778
- if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
3779
- const n = Math.trunc(value);
3780
- if (n <= 0) return DEFAULT_PER_PAGE;
3781
- return Math.min(n, MAX_PER_PAGE);
3782
- }
3783
- /**
3784
- * Coerce a Hono query value into a non-negative integer. Returns
3785
- * `undefined` for empty / non-numeric / negative inputs so
3786
- * {@link listThreads} can apply its built-in defaults.
3787
- */
3788
- function parseIntParam(value) {
3789
- if (!value) return void 0;
3790
- const n = Number(value);
3791
- if (!Number.isFinite(n) || n < 0) return void 0;
3792
- return Math.trunc(n);
3793
- }
3794
3973
 
3795
3974
  //#endregion
3796
3975
  //#region packages/appkit-mastra/src/plugin.ts
@@ -3821,8 +4000,13 @@ function parseIntParam(value) {
3821
4000
  * Mastra agent routes, the plugin registers `/route/history`
3822
4001
  * (load + clear a thread's messages), `/route/threads` (list the
3823
4002
  * caller's conversations + delete one), `/models`, `/suggestions`,
3824
- * and the generic `/embed/:type/:id` resolver for inline chart /
3825
- * data markers.
4003
+ * `/route/feedback` (log a thumbs / comment to MLflow when feedback
4004
+ * is enabled), and the generic `/embed/:type/:id` resolver for inline
4005
+ * chart / data markers. The stock `@mastra/express` surface is gated
4006
+ * by `config.apiAccess` (default `"scoped"`): only agent inference,
4007
+ * read-only agent metadata, the `/route/*` routes, and (when enabled)
4008
+ * MCP are dispatched to Mastra; admin / mutating / bulk-export routes
4009
+ * are refused with `403`. See {@link isMastraRequestAllowed}.
3826
4010
  * - MCP: opt in with `config.mcp` to expose the agents (and optionally
3827
4011
  * tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
3828
4012
  * instance via `mcpServers`, so `@mastra/express` serves the stock
@@ -3938,9 +4122,18 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3938
4122
  return {
3939
4123
  basePath: `/api/${this.name}`,
3940
4124
  defaultAgent: this.built?.defaultAgentId ?? FALLBACK_AGENT_ID,
3941
- agents: Object.keys(this.built?.agents ?? {})
4125
+ agents: Object.keys(this.built?.agents ?? {}),
4126
+ feedbackEnabled: this.feedbackEnabled()
3942
4127
  };
3943
4128
  }
4129
+ /**
4130
+ * Whether user feedback can be logged to MLflow. Delegates to
4131
+ * {@link resolveFeedbackEnabled} so the client-config flag and the
4132
+ * feedback route share the same gate as the server's trace-id header.
4133
+ */
4134
+ feedbackEnabled() {
4135
+ return resolveFeedbackEnabled(this.config.feedback);
4136
+ }
3944
4137
  injectRoutes(router) {
3945
4138
  router.get(MASTRA_ROUTES.models, (req, res, next) => {
3946
4139
  this.userScopedSelf(req).listModels().then((endpoints) => res.json({ endpoints })).catch(next);
@@ -3993,8 +4186,33 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3993
4186
  };
3994
4187
  router.get(MASTRA_ROUTES.suggestions, handleSuggestions);
3995
4188
  router.get(`${MASTRA_ROUTES.suggestions}/:agentId`, handleSuggestions);
4189
+ router.post(MASTRA_ROUTES.feedback, (req, res, next) => {
4190
+ if (!this.feedbackEnabled()) {
4191
+ res.status(404).json({ ok: false });
4192
+ return;
4193
+ }
4194
+ const parsed = MastraFeedbackRequestSchema.safeParse(req.body);
4195
+ if (!parsed.success) {
4196
+ res.status(400).json({
4197
+ ok: false,
4198
+ error: parsed.error.message
4199
+ });
4200
+ return;
4201
+ }
4202
+ this.userScopedSelf(req).logFeedback(parsed.data).then((assessmentId) => res.json({
4203
+ ok: assessmentId !== void 0,
4204
+ ...assessmentId ? { assessmentId } : {}
4205
+ })).catch(next);
4206
+ });
3996
4207
  router.use((req, res, next) => {
3997
4208
  if (!this.mastraApp) return res.status(503).end();
4209
+ if (!isMastraRequestAllowed(req.method, req.path, {
4210
+ access: this.config.apiAccess ?? "scoped",
4211
+ mcpEnabled: Boolean(this.config.mcp)
4212
+ })) {
4213
+ res.status(403).json({ error: "Endpoint not exposed to the client (apiAccess=scoped)" });
4214
+ return;
4215
+ }
3998
4216
  return this.userScopedSelf(req).dispatchMastra(req, res, next);
3999
4217
  });
4000
4218
  }
@@ -4029,6 +4247,22 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4029
4247
  });
4030
4248
  }
4031
4249
  /**
4250
+ * Implementation backing the `/route/feedback` route. Runs inside the
4251
+ * AppKit user-context proxy so `getExecutionContext()` returns the
4252
+ * OBO-scoped client and the assessment is attributed to the signed-in
4253
+ * user (their email / id as the assessment source). Returns the
4254
+ * created assessment id on success, or `undefined` on a soft failure
4255
+ * (see {@link logFeedback} in `./mlflow.js`).
4256
+ */
4257
+ async logFeedback(feedback) {
4258
+ const ctx = getExecutionContext();
4259
+ const sourceId = "userEmail" in ctx && ctx.userEmail ? ctx.userEmail : "userId" in ctx ? ctx.userId : ctx.serviceUserId;
4260
+ return logFeedback(ctx.client, {
4261
+ ...feedback,
4262
+ ...sourceId ? { sourceId } : {}
4263
+ });
4264
+ }
4265
+ /**
4032
4266
  * Implementation backing the `data` embed resolver
4033
4267
  * (`GET /embed/data/:id`). Runs inside the AppKit user-context proxy so
4034
4268
  * `getExecutionContext()` returns the OBO-scoped workspace
@@ -4185,6 +4419,7 @@ var MastraPlugin = class MastraPlugin extends Plugin {
4185
4419
  "/route/threads",
4186
4420
  "/models",
4187
4421
  "/suggestions",
4422
+ "/route/feedback",
4188
4423
  "/embed/:type/:id"
4189
4424
  ],
4190
4425
  instanceStorage: instanceStorage !== void 0,
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@dbx-tools/appkit-mastra",
3
- "version": "0.1.73",
3
+ "version": "0.1.75",
4
4
  "dependencies": {
5
5
  "@databricks/sdk-experimental": "^0.17",
6
- "@dbx-tools/appkit-mastra-shared": "0.1.73",
7
- "@dbx-tools/genie": "0.1.73",
8
- "@dbx-tools/genie-shared": "0.1.73",
9
- "@dbx-tools/model": "0.1.73",
10
- "@dbx-tools/shared": "0.1.73",
6
+ "@dbx-tools/appkit-mastra-shared": "0.1.75",
7
+ "@dbx-tools/genie": "0.1.75",
8
+ "@dbx-tools/genie-shared": "0.1.75",
9
+ "@dbx-tools/model": "0.1.75",
10
+ "@dbx-tools/shared": "0.1.75",
11
11
  "@mastra/ai-sdk": "^1",
12
12
  "@mastra/core": "^1",
13
13
  "@mastra/express": "^1",
@@ -17,6 +17,7 @@
17
17
  "@mastra/observability": "^1",
18
18
  "@mastra/otel-bridge": "^1",
19
19
  "@mastra/pg": "^1",
20
+ "@opentelemetry/api": "^1.9.0",
20
21
  "pg": "^8.20.0",
21
22
  "zod": "^4.3.6"
22
23
  },