@mastra/braintrust 1.0.0-beta.8 → 1.0.0
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/CHANGELOG.md +366 -0
- package/README.md +47 -5
- package/dist/formatter.d.ts +99 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/index.cjs +388 -203
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +389 -204
- package/dist/index.js.map +1 -1
- package/dist/thread-reconstruction.d.ts +47 -0
- package/dist/thread-reconstruction.d.ts.map +1 -0
- package/dist/tracing.d.ts +69 -25
- package/dist/tracing.d.ts.map +1 -1
- package/package.json +8 -7
package/dist/index.cjs
CHANGED
|
@@ -7,6 +7,136 @@ var braintrust = require('braintrust');
|
|
|
7
7
|
|
|
8
8
|
// src/tracing.ts
|
|
9
9
|
|
|
10
|
+
// src/formatter.ts
|
|
11
|
+
function removeNullish(obj) {
|
|
12
|
+
return Object.fromEntries(Object.entries(obj).filter(([_, v]) => v != null));
|
|
13
|
+
}
|
|
14
|
+
function convertContentPart(part) {
|
|
15
|
+
if (!part || typeof part !== "object") {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
switch (part.type) {
|
|
19
|
+
case "text":
|
|
20
|
+
return part.text || null;
|
|
21
|
+
case "image":
|
|
22
|
+
return "[image]";
|
|
23
|
+
case "file": {
|
|
24
|
+
const filePart = part;
|
|
25
|
+
if (filePart.filename || filePart.name) {
|
|
26
|
+
return `[file: ${filePart.filename || filePart.name}]`;
|
|
27
|
+
}
|
|
28
|
+
return "[file]";
|
|
29
|
+
}
|
|
30
|
+
case "reasoning": {
|
|
31
|
+
const reasoningPart = part;
|
|
32
|
+
if (typeof reasoningPart.text === "string" && reasoningPart.text.length > 0) {
|
|
33
|
+
return `[reasoning: ${reasoningPart.text.substring(0, 100)}${reasoningPart.text.length > 100 ? "..." : ""}]`;
|
|
34
|
+
}
|
|
35
|
+
return "[reasoning]";
|
|
36
|
+
}
|
|
37
|
+
case "tool-call":
|
|
38
|
+
return null;
|
|
39
|
+
case "tool-result":
|
|
40
|
+
return null;
|
|
41
|
+
default: {
|
|
42
|
+
const unknownPart = part;
|
|
43
|
+
if (typeof unknownPart.text === "string") {
|
|
44
|
+
return unknownPart.text;
|
|
45
|
+
}
|
|
46
|
+
if (typeof unknownPart.content === "string") {
|
|
47
|
+
return unknownPart.content;
|
|
48
|
+
}
|
|
49
|
+
return `[${unknownPart.type || "unknown"}]`;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function serializeToolResult(resultData) {
|
|
54
|
+
if (typeof resultData === "string") {
|
|
55
|
+
return resultData;
|
|
56
|
+
}
|
|
57
|
+
if (resultData && typeof resultData === "object" && "value" in resultData) {
|
|
58
|
+
const valueData = resultData.value;
|
|
59
|
+
return typeof valueData === "string" ? valueData : JSON.stringify(valueData);
|
|
60
|
+
}
|
|
61
|
+
if (resultData === void 0 || resultData === null) {
|
|
62
|
+
return "";
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
return JSON.stringify(resultData);
|
|
66
|
+
} catch {
|
|
67
|
+
return "[unserializable result]";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function convertAISDKMessage(message) {
|
|
71
|
+
if (!message || typeof message !== "object") {
|
|
72
|
+
return message;
|
|
73
|
+
}
|
|
74
|
+
const { role, content, ...rest } = message;
|
|
75
|
+
if (typeof content === "string") {
|
|
76
|
+
return message;
|
|
77
|
+
}
|
|
78
|
+
if (Array.isArray(content)) {
|
|
79
|
+
if (content.length === 0) {
|
|
80
|
+
return { role, content: "", ...rest };
|
|
81
|
+
}
|
|
82
|
+
if (role === "user" || role === "system") {
|
|
83
|
+
const contentParts = content.map((part) => convertContentPart(part)).filter(Boolean);
|
|
84
|
+
return {
|
|
85
|
+
role,
|
|
86
|
+
content: contentParts.length > 0 ? contentParts.join("\n") : "",
|
|
87
|
+
...rest
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
if (role === "assistant") {
|
|
91
|
+
const contentParts = content.filter((part) => part?.type !== "tool-call").map((part) => convertContentPart(part)).filter(Boolean);
|
|
92
|
+
const toolCallParts = content.filter((part) => part?.type === "tool-call");
|
|
93
|
+
const result = {
|
|
94
|
+
role,
|
|
95
|
+
content: contentParts.length > 0 ? contentParts.join("\n") : "",
|
|
96
|
+
...rest
|
|
97
|
+
};
|
|
98
|
+
if (toolCallParts.length > 0) {
|
|
99
|
+
result.tool_calls = toolCallParts.map((tc) => {
|
|
100
|
+
const toolCall = tc;
|
|
101
|
+
const toolCallId = toolCall.toolCallId;
|
|
102
|
+
const toolName = toolCall.toolName;
|
|
103
|
+
const args = toolCall.args ?? toolCall.input;
|
|
104
|
+
let argsString;
|
|
105
|
+
if (typeof args === "string") {
|
|
106
|
+
argsString = args;
|
|
107
|
+
} else if (args !== void 0 && args !== null) {
|
|
108
|
+
argsString = JSON.stringify(args);
|
|
109
|
+
} else {
|
|
110
|
+
argsString = "{}";
|
|
111
|
+
}
|
|
112
|
+
return {
|
|
113
|
+
id: toolCallId,
|
|
114
|
+
type: "function",
|
|
115
|
+
function: {
|
|
116
|
+
name: toolName,
|
|
117
|
+
arguments: argsString
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
}
|
|
124
|
+
if (role === "tool") {
|
|
125
|
+
const toolResult = content.find((part) => part?.type === "tool-result");
|
|
126
|
+
if (toolResult) {
|
|
127
|
+
const resultData = toolResult.output ?? toolResult.result;
|
|
128
|
+
const resultContent = serializeToolResult(resultData);
|
|
129
|
+
return {
|
|
130
|
+
role: "tool",
|
|
131
|
+
content: resultContent,
|
|
132
|
+
tool_call_id: toolResult.toolCallId
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return message;
|
|
138
|
+
}
|
|
139
|
+
|
|
10
140
|
// src/metrics.ts
|
|
11
141
|
function formatUsageMetrics(usage) {
|
|
12
142
|
const metrics = {};
|
|
@@ -31,8 +161,61 @@ function formatUsageMetrics(usage) {
|
|
|
31
161
|
return metrics;
|
|
32
162
|
}
|
|
33
163
|
|
|
164
|
+
// src/thread-reconstruction.ts
|
|
165
|
+
function reconstructThreadOutput(threadData, originalOutput) {
|
|
166
|
+
const messages = [];
|
|
167
|
+
const sortedSteps = [...threadData].sort((a, b) => a.stepIndex - b.stepIndex);
|
|
168
|
+
for (const step of sortedSteps) {
|
|
169
|
+
const sortedToolCalls = step.toolCalls ? [...step.toolCalls].sort((a, b) => {
|
|
170
|
+
if (!a.startTime || !b.startTime) return 0;
|
|
171
|
+
return a.startTime.getTime() - b.startTime.getTime();
|
|
172
|
+
}) : [];
|
|
173
|
+
if (sortedToolCalls.length > 0) {
|
|
174
|
+
messages.push({
|
|
175
|
+
role: "assistant",
|
|
176
|
+
content: step.text || "",
|
|
177
|
+
tool_calls: sortedToolCalls.map((tc) => {
|
|
178
|
+
const cleanArgs = tc.args && typeof tc.args === "object" && !Array.isArray(tc.args) ? removeNullish(tc.args) : tc.args;
|
|
179
|
+
return {
|
|
180
|
+
id: tc.toolCallId,
|
|
181
|
+
type: "function",
|
|
182
|
+
function: {
|
|
183
|
+
name: tc.toolName,
|
|
184
|
+
arguments: typeof cleanArgs === "string" ? cleanArgs : JSON.stringify(cleanArgs)
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
})
|
|
188
|
+
});
|
|
189
|
+
for (const tc of sortedToolCalls) {
|
|
190
|
+
if (tc.result !== void 0) {
|
|
191
|
+
messages.push({
|
|
192
|
+
role: "tool",
|
|
193
|
+
content: typeof tc.result === "string" ? tc.result : JSON.stringify(tc.result),
|
|
194
|
+
tool_call_id: tc.toolCallId
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} else if (step.text) {
|
|
199
|
+
messages.push({
|
|
200
|
+
role: "assistant",
|
|
201
|
+
content: step.text
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
if (messages.length > 0) {
|
|
206
|
+
const lastMessage = messages[messages.length - 1];
|
|
207
|
+
const originalText = originalOutput?.text;
|
|
208
|
+
if (originalText && lastMessage.role === "tool") {
|
|
209
|
+
messages.push({
|
|
210
|
+
role: "assistant",
|
|
211
|
+
content: originalText
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return messages;
|
|
216
|
+
}
|
|
217
|
+
|
|
34
218
|
// src/tracing.ts
|
|
35
|
-
var MASTRA_TRACE_ID_METADATA_KEY = "mastra-trace-id";
|
|
36
219
|
var DEFAULT_SPAN_TYPE = "task";
|
|
37
220
|
var SPAN_TYPE_EXCEPTIONS = {
|
|
38
221
|
[observability$1.SpanType.MODEL_GENERATION]: "llm",
|
|
@@ -44,246 +227,250 @@ var SPAN_TYPE_EXCEPTIONS = {
|
|
|
44
227
|
function mapSpanType(spanType) {
|
|
45
228
|
return SPAN_TYPE_EXCEPTIONS[spanType] ?? DEFAULT_SPAN_TYPE;
|
|
46
229
|
}
|
|
47
|
-
var BraintrustExporter = class extends observability.
|
|
230
|
+
var BraintrustExporter = class extends observability.TrackingExporter {
|
|
48
231
|
name = "braintrust";
|
|
49
|
-
traceMap = /* @__PURE__ */ new Map();
|
|
50
|
-
config;
|
|
51
232
|
// Flags and logger for context-aware mode
|
|
52
|
-
useProvidedLogger;
|
|
53
|
-
providedLogger;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
233
|
+
#useProvidedLogger;
|
|
234
|
+
#providedLogger;
|
|
235
|
+
#localLogger;
|
|
236
|
+
constructor(config = {}) {
|
|
237
|
+
const resolvedApiKey = config.apiKey ?? process.env.BRAINTRUST_API_KEY;
|
|
238
|
+
const resolvedEndpoint = config.endpoint ?? process.env.BRAINTRUST_ENDPOINT;
|
|
239
|
+
super({
|
|
240
|
+
...config,
|
|
241
|
+
apiKey: resolvedApiKey,
|
|
242
|
+
endpoint: resolvedEndpoint
|
|
243
|
+
});
|
|
244
|
+
this.#useProvidedLogger = !!config.braintrustLogger;
|
|
245
|
+
if (this.#useProvidedLogger) {
|
|
246
|
+
this.#providedLogger = config.braintrustLogger;
|
|
60
247
|
} else {
|
|
61
|
-
if (!config.apiKey) {
|
|
62
|
-
this.setDisabled(
|
|
63
|
-
|
|
64
|
-
|
|
248
|
+
if (!this.config.apiKey) {
|
|
249
|
+
this.setDisabled(
|
|
250
|
+
`Missing required API key. Set BRAINTRUST_API_KEY environment variable or pass apiKey in config.`
|
|
251
|
+
);
|
|
65
252
|
return;
|
|
66
253
|
}
|
|
67
|
-
this
|
|
68
|
-
this.config = config;
|
|
254
|
+
this.#localLogger = void 0;
|
|
69
255
|
}
|
|
70
256
|
}
|
|
71
|
-
async
|
|
72
|
-
if (
|
|
73
|
-
|
|
74
|
-
return;
|
|
257
|
+
async getLocalLogger() {
|
|
258
|
+
if (this.#localLogger) {
|
|
259
|
+
return this.#localLogger;
|
|
75
260
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
261
|
+
try {
|
|
262
|
+
const logger = await braintrust.initLogger({
|
|
263
|
+
projectName: this.config.projectName ?? "mastra-tracing",
|
|
264
|
+
apiKey: this.config.apiKey,
|
|
265
|
+
appUrl: this.config.endpoint,
|
|
266
|
+
...this.config.tuningParameters
|
|
267
|
+
});
|
|
268
|
+
this.#localLogger = logger;
|
|
269
|
+
return logger;
|
|
270
|
+
} catch (err) {
|
|
271
|
+
this.logger.error("Braintrust exporter: Failed to initialize logger", { error: err });
|
|
272
|
+
this.setDisabled("Failed to initialize Braintrust logger");
|
|
86
273
|
}
|
|
87
274
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
if (this.useProvidedLogger) {
|
|
91
|
-
await this.initLoggerOrUseContext(span);
|
|
92
|
-
} else {
|
|
93
|
-
await this.initLoggerPerTrace(span);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
const method = "handleSpanStarted";
|
|
97
|
-
const spanData = this.getSpanData({ span, method });
|
|
98
|
-
if (!spanData) {
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
if (!span.isEvent) {
|
|
102
|
-
spanData.activeIds.add(span.id);
|
|
103
|
-
}
|
|
104
|
-
const braintrustParent = this.getBraintrustParent({ spanData, span, method });
|
|
105
|
-
if (!braintrustParent) {
|
|
106
|
-
return;
|
|
107
|
-
}
|
|
275
|
+
startSpan(args) {
|
|
276
|
+
const { parent, span } = args;
|
|
108
277
|
const payload = this.buildSpanPayload(span);
|
|
109
|
-
const braintrustSpan =
|
|
278
|
+
const braintrustSpan = parent.startSpan({
|
|
110
279
|
spanId: span.id,
|
|
111
280
|
name: span.name,
|
|
112
281
|
type: mapSpanType(span.type),
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
...span.isRootSpan && span.tags?.length ? { tags: span.tags } : {}
|
|
282
|
+
startTime: span.startTime.getTime() / 1e3,
|
|
283
|
+
event: {
|
|
284
|
+
id: span.id,
|
|
285
|
+
// Use Mastra span ID as Braintrust row ID for logFeedback() compatibility
|
|
286
|
+
...payload
|
|
287
|
+
}
|
|
120
288
|
});
|
|
121
|
-
|
|
289
|
+
const isModelGeneration = span.type === observability$1.SpanType.MODEL_GENERATION;
|
|
290
|
+
return {
|
|
291
|
+
span: braintrustSpan,
|
|
292
|
+
spanType: span.type,
|
|
293
|
+
threadData: isModelGeneration ? [] : void 0,
|
|
294
|
+
pendingToolResults: isModelGeneration ? /* @__PURE__ */ new Map() : void 0
|
|
295
|
+
};
|
|
122
296
|
}
|
|
123
|
-
async
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
const braintrustSpan = spanData.spans.get(span.id);
|
|
130
|
-
if (!braintrustSpan) {
|
|
131
|
-
this.logger.warn("Braintrust exporter: No Braintrust span found for span update/end", {
|
|
132
|
-
traceId: span.traceId,
|
|
133
|
-
spanId: span.id,
|
|
134
|
-
spanName: span.name,
|
|
135
|
-
spanType: span.type,
|
|
136
|
-
isRootSpan: span.isRootSpan,
|
|
137
|
-
parentSpanId: span.parentSpanId,
|
|
138
|
-
method
|
|
139
|
-
});
|
|
140
|
-
return;
|
|
141
|
-
}
|
|
142
|
-
braintrustSpan.log(this.buildSpanPayload(span));
|
|
143
|
-
if (isEnd) {
|
|
144
|
-
if (span.endTime) {
|
|
145
|
-
braintrustSpan.end({ endTime: span.endTime.getTime() / 1e3 });
|
|
297
|
+
async _buildRoot(_args) {
|
|
298
|
+
if (this.#useProvidedLogger) {
|
|
299
|
+
const externalSpan = braintrust.currentSpan();
|
|
300
|
+
if (externalSpan && externalSpan.id) {
|
|
301
|
+
return externalSpan;
|
|
146
302
|
} else {
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
if (!span.isEvent) {
|
|
150
|
-
spanData.activeIds.delete(span.id);
|
|
151
|
-
}
|
|
152
|
-
if (spanData.activeIds.size === 0 && !spanData.isExternal) {
|
|
153
|
-
this.traceMap.delete(span.traceId);
|
|
303
|
+
return this.#providedLogger;
|
|
154
304
|
}
|
|
305
|
+
} else {
|
|
306
|
+
return this.getLocalLogger();
|
|
155
307
|
}
|
|
156
308
|
}
|
|
157
|
-
async
|
|
309
|
+
async _buildSpan(args) {
|
|
310
|
+
const { span, traceData } = args;
|
|
158
311
|
if (span.isRootSpan) {
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
if (
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
await this.initLoggerPerTrace(span);
|
|
312
|
+
const root = traceData.getRoot();
|
|
313
|
+
if (root) {
|
|
314
|
+
return this.startSpan({ parent: root, span });
|
|
315
|
+
}
|
|
316
|
+
} else {
|
|
317
|
+
const parent = traceData.getParent(args);
|
|
318
|
+
if (parent) {
|
|
319
|
+
const parentSpan = "span" in parent ? parent.span : parent;
|
|
320
|
+
return this.startSpan({ parent: parentSpan, span });
|
|
169
321
|
}
|
|
170
322
|
}
|
|
171
|
-
|
|
172
|
-
|
|
323
|
+
}
|
|
324
|
+
async _buildEvent(args) {
|
|
325
|
+
const spanData = await this._buildSpan(args);
|
|
173
326
|
if (!spanData) {
|
|
174
327
|
return;
|
|
175
328
|
}
|
|
176
|
-
|
|
177
|
-
|
|
329
|
+
spanData.span.end({ endTime: args.span.startTime.getTime() / 1e3 });
|
|
330
|
+
return spanData.span;
|
|
331
|
+
}
|
|
332
|
+
async _updateSpan(args) {
|
|
333
|
+
const { span, traceData } = args;
|
|
334
|
+
const spanData = traceData.getSpan({ spanId: span.id });
|
|
335
|
+
if (!spanData) {
|
|
178
336
|
return;
|
|
179
337
|
}
|
|
180
|
-
|
|
181
|
-
const braintrustSpan = braintrustParent.startSpan({
|
|
182
|
-
spanId: span.id,
|
|
183
|
-
name: span.name,
|
|
184
|
-
type: mapSpanType(span.type),
|
|
185
|
-
startTime: span.startTime.getTime() / 1e3,
|
|
186
|
-
...payload
|
|
187
|
-
});
|
|
188
|
-
braintrustSpan.end({ endTime: span.startTime.getTime() / 1e3 });
|
|
338
|
+
spanData.span.log(this.buildSpanPayload(span, false));
|
|
189
339
|
}
|
|
190
|
-
|
|
191
|
-
const {
|
|
192
|
-
|
|
193
|
-
|
|
340
|
+
async _finishSpan(args) {
|
|
341
|
+
const { span, traceData } = args;
|
|
342
|
+
const spanData = traceData.getSpan({ spanId: span.id });
|
|
343
|
+
if (!spanData) {
|
|
194
344
|
return;
|
|
195
345
|
}
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
346
|
+
if (span.type === observability$1.SpanType.MODEL_STEP) {
|
|
347
|
+
this.accumulateModelStepData(span, traceData);
|
|
348
|
+
} else if (span.type === observability$1.SpanType.TOOL_CALL) {
|
|
349
|
+
this.accumulateToolCallResult(span, traceData);
|
|
350
|
+
}
|
|
351
|
+
const payload = span.type === observability$1.SpanType.MODEL_GENERATION ? this.buildModelGenerationPayload(span, spanData) : this.buildSpanPayload(span, false);
|
|
352
|
+
spanData.span.log(payload);
|
|
353
|
+
if (span.endTime) {
|
|
354
|
+
spanData.span.end({ endTime: span.endTime.getTime() / 1e3 });
|
|
355
|
+
} else {
|
|
356
|
+
spanData.span.end();
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async _abortSpan(args) {
|
|
360
|
+
const { span: spanData, reason } = args;
|
|
361
|
+
spanData.span.log({
|
|
362
|
+
error: reason.message,
|
|
363
|
+
metadata: { errorDetails: reason }
|
|
201
364
|
});
|
|
365
|
+
spanData.span.end();
|
|
202
366
|
}
|
|
367
|
+
// ==============================================================================
|
|
368
|
+
// Thread view reconstruction helpers
|
|
369
|
+
// ==============================================================================
|
|
203
370
|
/**
|
|
204
|
-
*
|
|
371
|
+
* Walk up the tree to find the MODEL_GENERATION ancestor span.
|
|
372
|
+
* Returns the BraintrustSpanData if found, undefined otherwise.
|
|
205
373
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
...this.config.tuningParameters
|
|
217
|
-
});
|
|
218
|
-
this.initTraceMap({ logger: loggerInstance, isExternal: false, traceId: span.traceId });
|
|
219
|
-
} catch (err) {
|
|
220
|
-
this.logger.error("Braintrust exporter: Failed to initialize logger", { error: err, traceId: span.traceId });
|
|
221
|
-
this.setDisabled("Failed to initialize Braintrust logger");
|
|
374
|
+
findModelGenerationAncestor(spanId, traceData) {
|
|
375
|
+
let currentId = spanId;
|
|
376
|
+
while (currentId) {
|
|
377
|
+
const parentId = traceData.getParentId({ spanId: currentId });
|
|
378
|
+
if (!parentId) return void 0;
|
|
379
|
+
const parentSpanData = traceData.getSpan({ spanId: parentId });
|
|
380
|
+
if (parentSpanData?.spanType === observability$1.SpanType.MODEL_GENERATION) {
|
|
381
|
+
return parentSpanData;
|
|
382
|
+
}
|
|
383
|
+
currentId = parentId;
|
|
222
384
|
}
|
|
385
|
+
return void 0;
|
|
223
386
|
}
|
|
224
387
|
/**
|
|
225
|
-
*
|
|
226
|
-
*
|
|
227
|
-
* Otherwise, uses the provided logger instance.
|
|
388
|
+
* Accumulate MODEL_STEP data to the parent MODEL_GENERATION's threadData.
|
|
389
|
+
* Called when a MODEL_STEP span ends.
|
|
228
390
|
*/
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
391
|
+
accumulateModelStepData(span, traceData) {
|
|
392
|
+
const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
|
|
393
|
+
if (!modelGenSpanData?.threadData) {
|
|
232
394
|
return;
|
|
233
395
|
}
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
396
|
+
const output = span.output;
|
|
397
|
+
const attributes = span.attributes;
|
|
398
|
+
const stepData = {
|
|
399
|
+
stepSpanId: span.id,
|
|
400
|
+
stepIndex: attributes?.stepIndex ?? 0,
|
|
401
|
+
text: output?.text,
|
|
402
|
+
toolCalls: output?.toolCalls?.map((tc) => ({
|
|
403
|
+
toolCallId: tc.toolCallId,
|
|
404
|
+
toolName: tc.toolName,
|
|
405
|
+
args: tc.args
|
|
406
|
+
}))
|
|
407
|
+
};
|
|
408
|
+
modelGenSpanData.threadData.push(stepData);
|
|
240
409
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
410
|
+
/**
|
|
411
|
+
* Store TOOL_CALL result in parent MODEL_GENERATION's pendingToolResults.
|
|
412
|
+
* Called when a TOOL_CALL span ends.
|
|
413
|
+
* Results are merged into threadData when MODEL_GENERATION ends.
|
|
414
|
+
*/
|
|
415
|
+
accumulateToolCallResult(span, traceData) {
|
|
416
|
+
const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
|
|
417
|
+
if (!modelGenSpanData?.pendingToolResults) {
|
|
418
|
+
return;
|
|
245
419
|
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
420
|
+
const input = span.input;
|
|
421
|
+
const toolCallId = input?.toolCallId;
|
|
422
|
+
if (!toolCallId) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
modelGenSpanData.pendingToolResults.set(toolCallId, {
|
|
426
|
+
result: span.output,
|
|
427
|
+
startTime: span.startTime
|
|
254
428
|
});
|
|
255
429
|
}
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
430
|
+
/**
|
|
431
|
+
* Build the payload for MODEL_GENERATION span, reconstructing output from threadData if available.
|
|
432
|
+
*/
|
|
433
|
+
buildModelGenerationPayload(span, spanData) {
|
|
434
|
+
const basePayload = this.buildSpanPayload(span, false);
|
|
435
|
+
const threadData = spanData.threadData;
|
|
436
|
+
if (!threadData || threadData.length === 0) {
|
|
437
|
+
return basePayload;
|
|
261
438
|
}
|
|
262
|
-
if (spanData.
|
|
263
|
-
|
|
439
|
+
if (spanData.pendingToolResults && spanData.pendingToolResults.size > 0) {
|
|
440
|
+
for (const step of threadData) {
|
|
441
|
+
if (step.toolCalls) {
|
|
442
|
+
for (const toolCall of step.toolCalls) {
|
|
443
|
+
const pendingResult = spanData.pendingToolResults.get(toolCall.toolCallId);
|
|
444
|
+
if (pendingResult) {
|
|
445
|
+
toolCall.result = pendingResult.result;
|
|
446
|
+
toolCall.startTime = pendingResult.startTime;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
264
451
|
}
|
|
265
|
-
|
|
266
|
-
|
|
452
|
+
const hasToolCalls = threadData.some((step) => step.toolCalls && step.toolCalls.length > 0);
|
|
453
|
+
if (!hasToolCalls) {
|
|
454
|
+
return basePayload;
|
|
267
455
|
}
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
isRootSpan: span.isRootSpan,
|
|
274
|
-
parentSpanId: span.parentSpanId,
|
|
275
|
-
method
|
|
276
|
-
});
|
|
456
|
+
const reconstructedOutput = reconstructThreadOutput(threadData, span.output);
|
|
457
|
+
return {
|
|
458
|
+
...basePayload,
|
|
459
|
+
output: reconstructedOutput
|
|
460
|
+
};
|
|
277
461
|
}
|
|
278
462
|
/**
|
|
279
463
|
* Transforms MODEL_GENERATION input to Braintrust Thread view format.
|
|
464
|
+
* Converts AI SDK messages (v4/v5) to OpenAI Chat Completion format, which Braintrust requires
|
|
465
|
+
* for proper rendering of threads (fixes #11023).
|
|
280
466
|
*/
|
|
281
467
|
transformInput(input, spanType) {
|
|
282
468
|
if (spanType === observability$1.SpanType.MODEL_GENERATION) {
|
|
283
|
-
if (
|
|
284
|
-
return input.
|
|
285
|
-
}
|
|
286
|
-
|
|
469
|
+
if (Array.isArray(input)) {
|
|
470
|
+
return input.map((msg) => convertAISDKMessage(msg));
|
|
471
|
+
}
|
|
472
|
+
if (input && typeof input === "object" && "messages" in input && Array.isArray(input.messages)) {
|
|
473
|
+
return input.messages.map((msg) => convertAISDKMessage(msg));
|
|
287
474
|
}
|
|
288
475
|
}
|
|
289
476
|
return input;
|
|
@@ -293,12 +480,15 @@ var BraintrustExporter = class extends observability.BaseExporter {
|
|
|
293
480
|
*/
|
|
294
481
|
transformOutput(output, spanType) {
|
|
295
482
|
if (spanType === observability$1.SpanType.MODEL_GENERATION) {
|
|
483
|
+
if (!output || typeof output !== "object") {
|
|
484
|
+
return output;
|
|
485
|
+
}
|
|
296
486
|
const { text, ...rest } = output;
|
|
297
|
-
return { role: "assistant", content: text, ...rest };
|
|
487
|
+
return { role: "assistant", content: text, ...removeNullish(rest) };
|
|
298
488
|
}
|
|
299
489
|
return output;
|
|
300
490
|
}
|
|
301
|
-
buildSpanPayload(span) {
|
|
491
|
+
buildSpanPayload(span, isCreate = true) {
|
|
302
492
|
const payload = {};
|
|
303
493
|
if (span.input !== void 0) {
|
|
304
494
|
payload.input = this.transformInput(span.input, span.type);
|
|
@@ -306,11 +496,17 @@ var BraintrustExporter = class extends observability.BaseExporter {
|
|
|
306
496
|
if (span.output !== void 0) {
|
|
307
497
|
payload.output = this.transformOutput(span.output, span.type);
|
|
308
498
|
}
|
|
499
|
+
if (isCreate && span.isRootSpan && span.tags?.length) {
|
|
500
|
+
payload.tags = span.tags;
|
|
501
|
+
}
|
|
309
502
|
payload.metrics = {};
|
|
310
503
|
payload.metadata = {
|
|
311
|
-
|
|
312
|
-
|
|
504
|
+
...span.metadata,
|
|
505
|
+
spanType: span.type
|
|
313
506
|
};
|
|
507
|
+
if (isCreate) {
|
|
508
|
+
payload.metadata["mastra-trace-id"] = span.traceId;
|
|
509
|
+
}
|
|
314
510
|
const attributes = span.attributes ?? {};
|
|
315
511
|
if (span.type === observability$1.SpanType.MODEL_GENERATION) {
|
|
316
512
|
const modelAttr = attributes;
|
|
@@ -345,20 +541,9 @@ var BraintrustExporter = class extends observability.BaseExporter {
|
|
|
345
541
|
if (Object.keys(payload.metrics).length === 0) {
|
|
346
542
|
delete payload.metrics;
|
|
347
543
|
}
|
|
544
|
+
payload.metadata = removeNullish(payload.metadata);
|
|
348
545
|
return payload;
|
|
349
546
|
}
|
|
350
|
-
async shutdown() {
|
|
351
|
-
if (!this.config) {
|
|
352
|
-
return;
|
|
353
|
-
}
|
|
354
|
-
for (const [_traceId, spanData] of this.traceMap) {
|
|
355
|
-
for (const [_spanId, span] of spanData.spans) {
|
|
356
|
-
span.end();
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
this.traceMap.clear();
|
|
360
|
-
await super.shutdown();
|
|
361
|
-
}
|
|
362
547
|
};
|
|
363
548
|
|
|
364
549
|
exports.BraintrustExporter = BraintrustExporter;
|