@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.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.BaseExporter {
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
- super(config);
56
- if (config.braintrustLogger) {
57
- this.useProvidedLogger = true;
58
- this.providedLogger = config.braintrustLogger;
59
- this.config = config;
237
+ const resolvedApiKey = config.apiKey ?? process.env.BRAINTRUST_API_KEY;
238
+ const resolvedEndpoint = config.endpoint ?? process.env.BRAINTRUST_ENDPOINT;
239
+ super({
240
+ ...config,
241
+ apiKey: resolvedApiKey,
242
+ endpoint: resolvedEndpoint
243
+ });
244
+ this.#useProvidedLogger = !!config.braintrustLogger;
245
+ if (this.#useProvidedLogger) {
246
+ this.#providedLogger = config.braintrustLogger;
60
247
  } else {
61
- const apiKey = config.apiKey ?? process.env.BRAINTRUST_API_KEY;
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.useProvidedLogger = false;
72
- this.config = {
73
- ...config,
74
- apiKey,
75
- endpoint
76
- };
254
+ this.#localLogger = void 0;
77
255
  }
78
256
  }
79
- async _exportTracingEvent(event) {
80
- if (event.exportedSpan.isEvent) {
81
- await this.handleEventSpan(event.exportedSpan);
82
- return;
257
+ async getLocalLogger() {
258
+ if (this.#localLogger) {
259
+ return this.#localLogger;
83
260
  }
84
- switch (event.type) {
85
- case "span_started":
86
- await this.handleSpanStarted(event.exportedSpan);
87
- break;
88
- case "span_updated":
89
- await this.handleSpanUpdateOrEnd(event.exportedSpan, false);
90
- break;
91
- case "span_ended":
92
- await this.handleSpanUpdateOrEnd(event.exportedSpan, true);
93
- break;
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
- async handleSpanStarted(span) {
97
- if (span.isRootSpan) {
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 = braintrustParent.startSpan({
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
- braintrustSpan.log({
124
- metadata: {
125
- [MASTRA_TRACE_ID_METADATA_KEY]: span.traceId
126
- },
127
- ...span.isRootSpan && span.tags?.length ? { tags: span.tags } : {}
128
- });
129
- spanData.spans.set(span.id, braintrustSpan);
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 handleSpanUpdateOrEnd(span, isEnd) {
132
- const method = isEnd ? "handleSpanEnd" : "handleSpanUpdate";
133
- const spanData = this.getSpanData({ span, method });
134
- if (!spanData) {
135
- return;
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
- braintrustSpan.end();
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 handleEventSpan(span) {
307
+ async _buildSpan(args) {
308
+ const { span, traceData } = args;
166
309
  if (span.isRootSpan) {
167
- this.logger.debug("Braintrust exporter: Creating logger for event", {
168
- traceId: span.traceId,
169
- spanId: span.id,
170
- spanName: span.name,
171
- method: "handleEventSpan"
172
- });
173
- if (this.useProvidedLogger) {
174
- await this.initLoggerOrUseContext(span);
175
- } else {
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
- const method = "handleEventSpan";
180
- const spanData = this.getSpanData({ span, method });
321
+ }
322
+ async _buildEvent(args) {
323
+ const spanData = await this._buildSpan(args);
181
324
  if (!spanData) {
182
325
  return;
183
326
  }
184
- const braintrustParent = this.getBraintrustParent({ spanData, span, method });
185
- if (!braintrustParent) {
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
- initTraceMap(params) {
199
- const { traceId, isExternal, logger } = params;
200
- if (this.traceMap.has(traceId)) {
201
- this.logger.debug("Braintrust exporter: Reusing existing trace from local map", { traceId });
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
- this.traceMap.set(traceId, {
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
- * Creates a new logger per trace using config credentials
213
- */
214
- async initLoggerPerTrace(span) {
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 (!this.config) {
220
- return;
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
- try {
223
- const loggerInstance = await braintrust.initLogger({
224
- projectName: this.config.projectName ?? "mastra-tracing",
225
- apiKey: this.config.apiKey,
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
- this.initTraceMap({ logger: this.providedLogger, isExternal: false, traceId: span.traceId });
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
- getBraintrustParent(options) {
268
- const { spanData, span, method } = options;
269
- const parentId = span.parentSpanId;
270
- if (!parentId) {
271
- return spanData.logger;
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
- * Converts AI SDK message format to OpenAI Chat Completion format for Braintrust.
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
- convertAISDKMessage(message) {
307
- if (!message || typeof message !== "object") {
308
- return message;
309
- }
310
- const { role, content, ...rest } = message;
311
- if (typeof content === "string") {
312
- return message;
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 message;
383
+ return void 0;
373
384
  }
374
385
  /**
375
- * Converts a content part to a string representation.
376
- * Handles text, image, file, reasoning, and other content types.
386
+ * Accumulate MODEL_STEP data to the parent MODEL_GENERATION's threadData.
387
+ * Called when a MODEL_STEP span ends.
377
388
  */
378
- convertContentPart(part) {
379
- if (!part || typeof part !== "object") {
380
- return null;
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
- * Serializes tool result data to a string for OpenAI format.
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
- serializeToolResult(resultData) {
421
- if (typeof resultData === "string") {
422
- return resultData;
413
+ accumulateToolCallResult(span, traceData) {
414
+ const modelGenSpanData = this.findModelGenerationAncestor(span.id, traceData);
415
+ if (!modelGenSpanData?.pendingToolResults) {
416
+ return;
423
417
  }
424
- if (resultData && typeof resultData === "object" && "value" in resultData) {
425
- return typeof resultData.value === "string" ? resultData.value : JSON.stringify(resultData.value);
418
+ const input = span.input;
419
+ const toolCallId = input?.toolCallId;
420
+ if (!toolCallId) {
421
+ return;
426
422
  }
427
- if (resultData === void 0 || resultData === null) {
428
- return "";
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
- try {
431
- return JSON.stringify(resultData);
432
- } catch {
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) => this.convertAISDKMessage(msg));
468
+ return input.map((msg) => convertAISDKMessage(msg));
445
469
  }
446
- if (input && Array.isArray(input.messages)) {
447
- return input.messages.map((msg) => this.convertAISDKMessage(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
- spanType: span.type,
473
- ...span.metadata
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;