langsmith 0.5.24 → 0.5.26

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.
Files changed (60) hide show
  1. package/dist/client.cjs +9 -3
  2. package/dist/client.js +9 -3
  3. package/dist/experimental/anthropic/context.cjs +419 -49
  4. package/dist/experimental/anthropic/context.js +420 -50
  5. package/dist/experimental/anthropic/index.cjs +78 -10
  6. package/dist/experimental/anthropic/index.js +80 -12
  7. package/dist/experimental/anthropic/messages.cjs +53 -0
  8. package/dist/experimental/anthropic/messages.d.ts +6 -0
  9. package/dist/experimental/anthropic/messages.js +52 -0
  10. package/dist/experimental/anthropic/transcripts.cjs +144 -0
  11. package/dist/experimental/anthropic/transcripts.d.ts +22 -0
  12. package/dist/experimental/anthropic/transcripts.js +141 -0
  13. package/dist/experimental/anthropic/types.d.ts +1 -0
  14. package/dist/experimental/anthropic/usage.cjs +19 -20
  15. package/dist/experimental/anthropic/usage.d.ts +2 -1
  16. package/dist/experimental/anthropic/usage.js +18 -20
  17. package/dist/experimental/opencode/index.cjs +36 -0
  18. package/dist/experimental/opencode/index.d.ts +3 -0
  19. package/dist/experimental/opencode/index.js +32 -0
  20. package/dist/experimental/opencode/tracer.cjs +389 -0
  21. package/dist/experimental/opencode/tracer.d.ts +30 -0
  22. package/dist/experimental/opencode/tracer.js +385 -0
  23. package/dist/experimental/otel/setup.cjs +1 -1
  24. package/dist/experimental/otel/setup.js +1 -1
  25. package/dist/experimental/sandbox/sandbox.cjs +6 -2
  26. package/dist/experimental/sandbox/sandbox.d.ts +5 -1
  27. package/dist/experimental/sandbox/sandbox.js +6 -2
  28. package/dist/experimental/sandbox/types.d.ts +3 -1
  29. package/dist/experimental/vercel/index.cjs +1 -1
  30. package/dist/experimental/vercel/index.js +1 -1
  31. package/dist/experimental/vercel/middleware.cjs +2 -1
  32. package/dist/experimental/vercel/middleware.js +2 -1
  33. package/dist/index.cjs +1 -1
  34. package/dist/index.d.ts +1 -1
  35. package/dist/index.js +1 -1
  36. package/dist/schemas.d.ts +4 -0
  37. package/dist/singletons/traceable.cjs +1 -3
  38. package/dist/singletons/traceable.js +1 -3
  39. package/dist/traceable.cjs +1 -3
  40. package/dist/traceable.js +1 -3
  41. package/dist/utils/_git.cjs +2 -2
  42. package/dist/utils/_git.js +2 -2
  43. package/dist/utils/env.cjs +2 -2
  44. package/dist/utils/env.js +2 -2
  45. package/dist/utils/error.cjs +2 -2
  46. package/dist/utils/error.js +2 -2
  47. package/dist/utils/jestlike/reporter.cjs +1 -1
  48. package/dist/utils/jestlike/reporter.js +1 -1
  49. package/dist/utils/jestlike/vendor/chain.cjs +2 -3
  50. package/dist/utils/jestlike/vendor/chain.js +2 -3
  51. package/dist/vitest/utils/esm.mjs +4 -4
  52. package/dist/wrappers/gemini.cjs +1 -1
  53. package/dist/wrappers/gemini.js +1 -1
  54. package/dist/wrappers/openai.cjs +2 -6
  55. package/dist/wrappers/openai.js +2 -6
  56. package/experimental/opencode.cjs +1 -0
  57. package/experimental/opencode.d.cts +1 -0
  58. package/experimental/opencode.d.ts +1 -0
  59. package/experimental/opencode.js +1 -0
  60. package/package.json +24 -18
@@ -4,14 +4,15 @@ exports.wrapClaudeAgentSDK = wrapClaudeAgentSDK;
4
4
  const traceable_js_1 = require("../../traceable.cjs");
5
5
  const context_js_1 = require("./context.cjs");
6
6
  const messages_js_1 = require("./messages.cjs");
7
+ const WRAPPED_TOOL_SYMBOL = Symbol.for("langsmith:claude_agent_sdk:wrapped_tool");
8
+ const WRAPPED_TOOL_HANDLER_SYMBOL = Symbol.for("langsmith:claude_agent_sdk:wrapped_tool_handler");
7
9
  /**
8
10
  * Wraps the Claude Agent SDK's query function to add LangSmith tracing.
9
11
  * Traces the entire agent interaction including all streaming messages.
10
12
  * @internal Use `wrapClaudeAgentSDK` instead.
11
13
  */
12
14
  function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
13
- async function* generator(originalGenerator, prompt) {
14
- const streamManager = new context_js_1.StreamManager();
15
+ async function* generator(originalGenerator, prompt, streamManager) {
15
16
  try {
16
17
  let systemCount = 0;
17
18
  for await (const message of originalGenerator) {
@@ -19,9 +20,9 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
19
20
  const content = getLatestInput(prompt, systemCount);
20
21
  systemCount += 1;
21
22
  if (content != null)
22
- streamManager.addMessage(content);
23
+ await streamManager.addMessage(content);
23
24
  }
24
- streamManager.addMessage(message);
25
+ await streamManager.addMessage(message);
25
26
  yield message;
26
27
  }
27
28
  }
@@ -78,6 +79,32 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
78
79
  },
79
80
  });
80
81
  }
82
+ function addLangSmithHooks(params, streamManager) {
83
+ const options = { ...(params.options ?? {}) };
84
+ const hooks = { ...(options.hooks ?? {}) };
85
+ const addHook = (eventName) => {
86
+ const existing = Array.isArray(hooks[eventName])
87
+ ? hooks[eventName]
88
+ : [];
89
+ hooks[eventName] = [
90
+ ...existing,
91
+ {
92
+ hooks: [
93
+ async (input, toolUseId) => {
94
+ streamManager.addHookEvent(input, toolUseId);
95
+ return {};
96
+ },
97
+ ],
98
+ },
99
+ ];
100
+ };
101
+ addHook("PreToolUse");
102
+ addHook("SubagentStart");
103
+ addHook("SubagentStop");
104
+ addHook("Stop");
105
+ options.hooks = hooks;
106
+ return { ...params, options };
107
+ }
81
108
  function processOutputs(rawOutputs) {
82
109
  if ("outputs" in rawOutputs && Array.isArray(rawOutputs.outputs)) {
83
110
  const sdkMessages = rawOutputs.outputs;
@@ -87,14 +114,17 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
87
114
  return true;
88
115
  return message.parent_tool_use_id == null;
89
116
  })
90
- .flatMap(messages_js_1.convertFromAnthropicMessage);
117
+ .flatMap(messages_js_1.convertFromAnthropicMessage)
118
+ .reduce((acc, message) => (0, messages_js_1.mergeMessagesById)(acc, [message]), []);
91
119
  return { output: { messages } };
92
120
  }
93
121
  return rawOutputs;
94
122
  }
95
123
  return (0, traceable_js_1.traceable)((params, ...args) => {
96
- const actualGenerator = queryFn.call(defaultThis, params, ...args);
97
- const wrappedGenerator = generator(actualGenerator, params.prompt);
124
+ const streamManager = new context_js_1.StreamManager();
125
+ const paramsWithHooks = addLangSmithHooks(params, streamManager);
126
+ const actualGenerator = queryFn.call(defaultThis, paramsWithHooks, ...args);
127
+ const wrappedGenerator = generator(actualGenerator, params.prompt, streamManager);
98
128
  for (const method of Object.getOwnPropertyNames(Object.getPrototypeOf(actualGenerator)).filter((method) => !["constructor", "next", "throw", "return"].includes(method))) {
99
129
  Object.defineProperty(wrappedGenerator, method, {
100
130
  get() {
@@ -151,16 +181,54 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
151
181
  function wrapClaudeAgentSDK(sdk, config) {
152
182
  const inputSdk = sdk;
153
183
  const wrappedSdk = { ...sdk };
154
- if ("query" in inputSdk && (0, traceable_js_1.isTraceableFunction)(inputSdk.query)) {
184
+ const toolAlreadyWrapped = "tool" in inputSdk &&
185
+ typeof inputSdk.tool === "function" &&
186
+ WRAPPED_TOOL_SYMBOL in inputSdk.tool;
187
+ if (("query" in inputSdk && (0, traceable_js_1.isTraceableFunction)(inputSdk.query)) ||
188
+ toolAlreadyWrapped) {
155
189
  throw new Error("This instance of Claude Agent SDK has been already wrapped by `wrapClaudeAgentSDK`.");
156
190
  }
157
191
  // Wrap the query method if it exists
158
192
  if ("query" in inputSdk && typeof inputSdk.query === "function") {
159
193
  wrappedSdk.query = wrapClaudeAgentQuery(inputSdk.query, inputSdk, config);
160
194
  }
161
- // Wrap the tool method if it exists
195
+ // Wrap the tool method if it exists. The SDK invokes MCP tool handlers in an
196
+ // async context that may not inherit the query trace context, so bind nested
197
+ // traceable calls back to the active tool run when possible.
162
198
  if ("tool" in inputSdk && typeof inputSdk.tool === "function") {
163
- wrappedSdk.tool = inputSdk.tool.bind(inputSdk);
199
+ wrappedSdk.tool = (...args) => {
200
+ const toolName = typeof args[0] === "string" ? args[0] : undefined;
201
+ let handlerIndex = -1;
202
+ for (let index = args.length - 1; index >= 0; index -= 1) {
203
+ if (typeof args[index] === "function") {
204
+ handlerIndex = index;
205
+ break;
206
+ }
207
+ }
208
+ if (handlerIndex < 0) {
209
+ return inputSdk.tool?.(...args);
210
+ }
211
+ const originalHandler = args[handlerIndex];
212
+ if (WRAPPED_TOOL_HANDLER_SYMBOL in originalHandler) {
213
+ return inputSdk.tool?.(...args);
214
+ }
215
+ const wrappedHandler = (...handlerArgs) => {
216
+ const activeToolRun = context_js_1.StreamManager.getActiveToolRun(toolName, handlerArgs[0]);
217
+ if (activeToolRun == null) {
218
+ return originalHandler(...handlerArgs);
219
+ }
220
+ return (0, traceable_js_1.withRunTree)(activeToolRun, () => originalHandler(...handlerArgs));
221
+ };
222
+ Object.defineProperty(wrappedHandler, WRAPPED_TOOL_HANDLER_SYMBOL, {
223
+ value: true,
224
+ });
225
+ const wrappedArgs = [...args];
226
+ wrappedArgs[handlerIndex] = wrappedHandler;
227
+ return inputSdk.tool?.(...wrappedArgs);
228
+ };
229
+ Object.defineProperty(wrappedSdk.tool, WRAPPED_TOOL_SYMBOL, {
230
+ value: true,
231
+ });
164
232
  }
165
233
  // Keep createSdkMcpServer and other methods as-is (bound to original SDK)
166
234
  if ("createSdkMcpServer" in inputSdk &&
@@ -1,14 +1,15 @@
1
- import { traceable, isTraceableFunction } from "../../traceable.js";
1
+ import { traceable, isTraceableFunction, withRunTree, } from "../../traceable.js";
2
2
  import { StreamManager } from "./context.js";
3
- import { convertFromAnthropicMessage } from "./messages.js";
3
+ import { convertFromAnthropicMessage, mergeMessagesById } from "./messages.js";
4
+ const WRAPPED_TOOL_SYMBOL = Symbol.for("langsmith:claude_agent_sdk:wrapped_tool");
5
+ const WRAPPED_TOOL_HANDLER_SYMBOL = Symbol.for("langsmith:claude_agent_sdk:wrapped_tool_handler");
4
6
  /**
5
7
  * Wraps the Claude Agent SDK's query function to add LangSmith tracing.
6
8
  * Traces the entire agent interaction including all streaming messages.
7
9
  * @internal Use `wrapClaudeAgentSDK` instead.
8
10
  */
9
11
  function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
10
- async function* generator(originalGenerator, prompt) {
11
- const streamManager = new StreamManager();
12
+ async function* generator(originalGenerator, prompt, streamManager) {
12
13
  try {
13
14
  let systemCount = 0;
14
15
  for await (const message of originalGenerator) {
@@ -16,9 +17,9 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
16
17
  const content = getLatestInput(prompt, systemCount);
17
18
  systemCount += 1;
18
19
  if (content != null)
19
- streamManager.addMessage(content);
20
+ await streamManager.addMessage(content);
20
21
  }
21
- streamManager.addMessage(message);
22
+ await streamManager.addMessage(message);
22
23
  yield message;
23
24
  }
24
25
  }
@@ -75,6 +76,32 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
75
76
  },
76
77
  });
77
78
  }
79
+ function addLangSmithHooks(params, streamManager) {
80
+ const options = { ...(params.options ?? {}) };
81
+ const hooks = { ...(options.hooks ?? {}) };
82
+ const addHook = (eventName) => {
83
+ const existing = Array.isArray(hooks[eventName])
84
+ ? hooks[eventName]
85
+ : [];
86
+ hooks[eventName] = [
87
+ ...existing,
88
+ {
89
+ hooks: [
90
+ async (input, toolUseId) => {
91
+ streamManager.addHookEvent(input, toolUseId);
92
+ return {};
93
+ },
94
+ ],
95
+ },
96
+ ];
97
+ };
98
+ addHook("PreToolUse");
99
+ addHook("SubagentStart");
100
+ addHook("SubagentStop");
101
+ addHook("Stop");
102
+ options.hooks = hooks;
103
+ return { ...params, options };
104
+ }
78
105
  function processOutputs(rawOutputs) {
79
106
  if ("outputs" in rawOutputs && Array.isArray(rawOutputs.outputs)) {
80
107
  const sdkMessages = rawOutputs.outputs;
@@ -84,14 +111,17 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
84
111
  return true;
85
112
  return message.parent_tool_use_id == null;
86
113
  })
87
- .flatMap(convertFromAnthropicMessage);
114
+ .flatMap(convertFromAnthropicMessage)
115
+ .reduce((acc, message) => mergeMessagesById(acc, [message]), []);
88
116
  return { output: { messages } };
89
117
  }
90
118
  return rawOutputs;
91
119
  }
92
120
  return traceable((params, ...args) => {
93
- const actualGenerator = queryFn.call(defaultThis, params, ...args);
94
- const wrappedGenerator = generator(actualGenerator, params.prompt);
121
+ const streamManager = new StreamManager();
122
+ const paramsWithHooks = addLangSmithHooks(params, streamManager);
123
+ const actualGenerator = queryFn.call(defaultThis, paramsWithHooks, ...args);
124
+ const wrappedGenerator = generator(actualGenerator, params.prompt, streamManager);
95
125
  for (const method of Object.getOwnPropertyNames(Object.getPrototypeOf(actualGenerator)).filter((method) => !["constructor", "next", "throw", "return"].includes(method))) {
96
126
  Object.defineProperty(wrappedGenerator, method, {
97
127
  get() {
@@ -148,16 +178,54 @@ function wrapClaudeAgentQuery(queryFn, defaultThis, baseConfig) {
148
178
  export function wrapClaudeAgentSDK(sdk, config) {
149
179
  const inputSdk = sdk;
150
180
  const wrappedSdk = { ...sdk };
151
- if ("query" in inputSdk && isTraceableFunction(inputSdk.query)) {
181
+ const toolAlreadyWrapped = "tool" in inputSdk &&
182
+ typeof inputSdk.tool === "function" &&
183
+ WRAPPED_TOOL_SYMBOL in inputSdk.tool;
184
+ if (("query" in inputSdk && isTraceableFunction(inputSdk.query)) ||
185
+ toolAlreadyWrapped) {
152
186
  throw new Error("This instance of Claude Agent SDK has been already wrapped by `wrapClaudeAgentSDK`.");
153
187
  }
154
188
  // Wrap the query method if it exists
155
189
  if ("query" in inputSdk && typeof inputSdk.query === "function") {
156
190
  wrappedSdk.query = wrapClaudeAgentQuery(inputSdk.query, inputSdk, config);
157
191
  }
158
- // Wrap the tool method if it exists
192
+ // Wrap the tool method if it exists. The SDK invokes MCP tool handlers in an
193
+ // async context that may not inherit the query trace context, so bind nested
194
+ // traceable calls back to the active tool run when possible.
159
195
  if ("tool" in inputSdk && typeof inputSdk.tool === "function") {
160
- wrappedSdk.tool = inputSdk.tool.bind(inputSdk);
196
+ wrappedSdk.tool = (...args) => {
197
+ const toolName = typeof args[0] === "string" ? args[0] : undefined;
198
+ let handlerIndex = -1;
199
+ for (let index = args.length - 1; index >= 0; index -= 1) {
200
+ if (typeof args[index] === "function") {
201
+ handlerIndex = index;
202
+ break;
203
+ }
204
+ }
205
+ if (handlerIndex < 0) {
206
+ return inputSdk.tool?.(...args);
207
+ }
208
+ const originalHandler = args[handlerIndex];
209
+ if (WRAPPED_TOOL_HANDLER_SYMBOL in originalHandler) {
210
+ return inputSdk.tool?.(...args);
211
+ }
212
+ const wrappedHandler = (...handlerArgs) => {
213
+ const activeToolRun = StreamManager.getActiveToolRun(toolName, handlerArgs[0]);
214
+ if (activeToolRun == null) {
215
+ return originalHandler(...handlerArgs);
216
+ }
217
+ return withRunTree(activeToolRun, () => originalHandler(...handlerArgs));
218
+ };
219
+ Object.defineProperty(wrappedHandler, WRAPPED_TOOL_HANDLER_SYMBOL, {
220
+ value: true,
221
+ });
222
+ const wrappedArgs = [...args];
223
+ wrappedArgs[handlerIndex] = wrappedHandler;
224
+ return inputSdk.tool?.(...wrappedArgs);
225
+ };
226
+ Object.defineProperty(wrappedSdk.tool, WRAPPED_TOOL_SYMBOL, {
227
+ value: true,
228
+ });
161
229
  }
162
230
  // Keep createSdkMcpServer and other methods as-is (bound to original SDK)
163
231
  if ("createSdkMcpServer" in inputSdk &&
@@ -1,10 +1,63 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeMessagesById = mergeMessagesById;
3
4
  exports.flattenContentBlocks = flattenContentBlocks;
4
5
  exports.convertFromAnthropicMessage = convertFromAnthropicMessage;
5
6
  exports.isTaskTool = isTaskTool;
6
7
  exports.isToolBlock = isToolBlock;
7
8
  const utils_js_1 = require("./utils.cjs");
9
+ function getContentBlockKey(block) {
10
+ if (typeof block !== "object" || block == null)
11
+ return undefined;
12
+ const record = block;
13
+ if (typeof record.id === "string")
14
+ return `id:${record.id}`;
15
+ if (typeof record.signature === "string")
16
+ return `signature:${record.signature}`;
17
+ if (typeof record.type === "string" && typeof record.text === "string") {
18
+ return `text:${record.text}`;
19
+ }
20
+ try {
21
+ return `json:${JSON.stringify(record)}`;
22
+ }
23
+ catch {
24
+ return undefined;
25
+ }
26
+ }
27
+ function mergeContentBlocks(existing, incoming) {
28
+ if (!Array.isArray(existing) || !Array.isArray(incoming))
29
+ return incoming;
30
+ const merged = [...existing];
31
+ const keys = new Set(merged.map(getContentBlockKey).filter(Boolean));
32
+ for (const block of incoming) {
33
+ const key = getContentBlockKey(block);
34
+ if (key != null && keys.has(key))
35
+ continue;
36
+ merged.push(block);
37
+ if (key != null)
38
+ keys.add(key);
39
+ }
40
+ return merged;
41
+ }
42
+ function mergeMessagesById(previousMessages, newMessages) {
43
+ const merged = [...previousMessages];
44
+ for (const message of newMessages) {
45
+ const existingIndex = merged.findIndex((prev) => prev.role === message.role &&
46
+ message.id != null &&
47
+ prev.id === message.id);
48
+ if (existingIndex < 0) {
49
+ merged.push(message);
50
+ continue;
51
+ }
52
+ const existing = merged[existingIndex];
53
+ merged[existingIndex] = {
54
+ ...existing,
55
+ ...message,
56
+ content: mergeContentBlocks(existing.content, message.content),
57
+ };
58
+ }
59
+ return merged;
60
+ }
8
61
  /**
9
62
  * Converts SDK content blocks into serializable objects.
10
63
  * Matches Python's flatten_content_blocks behavior.
@@ -1,4 +1,10 @@
1
1
  import type { BetaContentBlock } from "./types.js";
2
+ export type LangSmithMessage = {
3
+ content?: unknown;
4
+ role?: string;
5
+ id?: string;
6
+ } & Record<string, unknown>;
7
+ export declare function mergeMessagesById(previousMessages: LangSmithMessage[], newMessages: LangSmithMessage[]): LangSmithMessage[];
2
8
  /**
3
9
  * Converts SDK content blocks into serializable objects.
4
10
  * Matches Python's flatten_content_blocks behavior.
@@ -1,4 +1,56 @@
1
1
  import { isIterable } from "./utils.js";
2
+ function getContentBlockKey(block) {
3
+ if (typeof block !== "object" || block == null)
4
+ return undefined;
5
+ const record = block;
6
+ if (typeof record.id === "string")
7
+ return `id:${record.id}`;
8
+ if (typeof record.signature === "string")
9
+ return `signature:${record.signature}`;
10
+ if (typeof record.type === "string" && typeof record.text === "string") {
11
+ return `text:${record.text}`;
12
+ }
13
+ try {
14
+ return `json:${JSON.stringify(record)}`;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ function mergeContentBlocks(existing, incoming) {
21
+ if (!Array.isArray(existing) || !Array.isArray(incoming))
22
+ return incoming;
23
+ const merged = [...existing];
24
+ const keys = new Set(merged.map(getContentBlockKey).filter(Boolean));
25
+ for (const block of incoming) {
26
+ const key = getContentBlockKey(block);
27
+ if (key != null && keys.has(key))
28
+ continue;
29
+ merged.push(block);
30
+ if (key != null)
31
+ keys.add(key);
32
+ }
33
+ return merged;
34
+ }
35
+ export function mergeMessagesById(previousMessages, newMessages) {
36
+ const merged = [...previousMessages];
37
+ for (const message of newMessages) {
38
+ const existingIndex = merged.findIndex((prev) => prev.role === message.role &&
39
+ message.id != null &&
40
+ prev.id === message.id);
41
+ if (existingIndex < 0) {
42
+ merged.push(message);
43
+ continue;
44
+ }
45
+ const existing = merged[existingIndex];
46
+ merged[existingIndex] = {
47
+ ...existing,
48
+ ...message,
49
+ content: mergeContentBlocks(existing.content, message.content),
50
+ };
51
+ }
52
+ return merged;
53
+ }
2
54
  /**
3
55
  * Converts SDK content blocks into serializable objects.
4
56
  * Matches Python's flatten_content_blocks behavior.
@@ -0,0 +1,144 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readTranscript = readTranscript;
4
+ const promises_1 = require("node:fs/promises");
5
+ const messages_js_1 = require("./messages.cjs");
6
+ const usage_js_1 = require("./usage.cjs");
7
+ function parseTimestamp(value) {
8
+ if (typeof value !== "string" || value.length === 0)
9
+ return undefined;
10
+ const timestamp = Date.parse(value);
11
+ return Number.isNaN(timestamp) ? undefined : timestamp;
12
+ }
13
+ function isRecord(value) {
14
+ return typeof value === "object" && value != null && !Array.isArray(value);
15
+ }
16
+ function toSDKAssistantMessage(data) {
17
+ const message = data.message;
18
+ if (!isRecord(message))
19
+ return undefined;
20
+ const id = message.id;
21
+ if (typeof id !== "string" || id.length === 0)
22
+ return undefined;
23
+ return {
24
+ type: "assistant",
25
+ parent_tool_use_id: typeof data.parent_tool_use_id === "string"
26
+ ? data.parent_tool_use_id
27
+ : null,
28
+ message: {
29
+ ...message,
30
+ id,
31
+ content: Array.isArray(message.content)
32
+ ? message.content
33
+ : [],
34
+ },
35
+ };
36
+ }
37
+ function toSDKUserMessage(data) {
38
+ const message = data.message;
39
+ if (!isRecord(message))
40
+ return undefined;
41
+ return {
42
+ type: "user",
43
+ parent_tool_use_id: typeof data.parent_tool_use_id === "string"
44
+ ? data.parent_tool_use_id
45
+ : null,
46
+ session_id: typeof data.session_id === "string" ? data.session_id : "",
47
+ message: message,
48
+ tool_use_result: data.tool_use_result,
49
+ };
50
+ }
51
+ /**
52
+ * Read a Claude Agent SDK JSONL transcript and return final assistant turns,
53
+ * final usage by message id, and tool_result blocks.
54
+ *
55
+ * The transcript format is not a contracted SDK API. This mirrors the Python
56
+ * reconciler and is intentionally best-effort: malformed lines and unreadable
57
+ * files are ignored so tracing never affects the user conversation.
58
+ * @internal
59
+ */
60
+ async function readTranscript(filePath) {
61
+ let contents;
62
+ try {
63
+ contents = await (0, promises_1.readFile)(filePath, "utf8");
64
+ }
65
+ catch {
66
+ return { turns: [], usageByMessageId: {}, toolResults: [] };
67
+ }
68
+ const entriesById = new Map();
69
+ const usageByMessageId = {};
70
+ const toolResults = [];
71
+ const conversation = [];
72
+ for (const line of contents.split(/\r?\n/)) {
73
+ const trimmed = line.trim();
74
+ if (!trimmed)
75
+ continue;
76
+ let data;
77
+ try {
78
+ data = JSON.parse(trimmed);
79
+ }
80
+ catch {
81
+ continue;
82
+ }
83
+ if (!isRecord(data))
84
+ continue;
85
+ if (data.type === "user") {
86
+ const sdkMessage = toSDKUserMessage(data);
87
+ if (sdkMessage == null)
88
+ continue;
89
+ const content = sdkMessage.message.content;
90
+ const toolUseResultIsError = isRecord(sdkMessage.tool_use_result)
91
+ ? sdkMessage.tool_use_result.is_error === true ||
92
+ sdkMessage.tool_use_result.isError === true
93
+ : false;
94
+ if (Array.isArray(content)) {
95
+ for (const block of content) {
96
+ if (!isRecord(block) || block.type !== "tool_result")
97
+ continue;
98
+ const toolUseId = block.tool_use_id;
99
+ if (typeof toolUseId !== "string")
100
+ continue;
101
+ toolResults.push({
102
+ toolUseId,
103
+ content: block.content,
104
+ isError: block.is_error === true ||
105
+ block.isError === true ||
106
+ toolUseResultIsError,
107
+ });
108
+ }
109
+ }
110
+ conversation.push(...(0, messages_js_1.convertFromAnthropicMessage)(sdkMessage));
111
+ continue;
112
+ }
113
+ if (data.type !== "assistant")
114
+ continue;
115
+ const sdkMessage = toSDKAssistantMessage(data);
116
+ if (sdkMessage == null)
117
+ continue;
118
+ const messageId = sdkMessage.message.id;
119
+ const rawUsage = sdkMessage.message.usage;
120
+ if (rawUsage != null) {
121
+ usageByMessageId[messageId] = (0, usage_js_1.extractUsageMetadata)(rawUsage);
122
+ }
123
+ const turn = {
124
+ messageId,
125
+ model: sdkMessage.message.model,
126
+ content: sdkMessage.message.content,
127
+ usage: rawUsage,
128
+ usageMetadata: rawUsage != null ? (0, usage_js_1.extractUsageMetadata)(rawUsage) : undefined,
129
+ timestamp: parseTimestamp(data.timestamp),
130
+ inputMessages: conversation.slice(),
131
+ message: sdkMessage,
132
+ };
133
+ // Always overwrite. The final chunk (where stop_reason is set) appears last.
134
+ entriesById.set(messageId, turn);
135
+ if (sdkMessage.message.stop_reason) {
136
+ conversation.push(...(0, messages_js_1.convertFromAnthropicMessage)(sdkMessage));
137
+ }
138
+ }
139
+ return {
140
+ turns: Array.from(entriesById.values()).filter((turn) => turn.message.message.stop_reason),
141
+ usageByMessageId,
142
+ toolResults,
143
+ };
144
+ }
@@ -0,0 +1,22 @@
1
+ import { type LangSmithMessage } from "./messages.js";
2
+ import type { SDKAssistantMessage } from "./types.js";
3
+ export type TranscriptAssistantTurn = {
4
+ messageId: string;
5
+ model?: string;
6
+ content: Record<string, unknown>[];
7
+ usage?: Record<string, unknown>;
8
+ usageMetadata?: Record<string, unknown>;
9
+ timestamp?: number;
10
+ inputMessages: LangSmithMessage[];
11
+ message: SDKAssistantMessage;
12
+ };
13
+ export type TranscriptToolResult = {
14
+ toolUseId: string;
15
+ content: unknown;
16
+ isError?: boolean;
17
+ };
18
+ export type TranscriptData = {
19
+ turns: TranscriptAssistantTurn[];
20
+ usageByMessageId: Record<string, Record<string, unknown>>;
21
+ toolResults: TranscriptToolResult[];
22
+ };