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