langsmith 0.3.26 → 0.3.28-rc.0

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/client.cjs CHANGED
@@ -662,7 +662,7 @@ class Client {
662
662
  return itemPromise;
663
663
  }
664
664
  async _getServerInfo() {
665
- const response = await (0, fetch_js_1._getFetchImplementation)(this.debug)(`${this.apiUrl}/info`, {
665
+ const response = await this.caller.call((0, fetch_js_1._getFetchImplementation)(this.debug), `${this.apiUrl}/info`, {
666
666
  method: "GET",
667
667
  headers: { Accept: "application/json" },
668
668
  signal: AbortSignal.timeout(SERVER_INFO_REQUEST_TIMEOUT),
@@ -685,7 +685,7 @@ class Client {
685
685
  this._serverInfo = await this._getServerInfo();
686
686
  }
687
687
  catch (e) {
688
- console.warn(`[WARNING]: LangSmith failed to fetch info on supported operations. Falling back to batch operations and default limits.`);
688
+ console.warn(`[WARNING]: LangSmith failed to fetch info on supported operations with status code ${e.status}. Falling back to batch operations and default limits.`);
689
689
  }
690
690
  }
691
691
  return this._serverInfo ?? {};
package/dist/client.js CHANGED
@@ -624,7 +624,7 @@ export class Client {
624
624
  return itemPromise;
625
625
  }
626
626
  async _getServerInfo() {
627
- const response = await _getFetchImplementation(this.debug)(`${this.apiUrl}/info`, {
627
+ const response = await this.caller.call(_getFetchImplementation(this.debug), `${this.apiUrl}/info`, {
628
628
  method: "GET",
629
629
  headers: { Accept: "application/json" },
630
630
  signal: AbortSignal.timeout(SERVER_INFO_REQUEST_TIMEOUT),
@@ -647,7 +647,7 @@ export class Client {
647
647
  this._serverInfo = await this._getServerInfo();
648
648
  }
649
649
  catch (e) {
650
- console.warn(`[WARNING]: LangSmith failed to fetch info on supported operations. Falling back to batch operations and default limits.`);
650
+ console.warn(`[WARNING]: LangSmith failed to fetch info on supported operations with status code ${e.status}. Falling back to batch operations and default limits.`);
651
651
  }
652
652
  }
653
653
  return this._serverInfo ?? {};
package/dist/index.cjs CHANGED
@@ -8,4 +8,4 @@ Object.defineProperty(exports, "RunTree", { enumerable: true, get: function () {
8
8
  var fetch_js_1 = require("./singletons/fetch.cjs");
9
9
  Object.defineProperty(exports, "overrideFetchImplementation", { enumerable: true, get: function () { return fetch_js_1.overrideFetchImplementation; } });
10
10
  // Update using yarn bump-version
11
- exports.__version__ = "0.3.26";
11
+ exports.__version__ = "0.3.28-rc.0";
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ export { Client, type ClientConfig, type LangSmithTracingClientInterface, } from
2
2
  export type { Dataset, Example, TracerSession, Run, Feedback, RetrieverOutput, } from "./schemas.js";
3
3
  export { RunTree, type RunTreeConfig } from "./run_trees.js";
4
4
  export { overrideFetchImplementation } from "./singletons/fetch.js";
5
- export declare const __version__ = "0.3.26";
5
+ export declare const __version__ = "0.3.28-rc.0";
package/dist/index.js CHANGED
@@ -2,4 +2,4 @@ export { Client, } from "./client.js";
2
2
  export { RunTree } from "./run_trees.js";
3
3
  export { overrideFetchImplementation } from "./singletons/fetch.js";
4
4
  // Update using yarn bump-version
5
- export const __version__ = "0.3.26";
5
+ export const __version__ = "0.3.28-rc.0";
@@ -58,7 +58,14 @@ function printErrorStackTrace(e) {
58
58
  class LangSmithConflictError extends Error {
59
59
  constructor(message) {
60
60
  super(message);
61
+ Object.defineProperty(this, "status", {
62
+ enumerable: true,
63
+ configurable: true,
64
+ writable: true,
65
+ value: void 0
66
+ });
61
67
  this.name = "LangSmithConflictError";
68
+ this.status = 409;
62
69
  }
63
70
  }
64
71
  exports.LangSmithConflictError = LangSmithConflictError;
@@ -85,5 +92,7 @@ async function raiseForStatus(response, context, consume) {
85
92
  if (response.status === 409) {
86
93
  throw new LangSmithConflictError(fullMessage);
87
94
  }
88
- throw new Error(fullMessage);
95
+ const err = new Error(fullMessage);
96
+ err.status = response.status;
97
+ throw err;
89
98
  }
@@ -31,6 +31,7 @@ export declare function printErrorStackTrace(e: unknown): void;
31
31
  * @see https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/409
32
32
  */
33
33
  export declare class LangSmithConflictError extends Error {
34
+ status: number;
34
35
  constructor(message: string);
35
36
  }
36
37
  /**
@@ -53,7 +53,14 @@ export function printErrorStackTrace(e) {
53
53
  export class LangSmithConflictError extends Error {
54
54
  constructor(message) {
55
55
  super(message);
56
+ Object.defineProperty(this, "status", {
57
+ enumerable: true,
58
+ configurable: true,
59
+ writable: true,
60
+ value: void 0
61
+ });
56
62
  this.name = "LangSmithConflictError";
63
+ this.status = 409;
57
64
  }
58
65
  }
59
66
  /**
@@ -79,5 +86,7 @@ export async function raiseForStatus(response, context, consume) {
79
86
  if (response.status === 409) {
80
87
  throw new LangSmithConflictError(fullMessage);
81
88
  }
82
- throw new Error(fullMessage);
89
+ const err = new Error(fullMessage);
90
+ err.status = response.status;
91
+ throw err;
83
92
  }
package/dist/vercel.cjs CHANGED
@@ -182,9 +182,21 @@ function convertToTimestamp([seconds, nanoseconds]) {
182
182
  return Number(String(seconds) + ms);
183
183
  }
184
184
  function sortByHr(a, b) {
185
- if (a[0] !== b[0])
186
- return Math.sign(a[0] - b[0]);
187
- return Math.sign(a[1] - b[1]);
185
+ if (a.startTime[0] !== b.startTime[0]) {
186
+ return Math.sign(a.startTime[0] - b.startTime[0]);
187
+ }
188
+ else if (a.startTime[1] !== b.startTime[1]) {
189
+ return Math.sign(a.startTime[1] - b.startTime[1]);
190
+ }
191
+ else if (getParentSpanId(a) === b.spanContext().spanId) {
192
+ return -1;
193
+ }
194
+ else if (getParentSpanId(b) === a.spanContext().spanId) {
195
+ return 1;
196
+ }
197
+ else {
198
+ return 0;
199
+ }
188
200
  }
189
201
  const ROOT = "$";
190
202
  const RUN_ID_NAMESPACE = "5c718b20-9078-11ef-9a3d-325096b39f47";
@@ -269,6 +281,18 @@ class AISDKExporter {
269
281
  writable: true,
270
282
  value: {}
271
283
  });
284
+ Object.defineProperty(this, "seenSpanInfo", {
285
+ enumerable: true,
286
+ configurable: true,
287
+ writable: true,
288
+ value: {}
289
+ });
290
+ Object.defineProperty(this, "pendingSpans", {
291
+ enumerable: true,
292
+ configurable: true,
293
+ writable: true,
294
+ value: []
295
+ });
272
296
  Object.defineProperty(this, "debug", {
273
297
  enumerable: true,
274
298
  configurable: true,
@@ -608,104 +632,165 @@ class AISDKExporter {
608
632
  return false;
609
633
  }
610
634
  }
611
- export(spans, resultCallback) {
635
+ _export(spans, resultCallback) {
612
636
  this.logDebug("exporting spans", spans);
613
637
  const typedSpans = spans
638
+ .concat(this.pendingSpans)
614
639
  .slice()
615
- .sort((a, b) => sortByHr(a.startTime, b.startTime));
640
+ // Parent spans should go before child spans in the final order,
641
+ // but may have the same exact start time as their children.
642
+ // They will end earlier, so break ties by end time.
643
+ // TODO: Figure out why this happens.
644
+ .sort((a, b) => sortByHr(a, b));
645
+ // This is really important, as OTEL seems to do weird things with threads.
646
+ // Don't use a while loop.
647
+ this.pendingSpans = [];
648
+ const skippedSpans = [];
649
+ const potentialChildRootRunIds = [];
616
650
  for (const span of typedSpans) {
617
651
  const { traceId, spanId } = span.spanContext();
618
- const parentId = getParentSpanId(span);
652
+ const runId = (0, uuid_1.v5)(spanId, RUN_ID_NAMESPACE);
653
+ let parentId = getParentSpanId(span);
654
+ let parentRunId = parentId
655
+ ? (0, uuid_1.v5)(parentId, RUN_ID_NAMESPACE)
656
+ : undefined;
657
+ let parentSpanInfo = parentRunId
658
+ ? this.seenSpanInfo[parentRunId]
659
+ : undefined;
660
+ // Unrelated, untraced spans should behave as passthroughs from LangSmith's perspective.
661
+ while (parentSpanInfo != null &&
662
+ this.getRunCreate(parentSpanInfo.span) == null) {
663
+ parentId = getParentSpanId(parentSpanInfo.span);
664
+ if (parentId == null) {
665
+ break;
666
+ }
667
+ parentRunId = parentId ? (0, uuid_1.v5)(parentId, RUN_ID_NAMESPACE) : undefined;
668
+ parentSpanInfo = parentRunId
669
+ ? this.seenSpanInfo[parentRunId]
670
+ : undefined;
671
+ }
672
+ // Export may be called in any order, so we need to queue any spans with missing parents
673
+ // for retry later in order to determine whether their parents are tool calls
674
+ // and should not be reparented below.
675
+ if (parentRunId !== undefined && parentSpanInfo === undefined) {
676
+ skippedSpans.push(span);
677
+ continue;
678
+ }
619
679
  this.traceByMap[traceId] ??= {
620
680
  childMap: {},
621
681
  nodeMap: {},
622
682
  relativeExecutionOrder: {},
623
683
  };
624
- const runId = (0, uuid_1.v5)(spanId, RUN_ID_NAMESPACE);
625
- const parentRunId = parentId
626
- ? (0, uuid_1.v5)(parentId, RUN_ID_NAMESPACE)
627
- : undefined;
628
684
  const traceMap = this.traceByMap[traceId];
629
685
  const run = this.getRunCreate(span);
630
686
  traceMap.relativeExecutionOrder[parentRunId ?? ROOT] ??= -1;
631
687
  traceMap.relativeExecutionOrder[parentRunId ?? ROOT] += 1;
632
- const parentSpan = parentId
633
- ? typedSpans.find((i) => i.spanContext().spanId === parentId)
634
- : undefined;
635
688
  traceMap.nodeMap[runId] ??= {
636
689
  id: runId,
637
690
  startTime: span.startTime,
638
691
  run,
639
692
  sent: false,
640
- interop: this.parseInteropFromMetadata(span, parentSpan),
693
+ interop: this.parseInteropFromMetadata(span, parentSpanInfo?.span),
641
694
  executionOrder: traceMap.relativeExecutionOrder[parentRunId ?? ROOT],
642
695
  };
696
+ if (this.seenSpanInfo[runId] == null) {
697
+ this.seenSpanInfo[runId] = {
698
+ span,
699
+ dotOrder: joinDotOrder(parentSpanInfo?.dotOrder, getDotOrder(traceMap.nodeMap[runId])),
700
+ sent: false,
701
+ };
702
+ }
643
703
  if (this.debug)
644
704
  console.log(`[${span.name}] ${runId}`, run);
645
705
  traceMap.childMap[parentRunId ?? ROOT] ??= [];
646
706
  traceMap.childMap[parentRunId ?? ROOT].push(traceMap.nodeMap[runId]);
707
+ if (parentRunId != null) {
708
+ potentialChildRootRunIds.push(parentRunId);
709
+ }
647
710
  }
648
711
  const sampled = [];
649
712
  const actions = [];
650
713
  for (const traceId of Object.keys(this.traceByMap)) {
651
714
  const traceMap = this.traceByMap[traceId];
652
- const queue = traceMap.childMap[ROOT]?.map((item) => ({
653
- item,
654
- dotOrder: getDotOrder(item),
655
- })) ?? [];
715
+ const queue = Object.keys(traceMap.childMap)
716
+ .map((runId) => {
717
+ if (runId === ROOT || potentialChildRootRunIds.includes(runId)) {
718
+ return traceMap.childMap[runId];
719
+ }
720
+ return [];
721
+ })
722
+ .flat();
656
723
  const seen = new Set();
657
724
  while (queue.length) {
658
725
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
659
726
  const task = queue.shift();
660
- if (seen.has(task.item.id))
727
+ if (seen.has(task.id))
661
728
  continue;
662
- if (!task.item.sent) {
663
- if (task.item.run != null) {
664
- if (task.item.interop?.type === "user") {
729
+ let taskDotOrder = this.seenSpanInfo[task.id].dotOrder;
730
+ if (!task.sent) {
731
+ if (task.run != null) {
732
+ if (task.interop?.type === "user") {
665
733
  actions.push({
666
734
  type: "rename",
667
- sourceRunId: task.item.id,
668
- targetRunId: task.item.interop.userRunId,
735
+ sourceRunId: task.id,
736
+ targetRunId: task.interop.userRunId,
669
737
  });
670
738
  }
671
- if (task.item.interop?.type === "traceable") {
739
+ if (task.interop?.type === "traceable") {
672
740
  actions.push({
673
741
  type: "reparent",
674
- runId: task.item.id,
675
- parentDotOrder: task.item.interop.parentRunTree.dotted_order,
742
+ runId: task.id,
743
+ parentDotOrder: task.interop.parentRunTree.dotted_order,
676
744
  });
677
745
  }
678
- let dotOrder = task.dotOrder;
679
746
  for (const action of actions) {
680
747
  if (action.type === "delete") {
681
- dotOrder = removeDotOrder(dotOrder, action.runId);
748
+ taskDotOrder = removeDotOrder(taskDotOrder, action.runId);
682
749
  }
683
750
  if (action.type === "reparent") {
684
- dotOrder = reparentDotOrder(dotOrder, action.runId, action.parentDotOrder);
751
+ taskDotOrder = reparentDotOrder(taskDotOrder, action.runId, action.parentDotOrder);
685
752
  }
686
753
  if (action.type === "rename") {
687
- dotOrder = dotOrder.replace(action.sourceRunId, action.targetRunId);
754
+ taskDotOrder = taskDotOrder.replace(action.sourceRunId, action.targetRunId);
688
755
  }
689
756
  }
690
- sampled.push({
691
- ...task.item.run,
692
- ...getMutableRunCreate(dotOrder),
693
- });
757
+ this.seenSpanInfo[task.id].dotOrder = taskDotOrder;
758
+ if (!this.seenSpanInfo[task.id].sent) {
759
+ sampled.push({
760
+ ...task.run,
761
+ ...getMutableRunCreate(taskDotOrder),
762
+ });
763
+ }
764
+ this.seenSpanInfo[task.id].sent = true;
694
765
  }
695
766
  else {
696
- actions.push({ type: "delete", runId: task.item.id });
767
+ actions.push({ type: "delete", runId: task.id });
697
768
  }
698
- task.item.sent = true;
769
+ task.sent = true;
699
770
  }
700
- const children = traceMap.childMap[task.item.id] ?? [];
701
- queue.push(...children.map((item) => ({
702
- item,
703
- dotOrder: joinDotOrder(task.dotOrder, getDotOrder(item)),
704
- })));
771
+ const children = traceMap.childMap[task.id] ?? [];
772
+ queue.push(...children);
705
773
  }
706
774
  }
707
775
  this.logDebug(`sampled runs to be sent to LangSmith`, sampled);
708
- Promise.all(sampled.map((run) => this.client.createRun(run))).then(() => resultCallback({ code: 0 }), (error) => resultCallback({ code: 1, error }));
776
+ Promise.all(sampled.map((run) => this.client.createRun(run)))
777
+ .then(() => {
778
+ // OTEL seems to do weird things with threads, so we need to queue any spans
779
+ // with missing parents for retry at the end after async operations.
780
+ this.pendingSpans = skippedSpans;
781
+ })
782
+ .then(() => resultCallback({ code: 0 }), (error) => resultCallback({ code: 1, error }));
783
+ }
784
+ export(spans, resultCallback) {
785
+ this._export(spans, (result) => {
786
+ if (result.code === 0) {
787
+ // Empty export to try flushing pending spans to rule out any trace order shenanigans
788
+ this._export([], resultCallback);
789
+ }
790
+ else {
791
+ resultCallback(result);
792
+ }
793
+ });
709
794
  }
710
795
  async shutdown() {
711
796
  // find nodes which are incomplete
@@ -714,10 +799,13 @@ class AISDKExporter {
714
799
  if (incompleteNodes.length > 0) {
715
800
  console.warn("Some incomplete nodes were found before shutdown and not sent to LangSmith.");
716
801
  }
717
- await this.client?.awaitPendingTraceBatches();
802
+ await this.forceFlush();
718
803
  }
719
804
  async forceFlush() {
720
- await this.client?.awaitPendingTraceBatches();
805
+ await new Promise((resolve) => {
806
+ this.export([], resolve);
807
+ });
808
+ await this.client.awaitPendingTraceBatches();
721
809
  }
722
810
  logDebug(...args) {
723
811
  if (!this.debug)
package/dist/vercel.d.ts CHANGED
@@ -50,6 +50,8 @@ export interface TelemetrySettings extends AITelemetrySettings {
50
50
  export declare class AISDKExporter {
51
51
  private client;
52
52
  private traceByMap;
53
+ private seenSpanInfo;
54
+ private pendingSpans;
53
55
  private debug;
54
56
  constructor(args?: {
55
57
  client?: Client;
@@ -65,11 +67,15 @@ export declare class AISDKExporter {
65
67
  functionId?: string;
66
68
  tracer?: import("@opentelemetry/api").Tracer;
67
69
  };
70
+ _export(spans: unknown[], resultCallback: (result: {
71
+ code: 0 | 1;
72
+ error?: Error;
73
+ }) => void): void;
68
74
  export(spans: unknown[], resultCallback: (result: {
69
75
  code: 0 | 1;
70
76
  error?: Error;
71
77
  }) => void): void;
72
78
  shutdown(): Promise<void>;
73
- forceFlush?(): Promise<void>;
79
+ forceFlush(): Promise<void>;
74
80
  protected logDebug(...args: Parameters<typeof console.debug>): void;
75
81
  }
package/dist/vercel.js CHANGED
@@ -179,9 +179,21 @@ function convertToTimestamp([seconds, nanoseconds]) {
179
179
  return Number(String(seconds) + ms);
180
180
  }
181
181
  function sortByHr(a, b) {
182
- if (a[0] !== b[0])
183
- return Math.sign(a[0] - b[0]);
184
- return Math.sign(a[1] - b[1]);
182
+ if (a.startTime[0] !== b.startTime[0]) {
183
+ return Math.sign(a.startTime[0] - b.startTime[0]);
184
+ }
185
+ else if (a.startTime[1] !== b.startTime[1]) {
186
+ return Math.sign(a.startTime[1] - b.startTime[1]);
187
+ }
188
+ else if (getParentSpanId(a) === b.spanContext().spanId) {
189
+ return -1;
190
+ }
191
+ else if (getParentSpanId(b) === a.spanContext().spanId) {
192
+ return 1;
193
+ }
194
+ else {
195
+ return 0;
196
+ }
185
197
  }
186
198
  const ROOT = "$";
187
199
  const RUN_ID_NAMESPACE = "5c718b20-9078-11ef-9a3d-325096b39f47";
@@ -266,6 +278,18 @@ export class AISDKExporter {
266
278
  writable: true,
267
279
  value: {}
268
280
  });
281
+ Object.defineProperty(this, "seenSpanInfo", {
282
+ enumerable: true,
283
+ configurable: true,
284
+ writable: true,
285
+ value: {}
286
+ });
287
+ Object.defineProperty(this, "pendingSpans", {
288
+ enumerable: true,
289
+ configurable: true,
290
+ writable: true,
291
+ value: []
292
+ });
269
293
  Object.defineProperty(this, "debug", {
270
294
  enumerable: true,
271
295
  configurable: true,
@@ -605,104 +629,165 @@ export class AISDKExporter {
605
629
  return false;
606
630
  }
607
631
  }
608
- export(spans, resultCallback) {
632
+ _export(spans, resultCallback) {
609
633
  this.logDebug("exporting spans", spans);
610
634
  const typedSpans = spans
635
+ .concat(this.pendingSpans)
611
636
  .slice()
612
- .sort((a, b) => sortByHr(a.startTime, b.startTime));
637
+ // Parent spans should go before child spans in the final order,
638
+ // but may have the same exact start time as their children.
639
+ // They will end earlier, so break ties by end time.
640
+ // TODO: Figure out why this happens.
641
+ .sort((a, b) => sortByHr(a, b));
642
+ // This is really important, as OTEL seems to do weird things with threads.
643
+ // Don't use a while loop.
644
+ this.pendingSpans = [];
645
+ const skippedSpans = [];
646
+ const potentialChildRootRunIds = [];
613
647
  for (const span of typedSpans) {
614
648
  const { traceId, spanId } = span.spanContext();
615
- const parentId = getParentSpanId(span);
649
+ const runId = uuid5(spanId, RUN_ID_NAMESPACE);
650
+ let parentId = getParentSpanId(span);
651
+ let parentRunId = parentId
652
+ ? uuid5(parentId, RUN_ID_NAMESPACE)
653
+ : undefined;
654
+ let parentSpanInfo = parentRunId
655
+ ? this.seenSpanInfo[parentRunId]
656
+ : undefined;
657
+ // Unrelated, untraced spans should behave as passthroughs from LangSmith's perspective.
658
+ while (parentSpanInfo != null &&
659
+ this.getRunCreate(parentSpanInfo.span) == null) {
660
+ parentId = getParentSpanId(parentSpanInfo.span);
661
+ if (parentId == null) {
662
+ break;
663
+ }
664
+ parentRunId = parentId ? uuid5(parentId, RUN_ID_NAMESPACE) : undefined;
665
+ parentSpanInfo = parentRunId
666
+ ? this.seenSpanInfo[parentRunId]
667
+ : undefined;
668
+ }
669
+ // Export may be called in any order, so we need to queue any spans with missing parents
670
+ // for retry later in order to determine whether their parents are tool calls
671
+ // and should not be reparented below.
672
+ if (parentRunId !== undefined && parentSpanInfo === undefined) {
673
+ skippedSpans.push(span);
674
+ continue;
675
+ }
616
676
  this.traceByMap[traceId] ??= {
617
677
  childMap: {},
618
678
  nodeMap: {},
619
679
  relativeExecutionOrder: {},
620
680
  };
621
- const runId = uuid5(spanId, RUN_ID_NAMESPACE);
622
- const parentRunId = parentId
623
- ? uuid5(parentId, RUN_ID_NAMESPACE)
624
- : undefined;
625
681
  const traceMap = this.traceByMap[traceId];
626
682
  const run = this.getRunCreate(span);
627
683
  traceMap.relativeExecutionOrder[parentRunId ?? ROOT] ??= -1;
628
684
  traceMap.relativeExecutionOrder[parentRunId ?? ROOT] += 1;
629
- const parentSpan = parentId
630
- ? typedSpans.find((i) => i.spanContext().spanId === parentId)
631
- : undefined;
632
685
  traceMap.nodeMap[runId] ??= {
633
686
  id: runId,
634
687
  startTime: span.startTime,
635
688
  run,
636
689
  sent: false,
637
- interop: this.parseInteropFromMetadata(span, parentSpan),
690
+ interop: this.parseInteropFromMetadata(span, parentSpanInfo?.span),
638
691
  executionOrder: traceMap.relativeExecutionOrder[parentRunId ?? ROOT],
639
692
  };
693
+ if (this.seenSpanInfo[runId] == null) {
694
+ this.seenSpanInfo[runId] = {
695
+ span,
696
+ dotOrder: joinDotOrder(parentSpanInfo?.dotOrder, getDotOrder(traceMap.nodeMap[runId])),
697
+ sent: false,
698
+ };
699
+ }
640
700
  if (this.debug)
641
701
  console.log(`[${span.name}] ${runId}`, run);
642
702
  traceMap.childMap[parentRunId ?? ROOT] ??= [];
643
703
  traceMap.childMap[parentRunId ?? ROOT].push(traceMap.nodeMap[runId]);
704
+ if (parentRunId != null) {
705
+ potentialChildRootRunIds.push(parentRunId);
706
+ }
644
707
  }
645
708
  const sampled = [];
646
709
  const actions = [];
647
710
  for (const traceId of Object.keys(this.traceByMap)) {
648
711
  const traceMap = this.traceByMap[traceId];
649
- const queue = traceMap.childMap[ROOT]?.map((item) => ({
650
- item,
651
- dotOrder: getDotOrder(item),
652
- })) ?? [];
712
+ const queue = Object.keys(traceMap.childMap)
713
+ .map((runId) => {
714
+ if (runId === ROOT || potentialChildRootRunIds.includes(runId)) {
715
+ return traceMap.childMap[runId];
716
+ }
717
+ return [];
718
+ })
719
+ .flat();
653
720
  const seen = new Set();
654
721
  while (queue.length) {
655
722
  // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
656
723
  const task = queue.shift();
657
- if (seen.has(task.item.id))
724
+ if (seen.has(task.id))
658
725
  continue;
659
- if (!task.item.sent) {
660
- if (task.item.run != null) {
661
- if (task.item.interop?.type === "user") {
726
+ let taskDotOrder = this.seenSpanInfo[task.id].dotOrder;
727
+ if (!task.sent) {
728
+ if (task.run != null) {
729
+ if (task.interop?.type === "user") {
662
730
  actions.push({
663
731
  type: "rename",
664
- sourceRunId: task.item.id,
665
- targetRunId: task.item.interop.userRunId,
732
+ sourceRunId: task.id,
733
+ targetRunId: task.interop.userRunId,
666
734
  });
667
735
  }
668
- if (task.item.interop?.type === "traceable") {
736
+ if (task.interop?.type === "traceable") {
669
737
  actions.push({
670
738
  type: "reparent",
671
- runId: task.item.id,
672
- parentDotOrder: task.item.interop.parentRunTree.dotted_order,
739
+ runId: task.id,
740
+ parentDotOrder: task.interop.parentRunTree.dotted_order,
673
741
  });
674
742
  }
675
- let dotOrder = task.dotOrder;
676
743
  for (const action of actions) {
677
744
  if (action.type === "delete") {
678
- dotOrder = removeDotOrder(dotOrder, action.runId);
745
+ taskDotOrder = removeDotOrder(taskDotOrder, action.runId);
679
746
  }
680
747
  if (action.type === "reparent") {
681
- dotOrder = reparentDotOrder(dotOrder, action.runId, action.parentDotOrder);
748
+ taskDotOrder = reparentDotOrder(taskDotOrder, action.runId, action.parentDotOrder);
682
749
  }
683
750
  if (action.type === "rename") {
684
- dotOrder = dotOrder.replace(action.sourceRunId, action.targetRunId);
751
+ taskDotOrder = taskDotOrder.replace(action.sourceRunId, action.targetRunId);
685
752
  }
686
753
  }
687
- sampled.push({
688
- ...task.item.run,
689
- ...getMutableRunCreate(dotOrder),
690
- });
754
+ this.seenSpanInfo[task.id].dotOrder = taskDotOrder;
755
+ if (!this.seenSpanInfo[task.id].sent) {
756
+ sampled.push({
757
+ ...task.run,
758
+ ...getMutableRunCreate(taskDotOrder),
759
+ });
760
+ }
761
+ this.seenSpanInfo[task.id].sent = true;
691
762
  }
692
763
  else {
693
- actions.push({ type: "delete", runId: task.item.id });
764
+ actions.push({ type: "delete", runId: task.id });
694
765
  }
695
- task.item.sent = true;
766
+ task.sent = true;
696
767
  }
697
- const children = traceMap.childMap[task.item.id] ?? [];
698
- queue.push(...children.map((item) => ({
699
- item,
700
- dotOrder: joinDotOrder(task.dotOrder, getDotOrder(item)),
701
- })));
768
+ const children = traceMap.childMap[task.id] ?? [];
769
+ queue.push(...children);
702
770
  }
703
771
  }
704
772
  this.logDebug(`sampled runs to be sent to LangSmith`, sampled);
705
- Promise.all(sampled.map((run) => this.client.createRun(run))).then(() => resultCallback({ code: 0 }), (error) => resultCallback({ code: 1, error }));
773
+ Promise.all(sampled.map((run) => this.client.createRun(run)))
774
+ .then(() => {
775
+ // OTEL seems to do weird things with threads, so we need to queue any spans
776
+ // with missing parents for retry at the end after async operations.
777
+ this.pendingSpans = skippedSpans;
778
+ })
779
+ .then(() => resultCallback({ code: 0 }), (error) => resultCallback({ code: 1, error }));
780
+ }
781
+ export(spans, resultCallback) {
782
+ this._export(spans, (result) => {
783
+ if (result.code === 0) {
784
+ // Empty export to try flushing pending spans to rule out any trace order shenanigans
785
+ this._export([], resultCallback);
786
+ }
787
+ else {
788
+ resultCallback(result);
789
+ }
790
+ });
706
791
  }
707
792
  async shutdown() {
708
793
  // find nodes which are incomplete
@@ -711,10 +796,13 @@ export class AISDKExporter {
711
796
  if (incompleteNodes.length > 0) {
712
797
  console.warn("Some incomplete nodes were found before shutdown and not sent to LangSmith.");
713
798
  }
714
- await this.client?.awaitPendingTraceBatches();
799
+ await this.forceFlush();
715
800
  }
716
801
  async forceFlush() {
717
- await this.client?.awaitPendingTraceBatches();
802
+ await new Promise((resolve) => {
803
+ this.export([], resolve);
804
+ });
805
+ await this.client.awaitPendingTraceBatches();
718
806
  }
719
807
  logDebug(...args) {
720
808
  if (!this.debug)
@@ -166,7 +166,7 @@ function processChatCompletion(outputs) {
166
166
  * const patchedStream = await patchedClient.chat.completions.create(
167
167
  * {
168
168
  * messages: [{ role: "user", content: `Say 'foo'` }],
169
- * model: "gpt-3.5-turbo",
169
+ * model: "gpt-4.1-mini",
170
170
  * stream: true,
171
171
  * },
172
172
  * {
@@ -61,7 +61,7 @@ type PatchedOpenAIClient<T extends OpenAIType> = T & {
61
61
  * const patchedStream = await patchedClient.chat.completions.create(
62
62
  * {
63
63
  * messages: [{ role: "user", content: `Say 'foo'` }],
64
- * model: "gpt-3.5-turbo",
64
+ * model: "gpt-4.1-mini",
65
65
  * stream: true,
66
66
  * },
67
67
  * {
@@ -163,7 +163,7 @@ function processChatCompletion(outputs) {
163
163
  * const patchedStream = await patchedClient.chat.completions.create(
164
164
  * {
165
165
  * messages: [{ role: "user", content: `Say 'foo'` }],
166
- * model: "gpt-3.5-turbo",
166
+ * model: "gpt-4.1-mini",
167
167
  * stream: true,
168
168
  * },
169
169
  * {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.3.26",
3
+ "version": "0.3.28-rc.0",
4
4
  "description": "Client library to connect to the LangSmith LLM Tracing and Evaluation Platform.",
5
5
  "packageManager": "yarn@1.22.19",
6
6
  "files": [