@librechat/agents 3.1.36 → 3.1.38
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 +38 -29
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/stream.cjs +2 -1
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +90 -14
- package/dist/cjs/tools/ToolNode.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 +38 -29
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/stream.mjs +2 -1
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +90 -14
- package/dist/esm/tools/ToolNode.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/ToolNode.d.ts +10 -0
- package/dist/types/types/tools.d.ts +7 -1
- package/package.json +1 -1
- package/src/agents/AgentContext.ts +3 -0
- package/src/graphs/Graph.ts +41 -36
- 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/ToolNode.ts +120 -14
- 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 +7 -1
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) {
|
|
@@ -1110,46 +1110,51 @@ export class StandardGraph extends Graph<t.BaseGraphState, t.GraphNode> {
|
|
|
1110
1110
|
toolName === Constants.PROGRAMMATIC_TOOL_CALLING
|
|
1111
1111
|
) {
|
|
1112
1112
|
const artifact = output.artifact as t.CodeExecutionArtifact | undefined;
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
if (
|
|
1117
|
-
hasNewFiles &&
|
|
1118
|
-
artifact?.session_id != null &&
|
|
1119
|
-
artifact.session_id !== ''
|
|
1120
|
-
) {
|
|
1121
|
-
/**
|
|
1122
|
-
* Stamp each new file with its source session_id.
|
|
1123
|
-
* This enables files from different executions (parallel or sequential)
|
|
1124
|
-
* to be tracked and passed to subsequent calls.
|
|
1125
|
-
*/
|
|
1126
|
-
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
1127
|
-
...file,
|
|
1128
|
-
session_id: artifact.session_id,
|
|
1129
|
-
}));
|
|
1130
|
-
|
|
1113
|
+
if (artifact?.session_id != null && artifact.session_id !== '') {
|
|
1114
|
+
const newFiles = artifact.files ?? [];
|
|
1131
1115
|
const existingSession = this.sessions.get(Constants.EXECUTE_CODE) as
|
|
1132
1116
|
| t.CodeSessionContext
|
|
1133
1117
|
| undefined;
|
|
1134
1118
|
const existingFiles = existingSession?.files ?? [];
|
|
1135
1119
|
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
/**
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1120
|
+
if (newFiles.length > 0) {
|
|
1121
|
+
/**
|
|
1122
|
+
* Stamp each new file with its source session_id.
|
|
1123
|
+
* This enables files from different executions (parallel or sequential)
|
|
1124
|
+
* to be tracked and passed to subsequent calls.
|
|
1125
|
+
*/
|
|
1126
|
+
const filesWithSession: t.FileRefs = newFiles.map((file) => ({
|
|
1127
|
+
...file,
|
|
1128
|
+
session_id: artifact.session_id,
|
|
1129
|
+
}));
|
|
1130
|
+
|
|
1131
|
+
/**
|
|
1132
|
+
* Merge files, preferring latest versions by name.
|
|
1133
|
+
* If a file with the same name exists, replace it with the new version.
|
|
1134
|
+
* This handles cases where files are edited/recreated in subsequent executions.
|
|
1135
|
+
*/
|
|
1136
|
+
const newFileNames = new Set(filesWithSession.map((f) => f.name));
|
|
1137
|
+
const filteredExisting = existingFiles.filter(
|
|
1138
|
+
(f) => !newFileNames.has(f.name)
|
|
1139
|
+
);
|
|
1140
|
+
|
|
1141
|
+
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
1142
|
+
session_id: artifact.session_id,
|
|
1143
|
+
files: [...filteredExisting, ...filesWithSession],
|
|
1144
|
+
lastUpdated: Date.now(),
|
|
1145
|
+
});
|
|
1146
|
+
} else {
|
|
1147
|
+
/**
|
|
1148
|
+
* Store session_id even without new files for session continuity.
|
|
1149
|
+
* The CodeExecutor can fall back to the /files endpoint to discover
|
|
1150
|
+
* session files not explicitly returned in the exec response.
|
|
1151
|
+
*/
|
|
1152
|
+
this.sessions.set(Constants.EXECUTE_CODE, {
|
|
1153
|
+
session_id: artifact.session_id,
|
|
1154
|
+
files: existingFiles,
|
|
1155
|
+
lastUpdated: Date.now(),
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1153
1158
|
}
|
|
1154
1159
|
}
|
|
1155
1160
|
|
|
@@ -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
|
|