@mastra/langsmith 1.3.6-alpha.0 → 1.3.6

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,377 +1,356 @@
1
- import { SpanType } from '@mastra/core/observability';
2
- import { omitKeys } from '@mastra/core/utils';
3
- import { TrackingExporter } from '@mastra/observability';
4
- import { Client, RunTree } from 'langsmith';
5
-
6
- // src/tracing.ts
7
-
8
- // src/metrics.ts
1
+ import { SpanType } from "@mastra/core/observability";
2
+ import { omitKeys } from "@mastra/core/utils";
3
+ import { TrackingExporter } from "@mastra/observability";
4
+ import { Client, RunTree } from "langsmith";
5
+ //#region src/metrics.ts
6
+ /**
7
+ * Formats UsageStats to LangSmith's expected metric format.
8
+ */
9
9
  function formatUsageMetrics(usage) {
10
- const metrics = {};
11
- if (usage?.inputTokens !== void 0) {
12
- metrics.input_tokens = usage.inputTokens;
13
- }
14
- if (usage?.outputTokens !== void 0) {
15
- metrics.output_tokens = usage.outputTokens;
16
- }
17
- if (metrics.input_tokens !== void 0 && metrics.output_tokens !== void 0) {
18
- metrics.total_tokens = metrics.input_tokens + metrics.output_tokens;
19
- }
20
- if (usage?.outputDetails?.reasoning !== void 0) {
21
- metrics.output_token_details = {
22
- ...metrics.output_token_details ?? {},
23
- reasoning_tokens: usage.outputDetails.reasoning
24
- };
25
- }
26
- if (usage?.inputDetails?.cacheRead !== void 0) {
27
- metrics.input_token_details = {
28
- ...metrics.input_token_details ?? {},
29
- cache_read: usage.inputDetails.cacheRead
30
- };
31
- }
32
- if (usage?.inputDetails?.cacheWrite !== void 0) {
33
- metrics.input_token_details = {
34
- ...metrics.input_token_details ?? {},
35
- cache_write: usage.inputDetails.cacheWrite
36
- };
37
- }
38
- if (usage?.inputDetails?.audio !== void 0) {
39
- metrics.input_token_details = {
40
- ...metrics.input_token_details ?? {},
41
- audio: usage.inputDetails.audio
42
- };
43
- }
44
- if (usage?.outputDetails?.audio !== void 0) {
45
- metrics.output_token_details = {
46
- ...metrics.output_token_details ?? {},
47
- audio: usage.outputDetails.audio
48
- };
49
- }
50
- return metrics;
10
+ const metrics = {};
11
+ if (usage?.inputTokens !== void 0) metrics.input_tokens = usage.inputTokens;
12
+ if (usage?.outputTokens !== void 0) metrics.output_tokens = usage.outputTokens;
13
+ if (metrics.input_tokens !== void 0 && metrics.output_tokens !== void 0) metrics.total_tokens = metrics.input_tokens + metrics.output_tokens;
14
+ if (usage?.outputDetails?.reasoning !== void 0) metrics.output_token_details = {
15
+ ...metrics.output_token_details ?? {},
16
+ reasoning_tokens: usage.outputDetails.reasoning
17
+ };
18
+ if (usage?.inputDetails?.cacheRead !== void 0) metrics.input_token_details = {
19
+ ...metrics.input_token_details ?? {},
20
+ cache_read: usage.inputDetails.cacheRead
21
+ };
22
+ if (usage?.inputDetails?.cacheWrite !== void 0) metrics.input_token_details = {
23
+ ...metrics.input_token_details ?? {},
24
+ cache_write: usage.inputDetails.cacheWrite
25
+ };
26
+ if (usage?.inputDetails?.audio !== void 0) metrics.input_token_details = {
27
+ ...metrics.input_token_details ?? {},
28
+ audio: usage.inputDetails.audio
29
+ };
30
+ if (usage?.outputDetails?.audio !== void 0) metrics.output_token_details = {
31
+ ...metrics.output_token_details ?? {},
32
+ audio: usage.outputDetails.audio
33
+ };
34
+ return metrics;
51
35
  }
52
-
53
- // src/tracing.ts
54
- var DEFAULT_RUN_ID_CACHE_MAX_ENTRIES = 1e4;
55
- var DEFAULT_SPAN_TYPE = "chain";
56
- var SPAN_TYPE_EXCEPTIONS = {
57
- [SpanType.MODEL_GENERATION]: "llm",
58
- [SpanType.TOOL_CALL]: "tool",
59
- [SpanType.MCP_TOOL_CALL]: "tool",
60
- [SpanType.PROVIDER_TOOL_CALL]: "tool",
61
- [SpanType.WORKFLOW_CONDITIONAL_EVAL]: "chain",
62
- [SpanType.WORKFLOW_WAIT_EVENT]: "chain"
36
+ //#endregion
37
+ //#region src/tracing.ts
38
+ const DEFAULT_RUN_ID_CACHE_MAX_ENTRIES = 1e4;
39
+ const DEFAULT_SPAN_TYPE = "chain";
40
+ const SPAN_TYPE_EXCEPTIONS = {
41
+ [SpanType.MODEL_GENERATION]: "llm",
42
+ [SpanType.TOOL_CALL]: "tool",
43
+ [SpanType.MCP_TOOL_CALL]: "tool",
44
+ [SpanType.PROVIDER_TOOL_CALL]: "tool",
45
+ [SpanType.WORKFLOW_CONDITIONAL_EVAL]: "chain",
46
+ [SpanType.WORKFLOW_WAIT_EVENT]: "chain"
63
47
  };
64
48
  function mapSpanType(spanType) {
65
- return SPAN_TYPE_EXCEPTIONS[spanType] ?? DEFAULT_SPAN_TYPE;
49
+ return SPAN_TYPE_EXCEPTIONS[spanType] ?? DEFAULT_SPAN_TYPE;
66
50
  }
67
51
  function isKVMap(value) {
68
- return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
52
+ return value != null && typeof value === "object" && !Array.isArray(value) && !(value instanceof Date);
69
53
  }
70
54
  var LangSmithExporter = class extends TrackingExporter {
71
- name = "langsmith";
72
- #client;
73
- /**
74
- * Maps Mastra `span.id` to the runId LangSmith allocated when the corresponding
75
- * RunTree was created. LangSmith's `createFeedback` requires the LangSmith runId,
76
- * not the Mastra span id, so scores submitted via `onScoreEvent` look up the
77
- * LangSmith runId here before calling the API.
78
- *
79
- * Bounded LRU keyed by Mastra span id — entries are evicted oldest-first when
80
- * size exceeds `#runIdCacheMaxEntries` so the cache cannot grow without bound.
81
- */
82
- #langsmithRunIdBySpanId = /* @__PURE__ */ new Map();
83
- #runIdCacheMaxEntries;
84
- constructor(config = {}) {
85
- const apiKey = config.apiKey ?? process.env.LANGSMITH_API_KEY;
86
- super({
87
- ...config,
88
- apiKey
89
- });
90
- this.#runIdCacheMaxEntries = Math.max(1, config.runIdCacheMaxEntries ?? DEFAULT_RUN_ID_CACHE_MAX_ENTRIES);
91
- if (!apiKey) {
92
- this.setDisabled(`Missing required credentials (apiKey: ${!!apiKey})`);
93
- return;
94
- }
95
- this.#client = config.client ?? new Client(this.config);
96
- }
97
- /** Look up the LangSmith runId for a Mastra span id, refreshing its LRU position on hit. */
98
- #getLangsmithRunId(spanId) {
99
- const runId = this.#langsmithRunIdBySpanId.get(spanId);
100
- if (runId === void 0) return void 0;
101
- this.#langsmithRunIdBySpanId.delete(spanId);
102
- this.#langsmithRunIdBySpanId.set(spanId, runId);
103
- return runId;
104
- }
105
- /** Remember the LangSmith runId for a Mastra span id, evicting oldest entries when full. */
106
- #rememberLangsmithRunId(spanId, runId) {
107
- if (this.#langsmithRunIdBySpanId.has(spanId)) {
108
- this.#langsmithRunIdBySpanId.delete(spanId);
109
- }
110
- this.#langsmithRunIdBySpanId.set(spanId, runId);
111
- while (this.#langsmithRunIdBySpanId.size > this.#runIdCacheMaxEntries) {
112
- const oldest = this.#langsmithRunIdBySpanId.keys().next().value;
113
- if (oldest === void 0) break;
114
- this.#langsmithRunIdBySpanId.delete(oldest);
115
- }
116
- }
117
- /**
118
- * Flush pending trace batches to LangSmith.
119
- * The LangSmith Client internally batches API calls; this ensures
120
- * all queued runs are sent before the process exits or the flush
121
- * caller continues.
122
- */
123
- async _flush() {
124
- if (this.#client) {
125
- await this.#client.awaitPendingTraceBatches();
126
- }
127
- }
128
- async _postShutdown() {
129
- this.#langsmithRunIdBySpanId.clear();
130
- }
131
- async onScoreEvent(event) {
132
- if (!this.#client) return;
133
- const { score } = event;
134
- if (!score.spanId) {
135
- this.logger.warn("LangSmith exporter: dropping score with no spanId; trace-level scoring is not yet supported", {
136
- scorerId: score.scorerId
137
- });
138
- return;
139
- }
140
- const langsmithRunId = this.#getLangsmithRunId(score.spanId);
141
- if (!langsmithRunId) {
142
- this.logger.warn(
143
- "LangSmith exporter: dropping score for a span that was not previously emitted to LangSmith (span_started must be processed before submitting a score for it)",
144
- {
145
- traceId: score.traceId,
146
- spanId: score.spanId,
147
- scorerId: score.scorerId
148
- }
149
- );
150
- return;
151
- }
152
- const key = score.scorerName ?? score.scorerId;
153
- try {
154
- await this.#client.createFeedback(langsmithRunId, key, {
155
- score: score.score,
156
- ...score.reason ? { comment: score.reason } : {},
157
- feedbackId: score.scoreId,
158
- ...score.scoreTraceId ? { sourceRunId: score.scoreTraceId } : {},
159
- sourceInfo: {
160
- // User-supplied metadata is spread first so the authoritative reserved
161
- // fields below cannot be overwritten by `scorerId` / `scoreSource` keys
162
- // a caller may have set inside metadata.
163
- ...score.metadata ?? {},
164
- scorerId: score.scorerId,
165
- ...score.scoreSource ? { scoreSource: score.scoreSource } : {}
166
- }
167
- });
168
- } catch (err) {
169
- this.logger.error("LangSmith exporter: Failed to submit feedback", {
170
- error: err,
171
- traceId: score.traceId,
172
- spanId: score.spanId,
173
- scorerId: score.scorerId
174
- });
175
- }
176
- }
177
- skipBuildRootTask = true;
178
- async _buildRoot(_args) {
179
- throw new Error("Method not implemented.");
180
- }
181
- async _buildSpan(args) {
182
- const { span, traceData } = args;
183
- const parent = span.isRootSpan ? void 0 : traceData.getParent(args);
184
- if (!span.isRootSpan && !parent) {
185
- return;
186
- }
187
- const payload = {
188
- name: span.name,
189
- ...this.buildRunTreePayload(span, traceData, true)
190
- };
191
- const langSmithSpan = span.isRootSpan ? new RunTree(payload) : parent.createChild(payload);
192
- if (langSmithSpan.id) {
193
- this.#rememberLangsmithRunId(span.id, langSmithSpan.id);
194
- }
195
- await langSmithSpan.postRun();
196
- return langSmithSpan;
197
- }
198
- async _buildEvent(args) {
199
- const langSmithSpan = await this._buildSpan(args);
200
- if (!langSmithSpan) {
201
- return;
202
- }
203
- await langSmithSpan.end({ endTime: args.span.startTime.getTime() });
204
- await langSmithSpan.patchRun();
205
- return langSmithSpan;
206
- }
207
- async _updateSpan(args) {
208
- await this.handleSpanUpdateOrEnd({ ...args, isEnd: false });
209
- }
210
- async _finishSpan(args) {
211
- await this.handleSpanUpdateOrEnd({ ...args, isEnd: true });
212
- }
213
- async _abortSpan(args) {
214
- const { span, reason } = args;
215
- span.error = reason.message;
216
- span.metadata = {
217
- ...span.metadata,
218
- errorDetails: reason
219
- };
220
- await span.end();
221
- await span.patchRun();
222
- }
223
- async handleSpanUpdateOrEnd(args) {
224
- const { span, traceData, isEnd } = args;
225
- const langSmithSpan = traceData.getSpan({ spanId: span.id });
226
- if (!langSmithSpan) {
227
- return;
228
- }
229
- const updatePayload = this.buildRunTreePayload(span, traceData);
230
- langSmithSpan.metadata = {
231
- ...langSmithSpan.metadata,
232
- ...updatePayload.metadata
233
- };
234
- if (updatePayload.inputs != null) {
235
- langSmithSpan.inputs = updatePayload.inputs;
236
- }
237
- if (updatePayload.outputs != null) {
238
- langSmithSpan.outputs = updatePayload.outputs;
239
- }
240
- if (updatePayload.error != null) {
241
- langSmithSpan.error = updatePayload.error;
242
- }
243
- if (span.type === SpanType.MODEL_GENERATION) {
244
- const modelAttr = span.attributes ?? {};
245
- if (modelAttr.completionStartTime !== void 0) {
246
- langSmithSpan.addEvent({
247
- name: "new_token",
248
- time: modelAttr.completionStartTime.toISOString()
249
- });
250
- }
251
- }
252
- if (isEnd) {
253
- if (span.endTime) {
254
- await langSmithSpan.end({ endTime: span.endTime.getTime() });
255
- } else {
256
- await langSmithSpan.end();
257
- }
258
- }
259
- await langSmithSpan.patchRun();
260
- }
261
- /**
262
- * Find LangSmith vendor metadata by walking up the span hierarchy and merging.
263
- * Metadata is merged from ancestors with child values taking precedence over parent values.
264
- *
265
- * TODO(2.0): Extract shared `findVendorMetadata()` to base TrackingExporter class
266
- * and reuse here and in LangfuseExporter.findLangfusePrompt()
267
- */
268
- findLangsmithMetadata(traceData, span) {
269
- const metadataChain = [];
270
- let currentSpanId = span.id;
271
- while (currentSpanId) {
272
- const providerMetadata = traceData.getMetadata({ spanId: currentSpanId });
273
- if (providerMetadata) {
274
- metadataChain.push(providerMetadata);
275
- }
276
- currentSpanId = traceData.getParentId({ spanId: currentSpanId });
277
- }
278
- if (metadataChain.length === 0) {
279
- return void 0;
280
- }
281
- const merged = {};
282
- for (let i = metadataChain.length - 1; i >= 0; i--) {
283
- const meta = metadataChain[i];
284
- if (meta.projectName !== void 0) merged.projectName = meta.projectName;
285
- if (meta.sessionId !== void 0) merged.sessionId = meta.sessionId;
286
- if (meta.sessionName !== void 0) merged.sessionName = meta.sessionName;
287
- }
288
- this.logger.debug(`${this.name}: merged vendor metadata from hierarchy`, {
289
- traceId: span.traceId,
290
- spanId: span.id,
291
- metadataKeys: Object.keys(merged),
292
- ancestorCount: metadataChain.length
293
- });
294
- return merged;
295
- }
296
- buildRunTreePayload(span, traceData, isNew = false) {
297
- const vendorMetadata = this.findLangsmithMetadata(traceData, span);
298
- const spanMetadata = span.metadata ? omitKeys(span.metadata, ["langsmith"]) : {};
299
- const payload = {
300
- client: this.#client,
301
- metadata: {
302
- mastra_span_type: span.type,
303
- ...spanMetadata
304
- }
305
- };
306
- if (isNew) {
307
- payload.run_type = mapSpanType(span.type);
308
- payload.start_time = span.startTime.getTime();
309
- }
310
- const projectName = vendorMetadata?.projectName ?? this.config.projectName;
311
- if (projectName) {
312
- payload.project_name = projectName;
313
- }
314
- if (vendorMetadata?.sessionId) {
315
- payload.metadata.session_id = vendorMetadata.sessionId;
316
- }
317
- if (vendorMetadata?.sessionName) {
318
- payload.metadata.session_name = vendorMetadata.sessionName;
319
- }
320
- if (span.isRootSpan && span.tags?.length) {
321
- payload.tags = span.tags;
322
- }
323
- if (span.input !== void 0) {
324
- payload.inputs = isKVMap(span.input) ? span.input : { input: span.input };
325
- }
326
- if (span.output !== void 0) {
327
- payload.outputs = isKVMap(span.output) ? span.output : { output: span.output };
328
- }
329
- const attributes = span.attributes ?? {};
330
- if (span.type === SpanType.MODEL_GENERATION) {
331
- const modelAttr = attributes;
332
- if (modelAttr.model !== void 0) {
333
- payload.metadata.ls_model_name = modelAttr.model;
334
- }
335
- if (modelAttr.provider !== void 0) {
336
- payload.metadata.ls_provider = modelAttr.provider;
337
- }
338
- payload.metadata.usage_metadata = formatUsageMetrics(modelAttr.usage);
339
- if (modelAttr.parameters !== void 0) {
340
- payload.metadata.modelParameters = modelAttr.parameters;
341
- }
342
- const otherAttributes = omitKeys(attributes, ["model", "provider", "usage", "parameters", "completionStartTime"]);
343
- payload.metadata = {
344
- ...payload.metadata,
345
- ...otherAttributes
346
- };
347
- } else {
348
- payload.metadata = {
349
- ...payload.metadata,
350
- ...attributes
351
- };
352
- }
353
- if (span.errorInfo) {
354
- payload.error = span.errorInfo.message;
355
- payload.metadata.errorDetails = span.errorInfo;
356
- }
357
- return payload;
358
- }
55
+ name = "langsmith";
56
+ #client;
57
+ /**
58
+ * Maps Mastra `span.id` to the runId LangSmith allocated when the corresponding
59
+ * RunTree was created. LangSmith's `createFeedback` requires the LangSmith runId,
60
+ * not the Mastra span id, so scores submitted via `onScoreEvent` look up the
61
+ * LangSmith runId here before calling the API.
62
+ *
63
+ * Bounded LRU keyed by Mastra span id — entries are evicted oldest-first when
64
+ * size exceeds `#runIdCacheMaxEntries` so the cache cannot grow without bound.
65
+ */
66
+ #langsmithRunIdBySpanId = /* @__PURE__ */ new Map();
67
+ #runIdCacheMaxEntries;
68
+ constructor(config = {}) {
69
+ const apiKey = config.apiKey ?? process.env.LANGSMITH_API_KEY;
70
+ super({
71
+ ...config,
72
+ apiKey
73
+ });
74
+ this.#runIdCacheMaxEntries = Math.max(1, config.runIdCacheMaxEntries ?? DEFAULT_RUN_ID_CACHE_MAX_ENTRIES);
75
+ if (!apiKey) {
76
+ this.setDisabled(`Missing required credentials (apiKey: ${!!apiKey})`);
77
+ return;
78
+ }
79
+ this.#client = config.client ?? new Client(this.config);
80
+ }
81
+ /** Look up the LangSmith runId for a Mastra span id, refreshing its LRU position on hit. */
82
+ #getLangsmithRunId(spanId) {
83
+ const runId = this.#langsmithRunIdBySpanId.get(spanId);
84
+ if (runId === void 0) return void 0;
85
+ this.#langsmithRunIdBySpanId.delete(spanId);
86
+ this.#langsmithRunIdBySpanId.set(spanId, runId);
87
+ return runId;
88
+ }
89
+ /** Remember the LangSmith runId for a Mastra span id, evicting oldest entries when full. */
90
+ #rememberLangsmithRunId(spanId, runId) {
91
+ if (this.#langsmithRunIdBySpanId.has(spanId)) this.#langsmithRunIdBySpanId.delete(spanId);
92
+ this.#langsmithRunIdBySpanId.set(spanId, runId);
93
+ while (this.#langsmithRunIdBySpanId.size > this.#runIdCacheMaxEntries) {
94
+ const oldest = this.#langsmithRunIdBySpanId.keys().next().value;
95
+ if (oldest === void 0) break;
96
+ this.#langsmithRunIdBySpanId.delete(oldest);
97
+ }
98
+ }
99
+ /**
100
+ * Flush pending trace batches to LangSmith.
101
+ * The LangSmith Client internally batches API calls; this ensures
102
+ * all queued runs are sent before the process exits or the flush
103
+ * caller continues.
104
+ */
105
+ async _flush() {
106
+ if (this.#client) await this.#client.awaitPendingTraceBatches();
107
+ }
108
+ async _postShutdown() {
109
+ this.#langsmithRunIdBySpanId.clear();
110
+ }
111
+ async onScoreEvent(event) {
112
+ if (!this.#client) return;
113
+ const { score } = event;
114
+ if (!score.spanId) {
115
+ this.logger.warn("LangSmith exporter: dropping score with no spanId; trace-level scoring is not yet supported", { scorerId: score.scorerId });
116
+ return;
117
+ }
118
+ const langsmithRunId = this.#getLangsmithRunId(score.spanId);
119
+ if (!langsmithRunId) {
120
+ this.logger.warn("LangSmith exporter: dropping score for a span that was not previously emitted to LangSmith (span_started must be processed before submitting a score for it)", {
121
+ traceId: score.traceId,
122
+ spanId: score.spanId,
123
+ scorerId: score.scorerId
124
+ });
125
+ return;
126
+ }
127
+ const key = score.scorerName ?? score.scorerId;
128
+ try {
129
+ await this.#client.createFeedback(langsmithRunId, key, {
130
+ score: score.score,
131
+ ...score.reason ? { comment: score.reason } : {},
132
+ feedbackId: score.scoreId,
133
+ ...score.scoreTraceId ? { sourceRunId: score.scoreTraceId } : {},
134
+ sourceInfo: {
135
+ ...score.metadata ?? {},
136
+ scorerId: score.scorerId,
137
+ ...score.scoreSource ? { scoreSource: score.scoreSource } : {}
138
+ }
139
+ });
140
+ } catch (err) {
141
+ this.logger.error("LangSmith exporter: Failed to submit feedback", {
142
+ error: err,
143
+ traceId: score.traceId,
144
+ spanId: score.spanId,
145
+ scorerId: score.scorerId
146
+ });
147
+ }
148
+ }
149
+ skipBuildRootTask = true;
150
+ async _buildRoot(_args) {
151
+ throw new Error("Method not implemented.");
152
+ }
153
+ async _buildSpan(args) {
154
+ const { span, traceData } = args;
155
+ const parent = span.isRootSpan ? void 0 : traceData.getParent(args);
156
+ if (!span.isRootSpan && !parent) return;
157
+ const payload = {
158
+ name: span.name,
159
+ ...this.buildRunTreePayload(span, traceData, true)
160
+ };
161
+ const langSmithSpan = span.isRootSpan ? new RunTree(payload) : parent.createChild(payload);
162
+ if (langSmithSpan.id) this.#rememberLangsmithRunId(span.id, langSmithSpan.id);
163
+ await langSmithSpan.postRun();
164
+ return langSmithSpan;
165
+ }
166
+ async _buildEvent(args) {
167
+ const langSmithSpan = await this._buildSpan(args);
168
+ if (!langSmithSpan) return;
169
+ await langSmithSpan.end({ endTime: args.span.startTime.getTime() });
170
+ await langSmithSpan.patchRun();
171
+ return langSmithSpan;
172
+ }
173
+ async _updateSpan(args) {
174
+ await this.handleSpanUpdateOrEnd({
175
+ ...args,
176
+ isEnd: false
177
+ });
178
+ }
179
+ async _finishSpan(args) {
180
+ await this.handleSpanUpdateOrEnd({
181
+ ...args,
182
+ isEnd: true
183
+ });
184
+ }
185
+ async _abortSpan(args) {
186
+ const { span, reason } = args;
187
+ span.error = reason.message;
188
+ span.metadata = {
189
+ ...span.metadata,
190
+ errorDetails: reason
191
+ };
192
+ await span.end();
193
+ await span.patchRun();
194
+ }
195
+ async handleSpanUpdateOrEnd(args) {
196
+ const { span, traceData, isEnd } = args;
197
+ const langSmithSpan = traceData.getSpan({ spanId: span.id });
198
+ if (!langSmithSpan) return;
199
+ const updatePayload = this.buildRunTreePayload(span, traceData);
200
+ langSmithSpan.metadata = {
201
+ ...langSmithSpan.metadata,
202
+ ...updatePayload.metadata
203
+ };
204
+ if (updatePayload.inputs != null) langSmithSpan.inputs = updatePayload.inputs;
205
+ if (updatePayload.outputs != null) langSmithSpan.outputs = updatePayload.outputs;
206
+ if (updatePayload.error != null) langSmithSpan.error = updatePayload.error;
207
+ if (span.type === SpanType.MODEL_GENERATION) {
208
+ const modelAttr = span.attributes ?? {};
209
+ if (modelAttr.completionStartTime !== void 0) langSmithSpan.addEvent({
210
+ name: "new_token",
211
+ time: modelAttr.completionStartTime.toISOString()
212
+ });
213
+ }
214
+ if (isEnd) if (span.endTime) await langSmithSpan.end({ endTime: span.endTime.getTime() });
215
+ else await langSmithSpan.end();
216
+ await langSmithSpan.patchRun();
217
+ }
218
+ /**
219
+ * Find LangSmith vendor metadata by walking up the span hierarchy and merging.
220
+ * Metadata is merged from ancestors with child values taking precedence over parent values.
221
+ *
222
+ * TODO(2.0): Extract shared `findVendorMetadata()` to base TrackingExporter class
223
+ * and reuse here and in LangfuseExporter.findLangfusePrompt()
224
+ */
225
+ findLangsmithMetadata(traceData, span) {
226
+ const metadataChain = [];
227
+ let currentSpanId = span.id;
228
+ while (currentSpanId) {
229
+ const providerMetadata = traceData.getMetadata({ spanId: currentSpanId });
230
+ if (providerMetadata) metadataChain.push(providerMetadata);
231
+ currentSpanId = traceData.getParentId({ spanId: currentSpanId });
232
+ }
233
+ if (metadataChain.length === 0) return;
234
+ const merged = {};
235
+ for (let i = metadataChain.length - 1; i >= 0; i--) {
236
+ const meta = metadataChain[i];
237
+ if (meta.projectName !== void 0) merged.projectName = meta.projectName;
238
+ if (meta.sessionId !== void 0) merged.sessionId = meta.sessionId;
239
+ if (meta.sessionName !== void 0) merged.sessionName = meta.sessionName;
240
+ }
241
+ this.logger.debug(`${this.name}: merged vendor metadata from hierarchy`, {
242
+ traceId: span.traceId,
243
+ spanId: span.id,
244
+ metadataKeys: Object.keys(merged),
245
+ ancestorCount: metadataChain.length
246
+ });
247
+ return merged;
248
+ }
249
+ buildRunTreePayload(span, traceData, isNew = false) {
250
+ const vendorMetadata = this.findLangsmithMetadata(traceData, span);
251
+ const spanMetadata = span.metadata ? omitKeys(span.metadata, ["langsmith"]) : {};
252
+ const payload = {
253
+ client: this.#client,
254
+ metadata: {
255
+ mastra_span_type: span.type,
256
+ ...spanMetadata
257
+ }
258
+ };
259
+ if (isNew) {
260
+ payload.run_type = mapSpanType(span.type);
261
+ payload.start_time = span.startTime.getTime();
262
+ }
263
+ const projectName = vendorMetadata?.projectName ?? this.config.projectName;
264
+ if (projectName) payload.project_name = projectName;
265
+ if (vendorMetadata?.sessionId) payload.metadata.session_id = vendorMetadata.sessionId;
266
+ if (vendorMetadata?.sessionName) payload.metadata.session_name = vendorMetadata.sessionName;
267
+ if (span.isRootSpan && span.tags?.length) payload.tags = span.tags;
268
+ if (span.input !== void 0) payload.inputs = isKVMap(span.input) ? span.input : { input: span.input };
269
+ if (span.output !== void 0) payload.outputs = isKVMap(span.output) ? span.output : { output: span.output };
270
+ const attributes = span.attributes ?? {};
271
+ if (span.type === SpanType.MODEL_GENERATION) {
272
+ const modelAttr = attributes;
273
+ if (modelAttr.model !== void 0) payload.metadata.ls_model_name = modelAttr.model;
274
+ if (modelAttr.provider !== void 0) payload.metadata.ls_provider = modelAttr.provider;
275
+ payload.metadata.usage_metadata = formatUsageMetrics(modelAttr.usage);
276
+ if (modelAttr.parameters !== void 0) payload.metadata.modelParameters = modelAttr.parameters;
277
+ const otherAttributes = omitKeys(attributes, [
278
+ "model",
279
+ "provider",
280
+ "usage",
281
+ "parameters",
282
+ "completionStartTime"
283
+ ]);
284
+ payload.metadata = {
285
+ ...payload.metadata,
286
+ ...otherAttributes
287
+ };
288
+ } else payload.metadata = {
289
+ ...payload.metadata,
290
+ ...attributes
291
+ };
292
+ if (span.errorInfo) {
293
+ payload.error = span.errorInfo.message;
294
+ payload.metadata.errorDetails = span.errorInfo;
295
+ }
296
+ return payload;
297
+ }
359
298
  };
360
-
361
- // src/helpers.ts
299
+ //#endregion
300
+ //#region src/helpers.ts
301
+ /**
302
+ * Adds LangSmith metadata to the tracing options.
303
+ *
304
+ * The metadata is added under `metadata.langsmith` and allows you to:
305
+ * - Route traces to different LangSmith projects via `projectName`
306
+ * - Group traces by session via `sessionId` and `sessionName`
307
+ *
308
+ * @param metadata - The LangSmith metadata to add
309
+ * @returns A TracingOptionsUpdater function for use with `buildTracingOptions`
310
+ *
311
+ * @example
312
+ * ```typescript
313
+ * import { buildTracingOptions } from '@mastra/observability';
314
+ * import { withLangsmithMetadata } from '@mastra/langsmith';
315
+ *
316
+ * // Route traces to a specific project
317
+ * const tracingOptions = buildTracingOptions(
318
+ * withLangsmithMetadata({ projectName: 'customer-support' }),
319
+ * );
320
+ *
321
+ * // Or set multiple fields
322
+ * const tracingOptions = buildTracingOptions(
323
+ * withLangsmithMetadata({
324
+ * projectName: 'my-project',
325
+ * sessionId: 'session-123',
326
+ * }),
327
+ * );
328
+ *
329
+ * // Use in agent config
330
+ * const agent = new Agent({
331
+ * name: 'support-agent',
332
+ * model: openai('gpt-4o'),
333
+ * defaultGenerateOptions: {
334
+ * tracingOptions: buildTracingOptions(
335
+ * withLangsmithMetadata({ projectName: 'support-traces' })
336
+ * ),
337
+ * },
338
+ * });
339
+ * ```
340
+ */
362
341
  function withLangsmithMetadata(metadata) {
363
- return (opts) => ({
364
- ...opts,
365
- metadata: {
366
- ...opts.metadata,
367
- langsmith: {
368
- ...opts.metadata?.langsmith,
369
- ...metadata
370
- }
371
- }
372
- });
342
+ return (opts) => ({
343
+ ...opts,
344
+ metadata: {
345
+ ...opts.metadata,
346
+ langsmith: {
347
+ ...opts.metadata?.langsmith,
348
+ ...metadata
349
+ }
350
+ }
351
+ });
373
352
  }
374
-
353
+ //#endregion
375
354
  export { LangSmithExporter, withLangsmithMetadata };
376
- //# sourceMappingURL=index.js.map
355
+
377
356
  //# sourceMappingURL=index.js.map