@librechat/agents 3.3.8 → 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 +35 -2
- 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/prune.cjs +13 -1
- package/dist/cjs/messages/prune.cjs.map +1 -1
- package/dist/cjs/prompts/activityLabel.cjs +24 -12
- package/dist/cjs/prompts/activityLabel.cjs.map +1 -1
- package/dist/cjs/run.cjs +47 -22
- 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 +5 -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 +35 -2
- 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/prune.mjs +13 -1
- package/dist/esm/messages/prune.mjs.map +1 -1
- package/dist/esm/prompts/activityLabel.mjs +24 -12
- package/dist/esm/prompts/activityLabel.mjs.map +1 -1
- package/dist/esm/run.mjs +47 -22
- 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 +5 -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/session/types.d.ts +1 -0
- package/dist/types/tools/ToolNode.d.ts +7 -1
- package/dist/types/types/hitl.d.ts +8 -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 +56 -2
- 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/prune.ts +12 -1
- package/src/prompts/activityLabel.ts +23 -6
- package/src/run.ts +76 -45
- 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 +26 -10
- 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/node.ts +5 -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/hitl.ts +8 -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
|
}
|
package/src/messages/prune.ts
CHANGED
|
@@ -1403,7 +1403,18 @@ function createBoundedTruncationValue(
|
|
|
1403
1403
|
_originalChars: originalChars,
|
|
1404
1404
|
};
|
|
1405
1405
|
if (JSON.stringify(emptyEnvelope).length > normalizedMaxChars) {
|
|
1406
|
-
|
|
1406
|
+
/**
|
|
1407
|
+
* Even the empty envelope overflows the cap, so no preview survives —
|
|
1408
|
+
* but the result must still be a JSON OBJECT, never `null`. This value
|
|
1409
|
+
* replaces a `tool_use.input` / tool-call `args` on messages that are
|
|
1410
|
+
* mutated IN PLACE into graph state (`preFlightTruncateToolCallInputs`),
|
|
1411
|
+
* and Anthropic rejects a replayed non-object input with a 400
|
|
1412
|
+
* (`tool_use.input: Input should be an object`). Observed live: a tight
|
|
1413
|
+
* summarization budget shrank the cap below the envelope, nulled a
|
|
1414
|
+
* retained calculator call's input and args, and the next model call
|
|
1415
|
+
* failed on replay.
|
|
1416
|
+
*/
|
|
1417
|
+
return {};
|
|
1407
1418
|
}
|
|
1408
1419
|
|
|
1409
1420
|
let low = 0;
|
|
@@ -37,8 +37,8 @@ export function truncateForLabel(value: string, maxLength: number): string {
|
|
|
37
37
|
* Reduces a committed label to bounded single-line data.
|
|
38
38
|
*
|
|
39
39
|
* Sections in this prompt are delimited by blank lines, so a label carrying
|
|
40
|
-
* embedded newlines could otherwise forge an apparent
|
|
41
|
-
* `
|
|
40
|
+
* embedded newlines could otherwise forge an apparent entries section or
|
|
41
|
+
* `Header:` cue. Unlike every other input here, previous labels re-enter
|
|
42
42
|
* the prompt on EVERY later batch, so one malformed result — plain model
|
|
43
43
|
* noncompliance, or injection surfacing through a tool result — would
|
|
44
44
|
* persistently steer unrelated later labels rather than affecting one. The
|
|
@@ -47,7 +47,10 @@ export function truncateForLabel(value: string, maxLength: number): string {
|
|
|
47
47
|
* model's window and starve the run of labels entirely.
|
|
48
48
|
*/
|
|
49
49
|
function sanitizePreviousLabel(label: string): string {
|
|
50
|
-
return truncateForLabel(
|
|
50
|
+
return truncateForLabel(
|
|
51
|
+
label.replace(/\s+/g, ' ').trim(),
|
|
52
|
+
PREVIOUS_LABEL_LIMIT
|
|
53
|
+
);
|
|
51
54
|
}
|
|
52
55
|
|
|
53
56
|
const ABORT_SERIALIZATION = Symbol('abort-label-serialization');
|
|
@@ -150,7 +153,11 @@ export function buildActivityLabelPrompt({
|
|
|
150
153
|
* mean an earlier header may have been generated under ANOTHER agent's
|
|
151
154
|
* weaker policy — so they share the excerpts' wholesale drop rather than
|
|
152
155
|
* letting a handoff leak a looser agent's phrasing into this trace. */
|
|
153
|
-
if (
|
|
156
|
+
if (
|
|
157
|
+
!excerptsRedacted &&
|
|
158
|
+
previousLabels != null &&
|
|
159
|
+
previousLabels.length > 0
|
|
160
|
+
) {
|
|
154
161
|
const recent = previousLabels
|
|
155
162
|
.slice(-MAX_PREVIOUS_LABELS)
|
|
156
163
|
.map(sanitizePreviousLabel)
|
|
@@ -192,7 +199,14 @@ export function buildActivityLabelPrompt({
|
|
|
192
199
|
const shown = entries.slice(0, MAX_PROMPT_ENTRIES);
|
|
193
200
|
const omitted = entries.length - shown.length;
|
|
194
201
|
sections.push(
|
|
195
|
-
|
|
202
|
+
/** Frames the list as reference material, not the thing to
|
|
203
|
+
* transcribe. Ported from LibreChat's fallback builder (its
|
|
204
|
+
* runtime.ts documents that without this the model "hands back a
|
|
205
|
+
* transcription" of the list) after the eval harness measured it
|
|
206
|
+
* across three independent sweeps: fewer template-redundancy and
|
|
207
|
+
* length violations than a bare `Tool calls:` heading, with no
|
|
208
|
+
* per-case regressions (agents #360). */
|
|
209
|
+
'What it called, and what came back (do not restate these):\n' +
|
|
196
210
|
shown
|
|
197
211
|
.map((entry) => {
|
|
198
212
|
const input = clip(
|
|
@@ -220,6 +234,9 @@ export function buildActivityLabelPrompt({
|
|
|
220
234
|
: '')
|
|
221
235
|
);
|
|
222
236
|
}
|
|
223
|
-
|
|
237
|
+
/** The fallback builder's terminal cue, measured alongside the heading
|
|
238
|
+
* (same sweeps). The default system prompt already describes the
|
|
239
|
+
* output as "the header of a collapsed activity group". */
|
|
240
|
+
sections.push('Header:');
|
|
224
241
|
return sections.join('\n\n');
|
|
225
242
|
}
|