@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.
- package/README.md +73 -9
- package/index.ts +2 -0
- package/lib/index.d.ts +2 -0
- package/lib/index.js +3 -1
- package/lib/src/mlflow.d.ts +3 -1
- package/lib/src/mlflow.js +4 -2
- package/lib/src/observability.d.ts +9 -0
- package/lib/src/observability.js +17 -1
- package/lib/src/plugin.js +8 -1
- package/lib/src/trace-io.d.ts +53 -0
- package/lib/src/trace-io.js +117 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +13 -13
- package/src/mlflow.ts +3 -1
- package/src/observability.ts +16 -0
- package/src/plugin.ts +7 -0
- package/src/trace-io.ts +129 -0
|
@@ -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=
|
package/lib/tsconfig.tsbuildinfo
CHANGED
|
@@ -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/
|
|
36
|
-
"@dbx-tools/
|
|
37
|
-
"@dbx-tools/databricks": "0.6.
|
|
38
|
-
"@dbx-tools/
|
|
39
|
-
"@dbx-tools/
|
|
40
|
-
"@dbx-tools/
|
|
41
|
-
"@dbx-tools/
|
|
42
|
-
"@dbx-tools/shared-
|
|
43
|
-
"@dbx-tools/shared-
|
|
44
|
-
"@dbx-tools/shared-
|
|
45
|
-
"@dbx-tools/
|
|
46
|
-
"@dbx-tools/shared-
|
|
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.
|
|
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`).
|
|
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
|
*
|
package/src/observability.ts
CHANGED
|
@@ -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,
|
package/src/trace-io.ts
ADDED
|
@@ -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
|
+
}
|