@dbx-tools/appkit-mastra 0.6.13 → 0.6.15

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.
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Copy each Mastra chat turn's request/response onto the active OTel
3
+ * root span so Databricks-managed MLflow can display them.
4
+ *
5
+ * Why this exists: MLflow's UC `<target>_trace_unified` view picks the
6
+ * one span whose `parent_span_id` is empty and reads request/response
7
+ * from `mlflow.spanInputs` / `mlflow.spanOutputs` (or the `gen_ai.*`
8
+ * equivalents) on THAT span only. Mastra records the turn on its
9
+ * `invoke_agent` child under `mastra.agent_run.input` / `.output`, keys
10
+ * the view never reads, so chat traces arrive with both columns null
11
+ * even when the spans themselves look healthy.
12
+ *
13
+ * The active span inside the Mastra sub-app is still AppKit's HTTP
14
+ * server span (OTel context is async-local). With `OTEL_PROPAGATORS=none`
15
+ * that span is the trace root - see the package README's observability
16
+ * section for why Apps ingress `traceparent` otherwise hides every turn.
17
+ *
18
+ * @module
19
+ */
20
+ import { json, log, object } from "@dbx-tools/shared-core";
21
+ import { trace } from "@opentelemetry/api";
22
+ const logger = log.logger("mastra/trace-io");
23
+ /** Cap on each payload copied onto a span, so one turn cannot bloat the export. */
24
+ export const TRACE_IO_LIMIT = 8_000;
25
+ /**
26
+ * Mount-relative Mastra agent invoke paths that carry a chat turn body
27
+ * (`messages`) and stream an assistant answer. Resume / approve verbs
28
+ * are intentionally excluded: their bodies are tool decisions, not the
29
+ * user prompt, and they rarely produce a fresh answer worth surfacing.
30
+ */
31
+ const AGENT_TURN_ROUTE = /^\/agents\/[^/]+\/(stream|generate)(\/|$)/i;
32
+ /** Attribute keys the MLflow UC `*_trace_unified` view reads from the root span. */
33
+ export const MLFLOW_SPAN_INPUTS_ATTR = "mlflow.spanInputs";
34
+ export const MLFLOW_SPAN_OUTPUTS_ATTR = "mlflow.spanOutputs";
35
+ /**
36
+ * Concatenate the assistant's answer out of an AI SDK SSE transcript.
37
+ *
38
+ * The agent streams its reply as `text-delta` frames
39
+ * (`data: {"type":"text-delta","payload":{"text":"..."}}`), so the full
40
+ * answer only ever exists as deltas on the wire and has to be
41
+ * reassembled here.
42
+ */
43
+ export function assistantTextFromSse(body) {
44
+ const parts = [];
45
+ for (const line of body.split("\n")) {
46
+ if (!line.startsWith("data:"))
47
+ continue;
48
+ // SSE carries comment/keepalive lines that are not JSON; they never hold
49
+ // assistant text, so skipping them is the intended path rather than an error.
50
+ const frame = json.parse(line.slice(5));
51
+ if (!object.isRecord(frame) || frame.type !== "text-delta")
52
+ continue;
53
+ const text = object.isRecord(frame.payload) ? frame.payload.text : undefined;
54
+ if (typeof text === "string")
55
+ parts.push(text);
56
+ }
57
+ return parts.join("");
58
+ }
59
+ /**
60
+ * Express middleware that stamps chat turn I/O onto the active OTel span.
61
+ *
62
+ * Tee `res.write` / `res.end` instead of listening for `finish`: the HTTP
63
+ * instrumentation ends the root span on finish, and an attribute set after
64
+ * a span ends never reaches the exporter.
65
+ *
66
+ * `path` is mount-relative (what the Mastra sub-app sees), e.g.
67
+ * `/agents/support/stream`.
68
+ */
69
+ export function chatTurnTraceIoMiddleware(req, res, next) {
70
+ if (req.method !== "POST" || !AGENT_TURN_ROUTE.test(req.path)) {
71
+ next();
72
+ return;
73
+ }
74
+ const span = trace.getActiveSpan();
75
+ if (!span) {
76
+ next();
77
+ return;
78
+ }
79
+ const messages = req.body?.messages;
80
+ if (messages !== undefined) {
81
+ span.setAttribute(MLFLOW_SPAN_INPUTS_ATTR, JSON.stringify(messages).slice(0, TRACE_IO_LIMIT));
82
+ }
83
+ const chunks = [];
84
+ const passThroughWrite = res.write.bind(res);
85
+ const passThroughEnd = res.end.bind(res);
86
+ const collect = (chunk) => {
87
+ if (typeof chunk === "string")
88
+ chunks.push(chunk);
89
+ else if (Buffer.isBuffer(chunk))
90
+ chunks.push(chunk.toString("utf8"));
91
+ };
92
+ res.write = ((chunk, ...rest) => {
93
+ collect(chunk);
94
+ return passThroughWrite(chunk, ...rest);
95
+ });
96
+ res.end = ((chunk, ...rest) => {
97
+ collect(chunk);
98
+ const answer = assistantTextFromSse(chunks.join(""));
99
+ if (answer) {
100
+ span.setAttribute(MLFLOW_SPAN_OUTPUTS_ATTR, answer.slice(0, TRACE_IO_LIMIT));
101
+ }
102
+ return passThroughEnd(chunk, ...rest);
103
+ });
104
+ next();
105
+ }
106
+ /**
107
+ * Install {@link chatTurnTraceIoMiddleware} on a Mastra Express sub-app.
108
+ *
109
+ * Call before `MastraServer.init()` so the layer sits ahead of the agent
110
+ * routes. Safe to call unconditionally: with no active span (local, no
111
+ * OTLP) the middleware is a no-op.
112
+ */
113
+ export function attachChatTurnTraceIo(app) {
114
+ app.use(chatTurnTraceIoMiddleware);
115
+ logger.info("chat turn I/O middleware attached");
116
+ }
117
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoidHJhY2UtaW8uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi9zcmMvdHJhY2UtaW8udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUE7Ozs7Ozs7Ozs7Ozs7Ozs7OztHQWtCRztBQUVILE9BQU8sRUFBRSxJQUFJLEVBQUUsR0FBRyxFQUFFLE1BQU0sRUFBRSxNQUFNLHdCQUF3QixDQUFDO0FBQzNELE9BQU8sRUFBRSxLQUFLLEVBQUUsTUFBTSxvQkFBb0IsQ0FBQztBQUczQyxNQUFNLE1BQU0sR0FBRyxHQUFHLENBQUMsTUFBTSxDQUFDLGlCQUFpQixDQUFDLENBQUM7QUFFN0MsbUZBQW1GO0FBQ25GLE1BQU0sQ0FBQyxNQUFNLGNBQWMsR0FBRyxLQUFLLENBQUM7QUFFcEM7Ozs7O0dBS0c7QUFDSCxNQUFNLGdCQUFnQixHQUFHLDRDQUE0QyxDQUFDO0FBRXRFLG9GQUFvRjtBQUNwRixNQUFNLENBQUMsTUFBTSx1QkFBdUIsR0FBRyxtQkFBbUIsQ0FBQztBQUMzRCxNQUFNLENBQUMsTUFBTSx3QkFBd0IsR0FBRyxvQkFBb0IsQ0FBQztBQUU3RDs7Ozs7OztHQU9HO0FBQ0gsTUFBTSxVQUFVLG9CQUFvQixDQUFDLElBQVk7SUFDL0MsTUFBTSxLQUFLLEdBQWEsRUFBRSxDQUFDO0lBQzNCLEtBQUssTUFBTSxJQUFJLElBQUksSUFBSSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDO1FBQ3BDLElBQUksQ0FBQyxJQUFJLENBQUMsVUFBVSxDQUFDLE9BQU8sQ0FBQztZQUFFLFNBQVM7UUFDeEMseUVBQXlFO1FBQ3pFLDhFQUE4RTtRQUM5RSxNQUFNLEtBQUssR0FBRyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQztRQUN4QyxJQUFJLENBQUMsTUFBTSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsSUFBSSxLQUFLLENBQUMsSUFBSSxLQUFLLFlBQVk7WUFBRSxTQUFT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QVUscUJBQXFCLENBQUMsR0FBb0I7SUFDeEQsR0FBRyxDQUFDLEdBQUcsQ0FBQyx5QkFBeUIsQ0FBQyxDQUFDO0lBQ25DLE1BQU0sQ0FBQyxJQUFJLENBQUMsbUNBQW1DLENBQUMsQ0FBQztBQUNuRCxDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiLyoqXG4gKiBDb3B5IGVhY2ggTWFzdHJhIGNoYXQgdHVybidzIHJlcXVlc3QvcmVzcG9uc2Ugb250byB0aGUgYWN0aXZlIE9UZWxcbiAqIHJvb3Qgc3BhbiBzbyBEYXRhYnJpY2tzLW1hbmFnZWQgTUxmbG93IGNhbiBkaXNwbGF5IHRoZW0uXG4gKlxuICogV2h5IHRoaXMgZXhpc3RzOiBNTGZsb3cncyBVQyBgPHRhcmdldD5fdHJhY2VfdW5pZmllZGAgdmlldyBwaWNrcyB0aGVcbiAqIG9uZSBzcGFuIHdob3NlIGBwYXJlbnRfc3Bhbl9pZGAgaXMgZW1wdHkgYW5kIHJlYWRzIHJlcXVlc3QvcmVzcG9uc2VcbiAqIGZyb20gYG1sZmxvdy5zcGFuSW5wdXRzYCAvIGBtbGZsb3cuc3Bhbk91dHB1dHNgIChvciB0aGUgYGdlbl9haS4qYFxuICogZXF1aXZhbGVudHMpIG9uIFRIQVQgc3BhbiBvbmx5LiBNYXN0cmEgcmVjb3JkcyB0aGUgdHVybiBvbiBpdHNcbiAqIGBpbnZva2VfYWdlbnRgIGNoaWxkIHVuZGVyIGBtYXN0cmEuYWdlbnRfcnVuLmlucHV0YCAvIGAub3V0cHV0YCwga2V5c1xuICogdGhlIHZpZXcgbmV2ZXIgcmVhZHMsIHNvIGNoYXQgdHJhY2VzIGFycml2ZSB3aXRoIGJvdGggY29sdW1ucyBudWxsXG4gKiBldmVuIHdoZW4gdGhlIHNwYW5zIHRoZW1zZWx2ZXMgbG9vayBoZWFsdGh5LlxuICpcbiAqIFRoZSBhY3RpdmUgc3BhbiBpbnNpZGUgdGhlIE1hc3RyYSBzdWItYXBwIGlzIHN0aWxsIEFwcEtpdCdzIEhUVFBcbiAqIHNlcnZlciBzcGFuIChPVGVsIGNvbnRleHQgaXMgYXN5bmMtbG9jYWwpLiBXaXRoIGBPVEVMX1BST1BBR0FUT1JTPW5vbmVgXG4gKiB0aGF0IHNwYW4gaXMgdGhlIHRyYWNlIHJvb3QgLSBzZWUgdGhlIHBhY2thZ2UgUkVBRE1FJ3Mgb2JzZXJ2YWJpbGl0eVxuICogc2VjdGlvbiBmb3Igd2h5IEFwcHMgaW5ncmVzcyBgdHJhY2VwYXJlbnRgIG90aGVyd2lzZSBoaWRlcyBldmVyeSB0dXJuLlxuICpcbiAqIEBtb2R1bGVcbiAqL1xuXG5pbXBvcnQgeyBqc29uLCBsb2csIG9iamVjdCB9IGZyb20gXCJAZGJ4LXRvb2xzL3NoYXJlZC1jb3JlXCI7XG5pbXBvcnQgeyB0cmFjZSB9IGZyb20gXCJAb3BlbnRlbGVtZXRyeS9hcGlcIjtcbmltcG9ydCB0eXBlIGV4cHJlc3MgZnJvbSBcImV4cHJlc3NcIjtcblxuY29uc3QgbG9nZ2VyID0gbG9nLmxvZ2dlcihcIm1hc3RyYS90cmFjZS1pb1wiKTtcblxuLyoqIENhcCBvbiBlYWNoIHBheWxvYWQgY29waWVkIG9udG8gYSBzcGFuLCBzbyBvbmUgdHVybiBjYW5ub3QgYmxvYXQgdGhlIGV4cG9ydC4gKi9cbmV4cG9ydCBjb25zdCBUUkFDRV9JT19MSU1JVCA9IDhfMDAwO1xuXG4vKipcbiAqIE1vdW50LXJlbGF0aXZlIE1hc3RyYSBhZ2VudCBpbnZva2UgcGF0aHMgdGhhdCBjYXJyeSBhIGNoYXQgdHVybiBib2R5XG4gKiAoYG1lc3NhZ2VzYCkgYW5kIHN0cmVhbSBhbiBhc3Npc3RhbnQgYW5zd2VyLiBSZXN1bWUgLyBhcHByb3ZlIHZlcmJzXG4gKiBhcmUgaW50ZW50aW9uYWxseSBleGNsdWRlZDogdGhlaXIgYm9kaWVzIGFyZSB0b29sIGRlY2lzaW9ucywgbm90IHRoZVxuICogdXNlciBwcm9tcHQsIGFuZCB0aGV5IHJhcmVseSBwcm9kdWNlIGEgZnJlc2ggYW5zd2VyIHdvcnRoIHN1cmZhY2luZy5cbiAqL1xuY29uc3QgQUdFTlRfVFVSTl9ST1VURSA9IC9eXFwvYWdlbnRzXFwvW14vXStcXC8oc3RyZWFtfGdlbmVyYXRlKShcXC98JCkvaTtcblxuLyoqIEF0dHJpYnV0ZSBrZXlzIHRoZSBNTGZsb3cgVUMgYCpfdHJhY2VfdW5pZmllZGAgdmlldyByZWFkcyBmcm9tIHRoZSByb290IHNwYW4uICovXG5leHBvcnQgY29uc3QgTUxGTE9XX1NQQU5fSU5QVVRTX0FUVFIgPSBcIm1sZmxvdy5zcGFuSW5wdXRzXCI7XG5leHBvcnQgY29uc3QgTUxGTE9XX1NQQU5fT1VUUFVUU19BVFRSID0gXCJtbGZsb3cuc3Bhbk91dHB1dHNcIjtcblxuLyoqXG4gKiBDb25jYXRlbmF0ZSB0aGUgYXNzaXN0YW50J3MgYW5zd2VyIG91dCBvZiBhbiBBSSBTREsgU1NFIHRyYW5zY3JpcHQuXG4gKlxuICogVGhlIGFnZW50IHN0cmVhbXMgaXRzIHJlcGx5IGFzIGB0ZXh0LWRlbHRhYCBmcmFtZXNcbiAqIChgZGF0YToge1widHlwZVwiOlwidGV4dC1kZWx0YVwiLFwicGF5bG9hZFwiOntcInRleHRcIjpcIi4uLlwifX1gKSwgc28gdGhlIGZ1bGxcbiAqIGFuc3dlciBvbmx5IGV2ZXIgZXhpc3RzIGFzIGRlbHRhcyBvbiB0aGUgd2lyZSBhbmQgaGFzIHRvIGJlXG4gKiByZWFzc2VtYmxlZCBoZXJlLlxuICovXG5leHBvcnQgZnVuY3Rpb24gYXNzaXN0YW50VGV4dEZyb21Tc2UoYm9keTogc3RyaW5nKTogc3RyaW5nIHtcbiAgY29uc3QgcGFydHM6IHN0cmluZ1tdID0gW107XG4gIGZvciAoY29uc3QgbGluZSBvZiBib2R5LnNwbGl0KFwiXFxuXCIpKSB7XG4gICAgaWYgKCFsaW5lLnN0YXJ0c1dpdGgoXCJkYXRhOlwiKSkgY29udGludWU7XG4gICAgLy8gU1NFIGNhcnJpZXMgY29tbWVudC9rZWVwYWxpdmUgbGluZXMgdGhhdCBhcmUgbm90IEpTT047IHRoZXkgbmV2ZXIgaG9sZFxuICAgIC8vIGFzc2lzdGFudCB0ZXh0LCBzbyBza2lwcGluZyB0aGVtIGlzIHRoZSBpbnRlbmRlZCBwYXRoIHJhdGhlciB0aGFuIGFuIGVycm9yLlxuICAgIGNvbnN0IGZyYW1lID0ganNvbi5wYXJzZShsaW5lLnNsaWNlKDUpKTtcbiAgICBpZiAoIW9iamVjdC5pc1JlY29yZChmcmFtZSkgfHwgZnJhbWUudHlwZSAhPT0gXCJ0ZXh0LWRlbHRhXCIpIGNvbnRpbnVlO1xuICAgIGNvbnN0IHRleHQgPSBvYmplY3QuaXNSZWNvcmQoZnJhbWUucGF5bG9hZCkgPyBmcmFtZS5wYXlsb2FkLnRleHQgOiB1bmRlZmluZWQ7XG4gICAgaWYgKHR5cGVvZiB0ZXh0ID09PSBcInN0cmluZ1wiKSBwYXJ0cy5wdXNoKHRleHQpO1xuICB9XG4gIHJldHVybiBwYXJ0cy5qb2luKFwiXCIpO1xufVxuXG4vKipcbiAqIEV4cHJlc3MgbWlkZGxld2FyZSB0aGF0IHN0YW1wcyBjaGF0IHR1cm4gSS9PIG9udG8gdGhlIGFjdGl2ZSBPVGVsIHNwYW4uXG4gKlxuICogVGVlIGByZXMud3JpdGVgIC8gYHJlcy5lbmRgIGluc3RlYWQgb2YgbGlzdGVuaW5nIGZvciBgZmluaXNoYDogdGhlIEhUVFBcbiAqIGluc3RydW1lbnRhdGlvbiBlbmRzIHRoZSByb290IHNwYW4gb24gZmluaXNoLCBhbmQgYW4gYXR0cmlidXRlIHNldCBhZnRlclxuICogYSBzcGFuIGVuZHMgbmV2ZXIgcmVhY2hlcyB0aGUgZXhwb3J0ZXIuXG4gKlxuICogYHBhdGhgIGlzIG1vdW50LXJlbGF0aXZlICh3aGF0IHRoZSBNYXN0cmEgc3ViLWFwcCBzZWVzKSwgZS5nLlxuICogYC9hZ2VudHMvc3VwcG9ydC9zdHJlYW1gLlxuICovXG5leHBvcnQgZnVuY3Rpb24gY2hhdFR1cm5UcmFjZUlvTWlkZGxld2FyZShcbiAgcmVxOiBleHByZXNzLlJlcXVlc3QsXG4gIHJlczogZXhwcmVzcy5SZXNwb25zZSxcbiAgbmV4dDogZXhwcmVzcy5OZXh0RnVuY3Rpb24sXG4pOiB2b2lkIHtcbiAgaWYgKHJlcS5tZXRob2QgIT09IFwiUE9TVFwiIHx8ICFBR0VOVF9UVVJOX1JPVVRFLnRlc3QocmVxLnBhdGgpKSB7XG4gICAgbmV4dCgpO1xuICAgIHJldHVybjtcbiAgfVxuICBjb25zdCBzcGFuID0gdHJhY2UuZ2V0QWN0aXZlU3BhbigpO1xuICBpZiAoIXNwYW4pIHtcbiAgICBuZXh0KCk7XG4gICAgcmV0dXJuO1xuICB9XG5cbiAgY29uc3QgbWVzc2FnZXMgPSAocmVxLmJvZHkgYXMgeyBtZXNzYWdlcz86IHVua25vd24gfSB8IHVuZGVmaW5lZCk/Lm1lc3NhZ2VzO1xuICBpZiAobWVzc2FnZXMgIT09IHVuZGVmaW5lZCkge1xuICAgIHNwYW4uc2V0QXR0cmlidXRlKE1MRkxPV19TUEFOX0lOUFVUU19BVFRSLCBKU09OLnN0cmluZ2lmeShtZXNzYWdlcykuc2xpY2UoMCwgVFJBQ0VfSU9fTElNSVQpKTtcbiAgfVxuXG4gIGNvbnN0IGNodW5rczogc3RyaW5nW10gPSBbXTtcbiAgY29uc3QgcGFzc1Rocm91Z2hXcml0ZSA9IHJlcy53cml0ZS5iaW5kKHJlcykgYXMgKC4uLmFyZ3M6IHVua25vd25bXSkgPT4gYm9vbGVhbjtcbiAgY29uc3QgcGFzc1Rocm91Z2hFbmQgPSByZXMuZW5kLmJpbmQocmVzKSBhcyAoLi4uYXJnczogdW5rbm93bltdKSA9PiB1bmtub3duO1xuICBjb25zdCBjb2xsZWN0ID0gKGNodW5rOiB1bmtub3duKTogdm9pZCA9PiB7XG4gICAgaWYgKHR5cGVvZiBjaHVuayA9PT0gXCJzdHJpbmdcIikgY2h1bmtzLnB1c2goY2h1bmspO1xuICAgIGVsc2UgaWYgKEJ1ZmZlci5pc0J1ZmZlcihjaHVuaykpIGNodW5rcy5wdXNoKGNodW5rLnRvU3RyaW5nKFwidXRmOFwiKSk7XG4gIH07XG5cbiAgcmVzLndyaXRlID0gKChjaHVuazogdW5rbm93biwgLi4ucmVzdDogdW5rbm93bltdKSA9PiB7XG4gICAgY29sbGVjdChjaHVuayk7XG4gICAgcmV0dXJuIHBhc3NUaHJvdWdoV3JpdGUoY2h1bmssIC4uLnJlc3QpO1xuICB9KSBhcyB0eXBlb2YgcmVzLndyaXRlO1xuXG4gIHJlcy5lbmQgPSAoKGNodW5rPzogdW5rbm93biwgLi4ucmVzdDogdW5rbm93bltdKSA9PiB7XG4gICAgY29sbGVjdChjaHVuayk7XG4gICAgY29uc3QgYW5zd2VyID0gYXNzaXN0YW50VGV4dEZyb21Tc2UoY2h1bmtzLmpvaW4oXCJcIikpO1xuICAgIGlmIChhbnN3ZXIpIHtcbiAgICAgIHNwYW4uc2V0QXR0cmlidXRlKE1MRkxPV19TUEFOX09VVFBVVFNfQVRUUiwgYW5zd2VyLnNsaWNlKDAsIFRSQUNFX0lPX0xJTUlUKSk7XG4gICAgfVxuICAgIHJldHVybiBwYXNzVGhyb3VnaEVuZChjaHVuaywgLi4ucmVzdCk7XG4gIH0pIGFzIHR5cGVvZiByZXMuZW5kO1xuXG4gIG5leHQoKTtcbn1cblxuLyoqXG4gKiBJbnN0YWxsIHtAbGluayBjaGF0VHVyblRyYWNlSW9NaWRkbGV3YXJlfSBvbiBhIE1hc3RyYSBFeHByZXNzIHN1Yi1hcHAuXG4gKlxuICogQ2FsbCBiZWZvcmUgYE1hc3RyYVNlcnZlci5pbml0KClgIHNvIHRoZSBsYXllciBzaXRzIGFoZWFkIG9mIHRoZSBhZ2VudFxuICogcm91dGVzLiBTYWZlIHRvIGNhbGwgdW5jb25kaXRpb25hbGx5OiB3aXRoIG5vIGFjdGl2ZSBzcGFuIChsb2NhbCwgbm9cbiAqIE9UTFApIHRoZSBtaWRkbGV3YXJlIGlzIGEgbm8tb3AuXG4gKi9cbmV4cG9ydCBmdW5jdGlvbiBhdHRhY2hDaGF0VHVyblRyYWNlSW8oYXBwOiBleHByZXNzLkV4cHJlc3MpOiB2b2lkIHtcbiAgYXBwLnVzZShjaGF0VHVyblRyYWNlSW9NaWRkbGV3YXJlKTtcbiAgbG9nZ2VyLmluZm8oXCJjaGF0IHR1cm4gSS9PIG1pZGRsZXdhcmUgYXR0YWNoZWRcIik7XG59XG4iXX0=
@@ -1 +1 @@
1
- {"root":["../src/agents.ts","../src/chart.ts","../src/config.ts","../src/defaults.ts","../src/filesystems.ts","../src/genie.ts","../src/history.ts","../src/identity.ts","../src/mcp.ts","../src/memory.ts","../src/mlflow.ts","../src/model.ts","../src/observability.ts","../src/pagination.ts","../src/plugin.ts","../src/processors.ts","../src/remote-skills.ts","../src/rest.ts","../src/server.ts","../src/serving-sanitize.ts","../src/serving.ts","../src/statement.ts","../src/storage-schema.ts","../src/summarize.ts","../src/threads.ts","../src/validation.ts","../src/workspaces.ts","../src/writer.ts","../index.ts"],"version":"5.9.3"}
1
+ {"root":["../src/agents.ts","../src/chart.ts","../src/config.ts","../src/defaults.ts","../src/filesystems.ts","../src/genie.ts","../src/history.ts","../src/identity.ts","../src/mcp.ts","../src/memory.ts","../src/mlflow.ts","../src/model.ts","../src/observability.ts","../src/pagination.ts","../src/plugin.ts","../src/processors.ts","../src/remote-skills.ts","../src/rest.ts","../src/server.ts","../src/serving-sanitize.ts","../src/serving.ts","../src/statement.ts","../src/storage-schema.ts","../src/summarize.ts","../src/threads.ts","../src/trace-io.ts","../src/validation.ts","../src/workspaces.ts","../src/writer.ts","../index.ts"],"version":"5.9.3"}
package/package.json CHANGED
@@ -32,25 +32,25 @@
32
32
  "express": "^5.1.0",
33
33
  "pg": "^8.22.0",
34
34
  "zod": "4.3.6",
35
- "@dbx-tools/appkit": "0.6.13",
36
- "@dbx-tools/core": "0.6.13",
37
- "@dbx-tools/databricks": "0.6.13",
38
- "@dbx-tools/genie": "0.6.13",
39
- "@dbx-tools/fs": "0.6.13",
40
- "@dbx-tools/model": "0.6.13",
41
- "@dbx-tools/path": "0.6.13",
42
- "@dbx-tools/shared-genie": "0.6.13",
43
- "@dbx-tools/shared-model": "0.6.13",
44
- "@dbx-tools/shared-fs": "0.6.13",
45
- "@dbx-tools/shared-core": "0.6.13",
46
- "@dbx-tools/shared-mastra": "0.6.13"
35
+ "@dbx-tools/core": "0.6.15",
36
+ "@dbx-tools/appkit": "0.6.15",
37
+ "@dbx-tools/databricks": "0.6.15",
38
+ "@dbx-tools/fs": "0.6.15",
39
+ "@dbx-tools/model": "0.6.15",
40
+ "@dbx-tools/path": "0.6.15",
41
+ "@dbx-tools/shared-genie": "0.6.15",
42
+ "@dbx-tools/shared-core": "0.6.15",
43
+ "@dbx-tools/shared-fs": "0.6.15",
44
+ "@dbx-tools/shared-mastra": "0.6.15",
45
+ "@dbx-tools/genie": "0.6.15",
46
+ "@dbx-tools/shared-model": "0.6.15"
47
47
  },
48
48
  "main": "./lib/index.js",
49
49
  "license": "UNLICENSED",
50
50
  "publishConfig": {
51
51
  "access": "public"
52
52
  },
53
- "version": "0.6.13",
53
+ "version": "0.6.15",
54
54
  "types": "./lib/index.d.ts",
55
55
  "type": "module",
56
56
  "exports": {
package/src/mlflow.ts CHANGED
@@ -5,7 +5,9 @@
5
5
  *
6
6
  * Feedback attaches to a trace, and the plugin's spans reach MLflow
7
7
  * through the same OTel pipeline as every other AppKit span (see
8
- * `observability.ts`). MLflow derives its trace id from the OpenTelemetry
8
+ * `observability.ts`). On Databricks Apps that pipeline is the UC
9
+ * sidecar injected by `telemetry_export_destinations`, not a direct
10
+ * workspace OTLP URL. MLflow derives its trace id from the OpenTelemetry
9
11
  * trace id (`tr-<hex(otelTraceId)>`), so the server stamps the active
10
12
  * trace id on each turn's response and the client sends it back here.
11
13
  *
@@ -30,6 +30,15 @@
30
30
  * registered at all (unless `observability: true` forces it on), so
31
31
  * Mastra does not emit `[OtelBridge] No OTEL span found` warnings.
32
32
  *
33
+ * On Databricks Apps the supported sink is Unity Catalog via
34
+ * `telemetry_export_destinations` (the platform injects the local OTLP
35
+ * sidecar). Managed MLflow has no OTLP ingest endpoint - do not point
36
+ * `OTEL_EXPORTER_OTLP_*` at the workspace host. Set
37
+ * `OTEL_PROPAGATORS=none` so Apps ingress `traceparent` does not hide
38
+ * every request-scoped root span from the UC `*_trace_unified` view.
39
+ * Chat request/response on that root span is stamped by
40
+ * `traceIo.attachChatTurnTraceIo` (see `trace-io.ts`).
41
+ *
33
42
  * @module
34
43
  */
35
44
 
@@ -39,6 +48,7 @@ import { Observability } from "@mastra/observability";
39
48
  import { OtelBridge } from "@mastra/otel-bridge";
40
49
 
41
50
  import { TRACE_REQUEST_CONTEXT_KEYS } from "./config.ts";
51
+ import { mlflowEnabled } from "./mlflow.ts";
42
52
 
43
53
  const logger = log.logger("mastra/observability");
44
54
 
@@ -127,11 +137,17 @@ export async function buildObservability(
127
137
  : otelBase
128
138
  ? `${otelBase.replace(/\/+$/, "")}/v1/traces`
129
139
  : undefined;
140
+ // `mlflowEnabled` is the same gate that flips chat feedback on, so the
141
+ // boot line is the one-glance check that both halves of the Apps UC
142
+ // pipeline (OTLP sidecar + experiment id) are present.
143
+ const feedback = mlflowEnabled();
130
144
  logger.info("Mastra observability wired through OTel bridge", {
131
145
  serviceName,
132
146
  requestContextKeys,
133
147
  otelBase: otelBase ?? "<unset>",
134
148
  resolvedTracesUrl: resolvedTracesUrl ?? "<unset>",
149
+ feedback,
150
+ observability: feedback ? "mlflow" : "otel",
135
151
  });
136
152
 
137
153
  return new Observability({
package/src/plugin.ts CHANGED
@@ -103,6 +103,7 @@ import {
103
103
  isMastraRequestAllowed,
104
104
  MastraServer,
105
105
  } from "./server.ts";
106
+ import { attachChatTurnTraceIo } from "./trace-io.ts";
106
107
  import { resolveServingConfig } from "./serving.ts";
107
108
  import { fetchStatementData, STATEMENT_ROW_CAP } from "./statement.ts";
108
109
  import { threadsRoute } from "./threads.ts";
@@ -1061,6 +1062,12 @@ export class MastraPlugin extends Plugin<MastraPluginConfig> {
1061
1062
  });
1062
1063
  this.mastraApp = express();
1063
1064
  attachRoutePatchMiddleware(this.mastraApp);
1065
+ // Stamp chat request/response onto the HTTP root span so MLflow's
1066
+ // UC `*_trace_unified` view can surface them (Mastra's own
1067
+ // `mastra.agent_run.*` attributes sit on a child span the view
1068
+ // never reads). Must run before `MastraServer.init` so the layer
1069
+ // sits ahead of the agent routes.
1070
+ attachChatTurnTraceIo(this.mastraApp);
1064
1071
  this.mastraServer = new MastraServer(this.config, {
1065
1072
  app: this.mastraApp,
1066
1073
  mastra: this.mastra,
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Copy each Mastra chat turn's request/response onto the active OTel
3
+ * root span so Databricks-managed MLflow can display them.
4
+ *
5
+ * Why this exists: MLflow's UC `<target>_trace_unified` view picks the
6
+ * one span whose `parent_span_id` is empty and reads request/response
7
+ * from `mlflow.spanInputs` / `mlflow.spanOutputs` (or the `gen_ai.*`
8
+ * equivalents) on THAT span only. Mastra records the turn on its
9
+ * `invoke_agent` child under `mastra.agent_run.input` / `.output`, keys
10
+ * the view never reads, so chat traces arrive with both columns null
11
+ * even when the spans themselves look healthy.
12
+ *
13
+ * The active span inside the Mastra sub-app is still AppKit's HTTP
14
+ * server span (OTel context is async-local). With `OTEL_PROPAGATORS=none`
15
+ * that span is the trace root - see the package README's observability
16
+ * section for why Apps ingress `traceparent` otherwise hides every turn.
17
+ *
18
+ * @module
19
+ */
20
+
21
+ import { json, log, object } from "@dbx-tools/shared-core";
22
+ import { trace } from "@opentelemetry/api";
23
+ import type express from "express";
24
+
25
+ const logger = log.logger("mastra/trace-io");
26
+
27
+ /** Cap on each payload copied onto a span, so one turn cannot bloat the export. */
28
+ export const TRACE_IO_LIMIT = 8_000;
29
+
30
+ /**
31
+ * Mount-relative Mastra agent invoke paths that carry a chat turn body
32
+ * (`messages`) and stream an assistant answer. Resume / approve verbs
33
+ * are intentionally excluded: their bodies are tool decisions, not the
34
+ * user prompt, and they rarely produce a fresh answer worth surfacing.
35
+ */
36
+ const AGENT_TURN_ROUTE = /^\/agents\/[^/]+\/(stream|generate)(\/|$)/i;
37
+
38
+ /** Attribute keys the MLflow UC `*_trace_unified` view reads from the root span. */
39
+ export const MLFLOW_SPAN_INPUTS_ATTR = "mlflow.spanInputs";
40
+ export const MLFLOW_SPAN_OUTPUTS_ATTR = "mlflow.spanOutputs";
41
+
42
+ /**
43
+ * Concatenate the assistant's answer out of an AI SDK SSE transcript.
44
+ *
45
+ * The agent streams its reply as `text-delta` frames
46
+ * (`data: {"type":"text-delta","payload":{"text":"..."}}`), so the full
47
+ * answer only ever exists as deltas on the wire and has to be
48
+ * reassembled here.
49
+ */
50
+ export function assistantTextFromSse(body: string): string {
51
+ const parts: string[] = [];
52
+ for (const line of body.split("\n")) {
53
+ if (!line.startsWith("data:")) continue;
54
+ // SSE carries comment/keepalive lines that are not JSON; they never hold
55
+ // assistant text, so skipping them is the intended path rather than an error.
56
+ const frame = json.parse(line.slice(5));
57
+ if (!object.isRecord(frame) || frame.type !== "text-delta") continue;
58
+ const text = object.isRecord(frame.payload) ? frame.payload.text : undefined;
59
+ if (typeof text === "string") parts.push(text);
60
+ }
61
+ return parts.join("");
62
+ }
63
+
64
+ /**
65
+ * Express middleware that stamps chat turn I/O onto the active OTel span.
66
+ *
67
+ * Tee `res.write` / `res.end` instead of listening for `finish`: the HTTP
68
+ * instrumentation ends the root span on finish, and an attribute set after
69
+ * a span ends never reaches the exporter.
70
+ *
71
+ * `path` is mount-relative (what the Mastra sub-app sees), e.g.
72
+ * `/agents/support/stream`.
73
+ */
74
+ export function chatTurnTraceIoMiddleware(
75
+ req: express.Request,
76
+ res: express.Response,
77
+ next: express.NextFunction,
78
+ ): void {
79
+ if (req.method !== "POST" || !AGENT_TURN_ROUTE.test(req.path)) {
80
+ next();
81
+ return;
82
+ }
83
+ const span = trace.getActiveSpan();
84
+ if (!span) {
85
+ next();
86
+ return;
87
+ }
88
+
89
+ const messages = (req.body as { messages?: unknown } | undefined)?.messages;
90
+ if (messages !== undefined) {
91
+ span.setAttribute(MLFLOW_SPAN_INPUTS_ATTR, JSON.stringify(messages).slice(0, TRACE_IO_LIMIT));
92
+ }
93
+
94
+ const chunks: string[] = [];
95
+ const passThroughWrite = res.write.bind(res) as (...args: unknown[]) => boolean;
96
+ const passThroughEnd = res.end.bind(res) as (...args: unknown[]) => unknown;
97
+ const collect = (chunk: unknown): void => {
98
+ if (typeof chunk === "string") chunks.push(chunk);
99
+ else if (Buffer.isBuffer(chunk)) chunks.push(chunk.toString("utf8"));
100
+ };
101
+
102
+ res.write = ((chunk: unknown, ...rest: unknown[]) => {
103
+ collect(chunk);
104
+ return passThroughWrite(chunk, ...rest);
105
+ }) as typeof res.write;
106
+
107
+ res.end = ((chunk?: unknown, ...rest: unknown[]) => {
108
+ collect(chunk);
109
+ const answer = assistantTextFromSse(chunks.join(""));
110
+ if (answer) {
111
+ span.setAttribute(MLFLOW_SPAN_OUTPUTS_ATTR, answer.slice(0, TRACE_IO_LIMIT));
112
+ }
113
+ return passThroughEnd(chunk, ...rest);
114
+ }) as typeof res.end;
115
+
116
+ next();
117
+ }
118
+
119
+ /**
120
+ * Install {@link chatTurnTraceIoMiddleware} on a Mastra Express sub-app.
121
+ *
122
+ * Call before `MastraServer.init()` so the layer sits ahead of the agent
123
+ * routes. Safe to call unconditionally: with no active span (local, no
124
+ * OTLP) the middleware is a no-op.
125
+ */
126
+ export function attachChatTurnTraceIo(app: express.Express): void {
127
+ app.use(chatTurnTraceIoMiddleware);
128
+ logger.info("chat turn I/O middleware attached");
129
+ }