@librechat/agents 3.3.7 → 3.3.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.
- package/dist/cjs/graphs/Graph.cjs +47 -13
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/graphs/MultiAgentGraph.cjs +56 -6
- package/dist/cjs/graphs/MultiAgentGraph.cjs.map +1 -1
- package/dist/cjs/hitl/askUserQuestion.cjs +3 -2
- package/dist/cjs/hitl/askUserQuestion.cjs.map +1 -1
- package/dist/cjs/instrumentation.cjs +18 -48
- package/dist/cjs/instrumentation.cjs.map +1 -1
- package/dist/cjs/langfuse.cjs +174 -29
- package/dist/cjs/langfuse.cjs.map +1 -1
- package/dist/cjs/langfuseConfig.cjs +12 -0
- package/dist/cjs/langfuseConfig.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeContext.cjs +23 -2
- package/dist/cjs/langfuseRuntimeContext.cjs.map +1 -1
- package/dist/cjs/langfuseRuntimeScope.cjs +39 -8
- package/dist/cjs/langfuseRuntimeScope.cjs.map +1 -1
- package/dist/cjs/langfuseSpanRegistry.cjs +95 -0
- package/dist/cjs/langfuseSpanRegistry.cjs.map +1 -0
- package/dist/cjs/langfuseTraceShaping.cjs +121 -4
- package/dist/cjs/langfuseTraceShaping.cjs.map +1 -1
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs +39 -15
- package/dist/cjs/llm/anthropic/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs +25 -5
- package/dist/cjs/llm/bedrock/utils/message_inputs.cjs.map +1 -1
- package/dist/cjs/llm/init.cjs +3 -3
- package/dist/cjs/llm/invoke.cjs +5 -5
- package/dist/cjs/llm/openai/index.cjs +1 -1
- package/dist/cjs/main.cjs +10 -10
- package/dist/cjs/messages/format.cjs +124 -15
- package/dist/cjs/messages/format.cjs.map +1 -1
- package/dist/cjs/messages/injected.cjs +10 -1
- package/dist/cjs/messages/injected.cjs.map +1 -1
- package/dist/cjs/messages/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +51 -11
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +54 -24
- package/dist/cjs/run.cjs.map +1 -1
- package/dist/cjs/session/messageSerialization.cjs +6 -0
- package/dist/cjs/session/messageSerialization.cjs.map +1 -1
- package/dist/cjs/stream.cjs +21 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/summarization/node.cjs +60 -0
- package/dist/cjs/summarization/node.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +253 -24
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +1 -1
- package/dist/cjs/tools/search/tool.cjs +1 -1
- package/dist/cjs/tools/subagent/SubagentExecutor.cjs +1 -1
- package/dist/cjs/utils/index.cjs +2 -2
- package/dist/esm/graphs/Graph.mjs +48 -14
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/graphs/MultiAgentGraph.mjs +56 -6
- package/dist/esm/graphs/MultiAgentGraph.mjs.map +1 -1
- package/dist/esm/hitl/askUserQuestion.mjs +3 -2
- package/dist/esm/hitl/askUserQuestion.mjs.map +1 -1
- package/dist/esm/instrumentation.mjs +18 -48
- package/dist/esm/instrumentation.mjs.map +1 -1
- package/dist/esm/langfuse.mjs +176 -28
- package/dist/esm/langfuse.mjs.map +1 -1
- package/dist/esm/langfuseConfig.mjs +10 -1
- package/dist/esm/langfuseConfig.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeContext.mjs +21 -3
- package/dist/esm/langfuseRuntimeContext.mjs.map +1 -1
- package/dist/esm/langfuseRuntimeScope.mjs +39 -10
- package/dist/esm/langfuseRuntimeScope.mjs.map +1 -1
- package/dist/esm/langfuseSpanRegistry.mjs +91 -0
- package/dist/esm/langfuseSpanRegistry.mjs.map +1 -0
- package/dist/esm/langfuseTraceShaping.mjs +121 -4
- package/dist/esm/langfuseTraceShaping.mjs.map +1 -1
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs +39 -15
- package/dist/esm/llm/anthropic/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs +25 -5
- package/dist/esm/llm/bedrock/utils/message_inputs.mjs.map +1 -1
- package/dist/esm/llm/init.mjs +2 -2
- package/dist/esm/llm/invoke.mjs +5 -5
- package/dist/esm/llm/openai/index.mjs +1 -1
- package/dist/esm/main.mjs +8 -8
- package/dist/esm/messages/format.mjs +124 -15
- package/dist/esm/messages/format.mjs.map +1 -1
- package/dist/esm/messages/injected.mjs +10 -1
- package/dist/esm/messages/injected.mjs.map +1 -1
- package/dist/esm/messages/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +51 -11
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +54 -24
- package/dist/esm/run.mjs.map +1 -1
- package/dist/esm/session/messageSerialization.mjs +6 -0
- package/dist/esm/session/messageSerialization.mjs.map +1 -1
- package/dist/esm/stream.mjs +21 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/summarization/node.mjs +60 -0
- package/dist/esm/summarization/node.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +254 -25
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +1 -1
- package/dist/esm/tools/search/tool.mjs +1 -1
- package/dist/esm/tools/subagent/SubagentExecutor.mjs +1 -1
- package/dist/esm/utils/index.mjs +2 -2
- package/dist/types/graphs/Graph.d.ts +19 -0
- package/dist/types/hitl/askUserQuestion.d.ts +11 -1
- package/dist/types/langfuse.d.ts +16 -8
- package/dist/types/langfuseConfig.d.ts +6 -0
- package/dist/types/langfuseRuntimeContext.d.ts +27 -1
- package/dist/types/langfuseRuntimeScope.d.ts +17 -2
- package/dist/types/langfuseSpanRegistry.d.ts +17 -0
- package/dist/types/langfuseTraceShaping.d.ts +2 -1
- package/dist/types/llm/anthropic/utils/message_inputs.d.ts +1 -0
- package/dist/types/messages/format.d.ts +9 -8
- package/dist/types/prompts/activityLabel.d.ts +8 -1
- package/dist/types/run.d.ts +1 -1
- package/dist/types/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/activityLabel.d.ts +8 -0
- package/dist/types/types/hitl.d.ts +8 -0
- package/dist/types/types/stream.d.ts +19 -0
- package/dist/types/types/tools.d.ts +30 -0
- package/package.json +7 -4
- package/src/__tests__/stream.eagerArgsDivergence.test.ts +753 -0
- package/src/graphs/Graph.ts +69 -20
- package/src/graphs/MultiAgentGraph.ts +74 -6
- package/src/graphs/__tests__/composition.smoke.test.ts +4 -0
- package/src/hitl/askUserQuestion.ts +14 -1
- package/src/instrumentation.ts +35 -77
- package/src/langfuse.ts +320 -43
- package/src/langfuseConfig.ts +24 -0
- package/src/langfuseRuntimeContext.ts +43 -1
- package/src/langfuseRuntimeScope.ts +94 -21
- package/src/langfuseSpanRegistry.ts +131 -0
- package/src/langfuseTraceShaping.ts +194 -7
- package/src/llm/anthropic/utils/message_inputs.ts +70 -19
- package/src/llm/anthropic/utils/streaming-tool-input.test.ts +186 -11
- package/src/llm/bedrock/utils/message_inputs.test.ts +120 -4
- package/src/llm/bedrock/utils/message_inputs.ts +32 -7
- package/src/messages/format.ts +222 -50
- package/src/messages/formatAgentMessages.test.ts +308 -6
- package/src/messages/injected.test.ts +18 -1
- package/src/messages/injected.ts +8 -1
- package/src/messages/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +67 -2
- package/src/run.ts +86 -46
- package/src/scripts/activity-labels/captured.json +56 -0
- package/src/scripts/activity-labels/checks.cjs +205 -0
- package/src/scripts/activity-labels/corpus.cjs +473 -0
- package/src/scripts/activity-labels/report.cjs +203 -0
- package/src/scripts/activity-labels/rescore.cjs +102 -0
- package/src/scripts/activity-labels/run.ts +705 -0
- package/src/scripts/activity-labels/variants.ts +71 -0
- package/src/session/messageSerialization.ts +12 -1
- package/src/session/types.ts +1 -0
- package/src/specs/activity-label-prompt.test.ts +109 -0
- package/src/specs/agent-handoffs.test.ts +306 -0
- package/src/specs/langfuse-callbacks.test.ts +456 -0
- package/src/specs/langfuse-routing.integration.test.ts +138 -1
- package/src/specs/langfuse-span-registry.test.ts +70 -0
- package/src/specs/langfuse-trace-shaping.test.ts +294 -0
- package/src/specs/prune.test.ts +38 -1
- package/src/stream.ts +70 -6
- package/src/summarization/__tests__/node.test.ts +188 -0
- package/src/summarization/node.ts +72 -0
- package/src/tools/ToolNode.ts +400 -9
- package/src/tools/__tests__/ToolNode.invalidToolCalls.test.ts +757 -0
- package/src/tools/__tests__/hitl.test.ts +58 -0
- package/src/types/activityLabel.ts +8 -0
- package/src/types/hitl.ts +8 -0
- package/src/types/stream.ts +20 -0
- package/src/types/tools.ts +35 -1
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { AIMessage, HumanMessage } from '@langchain/core/messages';
|
|
3
|
+
import type { ToolCall } from '@langchain/core/messages/tool';
|
|
3
4
|
import type { BaseMessage } from '@langchain/core/messages';
|
|
4
|
-
import {
|
|
5
|
+
import type {
|
|
6
|
+
AnthropicMessageCreateParams,
|
|
7
|
+
AnthropicServerToolUseBlockParam,
|
|
8
|
+
AnthropicToolUseBlockParam,
|
|
9
|
+
} from '@/llm/anthropic/types';
|
|
5
10
|
import { _makeMessageChunkFromAnthropicEvent } from './message_outputs';
|
|
11
|
+
import { _convertMessagesToAnthropicPayload } from './message_inputs';
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
14
|
* Regression for @langchain/core >= 1.1.46 streaming aggregation: a tool call's
|
|
@@ -40,7 +46,9 @@ describe('_convertMessagesToAnthropicPayload — aggregated streaming tool input
|
|
|
40
46
|
];
|
|
41
47
|
|
|
42
48
|
it('does not throw on the orphaned text-with-input block', () => {
|
|
43
|
-
expect(() =>
|
|
49
|
+
expect(() =>
|
|
50
|
+
_convertMessagesToAnthropicPayload(buildHistory())
|
|
51
|
+
).not.toThrow();
|
|
44
52
|
});
|
|
45
53
|
|
|
46
54
|
it('restores tool_use input from message.tool_calls and drops the orphan block', () => {
|
|
@@ -60,13 +68,16 @@ describe('_convertMessagesToAnthropicPayload — aggregated streaming tool input
|
|
|
60
68
|
// No leftover delta: no text block carrying `input`, no input_json_delta.
|
|
61
69
|
expect(
|
|
62
70
|
blocks.find(
|
|
63
|
-
(b) =>
|
|
71
|
+
(b) =>
|
|
72
|
+
(b.type === 'text' && 'input' in b) || b.type === 'input_json_delta'
|
|
64
73
|
)
|
|
65
74
|
).toBeUndefined();
|
|
66
75
|
|
|
67
76
|
// The real assistant text is preserved.
|
|
68
77
|
expect(
|
|
69
|
-
blocks.some(
|
|
78
|
+
blocks.some(
|
|
79
|
+
(b) => b.type === 'text' && b.text === 'Let me calculate that.'
|
|
80
|
+
)
|
|
70
81
|
).toBe(true);
|
|
71
82
|
});
|
|
72
83
|
|
|
@@ -94,7 +105,9 @@ describe('_convertMessagesToAnthropicPayload — aggregated streaming tool input
|
|
|
94
105
|
];
|
|
95
106
|
const payload = _convertMessagesToAnthropicPayload(history);
|
|
96
107
|
const assistant = payload.messages.find((m: any) => m.role === 'assistant');
|
|
97
|
-
const toolUse = (assistant!.content as any[]).find(
|
|
108
|
+
const toolUse = (assistant!.content as any[]).find(
|
|
109
|
+
(b) => b.type === 'tool_use'
|
|
110
|
+
);
|
|
98
111
|
expect(toolUse.input).toEqual({ input: '2 + 2' });
|
|
99
112
|
});
|
|
100
113
|
|
|
@@ -105,8 +118,18 @@ describe('_convertMessagesToAnthropicPayload — aggregated streaming tool input
|
|
|
105
118
|
new HumanMessage('What\'s the weather in Seattle tomorrow?'),
|
|
106
119
|
new AIMessage({
|
|
107
120
|
content: [
|
|
108
|
-
{
|
|
109
|
-
|
|
121
|
+
{
|
|
122
|
+
type: 'text',
|
|
123
|
+
index: 1,
|
|
124
|
+
text: 'I need to call the get_weather tool',
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
type: 'tool_use',
|
|
128
|
+
index: 2,
|
|
129
|
+
name: 'get_weather',
|
|
130
|
+
id: 'tool_call_id',
|
|
131
|
+
input: '',
|
|
132
|
+
},
|
|
110
133
|
{ type: 'input_json_delta', index: 2, input: '{"city": "' },
|
|
111
134
|
{ type: 'input_json_delta', index: 2, input: 'Seattle", "da' },
|
|
112
135
|
{ type: 'input_json_delta', index: 2, input: 'te": "to' },
|
|
@@ -129,6 +152,145 @@ describe('_convertMessagesToAnthropicPayload — aggregated streaming tool input
|
|
|
129
152
|
});
|
|
130
153
|
});
|
|
131
154
|
|
|
155
|
+
/**
|
|
156
|
+
* Regression for the summarization-CI flake: context-pressure truncation
|
|
157
|
+
* (`preFlightTruncateToolCallInputs` under a tight budget) used to null BOTH a
|
|
158
|
+
* tool_use block's inline `input` and its `tool_calls` args in graph state.
|
|
159
|
+
* Replaying that message shipped `"input": null` and Anthropic rejected the
|
|
160
|
+
* request with 400 `tool_use.input: Input should be an object`. The payload
|
|
161
|
+
* conversion must never emit a non-object input, whatever shape history is in.
|
|
162
|
+
*/
|
|
163
|
+
describe('_convertMessagesToAnthropicPayload — non-object tool_use input replay', () => {
|
|
164
|
+
/** History shapes context-pressure truncation can leave behind in state. */
|
|
165
|
+
type DegradedToolInput = string | Record<string, unknown> | null | undefined;
|
|
166
|
+
|
|
167
|
+
const buildHistory = (
|
|
168
|
+
input: DegradedToolInput,
|
|
169
|
+
args: DegradedToolInput
|
|
170
|
+
): BaseMessage[] => [
|
|
171
|
+
new HumanMessage('What is 9 * 9?'),
|
|
172
|
+
new AIMessage({
|
|
173
|
+
content: [{ type: 'tool_use', id: 'toolu_x', name: 'calculator', input }],
|
|
174
|
+
tool_calls: [
|
|
175
|
+
{
|
|
176
|
+
id: 'toolu_x',
|
|
177
|
+
name: 'calculator',
|
|
178
|
+
args: args as ToolCall['args'],
|
|
179
|
+
type: 'tool_call',
|
|
180
|
+
},
|
|
181
|
+
],
|
|
182
|
+
}),
|
|
183
|
+
];
|
|
184
|
+
|
|
185
|
+
const findAssistantBlock = (
|
|
186
|
+
payload: AnthropicMessageCreateParams,
|
|
187
|
+
type: 'tool_use' | 'server_tool_use'
|
|
188
|
+
): AnthropicToolUseBlockParam | AnthropicServerToolUseBlockParam => {
|
|
189
|
+
const assistant = payload.messages.find((m) => m.role === 'assistant');
|
|
190
|
+
const content = assistant?.content;
|
|
191
|
+
const block = (Array.isArray(content) ? content : []).find(
|
|
192
|
+
(b): b is AnthropicToolUseBlockParam | AnthropicServerToolUseBlockParam =>
|
|
193
|
+
b.type === type
|
|
194
|
+
);
|
|
195
|
+
expect(block).toBeDefined();
|
|
196
|
+
return block!;
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const getToolUse = (
|
|
200
|
+
history: BaseMessage[]
|
|
201
|
+
): AnthropicToolUseBlockParam | AnthropicServerToolUseBlockParam =>
|
|
202
|
+
findAssistantBlock(_convertMessagesToAnthropicPayload(history), 'tool_use');
|
|
203
|
+
|
|
204
|
+
it('ships an empty object when input and args were both truncated to null', () => {
|
|
205
|
+
const toolUse = getToolUse(buildHistory(null, null));
|
|
206
|
+
expect(toolUse.input).toEqual({});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('restores object args when only the inline input was nulled', () => {
|
|
210
|
+
const toolUse = getToolUse(buildHistory(null, { input: '9 * 9' }));
|
|
211
|
+
expect(toolUse.input).toEqual({ input: '9 * 9' });
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('restores intact args when the inline input degraded to an empty object', () => {
|
|
215
|
+
// Asymmetric truncation: the raw string input serializes longer than the
|
|
216
|
+
// args object, so a near-envelope cap can degrade the inline input to {}
|
|
217
|
+
// while the tool_calls mirror survives. Replay must prefer the mirror.
|
|
218
|
+
const toolUse = getToolUse(
|
|
219
|
+
buildHistory({}, { input: '670592745 / 99991' })
|
|
220
|
+
);
|
|
221
|
+
expect(toolUse.input).toEqual({ input: '670592745 / 99991' });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('ships an empty object when the inline input is {} and args were nulled too', () => {
|
|
225
|
+
const toolUse = getToolUse(buildHistory({}, null));
|
|
226
|
+
expect(toolUse.input).toEqual({});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('coerces non-object inputs on the srvtoolu_ server-tool normalization branch', () => {
|
|
230
|
+
const cases: Array<[DegradedToolInput, Record<string, unknown>]> = [
|
|
231
|
+
['123', {}],
|
|
232
|
+
['[1,2]', {}],
|
|
233
|
+
['{"query": "x"}', { query: 'x' }],
|
|
234
|
+
[null, {}],
|
|
235
|
+
];
|
|
236
|
+
for (const [raw, expected] of cases) {
|
|
237
|
+
const history: BaseMessage[] = [
|
|
238
|
+
new HumanMessage('search'),
|
|
239
|
+
new AIMessage({
|
|
240
|
+
content: [
|
|
241
|
+
{
|
|
242
|
+
type: 'server_tool_use',
|
|
243
|
+
id: 'srvtoolu_abc',
|
|
244
|
+
name: 'web_search',
|
|
245
|
+
input: raw,
|
|
246
|
+
},
|
|
247
|
+
],
|
|
248
|
+
}),
|
|
249
|
+
];
|
|
250
|
+
const block = findAssistantBlock(
|
|
251
|
+
_convertMessagesToAnthropicPayload(history),
|
|
252
|
+
'server_tool_use'
|
|
253
|
+
);
|
|
254
|
+
expect(block.input).toEqual(expected);
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('coerces a string input that parses to a non-object', () => {
|
|
259
|
+
for (const raw of ['123', '[1,2]', '"text"', 'null']) {
|
|
260
|
+
const toolUse = getToolUse(buildHistory(raw, undefined));
|
|
261
|
+
expect(toolUse.input).toEqual({});
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('still parses a complete JSON-object string input', () => {
|
|
266
|
+
const toolUse = getToolUse(buildHistory('{"input": "9 * 9"}', undefined));
|
|
267
|
+
expect(toolUse.input).toEqual({ input: '9 * 9' });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('coerces non-object args on the string-content tool_calls path', () => {
|
|
271
|
+
const history: BaseMessage[] = [
|
|
272
|
+
new HumanMessage('What is 9 * 9?'),
|
|
273
|
+
new AIMessage({
|
|
274
|
+
content: '',
|
|
275
|
+
tool_calls: [
|
|
276
|
+
{
|
|
277
|
+
id: 'toolu_x',
|
|
278
|
+
name: 'calculator',
|
|
279
|
+
args: null as unknown as ToolCall['args'],
|
|
280
|
+
type: 'tool_call',
|
|
281
|
+
},
|
|
282
|
+
],
|
|
283
|
+
}),
|
|
284
|
+
];
|
|
285
|
+
const payload = _convertMessagesToAnthropicPayload(history);
|
|
286
|
+
const assistant = payload.messages.find((m: any) => m.role === 'assistant');
|
|
287
|
+
const toolUse = (assistant!.content as any[]).find(
|
|
288
|
+
(b) => b.type === 'tool_use'
|
|
289
|
+
);
|
|
290
|
+
expect(toolUse.input).toEqual({});
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
|
|
132
294
|
describe('_makeMessageChunkFromAnthropicEvent — streamed tool input merges into content', () => {
|
|
133
295
|
const fields = { streamUsage: true, coerceContentToString: false };
|
|
134
296
|
|
|
@@ -137,7 +299,12 @@ describe('_makeMessageChunkFromAnthropicEvent — streamed tool input merges int
|
|
|
137
299
|
{
|
|
138
300
|
type: 'content_block_start',
|
|
139
301
|
index: 0,
|
|
140
|
-
content_block: {
|
|
302
|
+
content_block: {
|
|
303
|
+
type: 'tool_use',
|
|
304
|
+
id: 'toolu_1',
|
|
305
|
+
name: 'calculator',
|
|
306
|
+
input: {},
|
|
307
|
+
},
|
|
141
308
|
},
|
|
142
309
|
{
|
|
143
310
|
type: 'content_block_delta',
|
|
@@ -168,13 +335,21 @@ describe('_makeMessageChunkFromAnthropicEvent — streamed tool input merges int
|
|
|
168
335
|
const blocks = merged.content as any[];
|
|
169
336
|
|
|
170
337
|
const toolUse = blocks.find((b) => b.type === 'tool_use');
|
|
171
|
-
expect(toolUse).toMatchObject({
|
|
338
|
+
expect(toolUse).toMatchObject({
|
|
339
|
+
type: 'tool_use',
|
|
340
|
+
id: 'toolu_1',
|
|
341
|
+
name: 'calculator',
|
|
342
|
+
});
|
|
172
343
|
const parsed =
|
|
173
|
-
typeof toolUse.input === 'string'
|
|
344
|
+
typeof toolUse.input === 'string'
|
|
345
|
+
? JSON.parse(toolUse.input)
|
|
346
|
+
: toolUse.input;
|
|
174
347
|
expect(parsed).toEqual({ input: '2 + 2' });
|
|
175
348
|
|
|
176
349
|
// no orphaned delta block survives aggregation
|
|
177
|
-
expect(
|
|
350
|
+
expect(
|
|
351
|
+
blocks.filter((b) => b.type !== 'tool_use' && 'input' in b)
|
|
352
|
+
).toHaveLength(0);
|
|
178
353
|
|
|
179
354
|
// tool_calls remain correctly aggregated
|
|
180
355
|
expect(merged.tool_calls?.[0]).toMatchObject({
|
|
@@ -79,6 +79,120 @@ describe('convertToConverseMessages — Anthropic tool replay', () => {
|
|
|
79
79
|
});
|
|
80
80
|
});
|
|
81
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Bedrock Converse requires `toolUse.input` to be a JSON object. History can
|
|
84
|
+
* carry non-object values: pre-3.x context-pressure truncation persisted
|
|
85
|
+
* `null` onto both a block's inline input and its `tool_calls` args (see the
|
|
86
|
+
* Anthropic-replay fix in PR #369), and Anthropic-shaped inline blocks keep
|
|
87
|
+
* the raw streamed JSON string. These must coerce — never ship as-is, never
|
|
88
|
+
* throw the whole request away.
|
|
89
|
+
*/
|
|
90
|
+
describe('convertToConverseMessages — non-object toolUse input coercion', () => {
|
|
91
|
+
const toolUseBlocks = (result: ConverseResult): ConverseBlock[] =>
|
|
92
|
+
assistantContent(result).filter((b) => b.toolUse != null);
|
|
93
|
+
|
|
94
|
+
it('coerces null args to {} when materializing from tool_calls', () => {
|
|
95
|
+
const messages: BaseMessage[] = [
|
|
96
|
+
new HumanMessage('What is 9 * 9?'),
|
|
97
|
+
new AIMessage({
|
|
98
|
+
content: toLangChainContent([
|
|
99
|
+
{
|
|
100
|
+
type: 'tool_use',
|
|
101
|
+
id: 'call_null',
|
|
102
|
+
name: 'calculator',
|
|
103
|
+
input: null,
|
|
104
|
+
},
|
|
105
|
+
]),
|
|
106
|
+
tool_calls: [
|
|
107
|
+
{
|
|
108
|
+
id: 'call_null',
|
|
109
|
+
name: 'calculator',
|
|
110
|
+
args: null as never,
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
}),
|
|
114
|
+
];
|
|
115
|
+
|
|
116
|
+
const [block] = toolUseBlocks(convertToConverseMessages(messages));
|
|
117
|
+
expect(block.toolUse?.input).toEqual({});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('parses a complete raw-string input on an unmirrored inline block instead of throwing', () => {
|
|
121
|
+
const messages: BaseMessage[] = [
|
|
122
|
+
new HumanMessage('Search'),
|
|
123
|
+
new AIMessage({
|
|
124
|
+
content: toLangChainContent([
|
|
125
|
+
{
|
|
126
|
+
type: 'tool_use',
|
|
127
|
+
id: 'call_str',
|
|
128
|
+
name: 'search',
|
|
129
|
+
input: '{"query": "test"}',
|
|
130
|
+
},
|
|
131
|
+
]),
|
|
132
|
+
tool_calls: [],
|
|
133
|
+
}),
|
|
134
|
+
];
|
|
135
|
+
|
|
136
|
+
expect(() => convertToConverseMessages(messages)).not.toThrow();
|
|
137
|
+
const [block] = toolUseBlocks(convertToConverseMessages(messages));
|
|
138
|
+
expect(block.toolUse?.input).toEqual({ query: 'test' });
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('degrades non-object inline inputs (partial string, number-string, null) to {}', () => {
|
|
142
|
+
for (const input of ['{"query": "tru', '123', null]) {
|
|
143
|
+
const messages: BaseMessage[] = [
|
|
144
|
+
new HumanMessage('Search'),
|
|
145
|
+
new AIMessage({
|
|
146
|
+
content: toLangChainContent([
|
|
147
|
+
{ type: 'tool_use', id: 'call_bad', name: 'search', input },
|
|
148
|
+
]),
|
|
149
|
+
tool_calls: [],
|
|
150
|
+
}),
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
const [block] = toolUseBlocks(convertToConverseMessages(messages));
|
|
154
|
+
expect(block.toolUse?.input).toEqual({});
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
it('still rejects a tool_use block missing its id or name', () => {
|
|
159
|
+
const messages: BaseMessage[] = [
|
|
160
|
+
new HumanMessage('Search'),
|
|
161
|
+
new AIMessage({
|
|
162
|
+
content: toLangChainContent([
|
|
163
|
+
{ type: 'tool_use', name: 'search', input: { query: 'x' } },
|
|
164
|
+
]),
|
|
165
|
+
tool_calls: [],
|
|
166
|
+
}),
|
|
167
|
+
];
|
|
168
|
+
|
|
169
|
+
expect(() => convertToConverseMessages(messages)).toThrow(
|
|
170
|
+
'Invalid Anthropic tool_use content block'
|
|
171
|
+
);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('coerces null args on the v1 tool_call and tool_calls fallback paths', () => {
|
|
175
|
+
const messages: BaseMessage[] = [
|
|
176
|
+
new HumanMessage('Run both'),
|
|
177
|
+
new AIMessage({
|
|
178
|
+
content: toLangChainContent([
|
|
179
|
+
{ type: 'tool_call', id: 'v1_block', name: 'search', args: null },
|
|
180
|
+
]),
|
|
181
|
+
tool_calls: [
|
|
182
|
+
{ id: 'v1_fallback', name: 'lookup', args: null as never },
|
|
183
|
+
],
|
|
184
|
+
response_metadata: { output_version: 'v1' },
|
|
185
|
+
}),
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
const blocks = toolUseBlocks(convertToConverseMessages(messages));
|
|
189
|
+
expect(blocks).toHaveLength(2);
|
|
190
|
+
for (const block of blocks) {
|
|
191
|
+
expect(block.toolUse?.input).toEqual({});
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
82
196
|
describe('convertToConverseMessages — native Bedrock reasoning serialization', () => {
|
|
83
197
|
it('drops a signature-only reasoning block, keeping text and tool calls', () => {
|
|
84
198
|
const messages: BaseMessage[] = [
|
|
@@ -491,9 +605,9 @@ describe('convertToConverseMessages — user-role run merging', () => {
|
|
|
491
605
|
'toolResult' in block ? 'toolResult' : 'text'
|
|
492
606
|
);
|
|
493
607
|
expect(blockKinds).toEqual(['toolResult', 'text']);
|
|
494
|
-
expect(
|
|
495
|
-
|
|
496
|
-
)
|
|
608
|
+
expect((merged.content ?? []).find((block) => 'text' in block)?.text).toBe(
|
|
609
|
+
'Actually, focus on the second result.'
|
|
610
|
+
);
|
|
497
611
|
});
|
|
498
612
|
|
|
499
613
|
it('still merges adjacent tool-result-only turns', () => {
|
|
@@ -513,7 +627,9 @@ describe('convertToConverseMessages — user-role run merging', () => {
|
|
|
513
627
|
|
|
514
628
|
expect(converseMessages.map((m) => m.role)).toEqual(['assistant', 'user']);
|
|
515
629
|
const toolResultIds = (converseMessages[1].content ?? [])
|
|
516
|
-
.map((block) =>
|
|
630
|
+
.map((block) =>
|
|
631
|
+
'toolResult' in block ? block.toolResult?.toolUseId : undefined
|
|
632
|
+
)
|
|
517
633
|
.filter(Boolean);
|
|
518
634
|
expect(toolResultIds).toEqual(['call_1', 'call_2']);
|
|
519
635
|
});
|
|
@@ -684,6 +684,33 @@ function convertSystemMessageToConverseMessage(
|
|
|
684
684
|
/**
|
|
685
685
|
* Convert an AI message to a Bedrock message.
|
|
686
686
|
*/
|
|
687
|
+
/**
|
|
688
|
+
* Bedrock Converse requires `toolUse.input` to be a JSON object document.
|
|
689
|
+
* History can carry non-object values: streaming leaves the raw partial-JSON
|
|
690
|
+
* string on Anthropic-shaped inline blocks, and context-pressure truncation
|
|
691
|
+
* (pre-3.x `createBoundedTruncationValue`) could persist `null` onto BOTH a
|
|
692
|
+
* block's inline input and its `tool_calls` args. A string is parsed when it
|
|
693
|
+
* forms a complete JSON object; every other shape degrades to `{}` — the call
|
|
694
|
+
* already executed, so the replayed input is informational. Twin of
|
|
695
|
+
* `coerceAnthropicToolUseInput` in the Anthropic fork; duplicated so each
|
|
696
|
+
* fork stays self-contained against its upstream.
|
|
697
|
+
*/
|
|
698
|
+
function coerceBedrockToolUseInput(input: unknown): Record<string, unknown> {
|
|
699
|
+
let candidate: unknown = input;
|
|
700
|
+
if (typeof candidate === 'string') {
|
|
701
|
+
try {
|
|
702
|
+
candidate = JSON.parse(candidate);
|
|
703
|
+
} catch {
|
|
704
|
+
return {};
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
return typeof candidate === 'object' &&
|
|
708
|
+
candidate !== null &&
|
|
709
|
+
!Array.isArray(candidate)
|
|
710
|
+
? (candidate as Record<string, unknown>)
|
|
711
|
+
: {};
|
|
712
|
+
}
|
|
713
|
+
|
|
687
714
|
function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
688
715
|
// Check for v1 format from other providers (PR #9766 fix)
|
|
689
716
|
const responseMetadata = msg.response_metadata as
|
|
@@ -731,9 +758,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
|
731
758
|
}
|
|
732
759
|
if (
|
|
733
760
|
typeof toolUse.id !== 'string' ||
|
|
734
|
-
typeof toolUse.name !== 'string'
|
|
735
|
-
toolUse.input == null ||
|
|
736
|
-
typeof toolUse.input !== 'object'
|
|
761
|
+
typeof toolUse.name !== 'string'
|
|
737
762
|
) {
|
|
738
763
|
throw new Error('Invalid Anthropic tool_use content block');
|
|
739
764
|
}
|
|
@@ -741,7 +766,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
|
741
766
|
toolUse: {
|
|
742
767
|
toolUseId: toolUse.id,
|
|
743
768
|
name: toolUse.name,
|
|
744
|
-
input: toolUse.input
|
|
769
|
+
input: coerceBedrockToolUseInput(toolUse.input),
|
|
745
770
|
},
|
|
746
771
|
} as BedrockContentBlock);
|
|
747
772
|
} else if (block.type === 'reasoning_content') {
|
|
@@ -804,7 +829,7 @@ function convertAIMessageToConverseMessage(msg: BaseMessage): BedrockMessage {
|
|
|
804
829
|
toolUse: {
|
|
805
830
|
toolUseId: toolCall.id,
|
|
806
831
|
name: toolCall.name,
|
|
807
|
-
input: toolCall.args
|
|
832
|
+
input: coerceBedrockToolUseInput(toolCall.args),
|
|
808
833
|
},
|
|
809
834
|
}));
|
|
810
835
|
assistantMsg.content = [
|
|
@@ -862,7 +887,7 @@ function convertFromV1ToChatBedrockConverseMessage(
|
|
|
862
887
|
toolUse: {
|
|
863
888
|
toolUseId: toolCall.id,
|
|
864
889
|
name: toolCall.name,
|
|
865
|
-
input: toolCall.args
|
|
890
|
+
input: coerceBedrockToolUseInput(toolCall.args),
|
|
866
891
|
},
|
|
867
892
|
} as BedrockContentBlock);
|
|
868
893
|
} else if (block.type === 'reasoning') {
|
|
@@ -914,7 +939,7 @@ function convertFromV1ToChatBedrockConverseMessage(
|
|
|
914
939
|
toolUse: {
|
|
915
940
|
toolUseId: tc.id,
|
|
916
941
|
name: tc.name,
|
|
917
|
-
input: tc.args
|
|
942
|
+
input: coerceBedrockToolUseInput(tc.args),
|
|
918
943
|
},
|
|
919
944
|
} as BedrockContentBlock);
|
|
920
945
|
}
|