@librechat/agents 3.1.37 → 3.1.39
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 +3 -0
- package/dist/cjs/agents/AgentContext.cjs.map +1 -1
- package/dist/cjs/graphs/Graph.cjs +1 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/messages/cache.cjs +2 -2
- package/dist/cjs/messages/cache.cjs.map +1 -1
- package/dist/cjs/stream.cjs +2 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/CodeExecutor.cjs +1 -0
- package/dist/cjs/tools/CodeExecutor.cjs.map +1 -1
- package/dist/cjs/tools/handlers.cjs +25 -8
- package/dist/cjs/tools/handlers.cjs.map +1 -1
- package/dist/esm/agents/AgentContext.mjs +3 -0
- package/dist/esm/agents/AgentContext.mjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +1 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/messages/cache.mjs +2 -2
- package/dist/esm/messages/cache.mjs.map +1 -1
- package/dist/esm/stream.mjs +2 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/CodeExecutor.mjs +1 -0
- package/dist/esm/tools/CodeExecutor.mjs.map +1 -1
- package/dist/esm/tools/handlers.mjs +25 -8
- package/dist/esm/tools/handlers.mjs.map +1 -1
- package/dist/types/agents/AgentContext.d.ts +2 -0
- package/dist/types/tools/CodeExecutor.d.ts +2 -2
- package/dist/types/types/tools.d.ts +1 -0
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +3 -0
- package/src/graphs/Graph.ts +1 -1
- package/src/messages/cache.test.ts +41 -0
- package/src/messages/cache.ts +2 -2
- package/src/scripts/bedrock-content-aggregation-test.ts +265 -0
- package/src/scripts/bedrock-parallel-tools-test.ts +203 -0
- package/src/scripts/tools.ts +3 -12
- package/src/stream.ts +2 -1
- package/src/tools/CodeExecutor.ts +1 -0
- package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
- package/src/tools/__tests__/handlers.test.ts +994 -0
- package/src/tools/handlers.ts +32 -13
- package/src/types/tools.ts +1 -0
|
@@ -142,6 +142,8 @@ export class AgentContext {
|
|
|
142
142
|
lastToken?: string;
|
|
143
143
|
/** Token type switch state */
|
|
144
144
|
tokenTypeSwitch?: 'reasoning' | 'content';
|
|
145
|
+
/** Tracks how many reasoning→text transitions have occurred (ensures unique post-reasoning step keys) */
|
|
146
|
+
reasoningTransitionCount = 0;
|
|
145
147
|
/** Current token type being processed */
|
|
146
148
|
currentTokenType: ContentTypes.TEXT | ContentTypes.THINK | 'think_and_text' =
|
|
147
149
|
ContentTypes.TEXT;
|
|
@@ -462,6 +464,7 @@ export class AgentContext {
|
|
|
462
464
|
this.pruneMessages = undefined;
|
|
463
465
|
this.lastStreamCall = undefined;
|
|
464
466
|
this.tokenTypeSwitch = undefined;
|
|
467
|
+
this.reasoningTransitionCount = 0;
|
|
465
468
|
this.currentTokenType = ContentTypes.TEXT;
|
|
466
469
|
this.discoveredToolNames.clear();
|
|
467
470
|
this.handoffContext = undefined;
|
package/src/graphs/Graph.ts
CHANGED
|
@@ -314,7 +314,7 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
314
314
|
) {
|
|
315
315
|
keyList.push('reasoning');
|
|
316
316
|
} else if (agentContext.tokenTypeSwitch === 'content') {
|
|
317
|
-
keyList.push(
|
|
317
|
+
keyList.push(`post-reasoning-${agentContext.reasoningTransitionCount}`);
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
if (this.invokedToolIds != null && this.invokedToolIds.size > 0) {
|
|
@@ -551,6 +551,47 @@ describe('addBedrockCacheControl (Bedrock cache checkpoints)', () => {
|
|
|
551
551
|
});
|
|
552
552
|
expect(secondLastContent[1]).toEqual({ cachePoint: { type: 'default' } });
|
|
553
553
|
});
|
|
554
|
+
|
|
555
|
+
it('skips cachePoint on AI messages with only whitespace text and reasoning (tool-call scenario)', () => {
|
|
556
|
+
const messages = [
|
|
557
|
+
{
|
|
558
|
+
role: 'user',
|
|
559
|
+
content: [
|
|
560
|
+
{
|
|
561
|
+
type: ContentTypes.TEXT,
|
|
562
|
+
text: 'What can you tell me about this PR?',
|
|
563
|
+
},
|
|
564
|
+
],
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
role: 'assistant',
|
|
568
|
+
content: [
|
|
569
|
+
{ type: ContentTypes.TEXT, text: '\n\n' },
|
|
570
|
+
{
|
|
571
|
+
type: 'reasoning_content',
|
|
572
|
+
reasoningText: { text: 'Let me look into that.', signature: 'abc' },
|
|
573
|
+
},
|
|
574
|
+
] as MessageContentComplex[],
|
|
575
|
+
},
|
|
576
|
+
{
|
|
577
|
+
role: 'user',
|
|
578
|
+
content: [{ type: ContentTypes.TEXT, text: 'tool result here' }],
|
|
579
|
+
getType: (): 'tool' => 'tool',
|
|
580
|
+
},
|
|
581
|
+
];
|
|
582
|
+
|
|
583
|
+
const result = addBedrockCacheControl(
|
|
584
|
+
messages as Parameters<typeof addBedrockCacheControl>[0]
|
|
585
|
+
);
|
|
586
|
+
const aiContent = result[1].content as MessageContentComplex[];
|
|
587
|
+
const hasCachePoint = aiContent.some((b) => 'cachePoint' in b);
|
|
588
|
+
expect(hasCachePoint).toBe(false);
|
|
589
|
+
|
|
590
|
+
const userContent = result[0].content as MessageContentComplex[];
|
|
591
|
+
expect(userContent[userContent.length - 1]).toEqual({
|
|
592
|
+
cachePoint: { type: 'default' },
|
|
593
|
+
});
|
|
594
|
+
});
|
|
554
595
|
});
|
|
555
596
|
|
|
556
597
|
describe('stripAnthropicCacheControl', () => {
|
package/src/messages/cache.ts
CHANGED
|
@@ -331,7 +331,7 @@ export function addBedrockCacheControl<
|
|
|
331
331
|
let hasCacheableContent = false;
|
|
332
332
|
for (const block of workingContent) {
|
|
333
333
|
if (block.type === ContentTypes.TEXT) {
|
|
334
|
-
if (typeof block.text === 'string' && block.text !== '') {
|
|
334
|
+
if (typeof block.text === 'string' && block.text.trim() !== '') {
|
|
335
335
|
hasCacheableContent = true;
|
|
336
336
|
break;
|
|
337
337
|
}
|
|
@@ -349,7 +349,7 @@ export function addBedrockCacheControl<
|
|
|
349
349
|
const type = (block as { type?: string }).type;
|
|
350
350
|
if (type === ContentTypes.TEXT || type === 'text') {
|
|
351
351
|
const text = (block as { text?: string }).text;
|
|
352
|
-
if (text === '' || text === undefined) {
|
|
352
|
+
if (text === '' || text === undefined || text.trim() === '') {
|
|
353
353
|
continue;
|
|
354
354
|
}
|
|
355
355
|
workingContent.splice(j + 1, 0, {
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
config();
|
|
3
|
+
import { HumanMessage, BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { UsageMetadata } from '@langchain/core/messages';
|
|
5
|
+
import * as t from '@/types';
|
|
6
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
7
|
+
import { createCodeExecutionTool } from '@/tools/CodeExecutor';
|
|
8
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
9
|
+
import { GraphEvents, ContentTypes, Providers } from '@/common';
|
|
10
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
11
|
+
import { Run } from '@/run';
|
|
12
|
+
|
|
13
|
+
const conversationHistory: BaseMessage[] = [];
|
|
14
|
+
let _contentParts: t.MessageContentComplex[] = [];
|
|
15
|
+
const collectedUsage: UsageMetadata[] = [];
|
|
16
|
+
|
|
17
|
+
async function testBedrockContentAggregation(): Promise<void> {
|
|
18
|
+
const instructions =
|
|
19
|
+
'You are a helpful AI assistant with coding capabilities. When answering questions, be thorough in your reasoning.';
|
|
20
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
21
|
+
_contentParts = contentParts as t.MessageContentComplex[];
|
|
22
|
+
|
|
23
|
+
const customHandlers = {
|
|
24
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
25
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
|
|
26
|
+
[GraphEvents.CHAT_MODEL_STREAM]: new ChatModelStreamHandler(),
|
|
27
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
28
|
+
handle: (
|
|
29
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
30
|
+
data: t.StreamEventData
|
|
31
|
+
): void => {
|
|
32
|
+
const result = (data as unknown as { result: t.ToolEndEvent }).result;
|
|
33
|
+
console.log(
|
|
34
|
+
`[ON_RUN_STEP_COMPLETED] stepId=${result.id} index=${result.index} type=${result.type} tool=${result.tool_call?.name ?? 'n/a'}`
|
|
35
|
+
);
|
|
36
|
+
aggregateContent({
|
|
37
|
+
event,
|
|
38
|
+
data: data as unknown as { result: t.ToolEndEvent },
|
|
39
|
+
});
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
43
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: t.RunStep) => {
|
|
44
|
+
const toolCalls =
|
|
45
|
+
data.stepDetails.type === 'tool_calls' && data.stepDetails.tool_calls
|
|
46
|
+
? (
|
|
47
|
+
data.stepDetails.tool_calls as Array<{
|
|
48
|
+
name?: string;
|
|
49
|
+
id?: string;
|
|
50
|
+
}>
|
|
51
|
+
)
|
|
52
|
+
.map((tc) => `${tc.name ?? '?'}(${tc.id ?? '?'})`)
|
|
53
|
+
.join(', ')
|
|
54
|
+
: 'none';
|
|
55
|
+
console.log(
|
|
56
|
+
`[ON_RUN_STEP] stepId=${data.id} index=${data.index} type=${data.type} stepIndex=${data.stepIndex} toolCalls=[${toolCalls}]`
|
|
57
|
+
);
|
|
58
|
+
aggregateContent({ event, data });
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
62
|
+
handle: (
|
|
63
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
64
|
+
data: t.RunStepDeltaEvent
|
|
65
|
+
) => {
|
|
66
|
+
const tcNames =
|
|
67
|
+
data.delta.tool_calls
|
|
68
|
+
?.map(
|
|
69
|
+
(tc) =>
|
|
70
|
+
`${tc.name ?? '?'}(args=${(tc.args ?? '').substring(0, 30)}...)`
|
|
71
|
+
)
|
|
72
|
+
.join(', ') ?? 'none';
|
|
73
|
+
console.log(
|
|
74
|
+
`[ON_RUN_STEP_DELTA] stepId=${data.id} type=${data.delta.type} toolCalls=[${tcNames}]`
|
|
75
|
+
);
|
|
76
|
+
aggregateContent({ event, data });
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
80
|
+
handle: (
|
|
81
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
82
|
+
data: t.MessageDeltaEvent
|
|
83
|
+
) => {
|
|
84
|
+
const preview = Array.isArray(data.delta.content)
|
|
85
|
+
? data.delta.content
|
|
86
|
+
.map(
|
|
87
|
+
(c) =>
|
|
88
|
+
`${c.type}:"${String((c as Record<string, unknown>).text ?? (c as Record<string, unknown>).think ?? '').substring(0, 40)}"`
|
|
89
|
+
)
|
|
90
|
+
.join(', ')
|
|
91
|
+
: String(data.delta.content).substring(0, 40);
|
|
92
|
+
console.log(
|
|
93
|
+
`[ON_MESSAGE_DELTA] stepId=${data.id} content=[${preview}]`
|
|
94
|
+
);
|
|
95
|
+
aggregateContent({ event, data });
|
|
96
|
+
},
|
|
97
|
+
},
|
|
98
|
+
[GraphEvents.ON_REASONING_DELTA]: {
|
|
99
|
+
handle: (
|
|
100
|
+
event: GraphEvents.ON_REASONING_DELTA,
|
|
101
|
+
data: t.ReasoningDeltaEvent
|
|
102
|
+
) => {
|
|
103
|
+
const preview = Array.isArray(data.delta.content)
|
|
104
|
+
? data.delta.content
|
|
105
|
+
.map(
|
|
106
|
+
(c) =>
|
|
107
|
+
`${c.type}:"${String((c as Record<string, unknown>).think ?? '').substring(0, 40)}"`
|
|
108
|
+
)
|
|
109
|
+
.join(', ')
|
|
110
|
+
: '?';
|
|
111
|
+
console.log(
|
|
112
|
+
`[ON_REASONING_DELTA] stepId=${data.id} content=[${preview}]`
|
|
113
|
+
);
|
|
114
|
+
aggregateContent({ event, data });
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const baseLlmConfig = getLLMConfig(Providers.BEDROCK);
|
|
120
|
+
|
|
121
|
+
const llmConfig = {
|
|
122
|
+
...baseLlmConfig,
|
|
123
|
+
model: 'global.anthropic.claude-opus-4-6-v1',
|
|
124
|
+
maxTokens: 16000,
|
|
125
|
+
additionalModelRequestFields: {
|
|
126
|
+
thinking: { type: 'enabled', budget_tokens: 10000 },
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const run = await Run.create<t.IState>({
|
|
131
|
+
runId: 'bedrock-content-aggregation-test',
|
|
132
|
+
graphConfig: {
|
|
133
|
+
instructions,
|
|
134
|
+
type: 'standard',
|
|
135
|
+
tools: [createCodeExecutionTool()],
|
|
136
|
+
llmConfig,
|
|
137
|
+
},
|
|
138
|
+
returnContent: true,
|
|
139
|
+
customHandlers: customHandlers as t.RunConfig['customHandlers'],
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
const streamConfig = {
|
|
143
|
+
configurable: {
|
|
144
|
+
thread_id: 'bedrock-content-aggregation-thread',
|
|
145
|
+
},
|
|
146
|
+
streamMode: 'values',
|
|
147
|
+
version: 'v2' as const,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const userMessage = `im testing edge cases with our code interpreter. i know we can persist files, but what happens when we put them in directories?`;
|
|
151
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
152
|
+
|
|
153
|
+
console.log('Running Bedrock content aggregation test...\n');
|
|
154
|
+
console.log(`Prompt: "${userMessage}"\n`);
|
|
155
|
+
|
|
156
|
+
const inputs = { messages: [...conversationHistory] };
|
|
157
|
+
await run.processStream(inputs, streamConfig);
|
|
158
|
+
|
|
159
|
+
console.log('\n\n========== CONTENT PARTS ANALYSIS ==========\n');
|
|
160
|
+
|
|
161
|
+
let hasEmptyToolCall = false;
|
|
162
|
+
let hasReasoningOrderIssue = false;
|
|
163
|
+
|
|
164
|
+
for (let i = 0; i < _contentParts.length; i++) {
|
|
165
|
+
const part = _contentParts[i];
|
|
166
|
+
if (!part) {
|
|
167
|
+
console.log(` [${i}] undefined`);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const partType = part.type;
|
|
172
|
+
if (partType === ContentTypes.TOOL_CALL) {
|
|
173
|
+
const tc = (part as t.ToolCallContent).tool_call;
|
|
174
|
+
if (!tc || !tc.name) {
|
|
175
|
+
hasEmptyToolCall = true;
|
|
176
|
+
console.log(` [${i}] TOOL_CALL *** EMPTY (no tool_call data) ***`);
|
|
177
|
+
} else {
|
|
178
|
+
const outputPreview = tc.output
|
|
179
|
+
? `output=${(tc.output as string).substring(0, 80)}...`
|
|
180
|
+
: 'no output';
|
|
181
|
+
console.log(` [${i}] TOOL_CALL name=${tc.name} ${outputPreview}`);
|
|
182
|
+
}
|
|
183
|
+
} else if (partType === ContentTypes.THINK) {
|
|
184
|
+
const think = (part as t.ReasoningContentText).think ?? '';
|
|
185
|
+
console.log(
|
|
186
|
+
` [${i}] THINK (${think.length} chars): "${think.substring(0, 80)}..."`
|
|
187
|
+
);
|
|
188
|
+
} else if (partType === ContentTypes.TEXT) {
|
|
189
|
+
const text = (part as t.MessageDeltaUpdate).text ?? '';
|
|
190
|
+
console.log(
|
|
191
|
+
` [${i}] TEXT (${text.length} chars): "${text.substring(0, 80)}..."`
|
|
192
|
+
);
|
|
193
|
+
} else {
|
|
194
|
+
console.log(` [${i}] ${partType}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Check reasoning ordering within a single invocation cycle.
|
|
200
|
+
* A tool_call resets the cycle — text before think across different
|
|
201
|
+
* invocations (e.g., text from invocation 2, think from invocation 3) is valid.
|
|
202
|
+
*/
|
|
203
|
+
let lastTextInCycle: number | null = null;
|
|
204
|
+
for (let i = 0; i < _contentParts.length; i++) {
|
|
205
|
+
const part = _contentParts[i];
|
|
206
|
+
if (!part) continue;
|
|
207
|
+
|
|
208
|
+
if (part.type === ContentTypes.TOOL_CALL) {
|
|
209
|
+
lastTextInCycle = null;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (part.type === ContentTypes.TEXT) {
|
|
214
|
+
lastTextInCycle = i;
|
|
215
|
+
} else if (part.type === ContentTypes.THINK && lastTextInCycle !== null) {
|
|
216
|
+
const prevText = _contentParts[lastTextInCycle] as t.MessageDeltaUpdate;
|
|
217
|
+
const thinkContent = (part as t.ReasoningContentText).think ?? '';
|
|
218
|
+
if (
|
|
219
|
+
prevText?.text &&
|
|
220
|
+
prevText.text.trim().length > 5 &&
|
|
221
|
+
thinkContent.length > 0
|
|
222
|
+
) {
|
|
223
|
+
hasReasoningOrderIssue = true;
|
|
224
|
+
console.log(
|
|
225
|
+
`\n *** ORDERING ISSUE (same invocation): TEXT at [${lastTextInCycle}] appears before THINK at [${i}]`
|
|
226
|
+
);
|
|
227
|
+
console.log(
|
|
228
|
+
` Text ends with: "...${prevText.text.substring(prevText.text.length - 60)}"`
|
|
229
|
+
);
|
|
230
|
+
console.log(
|
|
231
|
+
` Think starts with: "${thinkContent.substring(0, 60)}..."`
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
console.log('\n========== SUMMARY ==========\n');
|
|
238
|
+
console.log(`Total content parts: ${_contentParts.filter(Boolean).length}`);
|
|
239
|
+
console.log(
|
|
240
|
+
`Empty tool_call parts: ${hasEmptyToolCall ? 'YES (BUG)' : 'No'}`
|
|
241
|
+
);
|
|
242
|
+
console.log(
|
|
243
|
+
`Reasoning order issues: ${hasReasoningOrderIssue ? 'YES (BUG)' : 'No'}`
|
|
244
|
+
);
|
|
245
|
+
console.log('\nFull contentParts dump:');
|
|
246
|
+
console.dir(_contentParts, { depth: null });
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
250
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
251
|
+
console.log('Content parts:');
|
|
252
|
+
console.dir(_contentParts, { depth: null });
|
|
253
|
+
process.exit(1);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
process.on('uncaughtException', (err) => {
|
|
257
|
+
console.error('Uncaught Exception:', err);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
testBedrockContentAggregation().catch((err) => {
|
|
261
|
+
console.error(err);
|
|
262
|
+
console.log('Content parts:');
|
|
263
|
+
console.dir(_contentParts, { depth: null });
|
|
264
|
+
process.exit(1);
|
|
265
|
+
});
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { config } from 'dotenv';
|
|
2
|
+
config();
|
|
3
|
+
import { HumanMessage, BaseMessage } from '@langchain/core/messages';
|
|
4
|
+
import type { UsageMetadata } from '@langchain/core/messages';
|
|
5
|
+
import type { StandardGraph } from '@/graphs';
|
|
6
|
+
import * as t from '@/types';
|
|
7
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
8
|
+
import { GraphEvents, ContentTypes, Providers } from '@/common';
|
|
9
|
+
import { ToolEndHandler, ModelEndHandler } from '@/events';
|
|
10
|
+
import { getLLMConfig } from '@/utils/llmConfig';
|
|
11
|
+
import { Calculator } from '@/tools/Calculator';
|
|
12
|
+
import { Run } from '@/run';
|
|
13
|
+
|
|
14
|
+
const conversationHistory: BaseMessage[] = [];
|
|
15
|
+
let _contentParts: t.MessageContentComplex[] = [];
|
|
16
|
+
const collectedUsage: UsageMetadata[] = [];
|
|
17
|
+
|
|
18
|
+
async function testParallelToolCalls(): Promise<void> {
|
|
19
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
20
|
+
_contentParts = contentParts as t.MessageContentComplex[];
|
|
21
|
+
|
|
22
|
+
const customHandlers = {
|
|
23
|
+
[GraphEvents.TOOL_END]: new ToolEndHandler(),
|
|
24
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(collectedUsage),
|
|
25
|
+
[GraphEvents.CHAT_MODEL_STREAM]: {
|
|
26
|
+
handle: async (
|
|
27
|
+
event: string,
|
|
28
|
+
data: t.StreamEventData,
|
|
29
|
+
metadata?: Record<string, unknown>,
|
|
30
|
+
graph?: unknown
|
|
31
|
+
): Promise<void> => {
|
|
32
|
+
const chunk = data.chunk as Record<string, unknown> | undefined;
|
|
33
|
+
const tcc = chunk?.tool_call_chunks as
|
|
34
|
+
| Array<{ id?: string; name?: string; index?: number }>
|
|
35
|
+
| undefined;
|
|
36
|
+
if (tcc && tcc.length > 0) {
|
|
37
|
+
console.log(
|
|
38
|
+
`[CHAT_MODEL_STREAM] tool_call_chunks: ${JSON.stringify(tcc.map((c) => ({ id: c.id, name: c.name, index: c.index })))}`
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
const handler = new ChatModelStreamHandler();
|
|
42
|
+
return handler.handle(event, data, metadata, graph as StandardGraph);
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
[GraphEvents.ON_RUN_STEP_COMPLETED]: {
|
|
46
|
+
handle: (
|
|
47
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
48
|
+
data: t.StreamEventData
|
|
49
|
+
): void => {
|
|
50
|
+
const result = (data as unknown as { result: t.ToolEndEvent }).result;
|
|
51
|
+
console.log(
|
|
52
|
+
`[ON_RUN_STEP_COMPLETED] stepId=${result.id} index=${result.index} type=${result.type} tool=${result.tool_call?.name ?? 'n/a'}`
|
|
53
|
+
);
|
|
54
|
+
aggregateContent({
|
|
55
|
+
event,
|
|
56
|
+
data: data as unknown as { result: t.ToolEndEvent },
|
|
57
|
+
});
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
[GraphEvents.ON_RUN_STEP]: {
|
|
61
|
+
handle: (event: GraphEvents.ON_RUN_STEP, data: t.RunStep) => {
|
|
62
|
+
const toolCalls =
|
|
63
|
+
data.stepDetails.type === 'tool_calls' && data.stepDetails.tool_calls
|
|
64
|
+
? (
|
|
65
|
+
data.stepDetails.tool_calls as Array<{
|
|
66
|
+
name?: string;
|
|
67
|
+
id?: string;
|
|
68
|
+
}>
|
|
69
|
+
)
|
|
70
|
+
.map((tc) => `${tc.name ?? '?'}(${tc.id ?? '?'})`)
|
|
71
|
+
.join(', ')
|
|
72
|
+
: 'none';
|
|
73
|
+
console.log(
|
|
74
|
+
`[ON_RUN_STEP] stepId=${data.id} index=${data.index} type=${data.type} stepIndex=${data.stepIndex} toolCalls=[${toolCalls}]`
|
|
75
|
+
);
|
|
76
|
+
aggregateContent({ event, data });
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
[GraphEvents.ON_RUN_STEP_DELTA]: {
|
|
80
|
+
handle: (
|
|
81
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
82
|
+
data: t.RunStepDeltaEvent
|
|
83
|
+
) => {
|
|
84
|
+
aggregateContent({ event, data });
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
[GraphEvents.ON_MESSAGE_DELTA]: {
|
|
88
|
+
handle: (
|
|
89
|
+
event: GraphEvents.ON_MESSAGE_DELTA,
|
|
90
|
+
data: t.MessageDeltaEvent
|
|
91
|
+
) => {
|
|
92
|
+
aggregateContent({ event, data });
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
[GraphEvents.ON_REASONING_DELTA]: {
|
|
96
|
+
handle: (
|
|
97
|
+
event: GraphEvents.ON_REASONING_DELTA,
|
|
98
|
+
data: t.ReasoningDeltaEvent
|
|
99
|
+
) => {
|
|
100
|
+
aggregateContent({ event, data });
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const baseLlmConfig = getLLMConfig(Providers.BEDROCK);
|
|
106
|
+
|
|
107
|
+
const llmConfig = {
|
|
108
|
+
...baseLlmConfig,
|
|
109
|
+
model: 'global.anthropic.claude-opus-4-6-v1',
|
|
110
|
+
maxTokens: 16000,
|
|
111
|
+
additionalModelRequestFields: {
|
|
112
|
+
thinking: { type: 'enabled', budget_tokens: 10000 },
|
|
113
|
+
},
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const run = await Run.create<t.IState>({
|
|
117
|
+
runId: 'bedrock-parallel-tools-test',
|
|
118
|
+
graphConfig: {
|
|
119
|
+
instructions:
|
|
120
|
+
'You are a math assistant. When asked to calculate multiple things, use the calculator tool for ALL of them in parallel. Do NOT chain calculations sequentially.',
|
|
121
|
+
type: 'standard',
|
|
122
|
+
tools: [new Calculator()],
|
|
123
|
+
llmConfig,
|
|
124
|
+
},
|
|
125
|
+
returnContent: true,
|
|
126
|
+
customHandlers: customHandlers as t.RunConfig['customHandlers'],
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const streamConfig = {
|
|
130
|
+
configurable: { thread_id: 'bedrock-parallel-tools-thread' },
|
|
131
|
+
streamMode: 'values',
|
|
132
|
+
version: 'v2' as const,
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const userMessage =
|
|
136
|
+
'Calculate these 3 things at the same time using the calculator: 1) 123 * 456, 2) sqrt(144) + 7, 3) 2^10 - 24';
|
|
137
|
+
conversationHistory.push(new HumanMessage(userMessage));
|
|
138
|
+
|
|
139
|
+
console.log('Running Bedrock parallel tool calls test...\n');
|
|
140
|
+
console.log(`Prompt: "${userMessage}"\n`);
|
|
141
|
+
|
|
142
|
+
const inputs = { messages: [...conversationHistory] };
|
|
143
|
+
await run.processStream(inputs, streamConfig);
|
|
144
|
+
|
|
145
|
+
console.log('\n\n========== ANALYSIS ==========\n');
|
|
146
|
+
|
|
147
|
+
let toolCallCount = 0;
|
|
148
|
+
const toolCallNames: string[] = [];
|
|
149
|
+
let hasUndefined = false;
|
|
150
|
+
|
|
151
|
+
for (let i = 0; i < _contentParts.length; i++) {
|
|
152
|
+
const part = _contentParts[i];
|
|
153
|
+
if (!part) {
|
|
154
|
+
hasUndefined = true;
|
|
155
|
+
console.log(` [${i}] *** UNDEFINED ***`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (part.type === ContentTypes.TOOL_CALL) {
|
|
159
|
+
toolCallCount++;
|
|
160
|
+
const tc = (part as t.ToolCallContent).tool_call;
|
|
161
|
+
const hasData = tc && tc.name;
|
|
162
|
+
if (!hasData) {
|
|
163
|
+
console.log(` [${i}] TOOL_CALL *** EMPTY ***`);
|
|
164
|
+
} else {
|
|
165
|
+
toolCallNames.push(tc.name ?? '');
|
|
166
|
+
console.log(
|
|
167
|
+
` [${i}] TOOL_CALL name=${tc.name} id=${tc.id} output=${String(tc.output ?? '').substring(0, 40)}`
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
} else if (part.type === ContentTypes.THINK) {
|
|
171
|
+
const think = (part as t.ReasoningContentText).think ?? '';
|
|
172
|
+
console.log(` [${i}] THINK (${think.length} chars)`);
|
|
173
|
+
} else if (part.type === ContentTypes.TEXT) {
|
|
174
|
+
const text = (part as t.MessageDeltaUpdate).text ?? '';
|
|
175
|
+
console.log(
|
|
176
|
+
` [${i}] TEXT (${text.length} chars): "${text.substring(0, 80)}..."`
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
console.log('\n========== SUMMARY ==========\n');
|
|
182
|
+
console.log(`Total content parts: ${_contentParts.filter(Boolean).length}`);
|
|
183
|
+
console.log(`Tool calls found: ${toolCallCount}`);
|
|
184
|
+
console.log(`Tool call names: [${toolCallNames.join(', ')}]`);
|
|
185
|
+
console.log(`Undefined gaps: ${hasUndefined ? 'YES (BUG)' : 'No'}`);
|
|
186
|
+
console.log(
|
|
187
|
+
`Expected 3 tool calls: ${toolCallCount >= 3 ? 'PASS' : 'FAIL (only ' + toolCallCount + ')'}`
|
|
188
|
+
);
|
|
189
|
+
console.log('\nFull contentParts dump:');
|
|
190
|
+
console.dir(_contentParts, { depth: null });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
194
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
195
|
+
console.dir(_contentParts, { depth: null });
|
|
196
|
+
process.exit(1);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
testParallelToolCalls().catch((err) => {
|
|
200
|
+
console.error(err);
|
|
201
|
+
console.dir(_contentParts, { depth: null });
|
|
202
|
+
process.exit(1);
|
|
203
|
+
});
|
package/src/scripts/tools.ts
CHANGED
|
@@ -25,16 +25,7 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
25
25
|
return true;
|
|
26
26
|
}
|
|
27
27
|
),
|
|
28
|
-
[GraphEvents.CHAT_MODEL_END]:
|
|
29
|
-
handle: (
|
|
30
|
-
_event: string,
|
|
31
|
-
_data: t.StreamEventData,
|
|
32
|
-
metadata?: Record<string, unknown>
|
|
33
|
-
): void => {
|
|
34
|
-
console.log('\n====== CHAT_MODEL_END METADATA ======');
|
|
35
|
-
console.dir(metadata, { depth: null });
|
|
36
|
-
},
|
|
37
|
-
},
|
|
28
|
+
[GraphEvents.CHAT_MODEL_END]: new ModelEndHandler(),
|
|
38
29
|
[GraphEvents.CHAT_MODEL_START]: {
|
|
39
30
|
handle: (
|
|
40
31
|
_event: string,
|
|
@@ -158,9 +149,9 @@ async function testStandardStreaming(): Promise<void> {
|
|
|
158
149
|
conversationHistory.push(...finalMessages);
|
|
159
150
|
console.dir(conversationHistory, { depth: null });
|
|
160
151
|
}
|
|
161
|
-
console.dir(finalContentParts, { depth: null });
|
|
152
|
+
// console.dir(finalContentParts, { depth: null });
|
|
162
153
|
console.log('\n\n====================\n\n');
|
|
163
|
-
|
|
154
|
+
console.dir(contentParts, { depth: null });
|
|
164
155
|
}
|
|
165
156
|
|
|
166
157
|
process.on('unhandledRejection', (reason, promise) => {
|
package/src/stream.ts
CHANGED
|
@@ -411,6 +411,7 @@ hasToolCallChunks: ${hasToolCallChunks}
|
|
|
411
411
|
) {
|
|
412
412
|
agentContext.currentTokenType = ContentTypes.TEXT;
|
|
413
413
|
agentContext.tokenTypeSwitch = 'content';
|
|
414
|
+
agentContext.reasoningTransitionCount++;
|
|
414
415
|
} else if (
|
|
415
416
|
chunk.content != null &&
|
|
416
417
|
typeof chunk.content === 'string' &&
|
|
@@ -465,7 +466,7 @@ export function createContentAggregator(): t.ContentAggregatorResult {
|
|
|
465
466
|
return;
|
|
466
467
|
}
|
|
467
468
|
|
|
468
|
-
if (!contentParts[index]) {
|
|
469
|
+
if (!contentParts[index] && partType !== ContentTypes.TOOL_CALL) {
|
|
469
470
|
contentParts[index] = { type: partType };
|
|
470
471
|
}
|
|
471
472
|
|