langsmith 0.8.5 → 0.8.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.
@@ -2,7 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LangSmithTelemetry = LangSmithTelemetry;
4
4
  const run_trees_js_1 = require("../../run_trees.cjs");
5
- const traceable_js_1 = require("../../singletons/traceable.cjs");
5
+ const traceable_js_1 = require("../../traceable.cjs");
6
6
  const env_js_1 = require("../../env.cjs");
7
7
  const utils_js_1 = require("./utils.cjs");
8
8
  const utils_js_2 = require("./utils.cjs");
@@ -58,6 +58,12 @@ function _getLsAgentType(parentRunTree) {
58
58
  }
59
59
  return "root";
60
60
  }
61
+ function _hasNonzeroUsageMetadata(runTree) {
62
+ const usageMetadata = runTree.extra?.metadata?.usage_metadata;
63
+ if (!(0, types_js_1.isRecord)(usageMetadata))
64
+ return false;
65
+ return ["input_tokens", "output_tokens", "total_tokens"].some((key) => typeof usageMetadata[key] === "number" && usageMetadata[key] > 0);
66
+ }
61
67
  /**
62
68
  * Creates a LangSmith `Telemetry` for the Vercel AI SDK.
63
69
  *
@@ -82,6 +88,31 @@ function _getLsAgentType(parentRunTree) {
82
88
  */
83
89
  function LangSmithTelemetry(config) {
84
90
  const { name: customName, runType = "chain", metadata: customMetadata, tags: customTags, client, projectName, processInputs, processOutputs, processChildLLMRunInputs, processChildLLMRunOutputs, traceResponseMetadata, traceRawHttp, tracingEnabled, extra: customExtra, } = config ?? {};
91
+ function formatToolResultMessage(toolCallId, toolName, toolOutput) {
92
+ const rawOutput = (0, types_js_1.isRecord)(toolOutput)
93
+ ? toolOutput.type === "tool-result"
94
+ ? toolOutput.output
95
+ : "error" in toolOutput
96
+ ? toolOutput.error
97
+ : toolOutput
98
+ : toolOutput;
99
+ const artifact = rawOutput instanceof Error ? rawOutput.message : rawOutput;
100
+ const content = typeof artifact === "string"
101
+ ? artifact
102
+ : (JSON.stringify(artifact) ?? String(artifact));
103
+ return {
104
+ role: "tool",
105
+ content,
106
+ tool_call_id: toolCallId,
107
+ name: toolName,
108
+ artifact,
109
+ };
110
+ }
111
+ function appendToolResultMessage(harnessState, toolCallId, toolName, toolOutput) {
112
+ const message = formatToolResultMessage(toolCallId, toolName, toolOutput);
113
+ harnessState.completedStepMessages.push(message);
114
+ return message;
115
+ }
85
116
  function getOpenStepOrRoot(state) {
86
117
  let openStep;
87
118
  state.stepRunTrees.forEach((stepRt) => {
@@ -91,6 +122,16 @@ function LangSmithTelemetry(config) {
91
122
  });
92
123
  return openStep ?? state.rootRunTree;
93
124
  }
125
+ async function flushPiDeferredStepRun(state) {
126
+ const workaround = state.experimental_harness?.piAggregateUsageWorkaround;
127
+ if (workaround == null)
128
+ return;
129
+ const stepRunTree = workaround.deferredStepRunTree;
130
+ if (stepRunTree == null)
131
+ return;
132
+ workaround.deferredStepRunTree = undefined;
133
+ await stepRunTree.patchRun({ excludeInputs: true });
134
+ }
94
135
  async function finalizeOpenToolRuns(state, opts) {
95
136
  const entries = Array.from(state.toolRunTrees.entries());
96
137
  for (let i = 0; i < entries.length; i++) {
@@ -161,7 +202,6 @@ function LangSmithTelemetry(config) {
161
202
  ai_sdk_method: event.operationId,
162
203
  ls_agent_type: _getLsAgentType(parentRunTree),
163
204
  ls_model_name: event.modelId,
164
- ls_provider: event.provider,
165
205
  ls_integration: "vercel-ai-sdk-telemetry",
166
206
  },
167
207
  },
@@ -181,17 +221,29 @@ function LangSmithTelemetry(config) {
181
221
  rootRunTree,
182
222
  stepRunTrees: new Map(),
183
223
  toolRunTrees: new Map(),
224
+ experimental_harness: event.operationId === "ai.harness"
225
+ ? {
226
+ lastStepContent: [],
227
+ completedStepMessages: [],
228
+ toolResults: new Map(),
229
+ piAggregateUsageWorkaround: event.provider === "pi" ? {} : undefined,
230
+ }
231
+ : undefined,
184
232
  });
185
233
  };
186
234
  const onStepStart = async (event) => {
187
235
  const state = invocationsByCallId.get(event.callId);
188
236
  if (!state)
189
237
  return;
238
+ await flushPiDeferredStepRun(state);
190
239
  const stepNumber = event.stepNumber ?? 0;
191
240
  let inputs = {};
192
241
  if (event.recordInputs !== false) {
193
242
  if ("messages" in event && event.messages != null) {
194
- inputs.messages = _formatMessages(event.messages);
243
+ inputs.messages = [
244
+ ..._formatMessages(event.messages),
245
+ ...(state.experimental_harness?.completedStepMessages ?? []),
246
+ ];
195
247
  }
196
248
  if ("runtimeContext" in event && event.runtimeContext != null) {
197
249
  inputs.runtimeContext = event.runtimeContext;
@@ -212,9 +264,18 @@ function LangSmithTelemetry(config) {
212
264
  name: event.provider,
213
265
  run_type: "llm",
214
266
  inputs,
215
- extra: { metadata: { step_number: stepNumber } },
267
+ extra: {
268
+ metadata: {
269
+ step_number: stepNumber,
270
+ ls_model_name: event.modelId,
271
+ },
272
+ },
216
273
  });
217
274
  state.stepRunTrees.set(stepNumber, stepRunTree);
275
+ const piUsageWorkaround = state.experimental_harness?.piAggregateUsageWorkaround;
276
+ if (piUsageWorkaround != null) {
277
+ piUsageWorkaround.latestStepRunTree = stepRunTree;
278
+ }
218
279
  await stepRunTree.postRun();
219
280
  };
220
281
  const onLanguageModelCallStart = async (event) => {
@@ -254,22 +315,25 @@ function LangSmithTelemetry(config) {
254
315
  const state = invocationsByCallId.get(event.callId);
255
316
  if (!state)
256
317
  return;
257
- const parentRunTree = getOpenStepOrRoot(state);
318
+ const parentRunTree = state.rootRunTree;
258
319
  let inputs = {};
259
320
  if (event.recordInputs !== false) {
260
- if ((0, types_js_1.isRecord)(event.toolCall.input)) {
261
- inputs = { ...event.toolCall.input };
321
+ let toolCallInput = event.toolCall.input;
322
+ if (typeof toolCallInput === "string") {
323
+ try {
324
+ toolCallInput = JSON.parse(toolCallInput);
325
+ }
326
+ catch {
327
+ // The tool argument itself may be a plain string rather than JSON.
328
+ }
262
329
  }
263
- else if (typeof event.toolCall.input !== "undefined") {
264
- inputs = { input: event.toolCall.input };
330
+ if ((0, types_js_1.isRecord)(toolCallInput)) {
331
+ inputs = { ...toolCallInput };
265
332
  }
266
- if ("toolContext" in event && event.toolContext != null) {
267
- inputs.toolContext = event.toolContext;
333
+ else if (toolCallInput !== undefined) {
334
+ inputs = { input: toolCallInput };
268
335
  }
269
336
  }
270
- else {
271
- inputs = {};
272
- }
273
337
  const toolRunTree = parentRunTree.createChild({
274
338
  name: event.toolCall.toolName,
275
339
  run_type: "tool",
@@ -291,14 +355,20 @@ function LangSmithTelemetry(config) {
291
355
  const toolRunTree = state.toolRunTrees.get(event.toolCall.toolCallId);
292
356
  if (!toolRunTree)
293
357
  return;
294
- state.toolRunTrees.delete(event.toolCall.toolCallId);
358
+ const harnessState = state.experimental_harness;
359
+ if (harnessState != null) {
360
+ harnessState.toolResults.set(event.toolCall.toolCallId, {
361
+ toolName: event.toolCall.toolName,
362
+ output: event.toolOutput,
363
+ });
364
+ }
295
365
  let outputs;
296
366
  let error;
297
367
  if (event.recordOutputs !== false) {
298
368
  if (event.toolOutput.type === "tool-result") {
299
369
  outputs = { output: event.toolOutput.output };
300
370
  }
301
- else if (event.toolOutput.type === "tool-error") {
371
+ else if ((0, types_js_1.isRecord)(event.toolOutput) && "error" in event.toolOutput) {
302
372
  const err = event.toolOutput.error;
303
373
  error = err instanceof Error ? err.message : String(err);
304
374
  }
@@ -308,6 +378,7 @@ function LangSmithTelemetry(config) {
308
378
  }
309
379
  await toolRunTree.end(outputs, error, Math.floor(toolRunTree.start_time + event.toolExecutionMs));
310
380
  await toolRunTree.patchRun({ excludeInputs: true });
381
+ state.toolRunTrees.delete(event.toolCall.toolCallId);
311
382
  };
312
383
  const onStepFinish = async (event) => {
313
384
  const state = invocationsByCallId.get(event.callId);
@@ -317,8 +388,12 @@ function LangSmithTelemetry(config) {
317
388
  const stepRunTree = state.stepRunTrees.get(stepNumber);
318
389
  if (!stepRunTree)
319
390
  return;
391
+ const harnessState = state.experimental_harness;
320
392
  let outputs = {};
321
393
  if (event.recordOutputs !== false) {
394
+ if (harnessState != null && Array.isArray(event.content)) {
395
+ harnessState.lastStepContent = [...event.content];
396
+ }
322
397
  outputs = _formatStepOutput(event, traceRawHttp);
323
398
  if (processChildLLMRunOutputs) {
324
399
  try {
@@ -329,11 +404,38 @@ function LangSmithTelemetry(config) {
329
404
  }
330
405
  }
331
406
  }
407
+ if (harnessState != null && Array.isArray(event.content)) {
408
+ harnessState.completedStepMessages.push((0, utils_js_1.convertMessageToTracedFormat)({
409
+ role: "assistant",
410
+ content: event.content,
411
+ }));
412
+ for (const part of event.content) {
413
+ if (!(0, types_js_1.isRecord)(part) ||
414
+ part.type !== "tool-call" ||
415
+ typeof part.toolCallId !== "string") {
416
+ continue;
417
+ }
418
+ const toolResult = harnessState.toolResults.get(part.toolCallId);
419
+ if (toolResult) {
420
+ appendToolResultMessage(harnessState, part.toolCallId, toolResult.toolName, toolResult.output);
421
+ harnessState.toolResults.delete(part.toolCallId);
422
+ }
423
+ }
424
+ }
332
425
  // Set usage metadata
333
426
  // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
334
427
  (0, utils_js_2.setUsageMetadataOnRunTree)(event, stepRunTree);
335
428
  await stepRunTree.end(outputs);
336
- await stepRunTree.patchRun({ excludeInputs: true });
429
+ const piUsageWorkaround = harnessState?.piAggregateUsageWorkaround;
430
+ if (piUsageWorkaround != null && !_hasNonzeroUsageMetadata(stepRunTree)) {
431
+ // Pi reports zero usage for inferred step boundaries and sends the real
432
+ // aggregate at turn end. Defer the final PATCH so that aggregate can be
433
+ // attached to the last LLM run without issuing a duplicate update.
434
+ piUsageWorkaround.deferredStepRunTree = stepRunTree;
435
+ }
436
+ else {
437
+ await stepRunTree.patchRun({ excludeInputs: true });
438
+ }
337
439
  state.stepRunTrees.delete(stepNumber);
338
440
  };
339
441
  const onEnd = async (event) => {
@@ -343,6 +445,8 @@ function LangSmithTelemetry(config) {
343
445
  if (!state)
344
446
  return;
345
447
  const { rootRunTree } = state;
448
+ const harnessState = state.experimental_harness;
449
+ const piUsageWorkaround = harnessState?.piAggregateUsageWorkaround;
346
450
  await finalizeOpenToolRuns(state, { note: "closed on finish" });
347
451
  // Ensure any remaining step runs are closed
348
452
  const remainingSteps = Array.from(state.stepRunTrees.entries());
@@ -350,7 +454,12 @@ function LangSmithTelemetry(config) {
350
454
  const [stepNumber, stepRt] = remainingSteps[i];
351
455
  if (stepRt.end_time == null) {
352
456
  await stepRt.end({ note: "closed on finish" });
353
- await stepRt.patchRun({ excludeInputs: true });
457
+ if (piUsageWorkaround != null && !_hasNonzeroUsageMetadata(stepRt)) {
458
+ piUsageWorkaround.deferredStepRunTree = stepRt;
459
+ }
460
+ else {
461
+ await stepRt.patchRun({ excludeInputs: true });
462
+ }
354
463
  }
355
464
  state.stepRunTrees.delete(stepNumber);
356
465
  }
@@ -360,9 +469,18 @@ function LangSmithTelemetry(config) {
360
469
  if ("text" in event && event.text != null) {
361
470
  outputs.content = event.text;
362
471
  }
363
- else if ("content" in event && event.content != null) {
472
+ else if ("content" in event &&
473
+ event.content != null &&
474
+ (!Array.isArray(event.content) || event.content.length > 0)) {
364
475
  outputs.content = event.content;
365
476
  }
477
+ else if (harnessState != null &&
478
+ harnessState.lastStepContent.length > 0) {
479
+ // HarnessAgent currently reports an empty root `content` array even
480
+ // though each completed step contains assistant output. Only the final
481
+ // step belongs on the root; prior steps have their own child runs.
482
+ outputs.content = harnessState.lastStepContent;
483
+ }
366
484
  if (outputs.content != null) {
367
485
  outputs.role = "assistant";
368
486
  }
@@ -394,18 +512,28 @@ function LangSmithTelemetry(config) {
394
512
  }
395
513
  }
396
514
  }
397
- // Set aggregated usage on root
398
- if ("totalUsage" in event && event.totalUsage != null) {
399
- (0, utils_js_2.setUsageMetadataOnRunTree)(
400
- // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
401
- { usage: event.totalUsage, providerMetadata: event.providerMetadata }, rootRunTree);
402
- }
403
- else if ("usage" in event && event.usage != null) {
404
- // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
405
- (0, utils_js_2.setUsageMetadataOnRunTree)(event, rootRunTree);
515
+ // Pi reports zero usage on inferred step boundaries and provides the actual
516
+ // aggregate only when the turn ends. Keep that usage on an LLM run rather
517
+ // than incorrectly elevating it to the parent chain. The latest step is the
518
+ // only accurate place available until Harness Pi exposes per-step usage.
519
+ const latestStep = piUsageWorkaround?.latestStepRunTree;
520
+ if (latestStep?.run_type === "llm" &&
521
+ !_hasNonzeroUsageMetadata(latestStep)) {
522
+ if ("totalUsage" in event && event.totalUsage != null) {
523
+ (0, utils_js_2.setUsageMetadataOnRunTree)(
524
+ // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
525
+ { usage: event.totalUsage, providerMetadata: event.providerMetadata }, latestStep);
526
+ }
527
+ else if ("usage" in event && event.usage != null) {
528
+ // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
529
+ (0, utils_js_2.setUsageMetadataOnRunTree)(event, latestStep);
530
+ }
406
531
  }
407
532
  await rootRunTree.end(outputs);
408
- await rootRunTree.patchRun({ excludeInputs: true });
533
+ await Promise.all([
534
+ flushPiDeferredStepRun(state),
535
+ rootRunTree.patchRun({ excludeInputs: true }),
536
+ ]);
409
537
  invocationsByCallId.delete(event.callId);
410
538
  };
411
539
  const onError = async (payload) => {
@@ -436,6 +564,7 @@ function LangSmithTelemetry(config) {
436
564
  }
437
565
  state.stepRunTrees.delete(stepNumber);
438
566
  }
567
+ await flushPiDeferredStepRun(state);
439
568
  await rootRunTree.end(undefined, errorMsg);
440
569
  await rootRunTree.patchRun({ excludeInputs: true });
441
570
  invocationsByCallId.delete(callId);
@@ -1,5 +1,5 @@
1
1
  import { isRunTree, RunTree } from "../../run_trees.js";
2
- import { getCurrentRunTree, withRunTree } from "../../singletons/traceable.js";
2
+ import { getCurrentRunTree, withRunTree } from "../../traceable.js";
3
3
  import { isEnvTracingEnabled } from "../../env.js";
4
4
  import { convertMessageToTracedFormat } from "./utils.js";
5
5
  import { setUsageMetadataOnRunTree } from "./utils.js";
@@ -55,6 +55,12 @@ function _getLsAgentType(parentRunTree) {
55
55
  }
56
56
  return "root";
57
57
  }
58
+ function _hasNonzeroUsageMetadata(runTree) {
59
+ const usageMetadata = runTree.extra?.metadata?.usage_metadata;
60
+ if (!isRecord(usageMetadata))
61
+ return false;
62
+ return ["input_tokens", "output_tokens", "total_tokens"].some((key) => typeof usageMetadata[key] === "number" && usageMetadata[key] > 0);
63
+ }
58
64
  /**
59
65
  * Creates a LangSmith `Telemetry` for the Vercel AI SDK.
60
66
  *
@@ -79,6 +85,31 @@ function _getLsAgentType(parentRunTree) {
79
85
  */
80
86
  export function LangSmithTelemetry(config) {
81
87
  const { name: customName, runType = "chain", metadata: customMetadata, tags: customTags, client, projectName, processInputs, processOutputs, processChildLLMRunInputs, processChildLLMRunOutputs, traceResponseMetadata, traceRawHttp, tracingEnabled, extra: customExtra, } = config ?? {};
88
+ function formatToolResultMessage(toolCallId, toolName, toolOutput) {
89
+ const rawOutput = isRecord(toolOutput)
90
+ ? toolOutput.type === "tool-result"
91
+ ? toolOutput.output
92
+ : "error" in toolOutput
93
+ ? toolOutput.error
94
+ : toolOutput
95
+ : toolOutput;
96
+ const artifact = rawOutput instanceof Error ? rawOutput.message : rawOutput;
97
+ const content = typeof artifact === "string"
98
+ ? artifact
99
+ : (JSON.stringify(artifact) ?? String(artifact));
100
+ return {
101
+ role: "tool",
102
+ content,
103
+ tool_call_id: toolCallId,
104
+ name: toolName,
105
+ artifact,
106
+ };
107
+ }
108
+ function appendToolResultMessage(harnessState, toolCallId, toolName, toolOutput) {
109
+ const message = formatToolResultMessage(toolCallId, toolName, toolOutput);
110
+ harnessState.completedStepMessages.push(message);
111
+ return message;
112
+ }
82
113
  function getOpenStepOrRoot(state) {
83
114
  let openStep;
84
115
  state.stepRunTrees.forEach((stepRt) => {
@@ -88,6 +119,16 @@ export function LangSmithTelemetry(config) {
88
119
  });
89
120
  return openStep ?? state.rootRunTree;
90
121
  }
122
+ async function flushPiDeferredStepRun(state) {
123
+ const workaround = state.experimental_harness?.piAggregateUsageWorkaround;
124
+ if (workaround == null)
125
+ return;
126
+ const stepRunTree = workaround.deferredStepRunTree;
127
+ if (stepRunTree == null)
128
+ return;
129
+ workaround.deferredStepRunTree = undefined;
130
+ await stepRunTree.patchRun({ excludeInputs: true });
131
+ }
91
132
  async function finalizeOpenToolRuns(state, opts) {
92
133
  const entries = Array.from(state.toolRunTrees.entries());
93
134
  for (let i = 0; i < entries.length; i++) {
@@ -158,7 +199,6 @@ export function LangSmithTelemetry(config) {
158
199
  ai_sdk_method: event.operationId,
159
200
  ls_agent_type: _getLsAgentType(parentRunTree),
160
201
  ls_model_name: event.modelId,
161
- ls_provider: event.provider,
162
202
  ls_integration: "vercel-ai-sdk-telemetry",
163
203
  },
164
204
  },
@@ -178,17 +218,29 @@ export function LangSmithTelemetry(config) {
178
218
  rootRunTree,
179
219
  stepRunTrees: new Map(),
180
220
  toolRunTrees: new Map(),
221
+ experimental_harness: event.operationId === "ai.harness"
222
+ ? {
223
+ lastStepContent: [],
224
+ completedStepMessages: [],
225
+ toolResults: new Map(),
226
+ piAggregateUsageWorkaround: event.provider === "pi" ? {} : undefined,
227
+ }
228
+ : undefined,
181
229
  });
182
230
  };
183
231
  const onStepStart = async (event) => {
184
232
  const state = invocationsByCallId.get(event.callId);
185
233
  if (!state)
186
234
  return;
235
+ await flushPiDeferredStepRun(state);
187
236
  const stepNumber = event.stepNumber ?? 0;
188
237
  let inputs = {};
189
238
  if (event.recordInputs !== false) {
190
239
  if ("messages" in event && event.messages != null) {
191
- inputs.messages = _formatMessages(event.messages);
240
+ inputs.messages = [
241
+ ..._formatMessages(event.messages),
242
+ ...(state.experimental_harness?.completedStepMessages ?? []),
243
+ ];
192
244
  }
193
245
  if ("runtimeContext" in event && event.runtimeContext != null) {
194
246
  inputs.runtimeContext = event.runtimeContext;
@@ -209,9 +261,18 @@ export function LangSmithTelemetry(config) {
209
261
  name: event.provider,
210
262
  run_type: "llm",
211
263
  inputs,
212
- extra: { metadata: { step_number: stepNumber } },
264
+ extra: {
265
+ metadata: {
266
+ step_number: stepNumber,
267
+ ls_model_name: event.modelId,
268
+ },
269
+ },
213
270
  });
214
271
  state.stepRunTrees.set(stepNumber, stepRunTree);
272
+ const piUsageWorkaround = state.experimental_harness?.piAggregateUsageWorkaround;
273
+ if (piUsageWorkaround != null) {
274
+ piUsageWorkaround.latestStepRunTree = stepRunTree;
275
+ }
215
276
  await stepRunTree.postRun();
216
277
  };
217
278
  const onLanguageModelCallStart = async (event) => {
@@ -251,22 +312,25 @@ export function LangSmithTelemetry(config) {
251
312
  const state = invocationsByCallId.get(event.callId);
252
313
  if (!state)
253
314
  return;
254
- const parentRunTree = getOpenStepOrRoot(state);
315
+ const parentRunTree = state.rootRunTree;
255
316
  let inputs = {};
256
317
  if (event.recordInputs !== false) {
257
- if (isRecord(event.toolCall.input)) {
258
- inputs = { ...event.toolCall.input };
318
+ let toolCallInput = event.toolCall.input;
319
+ if (typeof toolCallInput === "string") {
320
+ try {
321
+ toolCallInput = JSON.parse(toolCallInput);
322
+ }
323
+ catch {
324
+ // The tool argument itself may be a plain string rather than JSON.
325
+ }
259
326
  }
260
- else if (typeof event.toolCall.input !== "undefined") {
261
- inputs = { input: event.toolCall.input };
327
+ if (isRecord(toolCallInput)) {
328
+ inputs = { ...toolCallInput };
262
329
  }
263
- if ("toolContext" in event && event.toolContext != null) {
264
- inputs.toolContext = event.toolContext;
330
+ else if (toolCallInput !== undefined) {
331
+ inputs = { input: toolCallInput };
265
332
  }
266
333
  }
267
- else {
268
- inputs = {};
269
- }
270
334
  const toolRunTree = parentRunTree.createChild({
271
335
  name: event.toolCall.toolName,
272
336
  run_type: "tool",
@@ -288,14 +352,20 @@ export function LangSmithTelemetry(config) {
288
352
  const toolRunTree = state.toolRunTrees.get(event.toolCall.toolCallId);
289
353
  if (!toolRunTree)
290
354
  return;
291
- state.toolRunTrees.delete(event.toolCall.toolCallId);
355
+ const harnessState = state.experimental_harness;
356
+ if (harnessState != null) {
357
+ harnessState.toolResults.set(event.toolCall.toolCallId, {
358
+ toolName: event.toolCall.toolName,
359
+ output: event.toolOutput,
360
+ });
361
+ }
292
362
  let outputs;
293
363
  let error;
294
364
  if (event.recordOutputs !== false) {
295
365
  if (event.toolOutput.type === "tool-result") {
296
366
  outputs = { output: event.toolOutput.output };
297
367
  }
298
- else if (event.toolOutput.type === "tool-error") {
368
+ else if (isRecord(event.toolOutput) && "error" in event.toolOutput) {
299
369
  const err = event.toolOutput.error;
300
370
  error = err instanceof Error ? err.message : String(err);
301
371
  }
@@ -305,6 +375,7 @@ export function LangSmithTelemetry(config) {
305
375
  }
306
376
  await toolRunTree.end(outputs, error, Math.floor(toolRunTree.start_time + event.toolExecutionMs));
307
377
  await toolRunTree.patchRun({ excludeInputs: true });
378
+ state.toolRunTrees.delete(event.toolCall.toolCallId);
308
379
  };
309
380
  const onStepFinish = async (event) => {
310
381
  const state = invocationsByCallId.get(event.callId);
@@ -314,8 +385,12 @@ export function LangSmithTelemetry(config) {
314
385
  const stepRunTree = state.stepRunTrees.get(stepNumber);
315
386
  if (!stepRunTree)
316
387
  return;
388
+ const harnessState = state.experimental_harness;
317
389
  let outputs = {};
318
390
  if (event.recordOutputs !== false) {
391
+ if (harnessState != null && Array.isArray(event.content)) {
392
+ harnessState.lastStepContent = [...event.content];
393
+ }
319
394
  outputs = _formatStepOutput(event, traceRawHttp);
320
395
  if (processChildLLMRunOutputs) {
321
396
  try {
@@ -326,11 +401,38 @@ export function LangSmithTelemetry(config) {
326
401
  }
327
402
  }
328
403
  }
404
+ if (harnessState != null && Array.isArray(event.content)) {
405
+ harnessState.completedStepMessages.push(convertMessageToTracedFormat({
406
+ role: "assistant",
407
+ content: event.content,
408
+ }));
409
+ for (const part of event.content) {
410
+ if (!isRecord(part) ||
411
+ part.type !== "tool-call" ||
412
+ typeof part.toolCallId !== "string") {
413
+ continue;
414
+ }
415
+ const toolResult = harnessState.toolResults.get(part.toolCallId);
416
+ if (toolResult) {
417
+ appendToolResultMessage(harnessState, part.toolCallId, toolResult.toolName, toolResult.output);
418
+ harnessState.toolResults.delete(part.toolCallId);
419
+ }
420
+ }
421
+ }
329
422
  // Set usage metadata
330
423
  // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
331
424
  setUsageMetadataOnRunTree(event, stepRunTree);
332
425
  await stepRunTree.end(outputs);
333
- await stepRunTree.patchRun({ excludeInputs: true });
426
+ const piUsageWorkaround = harnessState?.piAggregateUsageWorkaround;
427
+ if (piUsageWorkaround != null && !_hasNonzeroUsageMetadata(stepRunTree)) {
428
+ // Pi reports zero usage for inferred step boundaries and sends the real
429
+ // aggregate at turn end. Defer the final PATCH so that aggregate can be
430
+ // attached to the last LLM run without issuing a duplicate update.
431
+ piUsageWorkaround.deferredStepRunTree = stepRunTree;
432
+ }
433
+ else {
434
+ await stepRunTree.patchRun({ excludeInputs: true });
435
+ }
334
436
  state.stepRunTrees.delete(stepNumber);
335
437
  };
336
438
  const onEnd = async (event) => {
@@ -340,6 +442,8 @@ export function LangSmithTelemetry(config) {
340
442
  if (!state)
341
443
  return;
342
444
  const { rootRunTree } = state;
445
+ const harnessState = state.experimental_harness;
446
+ const piUsageWorkaround = harnessState?.piAggregateUsageWorkaround;
343
447
  await finalizeOpenToolRuns(state, { note: "closed on finish" });
344
448
  // Ensure any remaining step runs are closed
345
449
  const remainingSteps = Array.from(state.stepRunTrees.entries());
@@ -347,7 +451,12 @@ export function LangSmithTelemetry(config) {
347
451
  const [stepNumber, stepRt] = remainingSteps[i];
348
452
  if (stepRt.end_time == null) {
349
453
  await stepRt.end({ note: "closed on finish" });
350
- await stepRt.patchRun({ excludeInputs: true });
454
+ if (piUsageWorkaround != null && !_hasNonzeroUsageMetadata(stepRt)) {
455
+ piUsageWorkaround.deferredStepRunTree = stepRt;
456
+ }
457
+ else {
458
+ await stepRt.patchRun({ excludeInputs: true });
459
+ }
351
460
  }
352
461
  state.stepRunTrees.delete(stepNumber);
353
462
  }
@@ -357,9 +466,18 @@ export function LangSmithTelemetry(config) {
357
466
  if ("text" in event && event.text != null) {
358
467
  outputs.content = event.text;
359
468
  }
360
- else if ("content" in event && event.content != null) {
469
+ else if ("content" in event &&
470
+ event.content != null &&
471
+ (!Array.isArray(event.content) || event.content.length > 0)) {
361
472
  outputs.content = event.content;
362
473
  }
474
+ else if (harnessState != null &&
475
+ harnessState.lastStepContent.length > 0) {
476
+ // HarnessAgent currently reports an empty root `content` array even
477
+ // though each completed step contains assistant output. Only the final
478
+ // step belongs on the root; prior steps have their own child runs.
479
+ outputs.content = harnessState.lastStepContent;
480
+ }
363
481
  if (outputs.content != null) {
364
482
  outputs.role = "assistant";
365
483
  }
@@ -391,18 +509,28 @@ export function LangSmithTelemetry(config) {
391
509
  }
392
510
  }
393
511
  }
394
- // Set aggregated usage on root
395
- if ("totalUsage" in event && event.totalUsage != null) {
396
- setUsageMetadataOnRunTree(
397
- // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
398
- { usage: event.totalUsage, providerMetadata: event.providerMetadata }, rootRunTree);
399
- }
400
- else if ("usage" in event && event.usage != null) {
401
- // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
402
- setUsageMetadataOnRunTree(event, rootRunTree);
512
+ // Pi reports zero usage on inferred step boundaries and provides the actual
513
+ // aggregate only when the turn ends. Keep that usage on an LLM run rather
514
+ // than incorrectly elevating it to the parent chain. The latest step is the
515
+ // only accurate place available until Harness Pi exposes per-step usage.
516
+ const latestStep = piUsageWorkaround?.latestStepRunTree;
517
+ if (latestStep?.run_type === "llm" &&
518
+ !_hasNonzeroUsageMetadata(latestStep)) {
519
+ if ("totalUsage" in event && event.totalUsage != null) {
520
+ setUsageMetadataOnRunTree(
521
+ // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
522
+ { usage: event.totalUsage, providerMetadata: event.providerMetadata }, latestStep);
523
+ }
524
+ else if ("usage" in event && event.usage != null) {
525
+ // @ts-expect-error SharedV4ProviderMetadata is not assignable to SharedV2ProviderMetadata
526
+ setUsageMetadataOnRunTree(event, latestStep);
527
+ }
403
528
  }
404
529
  await rootRunTree.end(outputs);
405
- await rootRunTree.patchRun({ excludeInputs: true });
530
+ await Promise.all([
531
+ flushPiDeferredStepRun(state),
532
+ rootRunTree.patchRun({ excludeInputs: true }),
533
+ ]);
406
534
  invocationsByCallId.delete(event.callId);
407
535
  };
408
536
  const onError = async (payload) => {
@@ -433,6 +561,7 @@ export function LangSmithTelemetry(config) {
433
561
  }
434
562
  state.stepRunTrees.delete(stepNumber);
435
563
  }
564
+ await flushPiDeferredStepRun(state);
436
565
  await rootRunTree.end(undefined, errorMsg);
437
566
  await rootRunTree.patchRun({ excludeInputs: true });
438
567
  invocationsByCallId.delete(callId);
package/dist/index.cjs CHANGED
@@ -35,6 +35,6 @@ Object.defineProperty(exports, "UnprocessableEntityError", { enumerable: true, g
35
35
  Object.defineProperty(exports, "RateLimitError", { enumerable: true, get: function () { return error_js_1.RateLimitError; } });
36
36
  Object.defineProperty(exports, "InternalServerError", { enumerable: true, get: function () { return error_js_1.InternalServerError; } });
37
37
  // Update using pnpm bump-version
38
- exports.__version__ = "0.8.5";
38
+ exports.__version__ = "0.8.6";
39
39
  // Metadata key to hide a traced run from LangSmith's Messages View.
40
40
  exports.LS_MESSAGE_VIEW_EXCLUDE = "ls_message_view_exclude";
package/dist/index.d.ts CHANGED
@@ -7,5 +7,5 @@ export { computeRunIdForSecondaryReplica, uuid7, uuid7FromTime, } from "./uuid.j
7
7
  export { isTracingEnabled } from "./utils/guard.js";
8
8
  export { Cache, PromptCache, type CacheConfig, type CacheMetrics, configureGlobalPromptCache, promptCacheSingleton, } from "./utils/prompt_cache/index.js";
9
9
  export { LangsmithError, APIError, APIUserAbortError, APIConnectionError, APIConnectionTimeoutError, BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError, InternalServerError, } from "./_openapi_client/core/error.js";
10
- export declare const __version__ = "0.8.5";
10
+ export declare const __version__ = "0.8.6";
11
11
  export declare const LS_MESSAGE_VIEW_EXCLUDE: "ls_message_view_exclude";
package/dist/index.js CHANGED
@@ -7,6 +7,6 @@ export { isTracingEnabled } from "./utils/guard.js";
7
7
  export { Cache, PromptCache, configureGlobalPromptCache, promptCacheSingleton, } from "./utils/prompt_cache/index.js";
8
8
  export { LangsmithError, APIError, APIUserAbortError, APIConnectionError, APIConnectionTimeoutError, BadRequestError, AuthenticationError, PermissionDeniedError, NotFoundError, ConflictError, UnprocessableEntityError, RateLimitError, InternalServerError, } from "./_openapi_client/core/error.js";
9
9
  // Update using pnpm bump-version
10
- export const __version__ = "0.8.5";
10
+ export const __version__ = "0.8.6";
11
11
  // Metadata key to hide a traced run from LangSmith's Messages View.
12
12
  export const LS_MESSAGE_VIEW_EXCLUDE = "ls_message_view_exclude";
@@ -147,7 +147,7 @@ class CommandHandle {
147
147
  return;
148
148
  const firstResult = await this._stream.next();
149
149
  if (firstResult.done) {
150
- throw new errors_js_1.LangSmithSandboxOperationError("Command stream ended before 'started' message", "command");
150
+ throw new errors_js_1.LangSmithStreamEndedBeforeStartedError("Command stream ended before 'started' message");
151
151
  }
152
152
  const firstMsg = firstResult.value;
153
153
  if (firstMsg.type !== "started") {
@@ -4,7 +4,7 @@
4
4
  *
5
5
  * Port of Python's AsyncCommandHandle to TypeScript.
6
6
  */
7
- import { LangSmithSandboxConnectionError, LangSmithSandboxOperationError, } from "./errors.js";
7
+ import { LangSmithSandboxConnectionError, LangSmithSandboxOperationError, LangSmithStreamEndedBeforeStartedError, } from "./errors.js";
8
8
  /**
9
9
  * Async handle to a running command with streaming output and auto-reconnect.
10
10
  *
@@ -144,7 +144,7 @@ export class CommandHandle {
144
144
  return;
145
145
  const firstResult = await this._stream.next();
146
146
  if (firstResult.done) {
147
- throw new LangSmithSandboxOperationError("Command stream ended before 'started' message", "command");
147
+ throw new LangSmithStreamEndedBeforeStartedError("Command stream ended before 'started' message");
148
148
  }
149
149
  const firstMsg = firstResult.value;
150
150
  if (firstMsg.type !== "started") {
@@ -7,7 +7,7 @@
7
7
  * properties for specific handling when needed.
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.LangSmithSandboxServerReloadError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = void 0;
10
+ exports.LangSmithSandboxServerReloadError = exports.LangSmithStreamEndedBeforeStartedError = exports.LangSmithCommandTimeoutError = exports.LangSmithSandboxOperationError = exports.LangSmithSandboxNotReadyError = exports.LangSmithDataplaneNotConfiguredError = exports.LangSmithSandboxCreationError = exports.LangSmithResourceCreationError = exports.LangSmithQuotaExceededError = exports.LangSmithValidationError = exports.LangSmithResourceNameConflictError = exports.LangSmithResourceAlreadyExistsError = exports.LangSmithResourceInUseError = exports.LangSmithResourceTimeoutError = exports.LangSmithResourceNotFoundError = exports.LangSmithSandboxConnectionError = exports.LangSmithSandboxAuthenticationError = exports.LangSmithSandboxAPIError = exports.LangSmithSandboxError = void 0;
11
11
  /**
12
12
  * Base exception for sandbox client errors.
13
13
  */
@@ -357,6 +357,21 @@ class LangSmithCommandTimeoutError extends LangSmithSandboxOperationError {
357
357
  }
358
358
  }
359
359
  exports.LangSmithCommandTimeoutError = LangSmithCommandTimeoutError;
360
+ /**
361
+ * Internal: a command WebSocket closed before the guest sent its 'started'
362
+ * frame (the proxied tunnel was torn down gracefully mid-handshake).
363
+ *
364
+ * Marks the idempotently-retryable early close, as distinct from a
365
+ * command-level failure. Subclasses the operation error so callers that catch
366
+ * the public type are unaffected.
367
+ */
368
+ class LangSmithStreamEndedBeforeStartedError extends LangSmithSandboxOperationError {
369
+ constructor(message) {
370
+ super(message, "command");
371
+ this.name = "LangSmithStreamEndedBeforeStartedError";
372
+ }
373
+ }
374
+ exports.LangSmithStreamEndedBeforeStartedError = LangSmithStreamEndedBeforeStartedError;
360
375
  /**
361
376
  * Raised when the sandbox server is reloading (close code 1001).
362
377
  *
@@ -154,6 +154,17 @@ export declare class LangSmithCommandTimeoutError extends LangSmithSandboxOperat
154
154
  timeout?: number;
155
155
  constructor(message: string, timeout?: number);
156
156
  }
157
+ /**
158
+ * Internal: a command WebSocket closed before the guest sent its 'started'
159
+ * frame (the proxied tunnel was torn down gracefully mid-handshake).
160
+ *
161
+ * Marks the idempotently-retryable early close, as distinct from a
162
+ * command-level failure. Subclasses the operation error so callers that catch
163
+ * the public type are unaffected.
164
+ */
165
+ export declare class LangSmithStreamEndedBeforeStartedError extends LangSmithSandboxOperationError {
166
+ constructor(message: string);
167
+ }
157
168
  /**
158
169
  * Raised when the sandbox server is reloading (close code 1001).
159
170
  *
@@ -337,6 +337,20 @@ export class LangSmithCommandTimeoutError extends LangSmithSandboxOperationError
337
337
  this.timeout = timeout;
338
338
  }
339
339
  }
340
+ /**
341
+ * Internal: a command WebSocket closed before the guest sent its 'started'
342
+ * frame (the proxied tunnel was torn down gracefully mid-handshake).
343
+ *
344
+ * Marks the idempotently-retryable early close, as distinct from a
345
+ * command-level failure. Subclasses the operation error so callers that catch
346
+ * the public type are unaffected.
347
+ */
348
+ export class LangSmithStreamEndedBeforeStartedError extends LangSmithSandboxOperationError {
349
+ constructor(message) {
350
+ super(message, "command");
351
+ this.name = "LangSmithStreamEndedBeforeStartedError";
352
+ }
353
+ }
340
354
  /**
341
355
  * Raised when the sandbox server is reloading (close code 1001).
342
356
  *
@@ -4,6 +4,7 @@
4
4
  */
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Sandbox = void 0;
7
+ const uuid_js_1 = require("../uuid.cjs");
7
8
  const errors_js_1 = require("./errors.cjs");
8
9
  const helpers_js_1 = require("./helpers.cjs");
9
10
  const command_handle_js_1 = require("./command_handle.cjs");
@@ -229,26 +230,49 @@ class Sandbox {
229
230
  const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
230
231
  const dataplaneUrl = this.requireDataplaneUrl();
231
232
  const clientHeaders = this._client.getDefaultHeaders();
232
- const [stream, control] = await (0, ws_execute_js_1.runWsStream)(dataplaneUrl, this._client.getApiKey(), command, {
233
- timeout,
234
- env,
235
- cwd,
236
- shell,
237
- commandId,
238
- idleTimeout,
239
- killOnDisconnect,
240
- ttlSeconds,
241
- pty,
242
- ...(Object.keys(clientHeaders).length > 0
243
- ? { headers: clientHeaders }
244
- : {}),
245
- });
246
- const handle = new command_handle_js_1.CommandHandle(stream, control, this, {
247
- onStdout,
248
- onStderr,
249
- });
250
- await handle._ensureStarted();
251
- return handle;
233
+ // A client-supplied command_id makes execute idempotent: the daemon does
234
+ // get-or-create keyed on it, so if the tunnel closes before "started" we
235
+ // can re-issue the same id and reattach to the existing command instead of
236
+ // spawning a second one.
237
+ const execCommandId = commandId ?? (0, uuid_js_1.uuid7)();
238
+ let attempt = 0;
239
+ // eslint-disable-next-line no-constant-condition
240
+ while (true) {
241
+ const [stream, control] = await (0, ws_execute_js_1.runWsStream)(dataplaneUrl, this._client.getApiKey(), command, {
242
+ timeout,
243
+ env,
244
+ cwd,
245
+ shell,
246
+ commandId: execCommandId,
247
+ idleTimeout,
248
+ killOnDisconnect,
249
+ ttlSeconds,
250
+ pty,
251
+ ...(Object.keys(clientHeaders).length > 0
252
+ ? { headers: clientHeaders }
253
+ : {}),
254
+ });
255
+ const handle = new command_handle_js_1.CommandHandle(stream, control, this, {
256
+ onStdout,
257
+ onStderr,
258
+ });
259
+ try {
260
+ await handle._ensureStarted();
261
+ return handle;
262
+ }
263
+ catch (e) {
264
+ // Idempotent re-issue (same command_id) after an early close.
265
+ if (!(e instanceof errors_js_1.LangSmithStreamEndedBeforeStartedError)) {
266
+ throw e;
267
+ }
268
+ attempt++;
269
+ if (attempt > command_handle_js_1.CommandHandle.MAX_AUTO_RECONNECTS) {
270
+ throw e;
271
+ }
272
+ const delay = Math.min(command_handle_js_1.CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), command_handle_js_1.CommandHandle.BACKOFF_MAX);
273
+ await new Promise((r) => setTimeout(r, delay * 1000));
274
+ }
275
+ }
252
276
  }
253
277
  /**
254
278
  * Execute a command via HTTP POST (blocking).
@@ -1,7 +1,8 @@
1
1
  /**
2
2
  * Sandbox class for interacting with a specific sandbox instance.
3
3
  */
4
- import { LangSmithDataplaneNotConfiguredError } from "./errors.js";
4
+ import { uuid7 } from "../uuid.js";
5
+ import { LangSmithDataplaneNotConfiguredError, LangSmithStreamEndedBeforeStartedError, } from "./errors.js";
5
6
  import { handleSandboxHttpError } from "./helpers.js";
6
7
  import { CommandHandle } from "./command_handle.js";
7
8
  import { reconnectWsStream, runWsStream } from "./ws_execute.js";
@@ -226,26 +227,49 @@ export class Sandbox {
226
227
  const { timeout = 60, env, cwd, shell = "/bin/bash", onStdout, onStderr, commandId, idleTimeout, killOnDisconnect, ttlSeconds, pty, } = options;
227
228
  const dataplaneUrl = this.requireDataplaneUrl();
228
229
  const clientHeaders = this._client.getDefaultHeaders();
229
- const [stream, control] = await runWsStream(dataplaneUrl, this._client.getApiKey(), command, {
230
- timeout,
231
- env,
232
- cwd,
233
- shell,
234
- commandId,
235
- idleTimeout,
236
- killOnDisconnect,
237
- ttlSeconds,
238
- pty,
239
- ...(Object.keys(clientHeaders).length > 0
240
- ? { headers: clientHeaders }
241
- : {}),
242
- });
243
- const handle = new CommandHandle(stream, control, this, {
244
- onStdout,
245
- onStderr,
246
- });
247
- await handle._ensureStarted();
248
- return handle;
230
+ // A client-supplied command_id makes execute idempotent: the daemon does
231
+ // get-or-create keyed on it, so if the tunnel closes before "started" we
232
+ // can re-issue the same id and reattach to the existing command instead of
233
+ // spawning a second one.
234
+ const execCommandId = commandId ?? uuid7();
235
+ let attempt = 0;
236
+ // eslint-disable-next-line no-constant-condition
237
+ while (true) {
238
+ const [stream, control] = await runWsStream(dataplaneUrl, this._client.getApiKey(), command, {
239
+ timeout,
240
+ env,
241
+ cwd,
242
+ shell,
243
+ commandId: execCommandId,
244
+ idleTimeout,
245
+ killOnDisconnect,
246
+ ttlSeconds,
247
+ pty,
248
+ ...(Object.keys(clientHeaders).length > 0
249
+ ? { headers: clientHeaders }
250
+ : {}),
251
+ });
252
+ const handle = new CommandHandle(stream, control, this, {
253
+ onStdout,
254
+ onStderr,
255
+ });
256
+ try {
257
+ await handle._ensureStarted();
258
+ return handle;
259
+ }
260
+ catch (e) {
261
+ // Idempotent re-issue (same command_id) after an early close.
262
+ if (!(e instanceof LangSmithStreamEndedBeforeStartedError)) {
263
+ throw e;
264
+ }
265
+ attempt++;
266
+ if (attempt > CommandHandle.MAX_AUTO_RECONNECTS) {
267
+ throw e;
268
+ }
269
+ const delay = Math.min(CommandHandle.BACKOFF_BASE * 2 ** (attempt - 1), CommandHandle.BACKOFF_MAX);
270
+ await new Promise((r) => setTimeout(r, delay * 1000));
271
+ }
272
+ }
249
273
  }
250
274
  /**
251
275
  * Execute a command via HTTP POST (blocking).
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.syncExamplePromises = exports.evaluatorLogFeedbackPromises = exports.testWrapperAsyncLocalStorageInstance = exports.DEFAULT_TEST_CLIENT = void 0;
3
+ exports.evaluatorLogFeedbackPromises = exports.testWrapperAsyncLocalStorageInstance = exports.DEFAULT_TEST_CLIENT = void 0;
4
4
  exports.trackingEnabled = trackingEnabled;
5
5
  exports._logTestFeedback = _logTestFeedback;
6
6
  const node_async_hooks_1 = require("node:async_hooks");
@@ -19,7 +19,6 @@ function trackingEnabled(context) {
19
19
  return (0, env_js_2.isEnvTracingEnabled)();
20
20
  }
21
21
  exports.evaluatorLogFeedbackPromises = new Set();
22
- exports.syncExamplePromises = new Map();
23
22
  function _logTestFeedback(params) {
24
23
  const { exampleId, feedback, context, runTree, client, sourceRunId } = params;
25
24
  if (trackingEnabled(context)) {
@@ -33,7 +32,7 @@ function _logTestFeedback(params) {
33
32
  if (context.project == null) {
34
33
  throw new Error("Could not log feedback to LangSmith: missing project information. Please contact us for help.");
35
34
  }
36
- await exports.syncExamplePromises.get(exampleId);
35
+ await context.syncExamplePromises?.get(exampleId);
37
36
  await client?.logEvaluationFeedback({
38
37
  evaluatorResponse: feedback,
39
38
  run: runTree,
@@ -20,11 +20,11 @@ export type TestWrapperAsyncLocalStorageData = {
20
20
  suiteName: string;
21
21
  testRootRunTree?: RunTree;
22
22
  setupPromise?: Promise<void>;
23
+ syncExamplePromises?: Map<string, Promise<Example>>;
23
24
  };
24
25
  export declare const testWrapperAsyncLocalStorageInstance: AsyncLocalStorage<TestWrapperAsyncLocalStorageData>;
25
26
  export declare function trackingEnabled(context: TestWrapperAsyncLocalStorageData): boolean;
26
27
  export declare const evaluatorLogFeedbackPromises: Set<unknown>;
27
- export declare const syncExamplePromises: Map<any, any>;
28
28
  export declare function _logTestFeedback(params: {
29
29
  exampleId?: string;
30
30
  feedback: SimpleEvaluationResult;
@@ -14,7 +14,6 @@ export function trackingEnabled(context) {
14
14
  return isEnvTracingEnabled();
15
15
  }
16
16
  export const evaluatorLogFeedbackPromises = new Set();
17
- export const syncExamplePromises = new Map();
18
17
  export function _logTestFeedback(params) {
19
18
  const { exampleId, feedback, context, runTree, client, sourceRunId } = params;
20
19
  if (trackingEnabled(context)) {
@@ -28,7 +27,7 @@ export function _logTestFeedback(params) {
28
27
  if (context.project == null) {
29
28
  throw new Error("Could not log feedback to LangSmith: missing project information. Please contact us for help.");
30
29
  }
31
- await syncExamplePromises.get(exampleId);
30
+ await context.syncExamplePromises?.get(exampleId);
32
31
  await client?.logEvaluationFeedback({
33
32
  evaluatorResponse: feedback,
34
33
  run: runTree,
@@ -300,6 +300,7 @@ function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
300
300
  },
301
301
  enableTestTracking: experimentConfig?.enableTestTracking,
302
302
  setupPromise,
303
+ syncExamplePromises: new Map(),
303
304
  };
304
305
  beforeAll(async () => {
305
306
  const storageValue = await runDatasetSetup(context);
@@ -307,15 +308,14 @@ function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
307
308
  setupPromiseResolver();
308
309
  });
309
310
  afterAll(async () => {
311
+ const examples = await Promise.all(context.syncExamplePromises.values());
310
312
  await Promise.all([
311
313
  client.awaitPendingTraceBatches(),
312
- ...globals_js_1.syncExamplePromises.values(),
313
314
  ...globals_js_1.evaluatorLogFeedbackPromises.values(),
314
315
  ]);
315
316
  if (!(0, globals_js_1.trackingEnabled)(context)) {
316
317
  return;
317
318
  }
318
- const examples = [...globals_js_1.syncExamplePromises.values()];
319
319
  if (examples.length === 0) {
320
320
  return;
321
321
  }
@@ -364,6 +364,7 @@ function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
364
364
  await client.updateProject(datasetInfo.project.id, {
365
365
  metadata: {
366
366
  ...suiteMetadata,
367
+ dataset_version: finalModifiedAt,
367
368
  commit,
368
369
  branch,
369
370
  dirty,
@@ -546,8 +547,11 @@ function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
546
547
  // Currently run end time has to be after example modified time
547
548
  // for examples to render properly, so we must modify the example
548
549
  // first before running the test.
549
- if (globals_js_1.syncExamplePromises.get(exampleId) === undefined) {
550
- globals_js_1.syncExamplePromises.set(exampleId, await syncExample({
550
+ if (context.syncExamplePromises === undefined) {
551
+ throw new Error("Internal error: syncExamplePromises not initialized. This is a bug in the LangSmith SDK — please open an issue at https://github.com/langchain-ai/langsmith-sdk/issues.");
552
+ }
553
+ if (!context.syncExamplePromises.has(exampleId)) {
554
+ const syncPromise = syncExample({
551
555
  client,
552
556
  exampleId,
553
557
  datasetId: dataset.id,
@@ -556,8 +560,10 @@ function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
556
560
  metadata,
557
561
  split,
558
562
  createdAt,
559
- }));
563
+ });
564
+ context.syncExamplePromises.set(exampleId, syncPromise);
560
565
  }
566
+ await context.syncExamplePromises.get(exampleId);
561
567
  const traceableOptions = {
562
568
  reference_example_id: exampleId,
563
569
  project_name: project.name,
@@ -9,7 +9,7 @@ import { execSync } from "child_process";
9
9
  import { getCurrentRunTree, traceable } from "../../traceable.js";
10
10
  import { randomName } from "../../evaluation/_random_name.js";
11
11
  import { toBeRelativeCloseTo, toBeAbsoluteCloseTo, toBeSemanticCloseTo, } from "./matchers.js";
12
- import { evaluatorLogFeedbackPromises, testWrapperAsyncLocalStorageInstance, _logTestFeedback, syncExamplePromises, trackingEnabled, DEFAULT_TEST_CLIENT, } from "./globals.js";
12
+ import { evaluatorLogFeedbackPromises, testWrapperAsyncLocalStorageInstance, _logTestFeedback, trackingEnabled, DEFAULT_TEST_CLIENT, } from "./globals.js";
13
13
  import { wrapExpect } from "./vendor/chain.js";
14
14
  import { getEnvironmentVariable, isJsDom } from "../env.js";
15
15
  import { STRIP_ANSI_REGEX, TEST_ID_DELIMITER, UUID5_NAMESPACE, } from "./constants.js";
@@ -253,6 +253,7 @@ export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
253
253
  },
254
254
  enableTestTracking: experimentConfig?.enableTestTracking,
255
255
  setupPromise,
256
+ syncExamplePromises: new Map(),
256
257
  };
257
258
  beforeAll(async () => {
258
259
  const storageValue = await runDatasetSetup(context);
@@ -260,15 +261,14 @@ export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
260
261
  setupPromiseResolver();
261
262
  });
262
263
  afterAll(async () => {
264
+ const examples = await Promise.all(context.syncExamplePromises.values());
263
265
  await Promise.all([
264
266
  client.awaitPendingTraceBatches(),
265
- ...syncExamplePromises.values(),
266
267
  ...evaluatorLogFeedbackPromises.values(),
267
268
  ]);
268
269
  if (!trackingEnabled(context)) {
269
270
  return;
270
271
  }
271
- const examples = [...syncExamplePromises.values()];
272
272
  if (examples.length === 0) {
273
273
  return;
274
274
  }
@@ -317,6 +317,7 @@ export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
317
317
  await client.updateProject(datasetInfo.project.id, {
318
318
  metadata: {
319
319
  ...suiteMetadata,
320
+ dataset_version: finalModifiedAt,
320
321
  commit,
321
322
  branch,
322
323
  dirty,
@@ -499,8 +500,11 @@ export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
499
500
  // Currently run end time has to be after example modified time
500
501
  // for examples to render properly, so we must modify the example
501
502
  // first before running the test.
502
- if (syncExamplePromises.get(exampleId) === undefined) {
503
- syncExamplePromises.set(exampleId, await syncExample({
503
+ if (context.syncExamplePromises === undefined) {
504
+ throw new Error("Internal error: syncExamplePromises not initialized. This is a bug in the LangSmith SDK — please open an issue at https://github.com/langchain-ai/langsmith-sdk/issues.");
505
+ }
506
+ if (!context.syncExamplePromises.has(exampleId)) {
507
+ const syncPromise = syncExample({
504
508
  client,
505
509
  exampleId,
506
510
  datasetId: dataset.id,
@@ -509,8 +513,10 @@ export function generateWrapperFromJestlikeMethods(methods, testRunnerName) {
509
513
  metadata,
510
514
  split,
511
515
  createdAt,
512
- }));
516
+ });
517
+ context.syncExamplePromises.set(exampleId, syncPromise);
513
518
  }
519
+ await context.syncExamplePromises.get(exampleId);
514
520
  const traceableOptions = {
515
521
  reference_example_id: exampleId,
516
522
  project_name: project.name,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "langsmith",
3
- "version": "0.8.5",
3
+ "version": "0.8.6",
4
4
  "description": "Client library to connect to the LangSmith Observability and Evaluation Platform.",
5
5
  "packageManager": "pnpm@10.33.0",
6
6
  "files": [
@@ -156,12 +156,18 @@
156
156
  "p-queue": "6.6.2"
157
157
  },
158
158
  "devDependencies": {
159
+ "@ai-sdk/anthropic": "4.0.18",
160
+ "@ai-sdk/harness": "1.0.41",
161
+ "@ai-sdk/harness-claude-code": "1.0.42",
162
+ "@ai-sdk/harness-pi": "1.0.41",
159
163
  "@ai-sdk/openai": "4.0.4",
160
164
  "@ai-sdk/provider": "4.0.1",
161
- "@ai-sdk/anthropic": "4.0.0-beta.67",
165
+ "@ai-sdk/sandbox-just-bash": "1.0.40",
166
+ "@ai-sdk/sandbox-vercel": "1.0.41",
162
167
  "@anthropic-ai/claude-agent-sdk": "^0.3.150",
163
168
  "@anthropic-ai/sdk": "^0.98.0",
164
169
  "@babel/preset-env": "^8.0.2",
170
+ "@earendil-works/pi-coding-agent": "0.80.10",
165
171
  "@faker-js/faker": "^8.4.1",
166
172
  "@google/genai": "^2.0.1",
167
173
  "@jest/globals": "^30.4.1",
@@ -184,7 +190,7 @@
184
190
  "@types/ws": "^8.18.1",
185
191
  "@typescript-eslint/eslint-plugin": "^8.62.1",
186
192
  "@typescript-eslint/parser": "^8.62.1",
187
- "ai": "7.0.0-beta.178",
193
+ "ai": "7.0.36",
188
194
  "babel-jest": "^30.2.0",
189
195
  "cross-env": "^10.1.0",
190
196
  "dotenv": "^17.3.1",