@dbx-tools/appkit-mastra 0.1.71 → 0.1.72

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/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { ChartSchema, ChartTypeSchema, MASTRA_ROUTES, MODEL_OVERRIDE_BODY_FIELDS, MODEL_OVERRIDE_HEADER, MODEL_OVERRIDE_QUERY } from "@dbx-tools/appkit-mastra-shared";
2
- import { DEFAULT_FUZZY_THRESHOLD, DEFAULT_MODEL_CACHE_TTL_MS, FALLBACK_MODEL_IDS, ModelClass, clearServingEndpointsCache, listServingEndpoints, parseModelClass, selectModel } from "@dbx-tools/model";
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";
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";
5
5
  import { createTool, createTool as createTool$1 } from "@mastra/core/tools";
@@ -337,7 +337,7 @@ function sanitizeServingMessages(messages) {
337
337
  * the demo client and any other UI consumer share the exact same
338
338
  * shape this module reads and writes.
339
339
  */
340
- const log$5 = logUtils.logger("mastra/chart");
340
+ const log$8 = logUtils.logger("mastra/chart");
341
341
  /**
342
342
  * TTL for cached chart entries. One hour balances "long enough for
343
343
  * the host UI to fetch the chart well after the model finished
@@ -547,7 +547,7 @@ function getPlannerAgent(config) {
547
547
  name: "Chart Planner",
548
548
  description: "Picks chart type and axis encodings for a dataset.",
549
549
  instructions: CHART_PLANNER_INSTRUCTIONS,
550
- model: ({ requestContext }) => buildModel(config, requestContext, { modelClass: ModelClass.ChatFast })
550
+ model: ({ requestContext }) => buildModel(config, requestContext, { modelClass: ModelClass$1.ChatFast })
551
551
  });
552
552
  plannerAgents.set(config, agent);
553
553
  }
@@ -593,7 +593,7 @@ async function writeChart(entry) {
593
593
  const key = await chartCacheKey(entry.chartId);
594
594
  await CacheManager.getInstanceSync().set(key, entry, { ttl: CHART_CACHE_TTL_SEC });
595
595
  } catch (err) {
596
- log$5.warn("write-error", {
596
+ log$8.warn("write-error", {
597
597
  chartId: entry.chartId,
598
598
  error: commonUtils.errorMessage(err)
599
599
  });
@@ -608,7 +608,7 @@ async function readChart(chartId) {
608
608
  const key = await chartCacheKey(chartId);
609
609
  return await CacheManager.getInstanceSync().get(key) ?? void 0;
610
610
  } catch (err) {
611
- log$5.warn("read-error", {
611
+ log$8.warn("read-error", {
612
612
  chartId,
613
613
  error: commonUtils.errorMessage(err)
614
614
  });
@@ -633,7 +633,7 @@ async function readChart(chartId) {
633
633
  async function prepareChart(opts) {
634
634
  const chartId = commonUtils.id();
635
635
  await writeChart({ chartId });
636
- log$5.debug("queued", { chartId });
636
+ log$8.debug("queued", { chartId });
637
637
  runPrepareChart(chartId, opts);
638
638
  return { chartId };
639
639
  }
@@ -654,14 +654,14 @@ async function runPrepareChart(chartId, opts) {
654
654
  chartId,
655
655
  result
656
656
  });
657
- log$5.info("done", {
657
+ log$8.info("done", {
658
658
  chartId,
659
659
  chartType: result.chartType,
660
660
  elapsedMs: Date.now() - startedAt
661
661
  });
662
662
  } catch (err) {
663
663
  const error = commonUtils.errorMessage(err);
664
- log$5.warn("error", {
664
+ log$8.warn("error", {
665
665
  chartId,
666
666
  error
667
667
  });
@@ -848,6 +848,319 @@ function buildRenderDataTool(config) {
848
848
  });
849
849
  }
850
850
 
851
+ //#endregion
852
+ //#region packages/appkit-mastra/src/connectors/managed-memory/client.ts
853
+ /** Base path for the Unity Catalog memory-stores REST surface. */
854
+ const MEMORY_STORES_PATH = "/api/2.1/unity-catalog/memory-stores";
855
+ /**
856
+ * Error thrown for a non-2xx memory-store response. Carries the HTTP
857
+ * status so callers can distinguish "feature unavailable / store
858
+ * missing" (probe falls back) from genuine failures.
859
+ */
860
+ var ManagedMemoryError = class extends Error {
861
+ constructor(status, statusText, body) {
862
+ super(`managed-memory: ${status} ${statusText}${body ? ` - ${body}` : ""}`);
863
+ this.status = status;
864
+ this.statusText = statusText;
865
+ this.name = "ManagedMemoryError";
866
+ }
867
+ };
868
+ /**
869
+ * Split a three-level `catalog.schema.name` into its parts. Throws when
870
+ * the input isn't exactly three dot-separated, non-empty segments so a
871
+ * misconfigured `MEMORY_STORE` fails loudly at setup rather than
872
+ * silently hitting a malformed URL.
873
+ */
874
+ function parseStoreName(fullName) {
875
+ const parts = fullName.split(".");
876
+ if (parts.length !== 3 || parts.some((p) => p.trim() === "")) throw new Error(`managed-memory: store name must be three-level "catalog.schema.name", got "${fullName}"`);
877
+ const [catalog, schema, name] = parts;
878
+ return {
879
+ catalog,
880
+ schema,
881
+ name
882
+ };
883
+ }
884
+ /**
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
+ * Issue a request and parse the JSON body, throwing
904
+ * {@link ManagedMemoryError} on any non-2xx status. Used by the calls
905
+ * where anything but success is a real error (create / write / search).
906
+ */
907
+ 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);
911
+ }
912
+ /**
913
+ * Fetch a store by full name. Returns the raw store payload when it
914
+ * exists, or `null` on 404 so callers can treat "missing" as a normal
915
+ * condition (the capability probe and `ensureStore` both rely on this).
916
+ * Any other non-2xx status throws.
917
+ */
918
+ async function getStore(client, fullName, signal) {
919
+ const res = await rawFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
920
+ method: "GET",
921
+ ...signal ? { signal } : {}
922
+ });
923
+ if (res.status === 404) return null;
924
+ if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await safeText(res));
925
+ return safeJson(res);
926
+ }
927
+ /**
928
+ * Probe for the store and create it when missing. Returns `true` when
929
+ * the store exists (found or created). The create body splits the
930
+ * three-level name into the catalog / schema / name fields the API
931
+ * expects. A `getStore` 404 followed by a successful create is the
932
+ * common first-run path.
933
+ */
934
+ async function ensureStore(client, fullName, description, signal) {
935
+ if (await getStore(client, fullName, signal) !== null) return true;
936
+ const { catalog, schema, name } = parseStoreName(fullName);
937
+ await requestJson(client, MEMORY_STORES_PATH, {
938
+ method: "POST",
939
+ body: {
940
+ name,
941
+ catalog_name: catalog,
942
+ schema_name: schema,
943
+ description
944
+ },
945
+ ...signal ? { signal } : {}
946
+ });
947
+ return true;
948
+ }
949
+ /**
950
+ * Write (upsert) an entry into the store under `scope`. Scope is the
951
+ * OBO user id resolved in trusted code; it travels as a query parameter
952
+ * so the API isolates one user's memories from another's.
953
+ */
954
+ async function writeEntry(client, fullName, scope, entry, signal) {
955
+ await requestJson(client, `${MEMORY_STORES_PATH}/${fullName}/entries?scope=${encodeURIComponent(scope)}`, {
956
+ method: "POST",
957
+ body: {
958
+ path: entry.path,
959
+ contents: entry.contents,
960
+ ...entry.description ? { description: entry.description } : {}
961
+ },
962
+ ...signal ? { signal } : {}
963
+ });
964
+ }
965
+ /**
966
+ * Semantic search over a user's entries within `scope`. Returns up to
967
+ * `topK` entries, parsed defensively so a renamed / missing Beta field
968
+ * degrades to an empty list rather than crashing the turn.
969
+ */
970
+ async function search(client, fullName, scope, query, topK, signal) {
971
+ return parseEntries(await requestJson(client, `${MEMORY_STORES_PATH}/${fullName}/entries:search?scope=${encodeURIComponent(scope)}`, {
972
+ method: "POST",
973
+ body: {
974
+ query,
975
+ top_k: topK
976
+ },
977
+ ...signal ? { signal } : {}
978
+ })).slice(0, topK);
979
+ }
980
+ /**
981
+ * Defensively pull a `MemoryEntry[]` out of a search response. Accepts
982
+ * the documented `{ entries: [...] }` envelope plus a bare array, and
983
+ * tolerates snake_case / camelCase field drift. Entries without any
984
+ * textual content are dropped.
985
+ */
986
+ function parseEntries(body) {
987
+ const list = Array.isArray(body) ? body : Array.isArray(body?.entries) ? body.entries : Array.isArray(body?.results) ? body.results : [];
988
+ const out = [];
989
+ for (const raw of list) {
990
+ if (!raw || typeof raw !== "object") continue;
991
+ const obj = raw;
992
+ const contents = firstString(obj["contents"], obj["content"], obj["text"]);
993
+ if (!contents) continue;
994
+ const path = firstString(obj["path"]);
995
+ const description = firstString(obj["description"], obj["summary"]);
996
+ const score = typeof obj["score"] === "number" ? obj["score"] : void 0;
997
+ out.push({
998
+ contents,
999
+ ...path ? { path } : {},
1000
+ ...description ? { description } : {},
1001
+ ...score !== void 0 ? { score } : {}
1002
+ });
1003
+ }
1004
+ return out;
1005
+ }
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
+
1029
+ //#endregion
1030
+ //#region packages/appkit-mastra/src/connectors/managed-memory/context.ts
1031
+ /**
1032
+ * Per-request resolution of the OBO workspace client and the memory
1033
+ * scope (the user id memories are partitioned by). Shared by the
1034
+ * managed-memory tools and the recall processor so the "who is this
1035
+ * turn for, and as whom do we call Databricks" logic lives in one
1036
+ * place.
1037
+ *
1038
+ * Scope is always derived from trusted server state - the AppKit user
1039
+ * stamped on the request context, or the active execution context - and
1040
+ * never from anything the model controls. Returns `null` when there is
1041
+ * no resolvable user (a stateless turn, or an MCP call with no request
1042
+ * context), so callers cleanly no-op instead of leaking memories across
1043
+ * users.
1044
+ */
1045
+ /**
1046
+ * Resolve the OBO-scoped workspace client and the memory scope for the
1047
+ * current turn. Mirrors `model.ts`: prefer the AppKit user stamped on
1048
+ * the request context, fall back to the ambient execution context (the
1049
+ * active OBO scope or the service principal). Scope is the Mastra
1050
+ * resource id (the per-user thread owner) when present, else the user
1051
+ * id. Returns `null` when neither yields a usable scope.
1052
+ */
1053
+ function resolveMemoryContext(requestContext) {
1054
+ const user = requestContext?.get(MASTRA_USER_KEY);
1055
+ let executionContext;
1056
+ try {
1057
+ executionContext = user?.executionContext ?? getExecutionContext();
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ const resourceId = requestContext?.get(MASTRA_RESOURCE_ID_KEY);
1062
+ const scope = stringUtils.trimToNull(resourceId) ?? stringUtils.trimToNull(user?.id);
1063
+ if (!scope) return null;
1064
+ return {
1065
+ client: executionContext.client,
1066
+ scope
1067
+ };
1068
+ }
1069
+
1070
+ //#endregion
1071
+ //#region packages/appkit-mastra/src/connectors/managed-memory/tools.ts
1072
+ /**
1073
+ * Ambient Mastra tools for Databricks Managed Agent Memory:
1074
+ * `save_memory` (persist a durable fact) and `search_memory` (look up
1075
+ * prior memories). Built only when managed memory is the active
1076
+ * long-term backend and merged into every agent's tool set by
1077
+ * `buildAgents`.
1078
+ *
1079
+ * Both tools resolve the OBO user scope from trusted request state via
1080
+ * {@link resolveMemoryContext} - the model passes only the content /
1081
+ * query, never the scope (honoring the Managed Memory security
1082
+ * guidance). On a stateless turn (no resolvable user) the tools no-op
1083
+ * with a clear message instead of writing cross-user data, and any REST
1084
+ * failure degrades to a short error string rather than aborting the
1085
+ * turn.
1086
+ */
1087
+ const log$7 = logUtils.logger("mastra/managed-memory/tools");
1088
+ const saveMemoryInput = z.object({
1089
+ content: z.string().describe("The durable fact or preference to remember."),
1090
+ summary: z.string().optional().describe("Optional one-line summary used to label the memory.")
1091
+ });
1092
+ const searchMemoryInput = z.object({ query: z.string().describe("What to look up in the user's saved memories.") });
1093
+ /**
1094
+ * Build the `save_memory` / `search_memory` tool pair bound to a
1095
+ * resolved managed-memory runtime. The runtime supplies the store name,
1096
+ * recall size, and default entry path; the per-request client and scope
1097
+ * are resolved inside each `execute`.
1098
+ */
1099
+ function buildManagedMemoryTools(runtime) {
1100
+ return {
1101
+ save_memory: createTool$1({
1102
+ id: "save_memory",
1103
+ description: stringUtils.toDescription([`
1104
+ Save a durable fact or preference about the user to long-term
1105
+ memory so it can be recalled in future conversations. Use when
1106
+ the user states something worth remembering across sessions
1107
+ ("I'm in the EU, use EUR", "always show the SQL"). Pass the
1108
+ fact as \`content\` and an optional one-line \`summary\`.
1109
+ `, `
1110
+ The memory is automatically scoped to the current user - never
1111
+ include another person's data.
1112
+ `]),
1113
+ inputSchema: saveMemoryInput,
1114
+ execute: async (input, ctxRaw) => {
1115
+ const { content, summary } = input;
1116
+ const ctx = resolveMemoryContext(requestContextOf(ctxRaw));
1117
+ if (!ctx) return "Memory is not available for this conversation.";
1118
+ try {
1119
+ await writeEntry(ctx.client, runtime.storeName, ctx.scope, {
1120
+ path: runtime.entryPath,
1121
+ contents: content,
1122
+ ...summary ? { description: summary } : {}
1123
+ });
1124
+ log$7.debug("saved", { scope: ctx.scope });
1125
+ return "Saved to memory.";
1126
+ } catch (err) {
1127
+ log$7.warn("save:error", { error: commonUtils.errorMessage(err) });
1128
+ return "Could not save to memory right now.";
1129
+ }
1130
+ }
1131
+ }),
1132
+ search_memory: createTool$1({
1133
+ id: "search_memory",
1134
+ description: stringUtils.toDescription([`
1135
+ Search the user's long-term memory for relevant saved facts or
1136
+ preferences. Use before answering when prior context might
1137
+ help. Pass what you're looking for as \`query\`; returns the
1138
+ most relevant saved memories (scoped to the current user).
1139
+ `]),
1140
+ inputSchema: searchMemoryInput,
1141
+ execute: async (input, ctxRaw) => {
1142
+ const { query } = input;
1143
+ const ctx = resolveMemoryContext(requestContextOf(ctxRaw));
1144
+ if (!ctx) return { memories: [] };
1145
+ try {
1146
+ return { memories: (await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK)).map(renderEntry) };
1147
+ } catch (err) {
1148
+ log$7.warn("search:error", { error: commonUtils.errorMessage(err) });
1149
+ return { memories: [] };
1150
+ }
1151
+ }
1152
+ })
1153
+ };
1154
+ }
1155
+ /** Render an entry to a single recall line (summary prefix when present). */
1156
+ function renderEntry(entry) {
1157
+ return entry.description ? `${entry.description}: ${entry.contents}` : entry.contents;
1158
+ }
1159
+ /** Pull the Mastra `RequestContext` out of a tool's execution context arg. */
1160
+ function requestContextOf(ctxRaw) {
1161
+ return ctxRaw?.requestContext;
1162
+ }
1163
+
851
1164
  //#endregion
852
1165
  //#region packages/appkit-mastra/src/statement.ts
853
1166
  /**
@@ -970,7 +1283,7 @@ async function safeWrite(log, writer, chunk, context = {}) {
970
1283
  * `instructions` when you want the canonical "how to drive the
971
1284
  * Genie tools" guidance.
972
1285
  */
973
- const log$4 = logUtils.logger("mastra/genie");
1286
+ const log$6 = logUtils.logger("mastra/genie");
974
1287
  /** Default alias used when a single unnamed Genie space is wired up. */
975
1288
  const DEFAULT_GENIE_ALIAS = "default";
976
1289
  /**
@@ -1085,7 +1398,7 @@ async function readCachedConversationId(cacheKey) {
1085
1398
  try {
1086
1399
  return await CacheManager.getInstanceSync().get(cacheKey) ?? void 0;
1087
1400
  } catch (err) {
1088
- log$4.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1401
+ log$6.warn("conversation-cache:read-error", { error: commonUtils.errorMessage(err) });
1089
1402
  return;
1090
1403
  }
1091
1404
  }
@@ -1101,7 +1414,7 @@ async function saveCachedConversationId(cacheKey, conversationId) {
1101
1414
  try {
1102
1415
  await CacheManager.getInstanceSync().set(cacheKey, conversationId, { ttl: CONVERSATION_TTL_SEC });
1103
1416
  } catch (err) {
1104
- log$4.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1417
+ log$6.warn("conversation-cache:write-error", { error: commonUtils.errorMessage(err) });
1105
1418
  }
1106
1419
  }
1107
1420
  /** Force-evict a cached conversation id. Used on the stale-id recovery path. */
@@ -1110,7 +1423,7 @@ async function evictCachedConversationId(cacheKey) {
1110
1423
  try {
1111
1424
  await CacheManager.getInstanceSync().delete(cacheKey);
1112
1425
  } catch (err) {
1113
- log$4.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1426
+ log$6.warn("conversation-cache:delete-error", { error: commonUtils.errorMessage(err) });
1114
1427
  }
1115
1428
  }
1116
1429
  /**
@@ -1224,7 +1537,7 @@ function buildAskGenieTool(opts) {
1224
1537
  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`);
1225
1538
  const cacheKey = await conversationCacheKey(spaceId, threadId);
1226
1539
  await ensureConversationSeeded(requestContext, spaceId, cacheKey);
1227
- await safeWrite(log$4, writer, {
1540
+ await safeWrite(log$6, writer, {
1228
1541
  type: "started",
1229
1542
  spaceId,
1230
1543
  content: question
@@ -1237,7 +1550,7 @@ function buildAskGenieTool(opts) {
1237
1550
  ...seedConversationId ? { conversationId: seedConversationId } : {},
1238
1551
  ...signal ? { context: signal } : {}
1239
1552
  })) {
1240
- if (event.type !== "message") await safeWrite(log$4, writer, event);
1553
+ if (event.type !== "message") await safeWrite(log$6, writer, event);
1241
1554
  const eventConversationId = event.conversation_id;
1242
1555
  if (eventConversationId) writeContextConversationId(requestContext, spaceId, eventConversationId);
1243
1556
  if (event.type === "result") finalMessage = event.message;
@@ -1251,7 +1564,7 @@ function buildAskGenieTool(opts) {
1251
1564
  } catch (err) {
1252
1565
  const seeded = readContextConversationId(requestContext, spaceId);
1253
1566
  if (seeded && apiUtils.isNotFoundError(err)) {
1254
- log$4.warn("conversation-cache:stale, resetting", {
1567
+ log$6.warn("conversation-cache:stale, resetting", {
1255
1568
  spaceId,
1256
1569
  conversationId: seeded,
1257
1570
  error: commonUtils.errorMessage(err)
@@ -1736,7 +2049,7 @@ async function collectSpaceSuggestions(opts) {
1736
2049
  }));
1737
2050
  writeSuggestionCache(spaceId, questions);
1738
2051
  } catch (err) {
1739
- log$4.warn("suggestions:fetch-error", {
2052
+ log$6.warn("suggestions:fetch-error", {
1740
2053
  spaceId,
1741
2054
  error: commonUtils.errorMessage(err)
1742
2055
  });
@@ -1782,6 +2095,76 @@ var ResultProcessor = class {
1782
2095
  }
1783
2096
  };
1784
2097
 
2098
+ //#endregion
2099
+ //#region packages/appkit-mastra/src/processors/managed-memory-recall.ts
2100
+ /**
2101
+ * Mastra input processor that auto-recalls Databricks Managed Agent
2102
+ * Memory before each turn. This is the managed-memory replacement for
2103
+ * Postgres `PgVector` semantic recall: instead of Mastra reading a
2104
+ * vector index, this processor searches the user's memory-store scope
2105
+ * with the latest user message and injects the top entries as an
2106
+ * appended system message so the model has the user's durable context.
2107
+ *
2108
+ * Scope comes from trusted request state via {@link resolveMemoryContext}
2109
+ * (the OBO user), never from the model. On a stateless turn, an empty
2110
+ * query, or any REST failure the processor passes the messages through
2111
+ * unchanged so a recall hiccup never blocks the conversation.
2112
+ */
2113
+ const log$5 = logUtils.logger("mastra/processor/managed-memory-recall");
2114
+ /**
2115
+ * Build the recall processor bound to a managed-memory runtime. Created
2116
+ * once at setup (when managed memory is active) and wired onto every
2117
+ * agent's `inputProcessors` by {@link buildAgents}.
2118
+ */
2119
+ function buildManagedMemoryRecallProcessor(runtime) {
2120
+ return {
2121
+ id: "managed-memory-recall",
2122
+ description: "Recalls the user's Databricks Managed Memory entries and injects them as context before the model runs.",
2123
+ async processInput(args) {
2124
+ const ctx = resolveMemoryContext(args.requestContext);
2125
+ if (!ctx) return args.messages;
2126
+ const query = latestUserText(args.messages);
2127
+ if (!query) return args.messages;
2128
+ try {
2129
+ const entries = await search(ctx.client, runtime.storeName, ctx.scope, query, runtime.topK, args.abortSignal);
2130
+ if (entries.length === 0) return args.messages;
2131
+ const block = ["Relevant memories about the user (from long-term memory):", ...entries.map((e) => `- ${renderEntry(e)}`)].join("\n");
2132
+ log$5.debug("recalled", {
2133
+ scope: ctx.scope,
2134
+ entries: entries.length
2135
+ });
2136
+ return {
2137
+ messages: args.messages,
2138
+ systemMessages: [...args.systemMessages, {
2139
+ role: "system",
2140
+ content: block
2141
+ }]
2142
+ };
2143
+ } catch (err) {
2144
+ log$5.warn("recall:error", { error: commonUtils.errorMessage(err) });
2145
+ return args.messages;
2146
+ }
2147
+ }
2148
+ };
2149
+ }
2150
+ /**
2151
+ * Extract the most recent user message's text. Walks the message list
2152
+ * from the end, collecting `text` parts off the first `user` message.
2153
+ * Returns `null` when there is no user text to search with.
2154
+ */
2155
+ function latestUserText(messages) {
2156
+ for (let i = messages.length - 1; i >= 0; i--) {
2157
+ const message = messages[i];
2158
+ if (!message || message.role !== "user") continue;
2159
+ const content = message.content;
2160
+ const parts = content?.parts;
2161
+ if (!Array.isArray(parts)) return stringUtils.trimToNull(content?.content);
2162
+ const text = parts.filter((p) => p.type === "text" && typeof p.text === "string").map((p) => p.text).join(" ");
2163
+ return stringUtils.trimToNull(text);
2164
+ }
2165
+ return null;
2166
+ }
2167
+
1785
2168
  //#endregion
1786
2169
  //#region packages/appkit-mastra/src/processors/strip-stale-charts.ts
1787
2170
  /**
@@ -1808,7 +2191,7 @@ var ResultProcessor = class {
1808
2191
  * legacy `datasets[].chartId` payloads uniformly without coupling
1809
2192
  * to specific tool ids.
1810
2193
  */
1811
- const log$3 = logUtils.logger("mastra/processor/strip-stale-charts");
2194
+ const log$4 = logUtils.logger("mastra/processor/strip-stale-charts");
1812
2195
  /**
1813
2196
  * Recursively clone `value`, omitting any property whose key is
1814
2197
  * `chartId`. Arrays are mapped element-wise; primitives are
@@ -1856,11 +2239,135 @@ const stripStaleChartsProcessor = {
1856
2239
  }
1857
2240
  }
1858
2241
  }
1859
- if (stripped > 0) log$3.debug("stripped", { results: stripped });
2242
+ if (stripped > 0) log$4.debug("stripped", { results: stripped });
1860
2243
  return args.messages;
1861
2244
  }
1862
2245
  };
1863
2246
 
2247
+ //#endregion
2248
+ //#region packages/appkit-mastra/src/summarize.ts
2249
+ /**
2250
+ * Small-tier summarization for the Mastra plugin.
2251
+ *
2252
+ * Two surfaces, both backed by the fast / small chat tier
2253
+ * ({@link ModelClass.ChatFast}) resolved through the same
2254
+ * `/serving-endpoints` pipeline as the main agents:
2255
+ *
2256
+ * - A dedicated `summarize` tool (see {@link buildSummarizeTool})
2257
+ * agents can call to condense arbitrary text without burning the
2258
+ * heavyweight chat model.
2259
+ * - The model + instructions Mastra's memory uses to auto-name
2260
+ * conversation threads (`generateTitle`), so titling reuses the
2261
+ * same small tier rather than the agent's primary model.
2262
+ *
2263
+ * Mirrors the chart-planner wiring in `chart.ts`: a per-config cached
2264
+ * `Agent` on the fast tier, invoked via `agent.generate(...)` inside
2265
+ * the active `asUser` scope so tokens stay user-scoped.
2266
+ */
2267
+ /** Fast / small chat tier used for both titling and summaries. */
2268
+ const SUMMARY_MODEL_CLASS = ModelClass.ChatFast;
2269
+ /** System prompt for the summarizer agent (and the `summarize` tool). */
2270
+ const SUMMARIZER_INSTRUCTIONS = [
2271
+ "You are a summarization engine.",
2272
+ "Given a block of text, produce a faithful, concise summary of it.",
2273
+ "Default to a few sentences; follow any length guidance the caller gives.",
2274
+ "Plain prose. No preamble, no headers, no bullet points unless asked.",
2275
+ "Never use emojis. Use hyphens (-) only, never em dashes or en dashes.",
2276
+ "Never add information, opinions, or details not present in the input.",
2277
+ "Output only the summary."
2278
+ ].join("\n");
2279
+ /**
2280
+ * Instructions Mastra's `generateTitle` hands the small-tier model to
2281
+ * name a conversation thread from its opening turn. Kept terse so the
2282
+ * model returns a bare title with no decoration.
2283
+ */
2284
+ const TITLE_INSTRUCTIONS = [
2285
+ "Generate a short, specific title for this conversation, 3 to 6 words.",
2286
+ "Capture the user's topic, not the assistant's response.",
2287
+ "Plain text only: no surrounding quotes, no trailing punctuation,",
2288
+ "no emojis, and no em dashes. Output only the title."
2289
+ ].join(" ");
2290
+ /**
2291
+ * Resolve the small-tier model for summarization / titling. Reused by
2292
+ * both the summarizer agent and Mastra memory's `generateTitle`.
2293
+ *
2294
+ * Returned as a `requestContext`-taking function (a Mastra
2295
+ * `DynamicArgument<MastraModelConfig>`) so each call mints user-scoped
2296
+ * tokens via {@link buildModel}, exactly like the primary agents.
2297
+ */
2298
+ function summaryModel(config) {
2299
+ return ({ requestContext }) => buildModel(config, requestContext, { modelClass: SUMMARY_MODEL_CLASS });
2300
+ }
2301
+ /**
2302
+ * One summarizer `Agent` per plugin config, cached on config-object
2303
+ * identity so a hot tool path doesn't pay the constructor cost each
2304
+ * call. `WeakMap` lets retired configs (e.g. test reconfigurations)
2305
+ * release their agent without manual eviction.
2306
+ */
2307
+ const summarizerAgents = /* @__PURE__ */ new WeakMap();
2308
+ function getSummarizerAgent(config) {
2309
+ let agent = summarizerAgents.get(config);
2310
+ if (!agent) {
2311
+ agent = new Agent({
2312
+ id: "summarizer",
2313
+ name: "Summarizer",
2314
+ description: "Condenses text into a short summary using a fast, small model.",
2315
+ instructions: SUMMARIZER_INSTRUCTIONS,
2316
+ model: summaryModel(config)
2317
+ });
2318
+ summarizerAgents.set(config, agent);
2319
+ }
2320
+ return agent;
2321
+ }
2322
+ /** Inputs accepted by the `summarize` tool. */
2323
+ const summarizeInput = z.object({
2324
+ text: z.string().min(1).describe("The text to summarize."),
2325
+ instructions: z.string().optional().describe("Optional extra guidance for the summary, e.g. 'one sentence' or 'list the action items'."),
2326
+ maxWords: z.number().int().positive().optional().describe("Optional soft cap on the summary length, in words.")
2327
+ });
2328
+ /**
2329
+ * Summarize `text` with the small-tier summarizer agent, returning the
2330
+ * trimmed summary string. Throws on model failure (the caller decides
2331
+ * how to degrade).
2332
+ */
2333
+ async function summarizeText(config, text, options = {}) {
2334
+ const { instructions, maxWords, requestContext, abortSignal } = options;
2335
+ const prompt = stringUtils.toDescription({
2336
+ ...instructions ? { Guidance: instructions } : {},
2337
+ ...maxWords !== void 0 ? { "Max length (words)": String(maxWords) } : {},
2338
+ Text: text
2339
+ });
2340
+ return (await getSummarizerAgent(config).generate(prompt, {
2341
+ ...requestContext ? { requestContext } : {},
2342
+ ...abortSignal ? { abortSignal } : {}
2343
+ })).text.trim();
2344
+ }
2345
+ /**
2346
+ * Build the `summarize` tool. Exposed as an ambient system tool (like
2347
+ * `render_data`) so every agent can offload condensing long content,
2348
+ * notes, transcripts, or bulky tool results to the fast tier instead of
2349
+ * spending its primary chat model. The tool reads the live
2350
+ * `requestContext` / `abortSignal` off the Mastra execution context so
2351
+ * its model call stays user-scoped and cancels with the turn.
2352
+ */
2353
+ function buildSummarizeTool(config) {
2354
+ return createTool$1({
2355
+ id: "summarize",
2356
+ description: "Summarize a block of text using a fast, small model. Use it to condense long content, notes, transcripts, or tool results into a short summary without spending the main chat model.",
2357
+ inputSchema: summarizeInput,
2358
+ execute: async (input, context) => {
2359
+ const { text, instructions, maxWords } = input;
2360
+ const ctx = context;
2361
+ return { summary: await summarizeText(config, text, {
2362
+ ...instructions ? { instructions } : {},
2363
+ ...maxWords !== void 0 ? { maxWords } : {},
2364
+ ...ctx?.requestContext ? { requestContext: ctx.requestContext } : {},
2365
+ ...ctx?.abortSignal ? { abortSignal: ctx.abortSignal } : {}
2366
+ }) };
2367
+ }
2368
+ });
2369
+ }
2370
+
1864
2371
  //#endregion
1865
2372
  //#region packages/appkit-mastra/src/agents.ts
1866
2373
  /**
@@ -2022,18 +2529,24 @@ function composeInstructions(agentInstructions, style) {
2022
2529
  * resolved registry; this is a wiring bug, not a runtime condition.
2023
2530
  */
2024
2531
  async function buildAgents(opts) {
2025
- const { config, context, memoryBuilder, log } = opts;
2532
+ const { config, context, memoryBuilder, managedMemory, log } = opts;
2026
2533
  const definitions = resolveDefinitions(config);
2027
2534
  const ids = Object.keys(definitions);
2028
2535
  const defaultAgentId = config.defaultAgent ?? ids[0] ?? FALLBACK_AGENT_ID;
2029
2536
  const plugins = buildPluginsMap(config, context);
2030
- const ambientTools = {
2537
+ const systemTools = {
2031
2538
  render_data: buildRenderDataTool(config),
2539
+ summarize: buildSummarizeTool(config)
2540
+ };
2541
+ const managedMemoryTools = managedMemory?.tools ? buildManagedMemoryTools(managedMemory) : {};
2542
+ const ambientTools = {
2543
+ ...systemTools,
2544
+ ...managedMemoryTools,
2032
2545
  ...config.tools ?? {}
2033
2546
  };
2034
2547
  const style = resolveStyleInstructions(config);
2035
2548
  const outputProcessors = [new ResultProcessor()];
2036
- const inputProcessors = config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor];
2549
+ const inputProcessors = [...config.stripStaleCharts === false ? [] : [stripStaleChartsProcessor], ...managedMemory?.recall ? [buildManagedMemoryRecallProcessor(managedMemory)] : []];
2037
2550
  const agents = {};
2038
2551
  for (const [id, def] of Object.entries(definitions)) {
2039
2552
  const tools = await resolveTools(def.tools, plugins, ambientTools);
@@ -2278,6 +2791,72 @@ function toolkitEntryToMastraTool(entry, plugin) {
2278
2791
  });
2279
2792
  }
2280
2793
 
2794
+ //#endregion
2795
+ //#region packages/appkit-mastra/src/connectors/managed-memory/defaults.ts
2796
+ /**
2797
+ * Default values and environment variable names for the managed-memory
2798
+ * connector. Kept in a leaf module so the client, tools, recall
2799
+ * processor, and plugin setup can share them without a cycle.
2800
+ */
2801
+ /**
2802
+ * Environment variable holding the three-level Unity Catalog store name
2803
+ * (`catalog.schema.name`). Read when {@link ManagedMemoryConfig.store}
2804
+ * is omitted; also the sole enable signal in prefer-if-available mode
2805
+ * (managed memory stays off when it is unset).
2806
+ */
2807
+ const MEMORY_STORE_ENV = "MEMORY_STORE";
2808
+ /**
2809
+ * Default number of entries the recall processor injects and
2810
+ * `search_memory` returns. Matches the prior `PgVector`
2811
+ * `semanticRecall.topK` so behavior is unchanged after the swap.
2812
+ */
2813
+ const DEFAULT_TOPK = 3;
2814
+ /**
2815
+ * Default path `save_memory` writes land under. Managed Memory keys
2816
+ * entries by path within a scope, so a single stable path accumulates a
2817
+ * user's notes rather than scattering them.
2818
+ */
2819
+ const DEFAULT_ENTRY_PATH = "/memories/notes.md";
2820
+ /** Description stamped on the store when the connector auto-creates it. */
2821
+ const DEFAULT_STORE_DESCRIPTION = "Long-term agent memory managed by @dbx-tools/appkit-mastra.";
2822
+
2823
+ //#endregion
2824
+ //#region packages/appkit-mastra/src/connectors/managed-memory/resolve.ts
2825
+ /**
2826
+ * Resolve the managed-memory target from config + env, or `null` when
2827
+ * managed memory is disabled. Throws when explicitly enabled but no
2828
+ * store name can be resolved (config or `MEMORY_STORE`), so a
2829
+ * misconfiguration surfaces at setup rather than silently falling back.
2830
+ */
2831
+ function resolveManagedMemoryTarget(config) {
2832
+ const setting = config.managedMemory;
2833
+ if (setting === false) return null;
2834
+ const envStore = trimmedEnv(MEMORY_STORE_ENV);
2835
+ const obj = typeof setting === "object" ? setting : {};
2836
+ if (!(setting === true || typeof setting === "object") && !envStore) return null;
2837
+ const storeName = trimmed(obj.store) ?? envStore;
2838
+ if (!storeName) throw new Error(`mastra: managedMemory is enabled but no store name is set. Set managedMemory.store or the ${MEMORY_STORE_ENV} env var to a three-level Unity Catalog name (catalog.schema.name).`);
2839
+ return {
2840
+ storeName,
2841
+ description: trimmed(obj.description) ?? DEFAULT_STORE_DESCRIPTION,
2842
+ topK: obj.topK ?? DEFAULT_TOPK,
2843
+ entryPath: trimmed(obj.entryPath) ?? DEFAULT_ENTRY_PATH,
2844
+ autoCreate: obj.autoCreate ?? true,
2845
+ recall: obj.recall ?? true,
2846
+ tools: obj.tools ?? true
2847
+ };
2848
+ }
2849
+ /** Trimmed env var value, or undefined when unset / blank. */
2850
+ function trimmedEnv(name) {
2851
+ return trimmed(process.env[name]);
2852
+ }
2853
+ /** Trimmed string, or undefined when undefined / blank. */
2854
+ function trimmed(value) {
2855
+ if (typeof value !== "string") return void 0;
2856
+ const t = value.trim();
2857
+ return t === "" ? void 0 : t;
2858
+ }
2859
+
2281
2860
  //#endregion
2282
2861
  //#region packages/appkit-mastra/src/mcp.ts
2283
2862
  /** MCP server version advertised when the caller doesn't pin one. */
@@ -2320,11 +2899,11 @@ function buildMcpServer(opts) {
2320
2899
 
2321
2900
  //#endregion
2322
2901
  //#region packages/appkit-mastra/src/history.ts
2323
- const log$2 = logUtils.logger("mastra/history");
2902
+ const log$3 = logUtils.logger("mastra/history");
2324
2903
  /** Default history page size; matches the Mastra storage default. */
2325
- const DEFAULT_PER_PAGE = 20;
2904
+ const DEFAULT_PER_PAGE$1 = 20;
2326
2905
  /** Hard cap so a misbehaving client can't fetch the whole thread at once. */
2327
- const MAX_PER_PAGE = 200;
2906
+ const MAX_PER_PAGE$1 = 200;
2328
2907
  /**
2329
2908
  * Fetch a page of UI-formatted messages for a thread.
2330
2909
  *
@@ -2341,11 +2920,11 @@ const MAX_PER_PAGE = 200;
2341
2920
  * without sorting locally.
2342
2921
  */
2343
2922
  async function loadHistory(opts) {
2344
- const perPage = clampPerPage(opts.perPage);
2923
+ const perPage = clampPerPage$1(opts.perPage);
2345
2924
  const page = Math.max(0, Math.trunc(opts.page ?? 0));
2346
2925
  const memory = await opts.agent.getMemory();
2347
2926
  if (!memory) {
2348
- log$2.debug("recall:no-memory", {
2927
+ log$3.debug("recall:no-memory", {
2349
2928
  agentId: opts.agent.id,
2350
2929
  threadId: opts.threadId
2351
2930
  });
@@ -2369,7 +2948,7 @@ async function loadHistory(opts) {
2369
2948
  }
2370
2949
  });
2371
2950
  const uiMessages = toAISdkV5Messages(sortChronological(result.messages));
2372
- log$2.debug("recall:done", {
2951
+ log$3.debug("recall:done", {
2373
2952
  agentId: opts.agent.id,
2374
2953
  threadId: opts.threadId,
2375
2954
  page,
@@ -2403,7 +2982,7 @@ async function loadHistory(opts) {
2403
2982
  async function clearHistory(opts) {
2404
2983
  const memory = await opts.agent.getMemory();
2405
2984
  if (!memory) {
2406
- log$2.debug("clear:no-memory", {
2985
+ log$3.debug("clear:no-memory", {
2407
2986
  agentId: opts.agent.id,
2408
2987
  threadId: opts.threadId
2409
2988
  });
@@ -2417,7 +2996,7 @@ async function clearHistory(opts) {
2417
2996
  perPage: 1
2418
2997
  })).total;
2419
2998
  } catch (err) {
2420
- log$2.debug("clear:probe-failed", {
2999
+ log$3.debug("clear:probe-failed", {
2421
3000
  agentId: opts.agent.id,
2422
3001
  threadId: opts.threadId,
2423
3002
  error: commonUtils.errorMessage(err)
@@ -2427,13 +3006,13 @@ async function clearHistory(opts) {
2427
3006
  try {
2428
3007
  await memory.deleteThread(opts.threadId);
2429
3008
  } catch (err) {
2430
- log$2.warn("clear:delete-soft-failed", {
3009
+ log$3.warn("clear:delete-soft-failed", {
2431
3010
  agentId: opts.agent.id,
2432
3011
  threadId: opts.threadId,
2433
3012
  error: commonUtils.errorMessage(err)
2434
3013
  });
2435
3014
  }
2436
- log$2.info("clear:done", {
3015
+ log$3.info("clear:done", {
2437
3016
  agentId: opts.agent.id,
2438
3017
  threadId: opts.threadId,
2439
3018
  cleared,
@@ -2490,8 +3069,8 @@ function historyRoute(options) {
2490
3069
  agent: ctx.agent,
2491
3070
  threadId: ctx.threadId,
2492
3071
  ...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
2493
- page: parseIntParam(c.req.query("page")),
2494
- perPage: parseIntParam(c.req.query("perPage"))
3072
+ page: parseIntParam$1(c.req.query("page")),
3073
+ perPage: parseIntParam$1(c.req.query("perPage"))
2495
3074
  });
2496
3075
  return c.json(payload);
2497
3076
  }
@@ -2515,11 +3094,11 @@ function historyRoute(options) {
2515
3094
  })];
2516
3095
  }
2517
3096
  /** Coerce / clamp `perPage`; falls back to the page-size default. */
2518
- function clampPerPage(value) {
2519
- if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
3097
+ function clampPerPage$1(value) {
3098
+ if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE$1;
2520
3099
  const n = Math.trunc(value);
2521
- if (n <= 0) return DEFAULT_PER_PAGE;
2522
- return Math.min(n, MAX_PER_PAGE);
3100
+ if (n <= 0) return DEFAULT_PER_PAGE$1;
3101
+ return Math.min(n, MAX_PER_PAGE$1);
2523
3102
  }
2524
3103
  /**
2525
3104
  * Sort messages oldest-first by `createdAt`, falling back to whatever
@@ -2545,7 +3124,7 @@ function toEpoch(value) {
2545
3124
  * `undefined` for empty / non-numeric / negative inputs so
2546
3125
  * {@link loadHistory} can apply its built-in defaults.
2547
3126
  */
2548
- function parseIntParam(value) {
3127
+ function parseIntParam$1(value) {
2549
3128
  if (!value) return void 0;
2550
3129
  const n = Number(value);
2551
3130
  if (!Number.isFinite(n) || n < 0) return void 0;
@@ -2582,7 +3161,7 @@ function parseIntParam(value) {
2582
3161
  * (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
2583
3162
  * is registered); per-agent settings cascade on top of that.
2584
3163
  */
2585
- const log$1 = logUtils.logger("mastra/memory");
3164
+ const log$2 = logUtils.logger("mastra/memory");
2586
3165
  /**
2587
3166
  * Build a dedicated **service-principal** Lakebase pool for Mastra
2588
3167
  * memory from the lakebase plugin's resolved SP pg config.
@@ -2626,8 +3205,8 @@ function needsLakebase(config) {
2626
3205
  * Caches the shared `PgVector` singleton (built on first need) so each
2627
3206
  * agent build is O(1) after the first.
2628
3207
  */
2629
- function createMemoryBuilder(config, servicePrincipalPool) {
2630
- return new MemoryBuilder(config, servicePrincipalPool);
3208
+ function createMemoryBuilder(config, servicePrincipalPool, options = {}) {
3209
+ return new MemoryBuilder(config, servicePrincipalPool, options);
2631
3210
  }
2632
3211
  /**
2633
3212
  * Builds one `Memory` per agent against a shared service-principal
@@ -2636,9 +3215,10 @@ function createMemoryBuilder(config, servicePrincipalPool) {
2636
3215
  */
2637
3216
  var MemoryBuilder = class {
2638
3217
  sharedVector;
2639
- constructor(config, servicePrincipalPool) {
3218
+ constructor(config, servicePrincipalPool, options = {}) {
2640
3219
  this.config = config;
2641
3220
  this.servicePrincipalPool = servicePrincipalPool;
3221
+ this.options = options;
2642
3222
  }
2643
3223
  /**
2644
3224
  * Build a `Memory` for `agentId` after the plugin/agent cascade.
@@ -2673,10 +3253,10 @@ var MemoryBuilder = class {
2673
3253
  const storage = this.buildStorage(agentId, storageSetting);
2674
3254
  const vector = this.buildVector(memorySetting);
2675
3255
  if (!storage && !vector) {
2676
- log$1.debug("agent:stateless", { agentId });
3256
+ log$2.debug("agent:stateless", { agentId });
2677
3257
  return;
2678
3258
  }
2679
- log$1.debug("agent:configured", {
3259
+ log$2.debug("agent:configured", {
2680
3260
  agentId,
2681
3261
  storage: storage !== void 0,
2682
3262
  vector: vector !== void 0,
@@ -2693,6 +3273,10 @@ var MemoryBuilder = class {
2693
3273
  ...vector ? { semanticRecall: {
2694
3274
  topK: 3,
2695
3275
  messageRange: 2
3276
+ } } : {},
3277
+ ...storage ? { generateTitle: {
3278
+ model: summaryModel(this.config),
3279
+ instructions: TITLE_INSTRUCTIONS
2696
3280
  } } : {}
2697
3281
  }
2698
3282
  });
@@ -2716,6 +3300,7 @@ var MemoryBuilder = class {
2716
3300
  * - object: build a dedicated `PgVector` for this agent.
2717
3301
  */
2718
3302
  buildVector(setting) {
3303
+ if (this.options.suppressVector) return void 0;
2719
3304
  if (!setting) return void 0;
2720
3305
  if (typeof setting === "boolean") return this.getSharedVector();
2721
3306
  return buildPgVector(setting);
@@ -2817,7 +3402,7 @@ function withId(value, fallback) {
2817
3402
  * to the global noop tracer, mirroring how the `agents` plugin
2818
3403
  * silently no-ops in the same situation.
2819
3404
  */
2820
- const log = logUtils.logger("mastra/observability");
3405
+ const log$1 = logUtils.logger("mastra/observability");
2821
3406
  const DEFAULT_SERVICE_NAME = "mastra";
2822
3407
  /**
2823
3408
  * Build a Mastra `Observability` whose spans ride AppKit's global
@@ -2835,7 +3420,7 @@ async function buildObservability(options) {
2835
3420
  const otelBase = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
2836
3421
  const otelTracesOverride = process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT;
2837
3422
  const resolvedTracesUrl = otelTracesOverride ? otelTracesOverride : otelBase ? `${otelBase.replace(/\/+$/, "")}/v1/traces` : void 0;
2838
- log.info("Mastra observability wired through OTel bridge", {
3423
+ log$1.info("Mastra observability wired through OTel bridge", {
2839
3424
  serviceName,
2840
3425
  requestContextKeys,
2841
3426
  otelBase: otelBase ?? "<unset>",
@@ -2921,8 +3506,31 @@ var MastraServer$1 = class extends MastraServer {
2921
3506
  requestContext.set(MASTRA_REQUEST_ID_KEY, requestId);
2922
3507
  res.setHeader("X-Request-Id", requestId);
2923
3508
  }
3509
+ /**
3510
+ * Resolve the thread id this request targets and pin it on
3511
+ * `RequestContext` (consumed by the agent stream for persistence and
3512
+ * by the history / threads routes). Resolution order:
3513
+ *
3514
+ * 1. A client-supplied thread id (the thread-selection header /
3515
+ * `?threadId=` query). This is how the chat UI references a
3516
+ * specific conversation among the many a user owns - it picks a
3517
+ * thread id from the `/threads` listing (or mints one for a new
3518
+ * conversation) and stamps it here. The id is scoped to the
3519
+ * caller's resource by the recall / list routes, so a client
3520
+ * can only ever read or write its own threads.
3521
+ * 2. The per-session cookie (`appkit_<plugin-name>_session_id`),
3522
+ * minted on first contact. This is the default single-thread
3523
+ * fallback for clients that don't manage threads explicitly, so
3524
+ * existing embeds keep one stable conversation per session with
3525
+ * no client changes.
3526
+ */
2924
3527
  configureRequestContextThreadId(req, res, requestContext) {
2925
3528
  if (requestContext.get(MASTRA_THREAD_ID_KEY)) return;
3529
+ const requested = this.readRequestedThreadId(req);
3530
+ if (requested) {
3531
+ requestContext.set(MASTRA_THREAD_ID_KEY, requested);
3532
+ return;
3533
+ }
2926
3534
  const cookies = httpUtils.parseCookies(req.headers.cookie);
2927
3535
  const cookieName = stringUtils.toIdentifierWithOptions({
2928
3536
  delimiter: "_",
@@ -2940,6 +3548,17 @@ var MastraServer$1 = class extends MastraServer {
2940
3548
  }
2941
3549
  requestContext.set(MASTRA_THREAD_ID_KEY, sessionId);
2942
3550
  }
3551
+ /**
3552
+ * Read the client-selected thread id from the request, preferring
3553
+ * the thread-selection header over the `?threadId=` query. Returns
3554
+ * `null` when neither carries a non-empty value so the caller falls
3555
+ * back to the session cookie.
3556
+ */
3557
+ readRequestedThreadId(req) {
3558
+ const headerValue = req.headers[THREAD_ID_HEADER];
3559
+ const queryValue = req.query[THREAD_ID_QUERY];
3560
+ return stringUtils.trimToNull(Array.isArray(headerValue) ? headerValue[0] : headerValue) ?? stringUtils.trimToNull(Array.isArray(queryValue) ? queryValue[0] : queryValue);
3561
+ }
2943
3562
  configureRequestContextModelOverride(req, requestContext) {
2944
3563
  if (resolveServingConfig(this.config).allowOverride) {
2945
3564
  const override = extractModelOverride({
@@ -2973,6 +3592,206 @@ function attachRoutePatchMiddleware(app) {
2973
3592
  });
2974
3593
  }
2975
3594
 
3595
+ //#endregion
3596
+ //#region packages/appkit-mastra/src/threads.ts
3597
+ const log = logUtils.logger("mastra/threads");
3598
+ /** Default threads page size. */
3599
+ const DEFAULT_PER_PAGE = 30;
3600
+ /** Hard cap so a misbehaving client can't fetch every thread at once. */
3601
+ const MAX_PER_PAGE = 200;
3602
+ /**
3603
+ * Fetch a page of the resource's conversation threads, newest
3604
+ * (`updatedAt` DESC) first.
3605
+ *
3606
+ * Uses the agent's resolved `Memory` (`getMemory()`) so the per-agent
3607
+ * storage namespace (`mastra_<agentId>` schema) applies automatically.
3608
+ * When the agent has no memory configured the response is a successful
3609
+ * empty page so callers don't have to special-case stateless agents.
3610
+ */
3611
+ async function listThreads(opts) {
3612
+ const perPage = clampPerPage(opts.perPage);
3613
+ const page = Math.max(0, Math.trunc(opts.page ?? 0));
3614
+ const memory = await opts.agent.getMemory();
3615
+ if (!memory) {
3616
+ log.debug("list:no-memory", { agentId: opts.agent.id });
3617
+ return {
3618
+ threads: [],
3619
+ page,
3620
+ perPage,
3621
+ total: 0,
3622
+ hasMore: false
3623
+ };
3624
+ }
3625
+ const startedAt = Date.now();
3626
+ const result = await memory.listThreads({
3627
+ filter: { resourceId: opts.resourceId },
3628
+ page,
3629
+ perPage,
3630
+ orderBy: {
3631
+ field: "updatedAt",
3632
+ direction: "DESC"
3633
+ }
3634
+ });
3635
+ const threads = result.threads.map(toWireThread);
3636
+ log.debug("list:done", {
3637
+ agentId: opts.agent.id,
3638
+ resourceId: opts.resourceId,
3639
+ page,
3640
+ perPage,
3641
+ returned: threads.length,
3642
+ total: result.total,
3643
+ hasMore: result.hasMore,
3644
+ elapsedMs: Date.now() - startedAt
3645
+ });
3646
+ return {
3647
+ threads,
3648
+ page,
3649
+ perPage,
3650
+ total: result.total,
3651
+ hasMore: result.hasMore
3652
+ };
3653
+ }
3654
+ /**
3655
+ * Delete a single named thread (and every message on it).
3656
+ *
3657
+ * Ownership is enforced: the thread is only removed when it belongs to
3658
+ * the calling resource, so a client can't delete another user's
3659
+ * conversation by guessing its id. A thread that doesn't exist (or
3660
+ * isn't owned by the caller) is a successful no-op (`deleted: false`)
3661
+ * so the UI can fire-and-forget.
3662
+ */
3663
+ async function deleteThread(opts) {
3664
+ const memory = await opts.agent.getMemory();
3665
+ if (!memory) {
3666
+ log.debug("delete:no-memory", { agentId: opts.agent.id });
3667
+ return { deleted: false };
3668
+ }
3669
+ const existing = await memory.getThreadById({ threadId: opts.threadId });
3670
+ if (!existing || existing.resourceId !== opts.resourceId) {
3671
+ log.debug("delete:not-owned", {
3672
+ agentId: opts.agent.id,
3673
+ threadId: opts.threadId,
3674
+ found: existing !== null
3675
+ });
3676
+ return { deleted: false };
3677
+ }
3678
+ const startedAt = Date.now();
3679
+ await memory.deleteThread(opts.threadId);
3680
+ log.info("delete:done", {
3681
+ agentId: opts.agent.id,
3682
+ threadId: opts.threadId,
3683
+ elapsedMs: Date.now() - startedAt
3684
+ });
3685
+ return { deleted: true };
3686
+ }
3687
+ /**
3688
+ * Register the `<path>` Mastra custom API route. Handles two methods
3689
+ * on the same mount:
3690
+ *
3691
+ * - `GET`: a page of the resource's conversation threads
3692
+ * ({@link listThreads}).
3693
+ * - `DELETE`: remove the thread named by the thread-selection header
3694
+ * / `?threadId=` query ({@link deleteThread}). The id is read from
3695
+ * `RequestContext` (the auth middleware resolves it the same way it
3696
+ * does for streaming and history), so the client deletes any of its
3697
+ * threads by stamping the target id - no separate path param.
3698
+ *
3699
+ * Follows the `@mastra/ai-sdk` agent-binding convention: pass `agent`
3700
+ * for a fixed-agent mount, or include `:agentId` in the path for
3701
+ * dynamic routing. The plugin registers both `/route/threads` (default
3702
+ * agent) and `/route/threads/:agentId`.
3703
+ */
3704
+ function threadsRoute(options) {
3705
+ const { path } = options;
3706
+ const fixedAgent = "agent" in options ? options.agent : void 0;
3707
+ if (!fixedAgent && !path.includes(":agentId")) throw new Error("threadsRoute path must include `:agentId` or `agent` must be passed explicitly");
3708
+ const resolveContext = (c) => {
3709
+ const mastra = c.get("mastra");
3710
+ const requestContext = c.get("requestContext");
3711
+ const agentId = fixedAgent ?? c.req.param("agentId");
3712
+ if (!agentId) return { error: c.json({ error: "agentId is required" }, 400) };
3713
+ const agent = mastra.getAgentById(agentId);
3714
+ if (!agent) return { error: c.json({ error: `Unknown agent "${agentId}"` }, 404) };
3715
+ const resourceId = requestContext.get(MASTRA_RESOURCE_ID_KEY);
3716
+ if (!resourceId) return { error: c.json({ error: "resource id missing from request context" }, 400) };
3717
+ return {
3718
+ agentId,
3719
+ agent,
3720
+ requestContext,
3721
+ resourceId
3722
+ };
3723
+ };
3724
+ return [registerApiRoute(path, {
3725
+ method: "GET",
3726
+ handler: async (c) => {
3727
+ const ctx = resolveContext(c);
3728
+ if ("error" in ctx) return ctx.error;
3729
+ const payload = await listThreads({
3730
+ agent: ctx.agent,
3731
+ resourceId: ctx.resourceId,
3732
+ page: parseIntParam(c.req.query("page")),
3733
+ perPage: parseIntParam(c.req.query("perPage"))
3734
+ });
3735
+ return c.json(payload);
3736
+ }
3737
+ }), registerApiRoute(path, {
3738
+ method: "DELETE",
3739
+ handler: async (c) => {
3740
+ const ctx = resolveContext(c);
3741
+ if ("error" in ctx) return ctx.error;
3742
+ const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
3743
+ if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
3744
+ const { deleted } = await deleteThread({
3745
+ agent: ctx.agent,
3746
+ threadId,
3747
+ resourceId: ctx.resourceId
3748
+ });
3749
+ const payload = {
3750
+ ok: true,
3751
+ agentId: ctx.agentId,
3752
+ threadId,
3753
+ deleted
3754
+ };
3755
+ return c.json(payload);
3756
+ }
3757
+ })];
3758
+ }
3759
+ /** Coerce a Mastra `StorageThreadType` into the JSON-safe wire shape. */
3760
+ function toWireThread(thread) {
3761
+ return {
3762
+ id: thread.id,
3763
+ ...thread.title ? { title: thread.title } : {},
3764
+ resourceId: thread.resourceId,
3765
+ createdAt: toIso(thread.createdAt),
3766
+ updatedAt: toIso(thread.updatedAt),
3767
+ ...thread.metadata ? { metadata: thread.metadata } : {}
3768
+ };
3769
+ }
3770
+ /** Render a storage timestamp as an ISO-8601 string for the wire. */
3771
+ function toIso(value) {
3772
+ if (value instanceof Date) return value.toISOString();
3773
+ const parsed = new Date(value);
3774
+ return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date(0)).toISOString() : parsed.toISOString();
3775
+ }
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
+
2976
3795
  //#endregion
2977
3796
  //#region packages/appkit-mastra/src/plugin.ts
2978
3797
  /**
@@ -3000,9 +3819,10 @@ function attachRoutePatchMiddleware(app) {
3000
3819
  * - Server: the Express subapp wiring lives in `./server.js`.
3001
3820
  * - HTTP: AppKit mounts this plugin under `/api/mastra`. Alongside the
3002
3821
  * Mastra agent routes, the plugin registers `/route/history`
3003
- * (load + clear thread history), `/models`, `/suggestions`, and the
3004
- * generic `/embed/:type/:id` resolver for inline chart / data
3005
- * markers.
3822
+ * (load + clear a thread's messages), `/route/threads` (list the
3823
+ * caller's conversations + delete one), `/models`, `/suggestions`,
3824
+ * and the generic `/embed/:type/:id` resolver for inline chart /
3825
+ * data markers.
3006
3826
  * - MCP: opt in with `config.mcp` to expose the agents (and optionally
3007
3827
  * tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
3008
3828
  * instance via `mcpServers`, so `@mastra/express` serves the stock
@@ -3259,17 +4079,67 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3259
4079
  const client = getExecutionContext().client;
3260
4080
  return listServingEndpoints(client, (await client.config.getHost()).toString(), { ttlMs: resolveServingConfig(this.config).ttlMs });
3261
4081
  }
4082
+ /**
4083
+ * Resolve and probe the Databricks Managed Memory backend at setup.
4084
+ *
4085
+ * Returns the runtime (store + recall sizing + tool / recall toggles)
4086
+ * when managed memory is enabled and the workspace memory-stores API
4087
+ * is reachable, else `undefined` so the caller uses the PgVector path.
4088
+ *
4089
+ * The probe runs as the service principal (setup is outside any
4090
+ * `asUser` scope, so `getExecutionContext()` yields the SP client):
4091
+ * the SP owns the store and is the identity that can `CREATE MEMORY
4092
+ * STORE`. When `autoCreate` is on the store is created if missing.
4093
+ *
4094
+ * A misconfiguration where managed memory is explicitly enabled but no
4095
+ * store name is resolvable throws ({@link resolveManagedMemoryTarget})
4096
+ * - that is a wiring bug, not a runtime condition. Every other failure
4097
+ * (feature unavailable, permission denied, network) is caught, logged,
4098
+ * and degraded to a PgVector fallback so chat stays available.
4099
+ */
4100
+ async resolveManagedMemory() {
4101
+ const target = resolveManagedMemoryTarget(this.config);
4102
+ if (!target) return void 0;
4103
+ try {
4104
+ const client = getExecutionContext().client;
4105
+ if (!(target.autoCreate ? await ensureStore(client, target.storeName, target.description) : await getStore(client, target.storeName) !== null)) {
4106
+ this.log.warn("managed-memory:unavailable; using PgVector fallback", { store: target.storeName });
4107
+ return;
4108
+ }
4109
+ this.log.info("managed-memory:active", {
4110
+ store: target.storeName,
4111
+ topK: target.topK,
4112
+ autoCreate: target.autoCreate
4113
+ });
4114
+ return {
4115
+ storeName: target.storeName,
4116
+ topK: target.topK,
4117
+ entryPath: target.entryPath,
4118
+ tools: target.tools,
4119
+ recall: target.recall
4120
+ };
4121
+ } catch (err) {
4122
+ this.log.warn("managed-memory:probe failed; using PgVector fallback", {
4123
+ store: target.storeName,
4124
+ error: commonUtils.errorMessage(err)
4125
+ });
4126
+ return;
4127
+ }
4128
+ }
3262
4129
  async buildAgentAndServer() {
3263
4130
  if (needsLakebase(this.config)) this.servicePrincipalPool = await createServicePrincipalPool(appkitUtils.require(this.context, lakebase, this.config).exports().getPgConfig());
3264
- const memoryBuilder = this.servicePrincipalPool ? createMemoryBuilder(this.config, this.servicePrincipalPool) : void 0;
4131
+ const managedMemory = await this.resolveManagedMemory();
4132
+ const memoryBuilder = this.servicePrincipalPool ? createMemoryBuilder(this.config, this.servicePrincipalPool, { suppressVector: managedMemory !== void 0 }) : void 0;
3265
4133
  this.log.debug("build:start", {
3266
4134
  lakebase: memoryBuilder !== void 0,
4135
+ managedMemory: managedMemory !== void 0,
3267
4136
  stripStaleCharts: this.config.stripStaleCharts !== false
3268
4137
  });
3269
4138
  this.built = await buildAgents({
3270
4139
  config: this.config,
3271
4140
  context: this.context,
3272
4141
  memoryBuilder,
4142
+ ...managedMemory ? { managedMemory } : {},
3273
4143
  log: this.log
3274
4144
  });
3275
4145
  const instanceStorage = memoryBuilder?.instanceStorage();
@@ -3293,10 +4163,18 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3293
4163
  app: this.mastraApp,
3294
4164
  mastra: this.mastra,
3295
4165
  prefix: "",
3296
- customApiRoutes: [...historyRoute({
3297
- path: MASTRA_ROUTES.history,
3298
- agent: this.built.defaultAgentId
3299
- }), ...historyRoute({ path: `${MASTRA_ROUTES.history}/:agentId` })]
4166
+ customApiRoutes: [
4167
+ ...historyRoute({
4168
+ path: MASTRA_ROUTES.history,
4169
+ agent: this.built.defaultAgentId
4170
+ }),
4171
+ ...historyRoute({ path: `${MASTRA_ROUTES.history}/:agentId` }),
4172
+ ...threadsRoute({
4173
+ path: MASTRA_ROUTES.threads,
4174
+ agent: this.built.defaultAgentId
4175
+ }),
4176
+ ...threadsRoute({ path: `${MASTRA_ROUTES.threads}/:agentId` })
4177
+ ]
3300
4178
  });
3301
4179
  await this.mastraServer.init();
3302
4180
  this.log.debug("build:done", {
@@ -3304,6 +4182,7 @@ var MastraPlugin = class MastraPlugin extends Plugin {
3304
4182
  defaultAgent: this.built.defaultAgentId,
3305
4183
  routes: [
3306
4184
  "/route/history",
4185
+ "/route/threads",
3307
4186
  "/models",
3308
4187
  "/suggestions",
3309
4188
  "/embed/:type/:id"
@@ -3346,4 +4225,4 @@ function parseStatementLimit(raw) {
3346
4225
  const mastra = toPlugin(MastraPlugin);
3347
4226
 
3348
4227
  //#endregion
3349
- export { DEFAULT_AGENT_MAX_STEPS, DEFAULT_GENIE_ALIAS, DEFAULT_STYLE_INSTRUCTIONS, FALLBACK_AGENT_ID, GENIE_INSTRUCTIONS, MASTRA_MODEL_OVERRIDE_KEY, MASTRA_REQUEST_ID_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, MASTRA_USER_NAME_KEY, MastraPlugin, TRACE_REQUEST_CONTEXT_KEYS, buildAgents, buildGenieToolkitProvider, buildGenieTools, buildMcpServer, buildRenderDataTool, chartPlannerRequestSchema, collectSpaceSuggestions, createAgent, createTool, extractModelOverride, fetchChart, mastra, normalizeGenieSpaces, prepareChart, resolveGenieSpaces, tool };
4228
+ export { DEFAULT_AGENT_MAX_STEPS, DEFAULT_ENTRY_PATH, DEFAULT_GENIE_ALIAS, DEFAULT_STORE_DESCRIPTION, DEFAULT_STYLE_INSTRUCTIONS, DEFAULT_TOPK, FALLBACK_AGENT_ID, GENIE_INSTRUCTIONS, MASTRA_MODEL_OVERRIDE_KEY, MASTRA_REQUEST_ID_KEY, MASTRA_USER_EMAIL_KEY, MASTRA_USER_KEY, MASTRA_USER_NAME_KEY, MEMORY_STORE_ENV, ManagedMemoryError, MastraPlugin, TRACE_REQUEST_CONTEXT_KEYS, buildAgents, buildGenieToolkitProvider, buildGenieTools, buildManagedMemoryTools, buildMcpServer, buildRenderDataTool, buildSummarizeTool, chartPlannerRequestSchema, collectSpaceSuggestions, createAgent, createTool, ensureStore, extractModelOverride, fetchChart, getStore, mastra, normalizeGenieSpaces, parseStoreName, prepareChart, renderEntry, resolveGenieSpaces, resolveManagedMemoryTarget, resolveMemoryContext, search, summarizeText, tool, writeEntry };