@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/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 { BaseExporter } from '@mastra/observability';
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 BaseExporter {
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
- super(config);
54
- if (config.braintrustLogger) {
55
- this.useProvidedLogger = true;
56
- this.providedLogger = config.braintrustLogger;
57
- this.config = config;
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
- const apiKey = config.apiKey ?? process.env.BRAINTRUST_API_KEY;
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.useProvidedLogger = false;
70
- this.config = {
71
- ...config,
72
- apiKey,
73
- endpoint
74
- };
252
+ this.#localLogger = void 0;
75
253
  }
76
254
  }
77
- async _exportTracingEvent(event) {
78
- if (event.exportedSpan.isEvent) {
79
- await this.handleEventSpan(event.exportedSpan);
80
- return;
255
+ async getLocalLogger() {
256
+ if (this.#localLogger) {
257
+ return this.#localLogger;
81
258
  }
82
- switch (event.type) {
83
- case "span_started":
84
- await this.handleSpanStarted(event.exportedSpan);
85
- break;
86
- case "span_updated":
87
- await this.handleSpanUpdateOrEnd(event.exportedSpan, false);
88
- break;
89
- case "span_ended":
90
- await this.handleSpanUpdateOrEnd(event.exportedSpan, true);
91
- break;
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
- async handleSpanStarted(span) {
95
- if (span.isRootSpan) {
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 = braintrustParent.startSpan({
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
- braintrustSpan.log({
122
- metadata: {
123
- [MASTRA_TRACE_ID_METADATA_KEY]: span.traceId
124
- },
125
- ...span.isRootSpan && span.tags?.length ? { tags: span.tags } : {}
126
- });
127
- spanData.spans.set(span.id, braintrustSpan);
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 handleSpanUpdateOrEnd(span, isEnd) {
130
- const method = isEnd ? "handleSpanEnd" : "handleSpanUpdate";
131
- const spanData = this.getSpanData({ span, method });
132
- if (!spanData) {
133
- return;
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
- braintrustSpan.end();
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 handleEventSpan(span) {
305
+ async _buildSpan(args) {
306
+ const { span, traceData } = args;
164
307
  if (span.isRootSpan) {
165
- this.logger.debug("Braintrust exporter: Creating logger for event", {
166
- traceId: span.traceId,
167
- spanId: span.id,
168
- spanName: span.name,
169
- method: "handleEventSpan"
170
- });
171
- if (this.useProvidedLogger) {
172
- await this.initLoggerOrUseContext(span);
173
- } else {
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
- const method = "handleEventSpan";
178
- const spanData = this.getSpanData({ span, method });
319
+ }
320
+ async _buildEvent(args) {
321
+ const spanData = await this._buildSpan(args);
179
322
  if (!spanData) {
180
323
  return;
181
324
  }
182
- const braintrustParent = this.getBraintrustParent({ spanData, span, method });
183
- if (!braintrustParent) {
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
- initTraceMap(params) {
197
- const { traceId, isExternal, logger } = params;
198
- if (this.traceMap.has(traceId)) {
199
- this.logger.debug("Braintrust exporter: Reusing existing trace from local map", { traceId });
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
- this.traceMap.set(traceId, {
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
- * Creates a new logger per trace using config credentials
211
- */
212
- async initLoggerPerTrace(span) {
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 (!this.config) {
218
- return;
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
- try {
221
- const loggerInstance = await initLogger({
222
- projectName: this.config.projectName ?? "mastra-tracing",
223
- apiKey: this.config.apiKey,
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
- this.initTraceMap({ logger: this.providedLogger, isExternal: false, traceId: span.traceId });
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
- getBraintrustParent(options) {
266
- const { spanData, span, method } = options;
267
- const parentId = span.parentSpanId;
268
- if (!parentId) {
269
- return spanData.logger;
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
- * Converts AI SDK message format to OpenAI Chat Completion format for Braintrust.
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
- convertAISDKMessage(message) {
305
- if (!message || typeof message !== "object") {
306
- return message;
307
- }
308
- const { role, content, ...rest } = message;
309
- if (typeof content === "string") {
310
- return message;
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 message;
381
+ return void 0;
371
382
  }
372
383
  /**
373
- * Converts a content part to a string representation.
374
- * Handles text, image, file, reasoning, and other content types.
384
+ * Accumulate MODEL_STEP data to the parent MODEL_GENERATION's threadData.
385
+ * Called when a MODEL_STEP span ends.
375
386
  */
376
- convertContentPart(part) {
377
- if (!part || typeof part !== "object") {
378
- return null;
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
- * Serializes tool result data to a string for OpenAI format.
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
- serializeToolResult(resultData) {
419
- if (typeof resultData === "string") {
420
- return resultData;
411
+ accumulateToolCallResult(span, traceData) {
412
+ const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
413
+ if (!modelGenSpanData?.pendingToolResults) {
414
+ return;
421
415
  }
422
- if (resultData && typeof resultData === "object" && "value" in resultData) {
423
- return typeof resultData.value === "string" ? resultData.value : JSON.stringify(resultData.value);
416
+ const input = span.input;
417
+ const toolCallId = input?.toolCallId;
418
+ if (!toolCallId) {
419
+ return;
424
420
  }
425
- if (resultData === void 0 || resultData === null) {
426
- return "";
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
- try {
429
- return JSON.stringify(resultData);
430
- } catch {
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) => this.convertAISDKMessage(msg));
466
+ return input.map((msg) => convertAISDKMessage(msg));
443
467
  }
444
- if (input && Array.isArray(input.messages)) {
445
- return input.messages.map((msg) => this.convertAISDKMessage(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
- spanType: span.type,
471
- ...span.metadata
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 };