langsmith 0.4.8 → 0.4.9

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.
@@ -1,567 +1,97 @@
1
- import { traceable, getCurrentRunTree, isTraceableFunction, } from "../../traceable.js";
2
- import { convertAnthropicUsageToInputTokenDetails } from "../../utils/usage.js";
3
- import { getNumberProperty } from "./utils.js";
4
- const createQueryContext = () => ({
5
- activeToolRuns: new Map(),
6
- clientManagedRuns: new Map(),
7
- subagentSessions: new Map(),
8
- activeSubagentToolUseId: undefined,
9
- currentParentRun: undefined,
10
- });
11
- /**
12
- * PreToolUse hook that creates a tool span when a tool execution starts.
13
- * This traces ALL tools including built-in tools, external MCP tools, and SDK MCP tools.
14
- * Skips tools that are client-managed (subagent sessions and their children).
15
- */
16
- async function preToolUseHook(input, toolUseId, context) {
17
- if (!toolUseId)
18
- return {};
19
- // Skip if this tool run is already managed by the client (subagent or its children)
20
- if (context.clientManagedRuns.has(toolUseId)) {
21
- return {};
22
- }
23
- const toolName = input.tool_name || "unknown_tool";
24
- const toolInput = input.tool_input;
25
- try {
26
- const parent = context.currentParentRun || getCurrentRunTree();
27
- if (!parent) {
28
- return {};
29
- }
30
- const startTime = Date.now();
31
- const toolRun = await parent.createChild({
32
- name: toolName,
33
- run_type: "tool",
34
- inputs: toolInput ? { input: toolInput } : {},
35
- });
36
- await toolRun.postRun();
37
- context.activeToolRuns.set(toolUseId, { run: toolRun, startTime });
38
- }
39
- catch {
40
- // Silently fail - don't interrupt tool execution
41
- }
42
- return {};
43
- }
44
- /**
45
- * PostToolUse hook that ends the tool span when a tool execution completes.
46
- * Handles both regular tool runs and client-managed runs (subagents and their children).
47
- */
48
- async function postToolUseHook(input, toolUseId, context) {
49
- if (!toolUseId)
50
- return {};
51
- const toolResponse = input.tool_response;
52
- // Format outputs based on response type
53
- const formatOutputs = (response) => {
54
- let outputs;
55
- if (typeof response === "object" && response !== null) {
56
- if (Array.isArray(response)) {
57
- outputs = { content: response };
58
- }
59
- else {
60
- outputs = response;
61
- }
62
- }
63
- else {
64
- outputs = response ? { output: String(response) } : {};
65
- }
66
- const isError = typeof response === "object" &&
67
- response !== null &&
68
- "is_error" in response &&
69
- response.is_error === true;
70
- return { outputs, isError };
71
- };
72
- try {
73
- // Check if this is a client-managed run (subagent session or its children)
74
- const clientRun = context.clientManagedRuns.get(toolUseId);
75
- if (clientRun) {
76
- context.clientManagedRuns.delete(toolUseId);
77
- const { outputs, isError } = formatOutputs(toolResponse);
78
- await clientRun.end({
79
- outputs,
80
- error: isError ? outputs.output?.toString() : undefined,
81
- });
82
- await clientRun.patchRun();
83
- return {};
84
- }
85
- // Handle regular tool runs
86
- const runInfo = context.activeToolRuns.get(toolUseId);
87
- if (!runInfo) {
88
- return {};
89
- }
90
- context.activeToolRuns.delete(toolUseId);
91
- const { run: toolRun } = runInfo;
92
- const { outputs, isError } = formatOutputs(toolResponse);
93
- await toolRun.end({
94
- outputs,
95
- error: isError ? outputs.output?.toString() : undefined,
96
- });
97
- await toolRun.patchRun();
98
- }
99
- catch {
100
- // Silently fail - don't interrupt tool execution
101
- }
102
- return {};
103
- }
104
- /**
105
- * Creates hook matchers for LangSmith tracing.
106
- * Returns PreToolUse and PostToolUse hook configurations.
107
- */
108
- function createTracingHooks(context) {
109
- return {
110
- PreToolUse: [
111
- {
112
- matcher: undefined, // Match all tools
113
- hooks: [
114
- async (input, toolUseId, _options) => preToolUseHook(input, toolUseId, context),
115
- ],
116
- },
117
- ],
118
- PostToolUse: [
119
- {
120
- matcher: undefined, // Match all tools
121
- hooks: [
122
- async (input, toolUseId, _options) => postToolUseHook(input, toolUseId, context),
123
- ],
124
- },
125
- ],
126
- SessionEnd: [
127
- {
128
- matcher: undefined,
129
- hooks: [
130
- async (_input) => {
131
- // Clean up at end of session
132
- clearActiveToolRuns(context);
133
- return {};
134
- },
135
- ],
136
- },
137
- ],
138
- SubagentStop: [
139
- {
140
- matcher: undefined,
141
- hooks: [
142
- async (_input, toolUseId) => {
143
- // Clean up subagent session
144
- if (toolUseId) {
145
- context.subagentSessions.delete(toolUseId);
146
- context.clientManagedRuns.delete(toolUseId);
147
- }
148
- return {};
149
- },
150
- ],
151
- },
152
- ],
153
- Stop: [
154
- {
155
- matcher: undefined,
156
- hooks: [
157
- async (_input) => {
158
- // Clean up on stop - ensure all runs are finalized
159
- clearActiveToolRuns(context);
160
- return {};
161
- },
162
- ],
163
- },
164
- ],
165
- };
166
- }
167
- /**
168
- * Merges LangSmith tracing hooks with existing user hooks.
169
- */
170
- function mergeHooks(existingHooks, context) {
171
- const tracingHooks = createTracingHooks(context);
172
- if (!existingHooks)
173
- return tracingHooks;
174
- const merged = { ...existingHooks };
175
- // Prepend tracing hooks so they run first
176
- for (const [event, matchers] of Object.entries(tracingHooks)) {
177
- merged[event] = [...matchers, ...(merged[event] ?? [])];
178
- }
179
- return merged;
180
- }
181
- /**
182
- * Type assertion to check if a tool is a Task tool
183
- * @param tool - The tool to check
184
- * @returns True if the tool is a Task tool, false otherwise
185
- */
186
- function isTaskTool(tool) {
187
- return tool.type === "tool_use" && tool.name === "Task";
188
- }
189
- /**
190
- * Type-assertion to check for tool blocks
191
- */
192
- function isToolBlock(block) {
193
- if (!block || typeof block !== "object")
194
- return false;
195
- return block.type === "tool_use";
196
- }
197
- /**
198
- * Processes tool uses in an AssistantMessage to detect and create subagent sessions.
199
- * This matches Python's _handle_assistant_tool_uses behavior.
200
- *
201
- * @param message - The AssistantMessage to process
202
- * @param parentRun - The parent run tree (main conversation chain)
203
- */
204
- async function handleAssistantToolUses(message, parentRun, context) {
205
- if (!parentRun)
206
- return;
207
- const content = message.message?.content;
208
- if (!Array.isArray(content))
209
- return;
210
- const parentToolUseId = message.parent_tool_use_id;
211
- for (const block of content) {
212
- if (!isToolBlock(block) || !block.id)
213
- continue;
214
- try {
215
- // Check if this is a Task tool (subagent) at the top level
216
- if (isTaskTool(block) && !parentToolUseId) {
217
- // Extract subagent name from input
218
- const subagentName = block.input.subagent_type ||
219
- block.input.agent_type ||
220
- (block.input.description
221
- ? block.input.description.split(" ")[0]
222
- : null) ||
223
- "unknown-agent";
224
- const subagentSession = await parentRun.createChild({
225
- name: subagentName,
226
- run_type: "chain",
227
- inputs: block.input,
228
- });
229
- // Post the run to start it, but DON'T end it yet
230
- // It will be ended when we receive the tool result or at cleanup
231
- await subagentSession.postRun();
232
- // Store in both maps
233
- context.subagentSessions.set(block.id, subagentSession);
234
- context.clientManagedRuns.set(block.id, subagentSession);
235
- }
236
- // Check if tool use is within a subagent
237
- else if (parentToolUseId &&
238
- context.subagentSessions.has(parentToolUseId)) {
239
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
240
- const subagentSession = context.subagentSessions.get(parentToolUseId);
241
- // Create tool run as child of subagent
242
- const toolRun = await subagentSession.createChild({
243
- name: block.name || "unknown_tool",
244
- run_type: "tool",
245
- inputs: block.input ? { input: block.input } : {},
246
- });
247
- await toolRun.postRun();
248
- context.clientManagedRuns.set(block.id, toolRun);
249
- }
250
- }
251
- catch {
252
- // Silently fail - don't interrupt message processing
253
- }
254
- }
255
- }
256
- /**
257
- * Clears all active tool runs and client-managed runs. Called when a conversation ends.
258
- */
259
- function clearActiveToolRuns(context) {
260
- // Clean up client-managed runs (subagents and their children)
261
- for (const [, run] of context.clientManagedRuns) {
262
- try {
263
- run
264
- .end({ error: "Run not completed (conversation ended)" })
265
- .then(() => run.patchRun())
266
- .catch(() => { });
267
- }
268
- catch {
269
- // Ignore cleanup errors
270
- }
271
- }
272
- context.clientManagedRuns.clear();
273
- context.subagentSessions.clear();
274
- context.activeSubagentToolUseId = undefined;
275
- // Clean up regular tool runs
276
- for (const [, { run }] of context.activeToolRuns) {
277
- try {
278
- run
279
- .end({ error: "Tool run not completed (conversation ended)" })
280
- .then(() => run.patchRun())
281
- .catch(() => { });
282
- }
283
- catch {
284
- // Ignore cleanup errors
285
- }
286
- }
287
- context.activeToolRuns.clear();
288
- }
1
+ import { traceable, isTraceableFunction } from "../../traceable.js";
2
+ import { StreamManager } from "./context.js";
3
+ import { convertFromAnthropicMessage } from "./messages.js";
289
4
  /**
290
5
  * Wraps the Claude Agent SDK's query function to add LangSmith tracing.
291
6
  * Traces the entire agent interaction including all streaming messages.
292
- * Internal use only - use wrapClaudeAgentSDK instead.
7
+ * @internal Use `wrapClaudeAgentSDK` instead.
293
8
  */
294
9
  function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
295
- const getModifiedArgs = (args, context) => {
296
- const params = (args[0] ?? {});
297
- const { prompt, options = {} } = params;
298
- // Inject LangSmith tracing hooks into options
299
- const mergedHooks = mergeHooks(options.hooks, context);
300
- const modifiedOptions = { ...options, hooks: mergedHooks };
301
- const modifiedParams = { ...params, options: modifiedOptions };
302
- return {
303
- prompt,
304
- options: modifiedOptions,
305
- modifiedArgs: [modifiedParams, ...args.slice(1)],
306
- };
307
- };
308
- async function* generator(originalGenerator, prompt, options, context) {
309
- const finalResults = [];
310
- // Track assistant messages by their message ID for proper streaming handling
311
- // Each message ID maps to { message, startTime } - we keep the latest streaming update
312
- const pendingMessages = new Map();
313
- // Track which message IDs have already had spans created
314
- // This prevents creating duplicate spans when the SDK sends multiple updates
315
- // for the same message ID with stop_reason set
316
- const completedMessageIds = new Set();
317
- // Store child run promises for proper async handling
318
- const childRunEndPromises = [];
319
- // Track usage from ResultMessage to add to the parent span
320
- let resultUsage;
321
- // Track additional metadata from the SDK
322
- const extraMetadata = [];
323
- // Track usage from completed assistant message spans (by model)
324
- // Used to calculate remaining tokens for pending messages
325
- const completedUsageByModel = new Map();
326
- // Create an LLM span for a specific message ID
327
- const createLLMSpanForId = async (messageId) => {
328
- // Skip if we've already created a span for this message ID
329
- if (completedMessageIds.has(messageId)) {
330
- return;
331
- }
332
- const pending = pendingMessages.get(messageId);
333
- if (!pending)
334
- return;
335
- pendingMessages.delete(messageId);
336
- completedMessageIds.add(messageId);
337
- // Track the usage before creating the span
338
- const model = pending.message.message?.model;
339
- const usage = pending.message.message?.usage;
340
- if (model && usage) {
341
- const existing = completedUsageByModel.get(model) || {
342
- inputTokens: 0,
343
- outputTokens: 0,
344
- cacheReadTokens: 0,
345
- cacheCreationTokens: 0,
346
- };
347
- existing.inputTokens += usage.input_tokens || 0;
348
- existing.outputTokens += usage.output_tokens || 0;
349
- existing.cacheReadTokens += usage.cache_read_input_tokens || 0;
350
- existing.cacheCreationTokens += usage.cache_creation_input_tokens || 0;
351
- completedUsageByModel.set(model, existing);
352
- }
353
- const finalMessageContent = await createLLMSpanForMessages([pending.message], pending.messageHistory, options, pending.startTime, context);
354
- if (finalMessageContent)
355
- finalResults.push(finalMessageContent);
356
- };
10
+ async function* generator(originalGenerator, prompt) {
11
+ const streamManager = new StreamManager();
357
12
  try {
13
+ let systemCount = 0;
358
14
  for await (const message of originalGenerator) {
359
- const currentTime = Date.now();
360
15
  if (message.type === "system") {
361
- const content = getLatestInput(prompt);
16
+ const content = getLatestInput(prompt, systemCount);
17
+ systemCount += 1;
362
18
  if (content != null)
363
- finalResults.push(content);
364
- }
365
- // Handle assistant messages - group by message ID for streaming
366
- // Multiple messages with the same ID are streaming updates; use the last one
367
- if (message.type === "assistant") {
368
- const messageId = message.message?.id;
369
- // If we have an active subagent context and this message doesn't have parent_tool_use_id,
370
- // check if this is a new main conversation message (which would end the subagent execution)
371
- if (context.activeSubagentToolUseId && !message.parent_tool_use_id) {
372
- // Check if this message contains tool uses - if it does, it's part of main conversation
373
- const content = message.message?.content;
374
- if (Array.isArray(content)) {
375
- const hasToolUse = content.some((block) => block &&
376
- typeof block === "object" &&
377
- block.type === "tool_use");
378
- // If this message has tool uses and none are within the subagent, it's a new turn
379
- if (hasToolUse) {
380
- // Clean up the subagent session
381
- context.subagentSessions.delete(context.activeSubagentToolUseId);
382
- context.activeSubagentToolUseId = undefined;
383
- }
384
- }
385
- }
386
- if (messageId) {
387
- // Check if this is a new message or an update to existing
388
- const existing = pendingMessages.get(messageId);
389
- if (!existing) {
390
- // New message arrived - finalize all OTHER pending messages first
391
- // (they must be complete if we're seeing a new message)
392
- // Finalize all other pending messages
393
- for (const [otherId] of pendingMessages) {
394
- if (otherId !== messageId) {
395
- const spanPromise = createLLMSpanForId(otherId);
396
- childRunEndPromises.push(spanPromise);
397
- }
398
- }
399
- pendingMessages.set(messageId, {
400
- message,
401
- messageHistory: finalResults.slice(0),
402
- startTime: currentTime,
403
- });
404
- }
405
- else {
406
- // Streaming update - keep the start time, update the message
407
- pendingMessages.set(messageId, {
408
- message,
409
- messageHistory: finalResults.slice(0),
410
- startTime: existing.startTime,
411
- });
412
- }
413
- // Push the message to the final results,
414
- // Used to create spans with the full chat history as input
415
- if ("content" in message.message && message.message.content) {
416
- finalResults.push({
417
- content: flattenContentBlocks(message.message.content),
418
- role: "assistant",
419
- });
420
- }
421
- // Check if this message has a stop_reason (meaning it's complete)
422
- // If so, create the span now (createLLMSpanForId will skip if already created)
423
- if (message.message?.stop_reason) {
424
- const spanPromise = createLLMSpanForId(messageId);
425
- childRunEndPromises.push(spanPromise);
426
- }
427
- }
428
- // Process tool uses for subagent detection (matches Python's _handle_assistant_tool_uses)
429
- await handleAssistantToolUses(message, context.currentParentRun, context);
430
- }
431
- // Handle UserMessage - add to conversation history (matches Python)
432
- if (message.type === "user") {
433
- if ("content" in message.message && message.message.content) {
434
- finalResults.push({
435
- content: flattenContentBlocks(message.message.content),
436
- role: "user",
437
- });
438
- }
439
- // If this is a tool result for a Task tool (subagent), we're entering the subagent's execution
440
- // The subagent's assistant messages will come AFTER this result
441
- if (message.parent_tool_use_id &&
442
- context.subagentSessions.has(message.parent_tool_use_id)) {
443
- context.activeSubagentToolUseId = message.parent_tool_use_id;
444
- }
445
- }
446
- // Handle ResultMessage - extract usage and metadata
447
- if (message.type === "result") {
448
- // If modelUsage is available, aggregate from it (includes ALL models)
449
- // Otherwise fall back to top-level usage field
450
- if (message.modelUsage) {
451
- // Aggregate usage from modelUsage (includes ALL models)
452
- resultUsage = aggregateUsageFromModelUsage(message.modelUsage);
453
- // Patch token counts for pending messages using modelUsage
454
- // This handles the SDK limitation where the last assistant message
455
- // doesn't receive final streaming updates with accurate token counts
456
- for (const [, { message: pendingMsg }] of pendingMessages) {
457
- const model = pendingMsg.message?.model;
458
- if (model &&
459
- message.modelUsage[model] &&
460
- pendingMsg.message?.usage) {
461
- const modelStats = message.modelUsage[model];
462
- const completed = completedUsageByModel.get(model) || {
463
- inputTokens: 0,
464
- outputTokens: 0,
465
- cacheReadTokens: 0,
466
- cacheCreationTokens: 0,
467
- };
468
- // Calculate remaining tokens = total - completed
469
- const remainingOutput = (modelStats.outputTokens || 0) - completed.outputTokens;
470
- const remainingInput = (modelStats.inputTokens || 0) - completed.inputTokens;
471
- const remainingCacheRead = (modelStats.cacheReadInputTokens || 0) -
472
- completed.cacheReadTokens;
473
- const remainingCacheCreation = (modelStats.cacheCreationInputTokens || 0) -
474
- completed.cacheCreationTokens;
475
- // Update the pending message's usage with remaining tokens
476
- pendingMsg.message.usage.output_tokens = Math.max(0, remainingOutput);
477
- pendingMsg.message.usage.input_tokens = Math.max(0, remainingInput);
478
- if (remainingCacheRead > 0) {
479
- pendingMsg.message.usage.cache_read_input_tokens =
480
- remainingCacheRead;
481
- }
482
- if (remainingCacheCreation > 0) {
483
- pendingMsg.message.usage.cache_creation_input_tokens =
484
- remainingCacheCreation;
485
- }
486
- }
487
- }
488
- }
489
- else if (message.usage) {
490
- // Fall back to top-level usage if modelUsage not available
491
- resultUsage = extractUsageFromMessage(message);
492
- }
493
- // Add total_cost if available (LangSmith standard field)
494
- if (message.total_cost_usd != null && resultUsage) {
495
- resultUsage.total_cost = message.total_cost_usd;
496
- }
497
- // Add conversation-level metadata
498
- if (message.is_error != null) {
499
- extraMetadata.push(["is_error", message.is_error]);
500
- }
501
- if (message.num_turns != null) {
502
- extraMetadata.push(["num_turns", message.num_turns]);
503
- }
504
- if (message.session_id != null) {
505
- extraMetadata.push(["session_id", message.session_id]);
506
- }
507
- if (message.duration_ms != null) {
508
- extraMetadata.push(["duration_ms", message.duration_ms]);
509
- }
510
- if (message.duration_api_ms != null) {
511
- extraMetadata.push(["duration_api_ms", message.duration_api_ms]);
512
- }
19
+ streamManager.addMessage(content);
513
20
  }
21
+ streamManager.addMessage(message);
514
22
  yield message;
515
23
  }
516
- // Create spans for any remaining pending messages (those without stop_reason)
517
- for (const messageId of pendingMessages.keys()) {
518
- const spanPromise = createLLMSpanForId(messageId);
519
- childRunEndPromises.push(spanPromise);
520
- }
521
- // Wait for all child runs to complete
522
- await Promise.all(childRunEndPromises);
523
- // Apply usage metadata to the chain run using LangSmith's standard fields
524
- const currentRun = getCurrentRunTree();
525
- if (currentRun && (resultUsage || extraMetadata.length > 0)) {
526
- // Initialize metadata object if needed
527
- currentRun.extra ||= {};
528
- currentRun.extra.metadata ||= {};
529
- if (resultUsage) {
530
- // Add LangSmith-standard usage fields directly to metadata
531
- if (resultUsage.input_tokens !== undefined) {
532
- currentRun.extra.metadata.input_tokens = resultUsage.input_tokens;
533
- }
534
- if (resultUsage.output_tokens !== undefined) {
535
- currentRun.extra.metadata.output_tokens = resultUsage.output_tokens;
536
- }
537
- if (resultUsage.total_tokens !== undefined) {
538
- currentRun.extra.metadata.total_tokens = resultUsage.total_tokens;
539
- }
540
- if (resultUsage.input_token_details) {
541
- currentRun.extra.metadata.input_token_details =
542
- resultUsage.input_token_details;
543
- }
544
- if (resultUsage.total_cost !== undefined) {
545
- currentRun.extra.metadata.total_cost = resultUsage.total_cost;
546
- }
547
- }
548
- for (const [key, value] of extraMetadata) {
549
- currentRun.extra.metadata[key] = value;
550
- }
551
- }
552
24
  }
553
25
  finally {
554
- // Clean up parent run reference and any orphaned tool runs
555
- context.currentParentRun = undefined;
556
- clearActiveToolRuns(context);
557
- }
26
+ await streamManager.finish();
27
+ }
28
+ }
29
+ function getLatestInput(arg, systemCount) {
30
+ const value = (() => {
31
+ if (typeof arg !== "object" || arg == null)
32
+ return arg;
33
+ const toJSON = arg["toJSON"];
34
+ if (typeof toJSON !== "function")
35
+ return undefined;
36
+ const latest = toJSON();
37
+ return latest?.at(systemCount);
38
+ })();
39
+ if (value == null)
40
+ return undefined;
41
+ if (typeof value === "string") {
42
+ return {
43
+ type: "user",
44
+ message: { content: value, role: "user" },
45
+ parent_tool_use_id: null,
46
+ session_id: "",
47
+ };
48
+ }
49
+ return typeof value === "object" && value != null ? value : undefined;
50
+ }
51
+ async function processInputs(rawInputs) {
52
+ const inputs = rawInputs;
53
+ const newInputs = { ...inputs };
54
+ return Object.assign(newInputs, {
55
+ toJSON: () => {
56
+ const toJSON = (value) => {
57
+ if (typeof value !== "object" || value == null)
58
+ return value;
59
+ const fn = value?.toJSON;
60
+ if (typeof fn === "function")
61
+ return fn();
62
+ return value;
63
+ };
64
+ const prompt = toJSON(inputs.prompt);
65
+ const options = inputs.options != null
66
+ ? { ...inputs.options }
67
+ : undefined;
68
+ if (options?.mcpServers != null) {
69
+ options.mcpServers = Object.fromEntries(Object.entries(options.mcpServers ?? {}).map(([key, value]) => [
70
+ key,
71
+ { name: value.name, type: value.type },
72
+ ]));
73
+ }
74
+ return { messages: convertFromAnthropicMessage(prompt), options };
75
+ },
76
+ });
558
77
  }
559
- const wrapped = (...args) => {
560
- const context = createQueryContext();
561
- context.currentParentRun = getCurrentRunTree();
562
- const { prompt, options, modifiedArgs } = getModifiedArgs(args, context);
563
- const actualGenerator = queryFn.call(defaultThis, ...modifiedArgs);
564
- const wrappedGenerator = generator(actualGenerator, prompt, options, context);
78
+ function processOutputs(rawOutputs) {
79
+ if ("outputs" in rawOutputs && Array.isArray(rawOutputs.outputs)) {
80
+ const sdkMessages = rawOutputs.outputs;
81
+ const messages = sdkMessages
82
+ .filter((message) => {
83
+ if (!("message" in message))
84
+ return true;
85
+ return message.parent_tool_use_id == null;
86
+ })
87
+ .flatMap(convertFromAnthropicMessage);
88
+ return { output: { messages } };
89
+ }
90
+ return rawOutputs;
91
+ }
92
+ return traceable((params, ...args) => {
93
+ const actualGenerator = queryFn.call(defaultThis, params, ...args);
94
+ const wrappedGenerator = generator(actualGenerator, params.prompt);
565
95
  for (const method of Object.getOwnPropertyNames(Object.getPrototypeOf(actualGenerator)).filter((method) => !["constructor", "next", "throw", "return"].includes(method))) {
566
96
  Object.defineProperty(wrappedGenerator, method, {
567
97
  get() {
@@ -573,318 +103,14 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
573
103
  });
574
104
  }
575
105
  return wrappedGenerator;
576
- };
577
- // Wrap in traceable
578
- return traceable(wrapped, {
106
+ }, {
579
107
  name: "claude.conversation",
580
108
  run_type: "chain",
581
109
  ...baseConfig,
582
110
  metadata: { ...baseConfig?.metadata },
583
111
  __deferredSerializableArgOptions: { maxDepth: 1 },
584
- async processInputs(rawInputs) {
585
- const inputs = rawInputs;
586
- const newInputs = { ...inputs };
587
- return Object.assign(newInputs, {
588
- toJSON: () => {
589
- const toJSON = (value) => {
590
- if (typeof value !== "object" || value == null)
591
- return value;
592
- const fn = value?.toJSON;
593
- if (typeof fn === "function")
594
- return fn();
595
- return value;
596
- };
597
- const prompt = toJSON(inputs.prompt);
598
- const options = toJSON(inputs.options);
599
- const messages = (() => {
600
- if (prompt == null)
601
- return undefined;
602
- const result = [];
603
- if (typeof prompt === "string") {
604
- result.push({ content: prompt, role: "user" });
605
- }
606
- else {
607
- for (const { message } of prompt) {
608
- if (!message)
609
- continue;
610
- result.push({
611
- content: flattenContentBlocks(message.content),
612
- role: message.role,
613
- });
614
- }
615
- }
616
- return result;
617
- })();
618
- return { messages, options };
619
- },
620
- });
621
- },
622
- processOutputs(rawOutputs) {
623
- if ("outputs" in rawOutputs && Array.isArray(rawOutputs.outputs)) {
624
- const sdkMessages = rawOutputs.outputs;
625
- const messages = sdkMessages.flatMap((sdkMessage) => {
626
- if ("message" in sdkMessage && sdkMessage.message != null) {
627
- return {
628
- role: sdkMessage.message.role,
629
- content: flattenContentBlocks(sdkMessage.message.content),
630
- };
631
- }
632
- return [];
633
- });
634
- return { output: { messages } };
635
- }
636
- return rawOutputs;
637
- },
638
- });
639
- }
640
- /**
641
- * Wraps a Claude Agent SDK tool definition to add LangSmith tracing for tool executions.
642
- * Internal use only - use wrapClaudeAgentSDK instead.
643
- */
644
- function wrapClaudeAgentTool(toolDef, baseConfig) {
645
- return {
646
- ...toolDef,
647
- handler: traceable(toolDef.handler, {
648
- name: toolDef.name,
649
- run_type: "tool",
650
- ...baseConfig,
651
- }),
652
- };
653
- }
654
- /**
655
- * Aggregates usage from modelUsage breakdown (includes all models, including hidden ones).
656
- * This provides accurate totals when multiple models are used.
657
- */
658
- function aggregateUsageFromModelUsage(modelUsage) {
659
- const metrics = {};
660
- let totalInputTokens = 0;
661
- let totalOutputTokens = 0;
662
- let totalCacheReadTokens = 0;
663
- let totalCacheCreationTokens = 0;
664
- // Aggregate across all models
665
- for (const modelStats of Object.values(modelUsage)) {
666
- totalInputTokens += modelStats.inputTokens || 0;
667
- totalOutputTokens += modelStats.outputTokens || 0;
668
- totalCacheReadTokens += modelStats.cacheReadInputTokens || 0;
669
- totalCacheCreationTokens += modelStats.cacheCreationInputTokens || 0;
670
- }
671
- // Build input_token_details if we have cache tokens
672
- if (totalCacheReadTokens > 0 || totalCacheCreationTokens > 0) {
673
- metrics.input_token_details = {
674
- cache_read: totalCacheReadTokens,
675
- cache_creation: totalCacheCreationTokens,
676
- };
677
- }
678
- // Sum all input tokens (new + cache read + cache creation)
679
- const totalPromptTokens = totalInputTokens + totalCacheReadTokens + totalCacheCreationTokens;
680
- metrics.input_tokens = totalPromptTokens;
681
- metrics.output_tokens = totalOutputTokens;
682
- metrics.total_tokens = totalPromptTokens + totalOutputTokens;
683
- return metrics;
684
- }
685
- /**
686
- * Extracts and normalizes usage metrics from a Claude Agent SDK message.
687
- */
688
- function extractUsageFromMessage(message) {
689
- const metrics = {};
690
- // Assistant messages contain usage in message.message.usage
691
- // Result messages contain usage in message.usage
692
- let usage;
693
- if (message.type === "assistant") {
694
- usage = message.message?.usage;
695
- }
696
- else if (message.type === "result") {
697
- usage = message.usage;
698
- }
699
- if (!usage || typeof usage !== "object") {
700
- return metrics;
701
- }
702
- // Standard token counts - use LangSmith's expected field names
703
- const inputTokens = getNumberProperty(usage, "input_tokens") || 0;
704
- const outputTokens = getNumberProperty(usage, "output_tokens") || 0;
705
- // Get cache tokens
706
- const cacheRead = getNumberProperty(usage, "cache_read_input_tokens") || 0;
707
- const cacheCreation = getNumberProperty(usage, "cache_creation_input_tokens") || 0;
708
- // Build input_token_details if we have cache tokens
709
- if (cacheRead > 0 || cacheCreation > 0) {
710
- const inputTokenDetails = convertAnthropicUsageToInputTokenDetails(usage);
711
- if (Object.keys(inputTokenDetails).length > 0) {
712
- metrics.input_token_details = inputTokenDetails;
713
- }
714
- }
715
- // Sum cache tokens into input_tokens total (matching Python's sum_anthropic_tokens)
716
- const totalInputTokens = inputTokens + cacheRead + cacheCreation;
717
- metrics.input_tokens = totalInputTokens;
718
- metrics.output_tokens = outputTokens;
719
- metrics.total_tokens = totalInputTokens + outputTokens;
720
- return metrics;
721
- }
722
- function getLatestInput(arg) {
723
- const value = (() => {
724
- if (typeof arg !== "object" || arg == null)
725
- return arg;
726
- const toJSON = arg["toJSON"];
727
- if (typeof toJSON !== "function")
728
- return undefined;
729
- const latest = toJSON();
730
- return latest?.at(-1);
731
- })();
732
- if (typeof value == null)
733
- return undefined;
734
- if (typeof value === "string")
735
- return { content: value, role: "user" };
736
- const userMessage = value;
737
- if (typeof userMessage === "string") {
738
- return { content: userMessage, role: "user" };
739
- }
740
- if (typeof userMessage !== "object" || userMessage == null) {
741
- return undefined;
742
- }
743
- return {
744
- role: userMessage.message.role || "user",
745
- content: flattenContentBlocks(userMessage.message.content),
746
- };
747
- }
748
- /**
749
- * Creates an LLM span for a group of messages with the same message ID.
750
- * Returns the final message content to add to conversation history.
751
- * Handles subagent LLM turns by parenting them to the correct subagent session.
752
- */
753
- async function createLLMSpanForMessages(messages, conversationHistory, options, startTime, context) {
754
- if (messages.length === 0)
755
- return undefined;
756
- const lastMessage = messages[messages.length - 1];
757
- // Create LLM spans for all AssistantMessages, not just those with usage
758
- // (matches Python's behavior)
759
- if (lastMessage.type !== "assistant") {
760
- return undefined;
761
- }
762
- // Extract model from message first, fall back to options (matches Python)
763
- const model = lastMessage.message.model || options.model;
764
- const usage = extractUsageFromMessage(lastMessage);
765
- const input = conversationHistory.length > 0 ? conversationHistory : undefined;
766
- // Flatten content blocks for proper serialization (matches Python)
767
- const outputs = messages
768
- .map((m) => {
769
- if (!("message" in m) || !("role" in m.message))
770
- return undefined;
771
- return {
772
- content: flattenContentBlocks(m.message.content),
773
- role: m.message.role,
774
- };
775
- })
776
- .filter((c) => c !== undefined);
777
- // Check if this message belongs to a subagent
778
- // First check if message has explicit parent_tool_use_id
779
- const parentToolUseId = lastMessage.parent_tool_use_id;
780
- let subagentParent = parentToolUseId
781
- ? context.subagentSessions.get(parentToolUseId)
782
- : undefined;
783
- // If no explicit parent, check if we're in an active subagent context
784
- if (!subagentParent && context.activeSubagentToolUseId) {
785
- subagentParent = context.subagentSessions.get(context.activeSubagentToolUseId);
786
- }
787
- const endTime = Date.now();
788
- // Format inputs: if we have a single input, use it directly; otherwise wrap as messages
789
- const formattedInputs = input && input.length === 1 ? input[0] : input ? { messages: input } : {};
790
- if (subagentParent) {
791
- // Create LLM run as child of subagent session with proper start and end time
792
- try {
793
- const llmRun = await subagentParent.createChild({
794
- name: "claude.assistant.turn",
795
- run_type: "llm",
796
- inputs: formattedInputs,
797
- outputs: outputs[outputs.length - 1] || { content: outputs },
798
- start_time: startTime,
799
- end_time: endTime,
800
- extra: {
801
- metadata: {
802
- ...(model ? { ls_model_name: model } : {}),
803
- usage_metadata: usage,
804
- },
805
- },
806
- });
807
- await llmRun.postRun();
808
- }
809
- catch {
810
- // Silently fail
811
- }
812
- }
813
- else {
814
- // Regular LLM turn under main conversation
815
- // Note: traceable doesn't support start_time config, so we use getCurrentRunTree
816
- // and manually create the child run to preserve timing
817
- const currentRun = getCurrentRunTree();
818
- if (currentRun) {
819
- try {
820
- const llmRun = await currentRun.createChild({
821
- name: "claude.assistant.turn",
822
- run_type: "llm",
823
- inputs: formattedInputs,
824
- outputs: outputs[outputs.length - 1] || { content: outputs },
825
- start_time: startTime,
826
- end_time: endTime,
827
- extra: {
828
- metadata: {
829
- ...(model ? { ls_model_name: model } : {}),
830
- usage_metadata: usage,
831
- },
832
- },
833
- });
834
- await llmRun.postRun();
835
- }
836
- catch {
837
- // Silently fail
838
- }
839
- }
840
- }
841
- // Return flattened content for conversation history
842
- return lastMessage.message?.content && lastMessage.message?.role
843
- ? {
844
- content: flattenContentBlocks(lastMessage.message.content),
845
- role: lastMessage.message.role,
846
- }
847
- : undefined;
848
- }
849
- /**
850
- * Converts SDK content blocks into serializable objects.
851
- * Matches Python's flatten_content_blocks behavior.
852
- */
853
- function flattenContentBlocks(content) {
854
- if (!Array.isArray(content)) {
855
- return content;
856
- }
857
- return content.map((block) => {
858
- if (!block || typeof block !== "object" || !("type" in block)) {
859
- return block;
860
- }
861
- const blockType = block.type;
862
- switch (blockType) {
863
- case "text":
864
- return { type: "text", text: block.text || "" };
865
- case "thinking":
866
- return {
867
- type: "thinking",
868
- thinking: block.thinking || "",
869
- signature: block.signature || "",
870
- };
871
- case "tool_use":
872
- return {
873
- type: "tool_use",
874
- id: block.id,
875
- name: block.name,
876
- input: block.input,
877
- };
878
- case "tool_result":
879
- return {
880
- type: "tool_result",
881
- tool_use_id: block.tool_use_id,
882
- content: block.content,
883
- is_error: block.is_error || false,
884
- };
885
- default:
886
- return block;
887
- }
112
+ processInputs,
113
+ processOutputs,
888
114
  });
889
115
  }
890
116
  /**
@@ -927,14 +153,7 @@ export function wrapClaudeAgentSDK(sdk, config) {
927
153
  }
928
154
  // Wrap the tool method if it exists
929
155
  if ("tool" in inputSdk && typeof inputSdk.tool === "function") {
930
- const originalTool = inputSdk.tool;
931
- wrappedSdk.tool = function (...args) {
932
- const toolDef = originalTool.apply(sdk, args);
933
- if (toolDef && typeof toolDef === "object" && "handler" in toolDef) {
934
- return wrapClaudeAgentTool(toolDef, config);
935
- }
936
- return toolDef;
937
- };
156
+ wrappedSdk.tool = inputSdk.tool.bind(inputSdk);
938
157
  }
939
158
  // Keep createSdkMcpServer and other methods as-is (bound to original SDK)
940
159
  if ("createSdkMcpServer" in inputSdk &&