@dbx-tools/appkit-mastra 0.1.74 → 0.1.76
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 +70 -4
- package/dist/index.d.ts +32 -7
- package/dist/index.js +393 -284
- package/package.json +6 -6
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),
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
[`
|
|
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
|
|
|
@@ -113,6 +117,9 @@ management (registered alongside `/route/history`):
|
|
|
113
117
|
- `DELETE /route/threads` - delete the targeted thread (id from the
|
|
114
118
|
thread-selection header). Ownership is enforced server-side: a thread
|
|
115
119
|
is only removed when it belongs to the caller.
|
|
120
|
+
- `PATCH /route/threads` - rename the targeted thread (id from the
|
|
121
|
+
thread-selection header) to the `{ title }` in the JSON body. Same
|
|
122
|
+
ownership enforcement; existing thread metadata is preserved.
|
|
116
123
|
|
|
117
124
|
Each thread is auto-titled from its opening turn (Mastra's
|
|
118
125
|
`generateTitle`), but titling runs on the **small / fast chat tier**
|
|
@@ -267,6 +274,35 @@ entry settles. Cache entries carry a 1h TTL; after expiry the route
|
|
|
267
274
|
placement contract are in [`chart.ts`](src/chart.ts); the wire shapes
|
|
268
275
|
are in [`@dbx-tools/appkit-mastra-shared`](../appkit-mastra-shared).
|
|
269
276
|
|
|
277
|
+
## Feedback (MLflow)
|
|
278
|
+
|
|
279
|
+
When MLflow tracing is wired for the deployment, the plugin lets users
|
|
280
|
+
rate an assistant turn (thumbs up / down) and leave a comment, logged as
|
|
281
|
+
a HUMAN [assessment](https://mlflow.org/docs/latest/genai/assessments/)
|
|
282
|
+
on that turn's trace and attributed to the signed-in (OBO) user.
|
|
283
|
+
|
|
284
|
+
Enablement is auto-detected: it turns on when an OTLP exporter endpoint
|
|
285
|
+
(`OTEL_EXPORTER_OTLP_ENDPOINT` / `..._TRACES_ENDPOINT`) **and** an MLflow
|
|
286
|
+
experiment (`MLFLOW_EXPERIMENT_ID` / `MLFLOW_EXPERIMENT_NAME`) are
|
|
287
|
+
configured - the two signals that traces actually materialize in MLflow.
|
|
288
|
+
Set `config.feedback` to `true` / `false` to force it on or off
|
|
289
|
+
regardless of the env.
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
mastra({ agents: support, feedback: true }); // force-enable
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
Under the hood the server derives MLflow's trace id from the active
|
|
296
|
+
OpenTelemetry span (`tr-<hex(otelTraceId)>`) and stamps it on each
|
|
297
|
+
turn's response (`MLFLOW_TRACE_ID_HEADER`); the client sends it back to
|
|
298
|
+
`POST ${basePath}/route/feedback`, which posts the assessment to the
|
|
299
|
+
Databricks MLflow REST API. Trace export is asynchronous, so a
|
|
300
|
+
just-finished trace may not exist yet - the log call retries briefly on
|
|
301
|
+
"not found" and otherwise fails softly (the chat stays usable). The
|
|
302
|
+
current state is published to the client as
|
|
303
|
+
`clientConfig().feedbackEnabled`; see [`mlflow.ts`](src/mlflow.ts) and
|
|
304
|
+
the UI wiring in [`@dbx-tools/appkit-mastra-ui`](../appkit-mastra-ui).
|
|
305
|
+
|
|
270
306
|
## Model resolution
|
|
271
307
|
|
|
272
308
|
Each agent call resolves a model lazily (so concurrent requests keep
|
|
@@ -338,6 +374,36 @@ resolves its model and tools as the calling user. The resolved endpoint
|
|
|
338
374
|
paths are available in-process via
|
|
339
375
|
`appkitUtils.require(this.context, mastra).exports().getMcp()`.
|
|
340
376
|
|
|
377
|
+
## Client API surface (`apiAccess`)
|
|
378
|
+
|
|
379
|
+
`@mastra/express` registers its full management route table under the
|
|
380
|
+
plugin mount - agent inference *plus* admin / mutating routes (direct
|
|
381
|
+
tool execution, workflow control, raw memory read/write, telemetry,
|
|
382
|
+
logs, scores). AppKit authenticates every request as the OBO user, but
|
|
383
|
+
does not restrict *which* of those a browser client may call.
|
|
384
|
+
|
|
385
|
+
`config.apiAccess` gates that surface at the plugin's dispatch point:
|
|
386
|
+
|
|
387
|
+
- `"scoped"` (default): only the routes the chat client needs are
|
|
388
|
+
dispatched to Mastra - agent inference (`stream` / `generate` /
|
|
389
|
+
`network`), read-only agent metadata (`GET /agents`,
|
|
390
|
+
`GET /agents/:id`), this plugin's own OBO- and resource-scoped
|
|
391
|
+
`/route/*` routes (history / threads / feedback), and, when `mcp` is
|
|
392
|
+
enabled, the MCP transport. Everything else is refused with `403`
|
|
393
|
+
before it reaches Mastra.
|
|
394
|
+
- `"full"`: dispatch the entire stock Mastra API. Use only for a trusted
|
|
395
|
+
first-party console that genuinely needs the management surface.
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
mastra({ agents: support }); // scoped by default
|
|
399
|
+
mastra({ agents: support, apiAccess: "full" }); // opt into the full API
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
The gate is a pure allowlist (`isMastraRequestAllowed` in
|
|
403
|
+
[`server.ts`](src/server.ts)); the browser chat client only ever uses
|
|
404
|
+
the agent stream and the `/route/*` routes, so the default breaks
|
|
405
|
+
nothing.
|
|
406
|
+
|
|
341
407
|
## License
|
|
342
408
|
|
|
343
409
|
Apache-2.0
|
package/dist/index.d.ts
CHANGED
|
@@ -642,6 +642,28 @@ interface MastraPluginConfig extends BasePluginConfig {
|
|
|
642
642
|
* calling user.
|
|
643
643
|
*/
|
|
644
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";
|
|
645
667
|
}
|
|
646
668
|
//#endregion
|
|
647
669
|
//#region packages/appkit-mastra/src/memory.d.ts
|
|
@@ -1310,13 +1332,17 @@ declare function buildMcpServer(opts: {
|
|
|
1310
1332
|
}): ResolvedMcp | null;
|
|
1311
1333
|
//#endregion
|
|
1312
1334
|
//#region packages/appkit-mastra/src/server.d.ts
|
|
1335
|
+
/**
|
|
1336
|
+
* `@mastra/express` subclass that stamps `RequestContext` with the
|
|
1337
|
+
* AppKit user, resource id, and a thread id backed by an HTTP-only
|
|
1338
|
+
* session cookie (`appkit_<plugin-name>_session_id`).
|
|
1339
|
+
*/
|
|
1313
1340
|
declare class MastraServer$1 extends MastraServer {
|
|
1314
1341
|
private config;
|
|
1315
1342
|
private log;
|
|
1316
1343
|
/**
|
|
1317
|
-
* Whether to stamp the MLflow trace-id header on responses.
|
|
1318
|
-
*
|
|
1319
|
-
* wins, else auto-detect from the MLflow tracing env.
|
|
1344
|
+
* Whether to stamp the MLflow trace-id header on responses. Shares the
|
|
1345
|
+
* plugin's feedback gate via {@link resolveFeedbackEnabled}.
|
|
1320
1346
|
*/
|
|
1321
1347
|
private feedbackEnabled;
|
|
1322
1348
|
constructor(config: MastraPluginConfig, ...args: ConstructorParameters<typeof MastraServer>);
|
|
@@ -1486,10 +1512,9 @@ declare class MastraPlugin extends Plugin<MastraPluginConfig> {
|
|
|
1486
1512
|
};
|
|
1487
1513
|
clientConfig(): Record<string, unknown>;
|
|
1488
1514
|
/**
|
|
1489
|
-
* Whether user feedback can be logged to MLflow.
|
|
1490
|
-
*
|
|
1491
|
-
*
|
|
1492
|
-
* config flag and the feedback route so the two never disagree.
|
|
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.
|
|
1493
1518
|
*/
|
|
1494
1519
|
private feedbackEnabled;
|
|
1495
1520
|
injectRoutes(router: IAppRouter): void;
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
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";
|
|
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, MastraUpdateThreadRequestSchema, 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";
|
|
@@ -226,8 +226,8 @@ const SERVING_ENDPOINTS_PATH_PREFIX = "/serving-endpoints/";
|
|
|
226
226
|
*
|
|
227
227
|
* Safe to call from any hot path: {@link commonUtils.memoize} ensures
|
|
228
228
|
* the wrapper is installed at most once per process, so subsequent
|
|
229
|
-
* calls
|
|
230
|
-
*
|
|
229
|
+
* calls are a no-op even when {@link buildModel} fires on every agent
|
|
230
|
+
* step.
|
|
231
231
|
*/
|
|
232
232
|
const setupFetchInterceptor = commonUtils.memoize(() => {
|
|
233
233
|
const log = logUtils.logger("mastra/llm");
|
|
@@ -849,8 +849,63 @@ function buildRenderDataTool(config) {
|
|
|
849
849
|
});
|
|
850
850
|
}
|
|
851
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
|
+
|
|
852
895
|
//#endregion
|
|
853
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
|
+
*/
|
|
854
909
|
/** Base path for the Unity Catalog memory-stores REST surface. */
|
|
855
910
|
const MEMORY_STORES_PATH = "/api/2.1/unity-catalog/memory-stores";
|
|
856
911
|
/**
|
|
@@ -883,32 +938,14 @@ function parseStoreName(fullName) {
|
|
|
883
938
|
};
|
|
884
939
|
}
|
|
885
940
|
/**
|
|
886
|
-
* Resolve the workspace host and an authenticated header set off the
|
|
887
|
-
* client, then issue a `fetch`. Returns the raw `Response` so callers
|
|
888
|
-
* decide how to handle status codes (the store probe wants the 404, the
|
|
889
|
-
* mutating calls want a throw).
|
|
890
|
-
*/
|
|
891
|
-
async function rawFetch(client, path, init) {
|
|
892
|
-
const host = (await client.config.getHost()).toString();
|
|
893
|
-
const headers = new Headers({ "Content-Type": "application/json" });
|
|
894
|
-
await client.config.authenticate(headers);
|
|
895
|
-
const url = new URL(path, host).toString();
|
|
896
|
-
return fetch(url, {
|
|
897
|
-
method: init.method,
|
|
898
|
-
headers,
|
|
899
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
900
|
-
...init.signal ? { signal: init.signal } : {}
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
/**
|
|
904
941
|
* Issue a request and parse the JSON body, throwing
|
|
905
942
|
* {@link ManagedMemoryError} on any non-2xx status. Used by the calls
|
|
906
943
|
* where anything but success is a real error (create / write / search).
|
|
907
944
|
*/
|
|
908
945
|
async function requestJson(client, path, init) {
|
|
909
|
-
const res = await
|
|
910
|
-
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await
|
|
911
|
-
return
|
|
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);
|
|
912
949
|
}
|
|
913
950
|
/**
|
|
914
951
|
* Fetch a store by full name. Returns the raw store payload when it
|
|
@@ -917,13 +954,13 @@ async function requestJson(client, path, init) {
|
|
|
917
954
|
* Any other non-2xx status throws.
|
|
918
955
|
*/
|
|
919
956
|
async function getStore(client, fullName, signal) {
|
|
920
|
-
const res = await
|
|
957
|
+
const res = await databricksFetch(client, `${MEMORY_STORES_PATH}/${fullName}`, {
|
|
921
958
|
method: "GET",
|
|
922
959
|
...signal ? { signal } : {}
|
|
923
960
|
});
|
|
924
961
|
if (res.status === 404) return null;
|
|
925
|
-
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await
|
|
926
|
-
return
|
|
962
|
+
if (!res.ok) throw new ManagedMemoryError(res.status, res.statusText, await readResponseText(res));
|
|
963
|
+
return readResponseJson(res);
|
|
927
964
|
}
|
|
928
965
|
/**
|
|
929
966
|
* Probe for the store and create it when missing. Returns `true` when
|
|
@@ -990,10 +1027,14 @@ function parseEntries(body) {
|
|
|
990
1027
|
for (const raw of list) {
|
|
991
1028
|
if (!raw || typeof raw !== "object") continue;
|
|
992
1029
|
const obj = raw;
|
|
993
|
-
const contents =
|
|
1030
|
+
const contents = stringUtils.firstNonEmpty([
|
|
1031
|
+
obj["contents"],
|
|
1032
|
+
obj["content"],
|
|
1033
|
+
obj["text"]
|
|
1034
|
+
]);
|
|
994
1035
|
if (!contents) continue;
|
|
995
|
-
const path =
|
|
996
|
-
const description =
|
|
1036
|
+
const path = stringUtils.firstNonEmpty(obj["path"]);
|
|
1037
|
+
const description = stringUtils.firstNonEmpty([obj["description"], obj["summary"]]);
|
|
997
1038
|
const score = typeof obj["score"] === "number" ? obj["score"] : void 0;
|
|
998
1039
|
out.push({
|
|
999
1040
|
contents,
|
|
@@ -1004,28 +1045,6 @@ function parseEntries(body) {
|
|
|
1004
1045
|
}
|
|
1005
1046
|
return out;
|
|
1006
1047
|
}
|
|
1007
|
-
/** Return the first argument that is a non-empty string, else undefined. */
|
|
1008
|
-
function firstString(...values) {
|
|
1009
|
-
for (const v of values) if (typeof v === "string" && v.trim() !== "") return v;
|
|
1010
|
-
}
|
|
1011
|
-
/** Parse a response body as JSON, returning `{}` on empty / invalid bodies. */
|
|
1012
|
-
async function safeJson$1(res) {
|
|
1013
|
-
const text = await safeText$1(res);
|
|
1014
|
-
if (!text) return {};
|
|
1015
|
-
try {
|
|
1016
|
-
return JSON.parse(text);
|
|
1017
|
-
} catch {
|
|
1018
|
-
return {};
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
/** Read a response body as text, swallowing read errors. */
|
|
1022
|
-
async function safeText$1(res) {
|
|
1023
|
-
try {
|
|
1024
|
-
return await res.text();
|
|
1025
|
-
} catch {
|
|
1026
|
-
return "";
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
1048
|
|
|
1030
1049
|
//#endregion
|
|
1031
1050
|
//#region packages/appkit-mastra/src/connectors/managed-memory/context.ts
|
|
@@ -2898,6 +2917,27 @@ function buildMcpServer(opts) {
|
|
|
2898
2917
|
};
|
|
2899
2918
|
}
|
|
2900
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
|
+
|
|
2901
2941
|
//#endregion
|
|
2902
2942
|
//#region packages/appkit-mastra/src/history.ts
|
|
2903
2943
|
const log$4 = logUtils.logger("mastra/history");
|
|
@@ -2921,7 +2961,10 @@ const MAX_PER_PAGE$1 = 200;
|
|
|
2921
2961
|
* without sorting locally.
|
|
2922
2962
|
*/
|
|
2923
2963
|
async function loadHistory(opts) {
|
|
2924
|
-
const perPage = clampPerPage
|
|
2964
|
+
const perPage = clampPerPage(opts.perPage, {
|
|
2965
|
+
fallback: DEFAULT_PER_PAGE$1,
|
|
2966
|
+
max: MAX_PER_PAGE$1
|
|
2967
|
+
});
|
|
2925
2968
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
2926
2969
|
const memory = await opts.agent.getMemory();
|
|
2927
2970
|
if (!memory) {
|
|
@@ -3070,8 +3113,8 @@ function historyRoute(options) {
|
|
|
3070
3113
|
agent: ctx.agent,
|
|
3071
3114
|
threadId: ctx.threadId,
|
|
3072
3115
|
...ctx.resourceId ? { resourceId: ctx.resourceId } : {},
|
|
3073
|
-
page: parseIntParam
|
|
3074
|
-
perPage: parseIntParam
|
|
3116
|
+
page: parseIntParam(c.req.query("page")),
|
|
3117
|
+
perPage: parseIntParam(c.req.query("perPage"))
|
|
3075
3118
|
});
|
|
3076
3119
|
return c.json(payload);
|
|
3077
3120
|
}
|
|
@@ -3094,13 +3137,6 @@ function historyRoute(options) {
|
|
|
3094
3137
|
}
|
|
3095
3138
|
})];
|
|
3096
3139
|
}
|
|
3097
|
-
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
3098
|
-
function clampPerPage$1(value) {
|
|
3099
|
-
if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE$1;
|
|
3100
|
-
const n = Math.trunc(value);
|
|
3101
|
-
if (n <= 0) return DEFAULT_PER_PAGE$1;
|
|
3102
|
-
return Math.min(n, MAX_PER_PAGE$1);
|
|
3103
|
-
}
|
|
3104
3140
|
/**
|
|
3105
3141
|
* Sort messages oldest-first by `createdAt`, falling back to whatever
|
|
3106
3142
|
* order the storage returned them in. The native `recall` call honors
|
|
@@ -3120,155 +3156,6 @@ function toEpoch(value) {
|
|
|
3120
3156
|
}
|
|
3121
3157
|
return 0;
|
|
3122
3158
|
}
|
|
3123
|
-
/**
|
|
3124
|
-
* Coerce a Hono query value into a non-negative integer. Returns
|
|
3125
|
-
* `undefined` for empty / non-numeric / negative inputs so
|
|
3126
|
-
* {@link loadHistory} can apply its built-in defaults.
|
|
3127
|
-
*/
|
|
3128
|
-
function parseIntParam$1(value) {
|
|
3129
|
-
if (!value) return void 0;
|
|
3130
|
-
const n = Number(value);
|
|
3131
|
-
if (!Number.isFinite(n) || n < 0) return void 0;
|
|
3132
|
-
return Math.trunc(n);
|
|
3133
|
-
}
|
|
3134
|
-
|
|
3135
|
-
//#endregion
|
|
3136
|
-
//#region packages/appkit-mastra/src/rest.ts
|
|
3137
|
-
/**
|
|
3138
|
-
* Resolve the workspace host + an authenticated header set off the
|
|
3139
|
-
* client and issue a `fetch` against `path` (mounted on the host).
|
|
3140
|
-
* Runs as whatever identity the client carries - the per-request OBO
|
|
3141
|
-
* user when called from a request scope, the service principal
|
|
3142
|
-
* otherwise.
|
|
3143
|
-
*/
|
|
3144
|
-
async function databricksFetch(client, path, init) {
|
|
3145
|
-
const host = (await client.config.getHost()).toString();
|
|
3146
|
-
const headers = new Headers({
|
|
3147
|
-
"Content-Type": "application/json",
|
|
3148
|
-
...init.headers
|
|
3149
|
-
});
|
|
3150
|
-
await client.config.authenticate(headers);
|
|
3151
|
-
const url = new URL(path, host).toString();
|
|
3152
|
-
return fetch(url, {
|
|
3153
|
-
method: init.method,
|
|
3154
|
-
headers,
|
|
3155
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
3156
|
-
...init.signal ? { signal: init.signal } : {}
|
|
3157
|
-
});
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
//#endregion
|
|
3161
|
-
//#region packages/appkit-mastra/src/mlflow.ts
|
|
3162
|
-
/**
|
|
3163
|
-
* MLflow user-feedback logging: detect whether MLflow tracing is wired
|
|
3164
|
-
* for this deployment, and log a thumbs / comment as a trace
|
|
3165
|
-
* *assessment* via the Databricks MLflow REST API.
|
|
3166
|
-
*
|
|
3167
|
-
* Feedback attaches to a trace, and the plugin's spans reach MLflow
|
|
3168
|
-
* through the same OTel pipeline as every other AppKit span (see
|
|
3169
|
-
* `observability.ts`). MLflow derives its trace id from the OpenTelemetry
|
|
3170
|
-
* trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
|
|
3171
|
-
* trace id on each turn's response and the client sends it back here.
|
|
3172
|
-
*
|
|
3173
|
-
* There is no MLflow JS SDK, so this posts to the assessments REST
|
|
3174
|
-
* endpoint directly using the OBO-scoped workspace client (the feedback
|
|
3175
|
-
* is thus attributed to the signed-in user). Trace export is
|
|
3176
|
-
* asynchronous, so the just-finished trace may not exist in MLflow yet
|
|
3177
|
-
* when the user reacts; the log call retries briefly on "not found"
|
|
3178
|
-
* before giving up softly.
|
|
3179
|
-
*/
|
|
3180
|
-
const log$3 = logUtils.logger("mastra/mlflow");
|
|
3181
|
-
/** Assessments REST path for a trace. `3.0` is the current MLflow API version. */
|
|
3182
|
-
const assessmentsPath = (traceId) => `/api/3.0/mlflow/traces/${encodeURIComponent(traceId)}/assessments`;
|
|
3183
|
-
/** Number of times to retry a "trace not found" response before giving up. */
|
|
3184
|
-
const NOT_FOUND_RETRIES = 3;
|
|
3185
|
-
/** Base backoff between "trace not found" retries, in ms (grows linearly). */
|
|
3186
|
-
const NOT_FOUND_BACKOFF_MS = 1200;
|
|
3187
|
-
/**
|
|
3188
|
-
* Whether MLflow feedback logging is available for this deployment.
|
|
3189
|
-
*
|
|
3190
|
-
* Enabled when an OTLP exporter endpoint is configured (traces are
|
|
3191
|
-
* actually shipped somewhere) AND an MLflow experiment is named - the
|
|
3192
|
-
* two signals that the OTLP backend is MLflow and traces will
|
|
3193
|
-
* materialize there. Both are standard env vars, so no plugin config is
|
|
3194
|
-
* required; a deployment opts in simply by wiring MLflow tracing.
|
|
3195
|
-
*/
|
|
3196
|
-
function mlflowEnabled() {
|
|
3197
|
-
const hasExporter = Boolean(process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim() || process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim());
|
|
3198
|
-
const hasExperiment = Boolean(process.env.MLFLOW_EXPERIMENT_ID?.trim() || process.env.MLFLOW_EXPERIMENT_NAME?.trim());
|
|
3199
|
-
return hasExporter && hasExperiment;
|
|
3200
|
-
}
|
|
3201
|
-
/**
|
|
3202
|
-
* Log a HUMAN feedback assessment to a trace. Returns the created
|
|
3203
|
-
* assessment id on success, or `undefined` when the trace can't be
|
|
3204
|
-
* found (even after retrying for export lag) or the request otherwise
|
|
3205
|
-
* fails - callers surface that as a soft "not recorded" rather than an
|
|
3206
|
-
* error, keeping the chat usable.
|
|
3207
|
-
*/
|
|
3208
|
-
async function logFeedback(client, params) {
|
|
3209
|
-
const hasValue = params.value !== void 0;
|
|
3210
|
-
const name = params.name?.trim() || (hasValue ? DEFAULT_FEEDBACK_NAME : DEFAULT_COMMENT_NAME);
|
|
3211
|
-
const value = hasValue ? params.value : params.comment;
|
|
3212
|
-
const body = { assessment: {
|
|
3213
|
-
trace_id: params.traceId,
|
|
3214
|
-
assessment_name: name,
|
|
3215
|
-
source: {
|
|
3216
|
-
source_type: "HUMAN",
|
|
3217
|
-
source_id: params.sourceId?.trim() || "user"
|
|
3218
|
-
},
|
|
3219
|
-
feedback: { value },
|
|
3220
|
-
...hasValue && params.comment?.trim() ? { rationale: params.comment } : {}
|
|
3221
|
-
} };
|
|
3222
|
-
for (let attempt = 0; attempt <= NOT_FOUND_RETRIES; attempt++) {
|
|
3223
|
-
let res;
|
|
3224
|
-
try {
|
|
3225
|
-
res = await databricksFetch(client, assessmentsPath(params.traceId), {
|
|
3226
|
-
method: "POST",
|
|
3227
|
-
body
|
|
3228
|
-
});
|
|
3229
|
-
} catch (err) {
|
|
3230
|
-
log$3.warn("feedback request failed", {
|
|
3231
|
-
traceId: params.traceId,
|
|
3232
|
-
error: commonUtils.errorMessage(err)
|
|
3233
|
-
});
|
|
3234
|
-
return;
|
|
3235
|
-
}
|
|
3236
|
-
if (res.ok) {
|
|
3237
|
-
const parsed = await safeJson(res);
|
|
3238
|
-
const assessmentId = parsed?.assessment?.assessment_id ?? parsed?.assessment_id;
|
|
3239
|
-
return typeof assessmentId === "string" ? assessmentId : "";
|
|
3240
|
-
}
|
|
3241
|
-
if (res.status === 404 && attempt < NOT_FOUND_RETRIES) {
|
|
3242
|
-
await delay(NOT_FOUND_BACKOFF_MS * (attempt + 1));
|
|
3243
|
-
continue;
|
|
3244
|
-
}
|
|
3245
|
-
log$3.warn("feedback not recorded", {
|
|
3246
|
-
traceId: params.traceId,
|
|
3247
|
-
status: res.status,
|
|
3248
|
-
body: await safeText(res)
|
|
3249
|
-
});
|
|
3250
|
-
return;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
function delay(ms) {
|
|
3254
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
3255
|
-
}
|
|
3256
|
-
async function safeJson(res) {
|
|
3257
|
-
const text = await safeText(res);
|
|
3258
|
-
if (!text) return {};
|
|
3259
|
-
try {
|
|
3260
|
-
return JSON.parse(text);
|
|
3261
|
-
} catch {
|
|
3262
|
-
return {};
|
|
3263
|
-
}
|
|
3264
|
-
}
|
|
3265
|
-
async function safeText(res) {
|
|
3266
|
-
try {
|
|
3267
|
-
return await res.text();
|
|
3268
|
-
} catch {
|
|
3269
|
-
return "";
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
3159
|
|
|
3273
3160
|
//#endregion
|
|
3274
3161
|
//#region packages/appkit-mastra/src/memory.ts
|
|
@@ -3300,7 +3187,7 @@ async function safeText(res) {
|
|
|
3300
3187
|
* (auto-defaulted to `true` in `plugin.ts` when the `lakebase` plugin
|
|
3301
3188
|
* is registered); per-agent settings cascade on top of that.
|
|
3302
3189
|
*/
|
|
3303
|
-
const log$
|
|
3190
|
+
const log$3 = logUtils.logger("mastra/memory");
|
|
3304
3191
|
/**
|
|
3305
3192
|
* Build a dedicated **service-principal** Lakebase pool for Mastra
|
|
3306
3193
|
* memory from the lakebase plugin's resolved SP pg config.
|
|
@@ -3392,10 +3279,10 @@ var MemoryBuilder = class {
|
|
|
3392
3279
|
const storage = this.buildStorage(agentId, storageSetting);
|
|
3393
3280
|
const vector = this.buildVector(memorySetting);
|
|
3394
3281
|
if (!storage && !vector) {
|
|
3395
|
-
log$
|
|
3282
|
+
log$3.debug("agent:stateless", { agentId });
|
|
3396
3283
|
return;
|
|
3397
3284
|
}
|
|
3398
|
-
log$
|
|
3285
|
+
log$3.debug("agent:configured", {
|
|
3399
3286
|
agentId,
|
|
3400
3287
|
storage: storage !== void 0,
|
|
3401
3288
|
vector: vector !== void 0,
|
|
@@ -3507,6 +3394,110 @@ function withId(value, fallback) {
|
|
|
3507
3394
|
};
|
|
3508
3395
|
}
|
|
3509
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
|
+
|
|
3510
3501
|
//#endregion
|
|
3511
3502
|
//#region packages/appkit-mastra/src/observability.ts
|
|
3512
3503
|
/**
|
|
@@ -3581,30 +3572,29 @@ async function buildObservability(options) {
|
|
|
3581
3572
|
* behind an Express mount point.
|
|
3582
3573
|
*/
|
|
3583
3574
|
/**
|
|
3584
|
-
* `@mastra/express` subclass that stamps `RequestContext` with the
|
|
3585
|
-
* AppKit user, resource id, and a thread id backed by an HTTP-only
|
|
3586
|
-
* session cookie (`appkit_<plugin-name>_session_id`).
|
|
3587
|
-
*/
|
|
3588
|
-
/**
|
|
3589
3575
|
* OpenTelemetry's sentinel for "no valid trace" - 32 zero hex chars.
|
|
3590
3576
|
* `trace.getActiveSpan()` returns a non-recording span with this id
|
|
3591
3577
|
* when no SDK is registered, which must never be surfaced as a trace to
|
|
3592
3578
|
* attach feedback to.
|
|
3593
3579
|
*/
|
|
3594
3580
|
const INVALID_TRACE_ID = "0".repeat(32);
|
|
3581
|
+
/**
|
|
3582
|
+
* `@mastra/express` subclass that stamps `RequestContext` with the
|
|
3583
|
+
* AppKit user, resource id, and a thread id backed by an HTTP-only
|
|
3584
|
+
* session cookie (`appkit_<plugin-name>_session_id`).
|
|
3585
|
+
*/
|
|
3595
3586
|
var MastraServer$1 = class extends MastraServer {
|
|
3596
3587
|
log;
|
|
3597
3588
|
/**
|
|
3598
|
-
* Whether to stamp the MLflow trace-id header on responses.
|
|
3599
|
-
*
|
|
3600
|
-
* wins, else auto-detect from the MLflow tracing env.
|
|
3589
|
+
* Whether to stamp the MLflow trace-id header on responses. Shares the
|
|
3590
|
+
* plugin's feedback gate via {@link resolveFeedbackEnabled}.
|
|
3601
3591
|
*/
|
|
3602
3592
|
feedbackEnabled;
|
|
3603
3593
|
constructor(config, ...args) {
|
|
3604
3594
|
super(...args);
|
|
3605
3595
|
this.config = config;
|
|
3606
3596
|
this.log = logUtils.logger(config);
|
|
3607
|
-
this.feedbackEnabled = config.feedback
|
|
3597
|
+
this.feedbackEnabled = resolveFeedbackEnabled(config.feedback);
|
|
3608
3598
|
}
|
|
3609
3599
|
registerAuthMiddleware() {
|
|
3610
3600
|
super.registerAuthMiddleware();
|
|
@@ -3744,6 +3734,37 @@ var MastraServer$1 = class extends MastraServer {
|
|
|
3744
3734
|
}
|
|
3745
3735
|
};
|
|
3746
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
|
+
/**
|
|
3747
3768
|
* Patches around `@mastra/express`'s custom-route dispatcher so the
|
|
3748
3769
|
* plugin's custom API routes (e.g. `historyRoute`) work when
|
|
3749
3770
|
* `MastraServer` is hosted on an Express subapp mounted under a parent
|
|
@@ -3767,6 +3788,26 @@ function attachRoutePatchMiddleware(app) {
|
|
|
3767
3788
|
|
|
3768
3789
|
//#endregion
|
|
3769
3790
|
//#region packages/appkit-mastra/src/threads.ts
|
|
3791
|
+
/**
|
|
3792
|
+
* Conversation-thread listing exposed as a Mastra custom API route.
|
|
3793
|
+
*
|
|
3794
|
+
* Backed entirely by native Mastra: looks up the active agent by id,
|
|
3795
|
+
* asks its `Memory` instance to `listThreads` filtered to the caller's
|
|
3796
|
+
* resource, and shapes each into the JSON-safe {@link MastraThread}
|
|
3797
|
+
* wire type. A sibling `DELETE` removes a single named thread.
|
|
3798
|
+
*
|
|
3799
|
+
* A sibling `DELETE` removes a single named thread; a `PATCH` renames
|
|
3800
|
+
* one ({@link renameThread}).
|
|
3801
|
+
*
|
|
3802
|
+
* Like {@link historyRoute} this registers through Mastra's
|
|
3803
|
+
* `registerApiRoute` so it shares the `MastraServer` auth-middleware
|
|
3804
|
+
* pipeline (in `./server.ts`), which has already stamped the resource
|
|
3805
|
+
* id (`MASTRA_RESOURCE_ID_KEY`) and the targeted thread id
|
|
3806
|
+
* (`MASTRA_THREAD_ID_KEY`, resolved from the thread-selection header /
|
|
3807
|
+
* cookie) on `RequestContext` by the time a handler runs. Resource
|
|
3808
|
+
* scoping lives here so a caller can only ever see, rename, or delete
|
|
3809
|
+
* its own threads; no cookie or user lookups happen in this module.
|
|
3810
|
+
*/
|
|
3770
3811
|
const log = logUtils.logger("mastra/threads");
|
|
3771
3812
|
/** Default threads page size. */
|
|
3772
3813
|
const DEFAULT_PER_PAGE = 30;
|
|
@@ -3782,7 +3823,10 @@ const MAX_PER_PAGE = 200;
|
|
|
3782
3823
|
* empty page so callers don't have to special-case stateless agents.
|
|
3783
3824
|
*/
|
|
3784
3825
|
async function listThreads(opts) {
|
|
3785
|
-
const perPage = clampPerPage(opts.perPage
|
|
3826
|
+
const perPage = clampPerPage(opts.perPage, {
|
|
3827
|
+
fallback: DEFAULT_PER_PAGE,
|
|
3828
|
+
max: MAX_PER_PAGE
|
|
3829
|
+
});
|
|
3786
3830
|
const page = Math.max(0, Math.trunc(opts.page ?? 0));
|
|
3787
3831
|
const memory = await opts.agent.getMemory();
|
|
3788
3832
|
if (!memory) {
|
|
@@ -3858,7 +3902,47 @@ async function deleteThread(opts) {
|
|
|
3858
3902
|
return { deleted: true };
|
|
3859
3903
|
}
|
|
3860
3904
|
/**
|
|
3861
|
-
*
|
|
3905
|
+
* Rename a single thread, returning the updated wire thread.
|
|
3906
|
+
*
|
|
3907
|
+
* Ownership is enforced the same way {@link deleteThread} enforces it:
|
|
3908
|
+
* the title is only changed when the thread belongs to the calling
|
|
3909
|
+
* resource, so a client can't rename another user's conversation by
|
|
3910
|
+
* guessing its id. Existing thread `metadata` is preserved untouched
|
|
3911
|
+
* (Mastra's `updateThread` replaces the row, so it must be passed back
|
|
3912
|
+
* in). Returns `null` when the thread doesn't exist, isn't owned by the
|
|
3913
|
+
* caller, or the agent has no memory configured, letting the route map
|
|
3914
|
+
* that to a 404.
|
|
3915
|
+
*/
|
|
3916
|
+
async function renameThread(opts) {
|
|
3917
|
+
const memory = await opts.agent.getMemory();
|
|
3918
|
+
if (!memory) {
|
|
3919
|
+
log.debug("rename:no-memory", { agentId: opts.agent.id });
|
|
3920
|
+
return null;
|
|
3921
|
+
}
|
|
3922
|
+
const existing = await memory.getThreadById({ threadId: opts.threadId });
|
|
3923
|
+
if (!existing || existing.resourceId !== opts.resourceId) {
|
|
3924
|
+
log.debug("rename:not-owned", {
|
|
3925
|
+
agentId: opts.agent.id,
|
|
3926
|
+
threadId: opts.threadId,
|
|
3927
|
+
found: existing !== null
|
|
3928
|
+
});
|
|
3929
|
+
return null;
|
|
3930
|
+
}
|
|
3931
|
+
const startedAt = Date.now();
|
|
3932
|
+
const updated = await memory.updateThread({
|
|
3933
|
+
id: opts.threadId,
|
|
3934
|
+
title: opts.title,
|
|
3935
|
+
metadata: existing.metadata ?? {}
|
|
3936
|
+
});
|
|
3937
|
+
log.info("rename:done", {
|
|
3938
|
+
agentId: opts.agent.id,
|
|
3939
|
+
threadId: opts.threadId,
|
|
3940
|
+
elapsedMs: Date.now() - startedAt
|
|
3941
|
+
});
|
|
3942
|
+
return toWireThread(updated);
|
|
3943
|
+
}
|
|
3944
|
+
/**
|
|
3945
|
+
* Register the `<path>` Mastra custom API route. Handles three methods
|
|
3862
3946
|
* on the same mount:
|
|
3863
3947
|
*
|
|
3864
3948
|
* - `GET`: a page of the resource's conversation threads
|
|
@@ -3868,6 +3952,10 @@ async function deleteThread(opts) {
|
|
|
3868
3952
|
* `RequestContext` (the auth middleware resolves it the same way it
|
|
3869
3953
|
* does for streaming and history), so the client deletes any of its
|
|
3870
3954
|
* threads by stamping the target id - no separate path param.
|
|
3955
|
+
* - `PATCH`: rename the thread named by the thread-selection header /
|
|
3956
|
+
* `?threadId=` query to the `{ title }` in the JSON body
|
|
3957
|
+
* ({@link renameThread}). Targets a thread the same way `DELETE`
|
|
3958
|
+
* does; 404s when the thread isn't owned by the caller.
|
|
3871
3959
|
*
|
|
3872
3960
|
* Follows the `@mastra/ai-sdk` agent-binding convention: pass `agent`
|
|
3873
3961
|
* for a fixed-agent mount, or include `:agentId` in the path for
|
|
@@ -3894,40 +3982,67 @@ function threadsRoute(options) {
|
|
|
3894
3982
|
resourceId
|
|
3895
3983
|
};
|
|
3896
3984
|
};
|
|
3897
|
-
return [
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3985
|
+
return [
|
|
3986
|
+
registerApiRoute(path, {
|
|
3987
|
+
method: "GET",
|
|
3988
|
+
handler: async (c) => {
|
|
3989
|
+
const ctx = resolveContext(c);
|
|
3990
|
+
if ("error" in ctx) return ctx.error;
|
|
3991
|
+
const payload = await listThreads({
|
|
3992
|
+
agent: ctx.agent,
|
|
3993
|
+
resourceId: ctx.resourceId,
|
|
3994
|
+
page: parseIntParam(c.req.query("page")),
|
|
3995
|
+
perPage: parseIntParam(c.req.query("perPage"))
|
|
3996
|
+
});
|
|
3997
|
+
return c.json(payload);
|
|
3998
|
+
}
|
|
3999
|
+
}),
|
|
4000
|
+
registerApiRoute(path, {
|
|
4001
|
+
method: "DELETE",
|
|
4002
|
+
handler: async (c) => {
|
|
4003
|
+
const ctx = resolveContext(c);
|
|
4004
|
+
if ("error" in ctx) return ctx.error;
|
|
4005
|
+
const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
|
|
4006
|
+
if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
|
|
4007
|
+
const { deleted } = await deleteThread({
|
|
4008
|
+
agent: ctx.agent,
|
|
4009
|
+
threadId,
|
|
4010
|
+
resourceId: ctx.resourceId
|
|
4011
|
+
});
|
|
4012
|
+
const payload = {
|
|
4013
|
+
ok: true,
|
|
4014
|
+
agentId: ctx.agentId,
|
|
4015
|
+
threadId,
|
|
4016
|
+
deleted
|
|
4017
|
+
};
|
|
4018
|
+
return c.json(payload);
|
|
4019
|
+
}
|
|
4020
|
+
}),
|
|
4021
|
+
registerApiRoute(path, {
|
|
4022
|
+
method: "PATCH",
|
|
4023
|
+
handler: async (c) => {
|
|
4024
|
+
const ctx = resolveContext(c);
|
|
4025
|
+
if ("error" in ctx) return ctx.error;
|
|
4026
|
+
const threadId = ctx.requestContext.get(MASTRA_THREAD_ID_KEY);
|
|
4027
|
+
if (!threadId) return c.json({ error: "thread id missing from request context" }, 400);
|
|
4028
|
+
const body = MastraUpdateThreadRequestSchema.safeParse(await c.req.json());
|
|
4029
|
+
if (!body.success) return c.json({ error: body.error.message }, 400);
|
|
4030
|
+
const thread = await renameThread({
|
|
4031
|
+
agent: ctx.agent,
|
|
4032
|
+
threadId,
|
|
4033
|
+
resourceId: ctx.resourceId,
|
|
4034
|
+
title: body.data.title
|
|
4035
|
+
});
|
|
4036
|
+
if (!thread) return c.json({ error: `Unknown thread "${threadId}"` }, 404);
|
|
4037
|
+
const payload = {
|
|
4038
|
+
ok: true,
|
|
4039
|
+
agentId: ctx.agentId,
|
|
4040
|
+
thread
|
|
4041
|
+
};
|
|
4042
|
+
return c.json(payload);
|
|
4043
|
+
}
|
|
4044
|
+
})
|
|
4045
|
+
];
|
|
3931
4046
|
}
|
|
3932
4047
|
/** Coerce a Mastra `StorageThreadType` into the JSON-safe wire shape. */
|
|
3933
4048
|
function toWireThread(thread) {
|
|
@@ -3946,24 +4061,6 @@ function toIso(value) {
|
|
|
3946
4061
|
const parsed = new Date(value);
|
|
3947
4062
|
return Number.isNaN(parsed.getTime()) ? (/* @__PURE__ */ new Date(0)).toISOString() : parsed.toISOString();
|
|
3948
4063
|
}
|
|
3949
|
-
/** Coerce / clamp `perPage`; falls back to the page-size default. */
|
|
3950
|
-
function clampPerPage(value) {
|
|
3951
|
-
if (value === void 0 || Number.isNaN(value)) return DEFAULT_PER_PAGE;
|
|
3952
|
-
const n = Math.trunc(value);
|
|
3953
|
-
if (n <= 0) return DEFAULT_PER_PAGE;
|
|
3954
|
-
return Math.min(n, MAX_PER_PAGE);
|
|
3955
|
-
}
|
|
3956
|
-
/**
|
|
3957
|
-
* Coerce a Hono query value into a non-negative integer. Returns
|
|
3958
|
-
* `undefined` for empty / non-numeric / negative inputs so
|
|
3959
|
-
* {@link listThreads} can apply its built-in defaults.
|
|
3960
|
-
*/
|
|
3961
|
-
function parseIntParam(value) {
|
|
3962
|
-
if (!value) return void 0;
|
|
3963
|
-
const n = Number(value);
|
|
3964
|
-
if (!Number.isFinite(n) || n < 0) return void 0;
|
|
3965
|
-
return Math.trunc(n);
|
|
3966
|
-
}
|
|
3967
4064
|
|
|
3968
4065
|
//#endregion
|
|
3969
4066
|
//#region packages/appkit-mastra/src/plugin.ts
|
|
@@ -3994,8 +4091,13 @@ function parseIntParam(value) {
|
|
|
3994
4091
|
* Mastra agent routes, the plugin registers `/route/history`
|
|
3995
4092
|
* (load + clear a thread's messages), `/route/threads` (list the
|
|
3996
4093
|
* caller's conversations + delete one), `/models`, `/suggestions`,
|
|
3997
|
-
*
|
|
3998
|
-
*
|
|
4094
|
+
* `/route/feedback` (log a thumbs / comment to MLflow when feedback
|
|
4095
|
+
* is enabled), and the generic `/embed/:type/:id` resolver for inline
|
|
4096
|
+
* chart / data markers. The stock `@mastra/express` surface is gated
|
|
4097
|
+
* by `config.apiAccess` (default `"scoped"`): only agent inference,
|
|
4098
|
+
* read-only agent metadata, the `/route/*` routes, and (when enabled)
|
|
4099
|
+
* MCP are dispatched to Mastra; admin / mutating / bulk-export routes
|
|
4100
|
+
* are refused with `403`. See {@link isMastraRequestAllowed}.
|
|
3999
4101
|
* - MCP: opt in with `config.mcp` to expose the agents (and optionally
|
|
4000
4102
|
* tools) as a Mastra `MCPServer`. It is registered on the `Mastra`
|
|
4001
4103
|
* instance via `mcpServers`, so `@mastra/express` serves the stock
|
|
@@ -4116,13 +4218,12 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
4116
4218
|
};
|
|
4117
4219
|
}
|
|
4118
4220
|
/**
|
|
4119
|
-
* Whether user feedback can be logged to MLflow.
|
|
4120
|
-
*
|
|
4121
|
-
*
|
|
4122
|
-
* config flag and the feedback route so the two never disagree.
|
|
4221
|
+
* Whether user feedback can be logged to MLflow. Delegates to
|
|
4222
|
+
* {@link resolveFeedbackEnabled} so the client-config flag and the
|
|
4223
|
+
* feedback route share the same gate as the server's trace-id header.
|
|
4123
4224
|
*/
|
|
4124
4225
|
feedbackEnabled() {
|
|
4125
|
-
return this.config.feedback
|
|
4226
|
+
return resolveFeedbackEnabled(this.config.feedback);
|
|
4126
4227
|
}
|
|
4127
4228
|
injectRoutes(router) {
|
|
4128
4229
|
router.get(MASTRA_ROUTES.models, (req, res, next) => {
|
|
@@ -4196,6 +4297,13 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
4196
4297
|
});
|
|
4197
4298
|
router.use((req, res, next) => {
|
|
4198
4299
|
if (!this.mastraApp) return res.status(503).end();
|
|
4300
|
+
if (!isMastraRequestAllowed(req.method, req.path, {
|
|
4301
|
+
access: this.config.apiAccess ?? "scoped",
|
|
4302
|
+
mcpEnabled: Boolean(this.config.mcp)
|
|
4303
|
+
})) {
|
|
4304
|
+
res.status(403).json({ error: "Endpoint not exposed to the client (apiAccess=scoped)" });
|
|
4305
|
+
return;
|
|
4306
|
+
}
|
|
4199
4307
|
return this.userScopedSelf(req).dispatchMastra(req, res, next);
|
|
4200
4308
|
});
|
|
4201
4309
|
}
|
|
@@ -4402,6 +4510,7 @@ var MastraPlugin = class MastraPlugin extends Plugin {
|
|
|
4402
4510
|
"/route/threads",
|
|
4403
4511
|
"/models",
|
|
4404
4512
|
"/suggestions",
|
|
4513
|
+
"/route/feedback",
|
|
4405
4514
|
"/embed/:type/:id"
|
|
4406
4515
|
],
|
|
4407
4516
|
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.
|
|
3
|
+
"version": "0.1.76",
|
|
4
4
|
"dependencies": {
|
|
5
5
|
"@databricks/sdk-experimental": "^0.17",
|
|
6
|
-
"@dbx-tools/appkit-mastra-shared": "0.1.
|
|
7
|
-
"@dbx-tools/genie": "0.1.
|
|
8
|
-
"@dbx-tools/genie-shared": "0.1.
|
|
9
|
-
"@dbx-tools/model": "0.1.
|
|
10
|
-
"@dbx-tools/shared": "0.1.
|
|
6
|
+
"@dbx-tools/appkit-mastra-shared": "0.1.76",
|
|
7
|
+
"@dbx-tools/genie": "0.1.76",
|
|
8
|
+
"@dbx-tools/genie-shared": "0.1.76",
|
|
9
|
+
"@dbx-tools/model": "0.1.76",
|
|
10
|
+
"@dbx-tools/shared": "0.1.76",
|
|
11
11
|
"@mastra/ai-sdk": "^1",
|
|
12
12
|
"@mastra/core": "^1",
|
|
13
13
|
"@mastra/express": "^1",
|