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