@librechat/agents 3.3.3 → 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.
- package/dist/cjs/agents/AgentContext.cjs +2 -1
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +342 -109
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +3 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +79 -6
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +37 -10
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/google/utils/common.cjs +19 -7
- package/dist/cjs/llm/google/utils/common.cjs.map +1 -1
- package/dist/cjs/llm/invoke.cjs +64 -3
- package/dist/cjs/llm/invoke.cjs.map +1 -1
- package/dist/cjs/llm/openai/index.cjs +41 -5
- package/dist/cjs/llm/openai/index.cjs.map +1 -1
- package/dist/cjs/llm/openai/utils/index.cjs +25 -4
- package/dist/cjs/llm/openai/utils/index.cjs.map +1 -1
- package/dist/cjs/llm/openrouter/index.cjs +4 -1
- package/dist/cjs/llm/openrouter/index.cjs.map +1 -1
- package/dist/cjs/main.cjs +19 -2
- package/dist/cjs/messages/cache.cjs +54 -33
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/messages/contextPruning.cjs +17 -43
- package/dist/cjs/messages/contextPruning.cjs.map +1 -1
- package/dist/cjs/messages/core.cjs +315 -23
- package/dist/cjs/messages/core.cjs.map +1 -1
- package/dist/cjs/messages/format.cjs +218 -60
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/index.cjs +1 -1
- package/dist/cjs/messages/prune.cjs +503 -127
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/stream.cjs +7 -3
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +28 -14
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +54 -41
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +6 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/index.cjs +1 -1
- package/dist/cjs/utils/llm.cjs +1 -1
- package/dist/cjs/utils/llm.cjs.map +1 -1
- package/dist/cjs/utils/tokens.cjs +307 -20
- package/dist/cjs/utils/tokens.cjs.map +1 -1
- package/dist/cjs/utils/toolContent.cjs +1514 -0
- package/dist/cjs/utils/toolContent.cjs.map +1 -0
- package/dist/esm/agents/AgentContext.mjs +2 -1
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +349 -116
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +3 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +79 -6
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +37 -10
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/google/utils/common.mjs +19 -7
- package/dist/esm/llm/google/utils/common.mjs.map +1 -1
- package/dist/esm/llm/invoke.mjs +65 -5
- package/dist/esm/llm/invoke.mjs.map +1 -1
- package/dist/esm/llm/openai/index.mjs +41 -5
- package/dist/esm/llm/openai/index.mjs.map +1 -1
- package/dist/esm/llm/openai/utils/index.mjs +25 -4
- package/dist/esm/llm/openai/utils/index.mjs.map +1 -1
- package/dist/esm/llm/openrouter/index.mjs +4 -1
- package/dist/esm/llm/openrouter/index.mjs.map +1 -1
- package/dist/esm/main.mjs +7 -7
- package/dist/esm/messages/cache.mjs +55 -34
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/messages/contextPruning.mjs +17 -43
- package/dist/esm/messages/contextPruning.mjs.map +1 -1
- package/dist/esm/messages/core.mjs +305 -24
- package/dist/esm/messages/core.mjs.map +1 -1
- package/dist/esm/messages/format.mjs +218 -61
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/index.mjs +1 -1
- package/dist/esm/messages/prune.mjs +500 -128
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/stream.mjs +7 -3
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +28 -14
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +54 -41
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +6 -0
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/index.mjs +1 -1
- package/dist/esm/utils/llm.mjs +1 -1
- package/dist/esm/utils/llm.mjs.map +1 -1
- package/dist/esm/utils/tokens.mjs +307 -21
- package/dist/esm/utils/tokens.mjs.map +1 -1
- package/dist/esm/utils/toolContent.mjs +1503 -0
- package/dist/esm/utils/toolContent.mjs.map +1 -0
- package/dist/types/graphs/Graph.d.ts +1 -1
- package/dist/types/llm/invoke.d.ts +27 -1
- package/dist/types/llm/openai/index.d.ts +4 -0
- package/dist/types/llm/openai/utils/index.d.ts +2 -0
- package/dist/types/llm/openrouter/index.d.ts +5 -0
- package/dist/types/messages/contextPruning.d.ts +2 -1
- package/dist/types/messages/core.d.ts +36 -0
- package/dist/types/messages/format.d.ts +6 -0
- package/dist/types/messages/prune.d.ts +16 -12
- package/dist/types/types/stream.d.ts +1 -1
- package/dist/types/utils/tokens.d.ts +7 -0
- package/dist/types/utils/toolContent.d.ts +107 -0
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +74 -0
- package/src/agents/AgentContext.ts +1 -0
- package/src/graphs/Graph.ts +698 -200
- package/src/graphs/MultiAgentGraph.ts +6 -1
- package/src/graphs/__tests__/Graph.contextOverflow.test.ts +1128 -4
- package/src/llm/anthropic/utils/message_inputs.ts +158 -12
- package/src/llm/anthropic/utils/tool-id-normalization.test.ts +109 -0
- package/src/llm/bedrock/utils/message_inputs.test.ts +49 -1
- package/src/llm/bedrock/utils/message_inputs.ts +86 -13
- package/src/llm/google/utils/common.test.ts +75 -9
- package/src/llm/google/utils/common.ts +34 -3
- package/src/llm/invoke.test.ts +488 -0
- package/src/llm/invoke.ts +196 -2
- package/src/llm/openai/index.ts +106 -6
- package/src/llm/openai/utils/index.ts +109 -53
- package/src/llm/openai/utils/messages.test.ts +330 -1
- package/src/llm/openrouter/index.ts +12 -2
- package/src/llm/openrouter/reasoning.test.ts +314 -0
- package/src/messages/__tests__/observationMasking.test.ts +93 -2
- package/src/messages/cache.tail.test.ts +193 -0
- package/src/messages/cache.test.ts +113 -0
- package/src/messages/cache.ts +92 -60
- package/src/messages/contextPruning.test.ts +184 -0
- package/src/messages/contextPruning.ts +49 -42
- package/src/messages/core.ts +653 -40
- package/src/messages/ensureThinkingBlock.test.ts +49 -1
- package/src/messages/foldToollessToolBlocks.test.ts +163 -5
- package/src/messages/format.ts +363 -91
- package/src/messages/formatAgentMessages.test.ts +604 -14
- package/src/messages/formatAgentMessages.tools.test.ts +103 -0
- package/src/messages/prune.ts +996 -183
- package/src/specs/prune.test.ts +1083 -6
- package/src/specs/summarization-unit.test.ts +105 -0
- package/src/specs/tokens.test.ts +609 -32
- package/src/stream.ts +23 -15
- package/src/summarization/__tests__/node.test.ts +77 -0
- package/src/summarization/node.ts +46 -18
- package/src/tools/ToolNode.ts +147 -68
- package/src/tools/__tests__/ToolNode.onResultCompletion.test.ts +148 -1
- package/src/tools/__tests__/ToolNode.outputReferences.test.ts +112 -3
- package/src/tools/__tests__/annotateMessagesForLLM.test.ts +32 -0
- package/src/tools/__tests__/directToolHooks.test.ts +255 -7
- package/src/tools/toolOutputReferences.ts +6 -0
- package/src/types/stream.ts +1 -1
- package/src/utils/llm.test.ts +18 -0
- package/src/utils/llm.ts +4 -1
- package/src/utils/tokens.ts +586 -31
- package/src/utils/toolContent.test.ts +1432 -0
- package/src/utils/toolContent.ts +2707 -0
|
@@ -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
|
-
|
|
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 ('
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
)
|
|
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
|
-
|
|
219
|
-
? hoistToolResultCacheControl(
|
|
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
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
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.
|
|
926
|
-
|
|
927
|
-
block
|
|
928
|
-
|
|
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,
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { expect, test, describe } from '@jest/globals';
|
|
2
|
-
import { AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
-
import type {
|
|
2
|
+
import { AIMessage, AIMessageChunk } from '@langchain/core/messages';
|
|
3
|
+
import type {
|
|
4
|
+
Content,
|
|
5
|
+
EnhancedGenerateContentResponse,
|
|
6
|
+
} from '@google/generative-ai';
|
|
4
7
|
import {
|
|
5
8
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
6
9
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
@@ -8,9 +11,53 @@ import {
|
|
|
8
11
|
} from '@/tools/streamedToolCallSeals';
|
|
9
12
|
import {
|
|
10
13
|
convertResponseContentToChatGenerationChunk,
|
|
14
|
+
convertMessageContentToParts,
|
|
11
15
|
dropUnsupportedModelTurnPrefill,
|
|
12
16
|
rejectsModelTurnPrefill,
|
|
13
17
|
} from './common';
|
|
18
|
+
import { toLangChainContent } from '@/messages/langchain';
|
|
19
|
+
|
|
20
|
+
describe('convertMessageContentToParts cross-provider tool calls', () => {
|
|
21
|
+
test('deduplicates Anthropic tool_use content mirrored in parsed tool_calls', () => {
|
|
22
|
+
const message = new AIMessage({
|
|
23
|
+
content: toLangChainContent([
|
|
24
|
+
{ type: 'text', text: 'Checking.' },
|
|
25
|
+
{
|
|
26
|
+
type: 'tool_use',
|
|
27
|
+
id: 'call_weather',
|
|
28
|
+
name: 'weather',
|
|
29
|
+
input: '',
|
|
30
|
+
},
|
|
31
|
+
{ type: 'text', index: 0, input: '{"city":"NYC"}' },
|
|
32
|
+
]),
|
|
33
|
+
tool_calls: [
|
|
34
|
+
{
|
|
35
|
+
id: 'call_weather',
|
|
36
|
+
name: 'weather',
|
|
37
|
+
args: { city: 'NYC' },
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const parts = convertMessageContentToParts(
|
|
43
|
+
message,
|
|
44
|
+
false,
|
|
45
|
+
[],
|
|
46
|
+
'gemini-3.5-flash'
|
|
47
|
+
);
|
|
48
|
+
const functionCalls = parts.filter((part) => 'functionCall' in part);
|
|
49
|
+
|
|
50
|
+
expect(functionCalls).toHaveLength(1);
|
|
51
|
+
expect(parts).not.toContainEqual({});
|
|
52
|
+
expect(functionCalls[0]).toMatchObject({
|
|
53
|
+
functionCall: {
|
|
54
|
+
id: 'call_weather',
|
|
55
|
+
name: 'weather',
|
|
56
|
+
args: { city: 'NYC' },
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
});
|
|
14
61
|
|
|
15
62
|
function buildResponse(
|
|
16
63
|
parts: Array<Record<string, unknown>>
|
|
@@ -72,7 +119,9 @@ describe('rejectsModelTurnPrefill', () => {
|
|
|
72
119
|
expect(rejectsModelTurnPrefill('gemini-3.6-flash')).toBe(true);
|
|
73
120
|
expect(rejectsModelTurnPrefill('gemini-3.5-flash-lite')).toBe(true);
|
|
74
121
|
expect(rejectsModelTurnPrefill('models/gemini-3.6-flash')).toBe(true);
|
|
75
|
-
expect(rejectsModelTurnPrefill('google/gemini-3.5-flash-lite-latest')).toBe(
|
|
122
|
+
expect(rejectsModelTurnPrefill('google/gemini-3.5-flash-lite-latest')).toBe(
|
|
123
|
+
true
|
|
124
|
+
);
|
|
76
125
|
});
|
|
77
126
|
|
|
78
127
|
test('is false for models that still accept prefill and for empty input', () => {
|
|
@@ -86,34 +135,51 @@ describe('rejectsModelTurnPrefill', () => {
|
|
|
86
135
|
|
|
87
136
|
describe('dropUnsupportedModelTurnPrefill', () => {
|
|
88
137
|
const userTurn: Content = { role: 'user', parts: [{ text: 'Hi' }] };
|
|
89
|
-
const modelTurn: Content = {
|
|
138
|
+
const modelTurn: Content = {
|
|
139
|
+
role: 'model',
|
|
140
|
+
parts: [{ text: 'Hello, I am' }],
|
|
141
|
+
};
|
|
90
142
|
|
|
91
143
|
test('drops a trailing model turn for no-prefill models', () => {
|
|
92
144
|
const contents: Content[] = [userTurn, modelTurn];
|
|
93
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
145
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
146
|
+
contents,
|
|
147
|
+
'gemini-3.6-flash'
|
|
148
|
+
);
|
|
94
149
|
expect(result).toEqual([userTurn]);
|
|
95
150
|
});
|
|
96
151
|
|
|
97
152
|
test('drops multiple consecutive trailing model turns but keeps one turn', () => {
|
|
98
153
|
const contents: Content[] = [userTurn, modelTurn, modelTurn];
|
|
99
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
154
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
155
|
+
contents,
|
|
156
|
+
'gemini-3.5-flash-lite'
|
|
157
|
+
);
|
|
100
158
|
expect(result).toEqual([userTurn]);
|
|
101
159
|
});
|
|
102
160
|
|
|
103
161
|
test('leaves a trailing model turn for models that accept prefill', () => {
|
|
104
162
|
const contents: Content[] = [userTurn, modelTurn];
|
|
105
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
163
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
164
|
+
contents,
|
|
165
|
+
'gemini-3.5-flash'
|
|
166
|
+
);
|
|
106
167
|
expect(result).toBe(contents);
|
|
107
168
|
});
|
|
108
169
|
|
|
109
170
|
test('is a no-op when the request already ends with a user turn', () => {
|
|
110
171
|
const contents: Content[] = [modelTurn, userTurn];
|
|
111
|
-
const result = dropUnsupportedModelTurnPrefill(
|
|
172
|
+
const result = dropUnsupportedModelTurnPrefill(
|
|
173
|
+
contents,
|
|
174
|
+
'gemini-3.6-flash'
|
|
175
|
+
);
|
|
112
176
|
expect(result).toBe(contents);
|
|
113
177
|
});
|
|
114
178
|
|
|
115
179
|
test('is a no-op for empty or undefined contents', () => {
|
|
116
180
|
expect(dropUnsupportedModelTurnPrefill([], 'gemini-3.6-flash')).toEqual([]);
|
|
117
|
-
expect(
|
|
181
|
+
expect(
|
|
182
|
+
dropUnsupportedModelTurnPrefill(undefined, 'gemini-3.6-flash')
|
|
183
|
+
).toBeUndefined();
|
|
118
184
|
});
|
|
119
185
|
});
|