langsmith 0.8.4 → 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);