@librechat/agents 3.3.2 → 3.3.4

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 (162) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +2 -1
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +342 -109
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
  6. package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
  7. package/dist/cjs/langfuse.cjs +6 -0
  8. package/dist/cjs/langfuse.cjs.map +1 -1
  9. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
  10. package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
  11. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
  12. package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
  13. package/dist/cjs/llm/google/utils/common.cjs +19 -7
  14. package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
  15. package/dist/cjs/llm/invoke.cjs +64 -3
  16. package/dist/cjs/llm/invoke.cjs.map +1 -1
  17. package/dist/cjs/llm/openai/index.cjs +41 -5
  18. package/dist/cjs/llm/openai/index.cjs.map +1 -1
  19. package/dist/cjs/llm/openai/utils/index.cjs +25 -4
  20. package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
  21. package/dist/cjs/llm/openrouter/index.cjs +4 -1
  22. package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
  23. package/dist/cjs/main.cjs +19 -2
  24. package/dist/cjs/messages/cache.cjs +54 -33
  25. package/dist/cjs/messages/cache.cjs.map +1 -1
  26. package/dist/cjs/messages/contextPruning.cjs +17 -43
  27. package/dist/cjs/messages/contextPruning.cjs.map +1 -1
  28. package/dist/cjs/messages/core.cjs +315 -23
  29. package/dist/cjs/messages/core.cjs.map +1 -1
  30. package/dist/cjs/messages/format.cjs +218 -60
  31. package/dist/cjs/messages/format.cjs.map +1 -1
  32. package/dist/cjs/messages/index.cjs +1 -1
  33. package/dist/cjs/messages/prune.cjs +503 -127
  34. package/dist/cjs/messages/prune.cjs.map +1 -1
  35. package/dist/cjs/stream.cjs +7 -3
  36. package/dist/cjs/stream.cjs.map +1 -1
  37. package/dist/cjs/summarization/node.cjs +28 -14
  38. package/dist/cjs/summarization/node.cjs.map +1 -1
  39. package/dist/cjs/tools/ToolNode.cjs +54 -41
  40. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  41. package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
  42. package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
  43. package/dist/cjs/utils/index.cjs +1 -1
  44. package/dist/cjs/utils/llm.cjs +1 -1
  45. package/dist/cjs/utils/llm.cjs.map +1 -1
  46. package/dist/cjs/utils/tokens.cjs +307 -20
  47. package/dist/cjs/utils/tokens.cjs.map +1 -1
  48. package/dist/cjs/utils/toolContent.cjs +1514 -0
  49. package/dist/cjs/utils/toolContent.cjs.map +1 -0
  50. package/dist/esm/agents/AgentContext.mjs +2 -1
  51. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  52. package/dist/esm/graphs/Graph.mjs +349 -116
  53. package/dist/esm/graphs/Graph.mjs.map +1 -1
  54. package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
  55. package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
  56. package/dist/esm/langfuse.mjs +6 -0
  57. package/dist/esm/langfuse.mjs.map +1 -1
  58. package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
  59. package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
  60. package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
  61. package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
  62. package/dist/esm/llm/google/utils/common.mjs +19 -7
  63. package/dist/esm/llm/google/utils/common.mjs.map +1 -1
  64. package/dist/esm/llm/invoke.mjs +65 -5
  65. package/dist/esm/llm/invoke.mjs.map +1 -1
  66. package/dist/esm/llm/openai/index.mjs +41 -5
  67. package/dist/esm/llm/openai/index.mjs.map +1 -1
  68. package/dist/esm/llm/openai/utils/index.mjs +25 -4
  69. package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
  70. package/dist/esm/llm/openrouter/index.mjs +4 -1
  71. package/dist/esm/llm/openrouter/index.mjs.map +1 -1
  72. package/dist/esm/main.mjs +7 -7
  73. package/dist/esm/messages/cache.mjs +55 -34
  74. package/dist/esm/messages/cache.mjs.map +1 -1
  75. package/dist/esm/messages/contextPruning.mjs +17 -43
  76. package/dist/esm/messages/contextPruning.mjs.map +1 -1
  77. package/dist/esm/messages/core.mjs +305 -24
  78. package/dist/esm/messages/core.mjs.map +1 -1
  79. package/dist/esm/messages/format.mjs +218 -61
  80. package/dist/esm/messages/format.mjs.map +1 -1
  81. package/dist/esm/messages/index.mjs +1 -1
  82. package/dist/esm/messages/prune.mjs +500 -128
  83. package/dist/esm/messages/prune.mjs.map +1 -1
  84. package/dist/esm/stream.mjs +7 -3
  85. package/dist/esm/stream.mjs.map +1 -1
  86. package/dist/esm/summarization/node.mjs +28 -14
  87. package/dist/esm/summarization/node.mjs.map +1 -1
  88. package/dist/esm/tools/ToolNode.mjs +54 -41
  89. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  90. package/dist/esm/tools/toolOutputReferences.mjs +6 -0
  91. package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
  92. package/dist/esm/utils/index.mjs +1 -1
  93. package/dist/esm/utils/llm.mjs +1 -1
  94. package/dist/esm/utils/llm.mjs.map +1 -1
  95. package/dist/esm/utils/tokens.mjs +307 -21
  96. package/dist/esm/utils/tokens.mjs.map +1 -1
  97. package/dist/esm/utils/toolContent.mjs +1503 -0
  98. package/dist/esm/utils/toolContent.mjs.map +1 -0
  99. package/dist/types/graphs/Graph.d.ts +1 -1
  100. package/dist/types/llm/invoke.d.ts +27 -1
  101. package/dist/types/llm/openai/index.d.ts +4 -0
  102. package/dist/types/llm/openai/utils/index.d.ts +2 -0
  103. package/dist/types/llm/openrouter/index.d.ts +5 -0
  104. package/dist/types/messages/contextPruning.d.ts +2 -1
  105. package/dist/types/messages/core.d.ts +36 -0
  106. package/dist/types/messages/format.d.ts +6 -0
  107. package/dist/types/messages/prune.d.ts +16 -12
  108. package/dist/types/types/stream.d.ts +1 -1
  109. package/dist/types/utils/tokens.d.ts +7 -0
  110. package/dist/types/utils/toolContent.d.ts +107 -0
  111. package/package.json +13 -9
  112. package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
  113. package/src/agents/AgentContext.ts +1 -0
  114. package/src/graphs/Graph.ts +698 -200
  115. package/src/graphs/MultiAgentGraph.ts +6 -1
  116. package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
  117. package/src/langfuse.ts +15 -0
  118. package/src/llm/anthropic/utils/message_inputs.ts +158 -12
  119. package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
  120. package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
  121. package/src/llm/bedrock/utils/message_inputs.ts +86 -13
  122. package/src/llm/google/utils/common.test.ts +75 -9
  123. package/src/llm/google/utils/common.ts +34 -3
  124. package/src/llm/invoke.test.ts +488 -0
  125. package/src/llm/invoke.ts +196 -2
  126. package/src/llm/openai/index.ts +106 -6
  127. package/src/llm/openai/utils/index.ts +109 -53
  128. package/src/llm/openai/utils/messages.test.ts +330 -1
  129. package/src/llm/openrouter/index.ts +12 -2
  130. package/src/llm/openrouter/reasoning.test.ts +314 -0
  131. package/src/messages/__tests__/observationMasking.test.ts +93 -2
  132. package/src/messages/cache.tail.test.ts +193 -0
  133. package/src/messages/cache.test.ts +113 -0
  134. package/src/messages/cache.ts +92 -60
  135. package/src/messages/contextPruning.test.ts +184 -0
  136. package/src/messages/contextPruning.ts +49 -42
  137. package/src/messages/core.ts +653 -40
  138. package/src/messages/ensureThinkingBlock.test.ts +49 -1
  139. package/src/messages/foldToollessToolBlocks.test.ts +163 -5
  140. package/src/messages/format.ts +363 -91
  141. package/src/messages/formatAgentMessages.test.ts +604 -14
  142. package/src/messages/formatAgentMessages.tools.test.ts +103 -0
  143. package/src/messages/prune.ts +996 -183
  144. package/src/specs/langfuse-callbacks.test.ts +203 -1
  145. package/src/specs/prune.test.ts +1083 -6
  146. package/src/specs/summarization-unit.test.ts +105 -0
  147. package/src/specs/tokens.test.ts +609 -32
  148. package/src/stream.ts +23 -15
  149. package/src/summarization/__tests__/node.test.ts +77 -0
  150. package/src/summarization/node.ts +46 -18
  151. package/src/tools/ToolNode.ts +147 -68
  152. package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
  153. package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
  154. package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
  155. package/src/tools/__tests__/directToolHooks.test.ts +255 -7
  156. package/src/tools/toolOutputReferences.ts +6 -0
  157. package/src/types/stream.ts +1 -1
  158. package/src/utils/llm.test.ts +18 -0
  159. package/src/utils/llm.ts +4 -1
  160. package/src/utils/tokens.ts +586 -31
  161. package/src/utils/toolContent.test.ts +1432 -0
  162. package/src/utils/toolContent.ts +2707 -0
package/src/langfuse.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { CallbackHandler } from '@langfuse/langchain';
2
+ import { isParentCommand } from '@langchain/langgraph';
2
3
  import { context as otelContext } from '@opentelemetry/api';
3
4
  import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
4
5
  import {
@@ -204,6 +205,20 @@ class ScopedLangfuseCallbackHandler extends CallbackHandler {
204
205
  return this.withRuntimeContext(() => super.handleChainStart(...args));
205
206
  }
206
207
 
208
+ override handleChainError(
209
+ ...args: Parameters<CallbackHandler['handleChainError']>
210
+ ): ReturnType<CallbackHandler['handleChainError']> {
211
+ const [error, runId, parentRunId] = args;
212
+ if (error != null && parentRunId != null && isParentCommand(error)) {
213
+ return super.handleChainEnd(
214
+ { controlFlow: 'ParentCommand' },
215
+ runId,
216
+ parentRunId
217
+ );
218
+ }
219
+ return super.handleChainError(...args);
220
+ }
221
+
207
222
  override handleAgentAction(
208
223
  ...args: Parameters<CallbackHandler['handleAgentAction']>
209
224
  ): ReturnType<CallbackHandler['handleAgentAction']> {
@@ -4,13 +4,14 @@
4
4
  * This util file contains functions for converting LangChain messages to Anthropic messages.
5
5
  */
6
6
  import { createHash } from 'node:crypto';
7
+ import { isProxy } from 'node:util/types';
7
8
  import { ToolCall } from '@langchain/core/messages/tool';
8
9
  import {
9
10
  type BaseMessage,
10
11
  type SystemMessage,
11
12
  HumanMessage,
12
13
  type AIMessage,
13
- type ToolMessage,
14
+ ToolMessage,
14
15
  isAIMessage,
15
16
  type Data,
16
17
  type StandardContentBlockConverter,
@@ -33,6 +34,11 @@ import {
33
34
  AnthropicCompactionBlockParam,
34
35
  AnthropicToolResponse,
35
36
  } from '../types';
37
+ import {
38
+ cloneToolMessageWithContent,
39
+ serializeStructuredValueBounded,
40
+ } from '@/utils/toolContent';
41
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
36
42
  import { Constants } from '@/common';
37
43
 
38
44
  type StandardTextBlock = Data.StandardTextBlock;
@@ -157,11 +163,25 @@ function hoistToolResultCacheControl(
157
163
  }
158
164
  let cacheControl: unknown;
159
165
  const stripped = content.map((block) => {
160
- if ('cache_control' in block) {
161
- cacheControl ??= (block as Record<string, unknown>).cache_control;
162
- const clone = { ...(block as Record<string, unknown>) };
163
- delete clone.cache_control;
164
- return clone as MessageContentComplex;
166
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
167
+ return block;
168
+ }
169
+ try {
170
+ const descriptors = Object.getOwnPropertyDescriptors(block);
171
+ const cacheControlDescriptor = descriptors.cache_control;
172
+ if (
173
+ cacheControlDescriptor.enumerable === true &&
174
+ 'value' in cacheControlDescriptor
175
+ ) {
176
+ cacheControl ??= cacheControlDescriptor.value;
177
+ delete descriptors.cache_control;
178
+ return Object.create(
179
+ Object.getPrototypeOf(block),
180
+ descriptors
181
+ ) as MessageContentComplex;
182
+ }
183
+ } catch {
184
+ return block;
165
185
  }
166
186
  return block;
167
187
  });
@@ -169,6 +189,115 @@ function hoistToolResultCacheControl(
169
189
  return { content: stripped, cacheControl };
170
190
  }
171
191
 
192
+ /**
193
+ * A ToolMessage can already contain an Anthropic-native `tool_result` block
194
+ * (for example, when a tool returns provider-native structured content). The
195
+ * outer ToolMessage conversion supplies the one required result envelope, so
196
+ * unwrap an exact single nested envelope rather than sending an invalid
197
+ * `tool_result.content: [{ type: "tool_result", ... }]` payload.
198
+ */
199
+ function unwrapToolMessageResultContent(content: ToolMessage['content']): {
200
+ content: string | MessageContentComplex[] | undefined;
201
+ cacheControl: unknown;
202
+ isError: boolean | undefined;
203
+ } {
204
+ if (!Array.isArray(content) || content.length !== 1) {
205
+ return { content, cacheControl: undefined, isError: undefined };
206
+ }
207
+
208
+ const block = content[0];
209
+ if (typeof block !== 'object' || block == null || isProxy(block)) {
210
+ return { content, cacheControl: undefined, isError: undefined };
211
+ }
212
+
213
+ try {
214
+ const type = Object.getOwnPropertyDescriptor(block, 'type');
215
+ const nestedContent = Object.getOwnPropertyDescriptor(block, 'content');
216
+ if (
217
+ type?.enumerable !== true ||
218
+ !('value' in type) ||
219
+ type.value !== 'tool_result' ||
220
+ (nestedContent != null &&
221
+ (nestedContent.enumerable !== true || !('value' in nestedContent)))
222
+ ) {
223
+ return { content, cacheControl: undefined, isError: undefined };
224
+ }
225
+
226
+ const cacheControl = Object.getOwnPropertyDescriptor(
227
+ block,
228
+ 'cache_control'
229
+ );
230
+ const isError = Object.getOwnPropertyDescriptor(block, 'is_error');
231
+ const nested =
232
+ nestedContent != null && 'value' in nestedContent
233
+ ? nestedContent.value
234
+ : undefined;
235
+ if (typeof nested === 'string') {
236
+ return {
237
+ content: nested,
238
+ cacheControl:
239
+ cacheControl?.enumerable === true && 'value' in cacheControl
240
+ ? cacheControl.value
241
+ : undefined,
242
+ isError:
243
+ isError?.enumerable === true &&
244
+ 'value' in isError &&
245
+ typeof isError.value === 'boolean'
246
+ ? isError.value
247
+ : undefined,
248
+ };
249
+ }
250
+ if (Array.isArray(nested)) {
251
+ return {
252
+ content: nested as MessageContentComplex[],
253
+ cacheControl:
254
+ cacheControl?.enumerable === true && 'value' in cacheControl
255
+ ? cacheControl.value
256
+ : undefined,
257
+ isError:
258
+ isError?.enumerable === true &&
259
+ 'value' in isError &&
260
+ typeof isError.value === 'boolean'
261
+ ? isError.value
262
+ : undefined,
263
+ };
264
+ }
265
+ if (nested == null) {
266
+ return {
267
+ content: undefined,
268
+ cacheControl:
269
+ cacheControl?.enumerable === true && 'value' in cacheControl
270
+ ? cacheControl.value
271
+ : undefined,
272
+ isError:
273
+ isError?.enumerable === true &&
274
+ 'value' in isError &&
275
+ typeof isError.value === 'boolean'
276
+ ? isError.value
277
+ : undefined,
278
+ };
279
+ }
280
+ return {
281
+ content: serializeStructuredValueBounded(
282
+ nested,
283
+ HARD_MAX_TOOL_RESULT_CHARS
284
+ ).content,
285
+ cacheControl:
286
+ cacheControl?.enumerable === true && 'value' in cacheControl
287
+ ? cacheControl.value
288
+ : undefined,
289
+ isError:
290
+ isError?.enumerable === true &&
291
+ 'value' in isError &&
292
+ typeof isError.value === 'boolean'
293
+ ? isError.value
294
+ : undefined,
295
+ };
296
+ } catch {
297
+ return { content, cacheControl: undefined, isError: undefined };
298
+ }
299
+ }
300
+
172
301
  function _ensureMessageContents(
173
302
  messages: BaseMessage[]
174
303
  ): (SystemMessage | HumanMessage | AIMessage)[] {
@@ -209,21 +338,38 @@ function _ensureMessageContents(
209
338
  );
210
339
  }
211
340
  } else {
212
- const toolMessageContent = (
213
- message as { content?: BaseMessage['content'] | null }
214
- ).content;
341
+ const toolMessage = message as ToolMessage;
342
+ const toolMessageContent = toolMessage.content;
343
+ const unwrapped = unwrapToolMessageResultContent(toolMessageContent);
344
+ const formattedContent =
345
+ unwrapped.content == null
346
+ ? undefined
347
+ : _formatContent(
348
+ unwrapped.content === toolMessageContent
349
+ ? toolMessage
350
+ : cloneToolMessageWithContent(
351
+ toolMessage,
352
+ unwrapped.content as Parameters<
353
+ typeof cloneToolMessageWithContent
354
+ >[1]
355
+ )
356
+ );
215
357
  // Hoist a tail cache_control off the inner content onto the
216
358
  // tool_result block itself (the documented cacheable position).
217
- const { content: hoistedContent, cacheControl } =
218
- toolMessageContent != null
219
- ? hoistToolResultCacheControl(_formatContent(message))
359
+ const { content: hoistedContent, cacheControl: innerCacheControl } =
360
+ formattedContent != null
361
+ ? hoistToolResultCacheControl(formattedContent)
220
362
  : { content: undefined, cacheControl: undefined };
363
+ const cacheControl = unwrapped.cacheControl ?? innerCacheControl;
221
364
  updatedMsgs.push(
222
365
  new HumanMessage({
223
366
  content: [
224
367
  {
225
368
  type: 'tool_result',
226
369
  ...(hoistedContent != null ? { content: hoistedContent } : {}),
370
+ ...(unwrapped.isError != null
371
+ ? { is_error: unwrapped.isError }
372
+ : {}),
227
373
  ...(cacheControl != null
228
374
  ? { cache_control: cacheControl as { type: 'ephemeral' } }
229
375
  : {}),
@@ -84,6 +84,115 @@ describe('normalizeAnthropicToolCallId', () => {
84
84
  });
85
85
 
86
86
  describe('_convertMessagesToAnthropicPayload — cross-provider ID normalization', () => {
87
+ it('flattens an Anthropic-native tool_result returned by a tool', () => {
88
+ const toolCallId = 'toolu_native_result';
89
+ const payload = _convertMessagesToAnthropicPayload([
90
+ new HumanMessage('run the native tool'),
91
+ new AIMessage({
92
+ content: '',
93
+ tool_calls: [
94
+ {
95
+ id: toolCallId,
96
+ name: 'native_tool',
97
+ args: {},
98
+ type: 'tool_call',
99
+ },
100
+ ],
101
+ }),
102
+ new ToolMessage({
103
+ tool_call_id: toolCallId,
104
+ content: [
105
+ {
106
+ type: 'tool_result',
107
+ tool_use_id: toolCallId,
108
+ cache_control: { type: 'ephemeral', ttl: '1h' },
109
+ is_error: true,
110
+ content: [
111
+ {
112
+ type: 'text',
113
+ text: 'native result',
114
+ cache_control: { type: 'ephemeral' },
115
+ },
116
+ {
117
+ type: 'image_url',
118
+ image_url: 'data:image/png;base64,AA==',
119
+ },
120
+ ],
121
+ },
122
+ ],
123
+ }),
124
+ ]);
125
+
126
+ const result = (
127
+ payload.messages.find(
128
+ (message) =>
129
+ message.role === 'user' &&
130
+ Array.isArray(message.content) &&
131
+ message.content.some((block) => block.type === 'tool_result')
132
+ )!.content as unknown as Array<Record<string, unknown>>
133
+ ).find((block) => block.type === 'tool_result')!;
134
+
135
+ expect(result).toEqual({
136
+ type: 'tool_result',
137
+ tool_use_id: toolCallId,
138
+ cache_control: { type: 'ephemeral', ttl: '1h' },
139
+ is_error: true,
140
+ content: [
141
+ { type: 'text', text: 'native result' },
142
+ {
143
+ type: 'image',
144
+ source: {
145
+ type: 'base64',
146
+ media_type: 'image/png',
147
+ data: 'AA==',
148
+ },
149
+ },
150
+ ],
151
+ });
152
+ expect(
153
+ (result.content as unknown as Array<Record<string, unknown>>).some(
154
+ (block) => block.type === 'tool_result'
155
+ )
156
+ ).toBe(false);
157
+ });
158
+
159
+ it('flattens a content-less native error tool_result', () => {
160
+ const toolCallId = 'toolu_empty_error';
161
+ const payload = _convertMessagesToAnthropicPayload([
162
+ new AIMessage({
163
+ content: '',
164
+ tool_calls: [
165
+ {
166
+ id: toolCallId,
167
+ name: 'native_tool',
168
+ args: {},
169
+ type: 'tool_call',
170
+ },
171
+ ],
172
+ }),
173
+ new ToolMessage({
174
+ tool_call_id: toolCallId,
175
+ content: [
176
+ {
177
+ type: 'tool_result',
178
+ tool_use_id: toolCallId,
179
+ is_error: true,
180
+ },
181
+ ],
182
+ }),
183
+ ]);
184
+
185
+ const result = (
186
+ payload.messages.find((message) => message.role === 'user')!
187
+ .content as unknown as Array<Record<string, unknown>>
188
+ )[0];
189
+ expect(result).toEqual({
190
+ type: 'tool_result',
191
+ tool_use_id: toolCallId,
192
+ is_error: true,
193
+ });
194
+ });
195
+
87
196
  it('normalizes Responses-style IDs on tool_use AND matching tool_result', () => {
88
197
  const responsesId = 'fc_67abc1234def567|call_abc123def456ghi789jkl0mnopqrs';
89
198
 
@@ -1,6 +1,7 @@
1
- import { AIMessage, HumanMessage } from '@langchain/core/messages';
1
+ import { AIMessage, HumanMessage, ToolMessage } from '@langchain/core/messages';
2
2
  import type { BaseMessage } from '@langchain/core/messages';
3
3
  import { convertToConverseMessages } from './message_inputs';
4
+ import { toLangChainContent } from '@/messages/langchain';
4
5
 
5
6
  /**
6
7
  * Native-Bedrock reasoning serialization. A `reasoning_content` block whose
@@ -31,6 +32,53 @@ const assistantContent = (result: ConverseResult): ConverseBlock[] => {
31
32
  return (msg?.content ?? []) as ConverseBlock[];
32
33
  };
33
34
 
35
+ describe('convertToConverseMessages — Anthropic tool replay', () => {
36
+ it('deduplicates raw tool_use blocks and unwraps tool_result content', () => {
37
+ const messages: BaseMessage[] = [
38
+ new HumanMessage('Search'),
39
+ new AIMessage({
40
+ content: toLangChainContent([
41
+ { type: 'text', text: 'Searching.' },
42
+ {
43
+ type: 'tool_use',
44
+ id: 'call_search',
45
+ name: 'search',
46
+ input: '',
47
+ },
48
+ ]),
49
+ tool_calls: [
50
+ {
51
+ id: 'call_search',
52
+ name: 'search',
53
+ args: { query: 'test' },
54
+ },
55
+ ],
56
+ }),
57
+ new ToolMessage({
58
+ content: toLangChainContent([
59
+ {
60
+ type: 'tool_result',
61
+ tool_use_id: 'call_search',
62
+ is_error: true,
63
+ content: 'result body',
64
+ },
65
+ ]),
66
+ tool_call_id: 'call_search',
67
+ }),
68
+ ];
69
+
70
+ const converted = convertToConverseMessages(messages);
71
+ const serialized = JSON.stringify(converted);
72
+
73
+ expect(serialized.match(/"toolUseId":"call_search"/g)).toHaveLength(2);
74
+ expect(serialized.match(/"toolUse":/g)).toHaveLength(1);
75
+ expect(serialized.match(/"toolResult":/g)).toHaveLength(1);
76
+ expect(serialized).toContain('"text":"result body"');
77
+ expect(serialized).toContain('"status":"error"');
78
+ expect(serialized).not.toContain('"type":"tool_result"');
79
+ });
80
+ });
81
+
34
82
  describe('convertToConverseMessages — native Bedrock reasoning serialization', () => {
35
83
  it('drops a signature-only reasoning block, keeping text and tool calls', () => {
36
84
  const messages: BaseMessage[] = [
@@ -4,6 +4,7 @@
4
4
  */
5
5
  import {
6
6
  type BaseMessage,
7
+ type ToolMessage,
7
8
  isAIMessage,
8
9
  type Data,
9
10
  parseBase64DataUrl,
@@ -27,6 +28,8 @@ import type {
27
28
  BedrockContentBlock,
28
29
  MessageContentReasoningBlock,
29
30
  } from '../types';
31
+ import { serializeStructuredValueBounded } from '@/utils/toolContent';
32
+ import { HARD_MAX_TOOL_RESULT_CHARS } from '@/utils/truncation';
30
33
 
31
34
  /**
32
35
  * Reasoning blocks from other providers, relative to Bedrock. Bedrock's native
@@ -698,6 +701,13 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
698
701
  if (typeof msg.content === 'string' && msg.content !== '') {
699
702
  assistantMsg.content?.push({ text: msg.content });
700
703
  } else if (Array.isArray(msg.content)) {
704
+ const parsedToolCallIds = new Set(
705
+ isAIMessage(msg)
706
+ ? (msg.tool_calls ?? []).flatMap((toolCall) =>
707
+ toolCall.id != null ? [toolCall.id] : []
708
+ )
709
+ : []
710
+ );
701
711
  const concatenatedBlocks = concatenateLangchainReasoningBlocks(
702
712
  msg.content as Array<MessageContentComplex | MessageContentReasoningBlock>
703
713
  );
@@ -707,6 +717,33 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
707
717
  if (block.type === 'text') {
708
718
  const text = (block as { text?: string }).text ?? '';
709
719
  appendSerializableBedrockTextBlock(contentBlocks, text);
720
+ } else if (block.type === 'tool_use') {
721
+ const toolUse = block as {
722
+ id?: unknown;
723
+ name?: unknown;
724
+ input?: unknown;
725
+ };
726
+ if (
727
+ typeof toolUse.id === 'string' &&
728
+ parsedToolCallIds.has(toolUse.id)
729
+ ) {
730
+ return;
731
+ }
732
+ if (
733
+ typeof toolUse.id !== 'string' ||
734
+ typeof toolUse.name !== 'string' ||
735
+ toolUse.input == null ||
736
+ typeof toolUse.input !== 'object'
737
+ ) {
738
+ throw new Error('Invalid Anthropic tool_use content block');
739
+ }
740
+ contentBlocks.push({
741
+ toolUse: {
742
+ toolUseId: toolUse.id,
743
+ name: toolUse.name,
744
+ input: toolUse.input as Record<string, unknown>,
745
+ },
746
+ } as BedrockContentBlock);
710
747
  } else if (block.type === 'reasoning_content') {
711
748
  const reasoningBlock = block as MessageContentReasoningBlock;
712
749
  // Bedrock Converse rejects reasoningContent whose reasoningText.text is
@@ -756,13 +793,20 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
756
793
 
757
794
  // Important: this must be placed after any reasoning content blocks
758
795
  if (isAIMessage(msg) && msg.tool_calls != null && msg.tool_calls.length > 0) {
759
- const toolUseBlocks = msg.tool_calls.map((tc) => ({
760
- toolUse: {
761
- toolUseId: tc.id,
762
- name: tc.name,
763
- input: tc.args as Record<string, unknown>,
764
- },
765
- }));
796
+ const existingToolUseIds = new Set(
797
+ (assistantMsg.content ?? [])
798
+ .filter((content) => 'toolUse' in content)
799
+ .map((content) => content.toolUse?.toolUseId)
800
+ );
801
+ const toolUseBlocks = msg.tool_calls
802
+ .filter((toolCall) => !existingToolUseIds.has(toolCall.id))
803
+ .map((toolCall) => ({
804
+ toolUse: {
805
+ toolUseId: toolCall.id,
806
+ name: toolCall.name,
807
+ input: toolCall.args as Record<string, unknown>,
808
+ },
809
+ }));
766
810
  assistantMsg.content = [
767
811
  ...(assistantMsg.content ?? []),
768
812
  ...toolUseBlocks,
@@ -917,17 +961,45 @@ function convertHumanMessageToConverseMessage(
917
961
  */
918
962
  function convertToolMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
919
963
  const toolCallId = (msg as { tool_call_id?: string }).tool_call_id;
964
+ let isError = (msg as ToolMessage).status === 'error';
920
965
 
921
966
  let content: BedrockContentBlock[];
922
967
  if (typeof msg.content === 'string') {
923
968
  content = [{ text: msg.content }];
924
969
  } else if (Array.isArray(msg.content)) {
925
- content = msg.content.map((block) =>
926
- convertLangChainContentBlockToConverseContentBlock({
927
- block,
928
- onUnknown: 'passthrough',
929
- })
930
- );
970
+ content = msg.content.flatMap((block) => {
971
+ if (typeof block === 'object' && block.type === 'tool_result') {
972
+ if ((block as { is_error?: unknown }).is_error === true) {
973
+ isError = true;
974
+ }
975
+ const toolResultContent = (block as { content?: unknown }).content;
976
+ if (typeof toolResultContent === 'string') {
977
+ return [{ text: toolResultContent }];
978
+ }
979
+ if (Array.isArray(toolResultContent)) {
980
+ return toolResultContent.map((nestedBlock) =>
981
+ convertLangChainContentBlockToConverseContentBlock({
982
+ block: nestedBlock,
983
+ onUnknown: 'passthrough',
984
+ })
985
+ );
986
+ }
987
+ return [
988
+ {
989
+ text: serializeStructuredValueBounded(
990
+ toolResultContent,
991
+ HARD_MAX_TOOL_RESULT_CHARS
992
+ ).content,
993
+ },
994
+ ];
995
+ }
996
+ return [
997
+ convertLangChainContentBlockToConverseContentBlock({
998
+ block,
999
+ onUnknown: 'passthrough',
1000
+ }),
1001
+ ];
1002
+ });
931
1003
  } else {
932
1004
  content = [{ text: String(msg.content) }];
933
1005
  }
@@ -958,6 +1030,7 @@ function convertToolMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
958
1030
  toolResult: {
959
1031
  toolUseId: toolCallId,
960
1032
  content: toolResultContent as { text: string }[],
1033
+ ...(isError ? { status: 'error' as const } : {}),
961
1034
  },
962
1035
  },
963
1036
  ...trailingCachePoints,