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