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