@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.
Files changed (35) hide show
  1. package/dist/cjs/agents/AgentContext.cjs +3 -0
  2. package/dist/cjs/agents/AgentContext.cjs.map +1 -1
  3. package/dist/cjs/graphs/Graph.cjs +38 -29
  4. package/dist/cjs/graphs/Graph.cjs.map +1 -1
  5. package/dist/cjs/stream.cjs +2 -1
  6. package/dist/cjs/stream.cjs.map +1 -1
  7. package/dist/cjs/tools/ToolNode.cjs +90 -14
  8. package/dist/cjs/tools/ToolNode.cjs.map +1 -1
  9. package/dist/cjs/tools/handlers.cjs +25 -8
  10. package/dist/cjs/tools/handlers.cjs.map +1 -1
  11. package/dist/esm/agents/AgentContext.mjs +3 -0
  12. package/dist/esm/agents/AgentContext.mjs.map +1 -1
  13. package/dist/esm/graphs/Graph.mjs +38 -29
  14. package/dist/esm/graphs/Graph.mjs.map +1 -1
  15. package/dist/esm/stream.mjs +2 -1
  16. package/dist/esm/stream.mjs.map +1 -1
  17. package/dist/esm/tools/ToolNode.mjs +90 -14
  18. package/dist/esm/tools/ToolNode.mjs.map +1 -1
  19. package/dist/esm/tools/handlers.mjs +25 -8
  20. package/dist/esm/tools/handlers.mjs.map +1 -1
  21. package/dist/types/agents/AgentContext.d.ts +2 -0
  22. package/dist/types/tools/ToolNode.d.ts +10 -0
  23. package/dist/types/types/tools.d.ts +7 -1
  24. package/package.json +1 -1
  25. package/src/agents/AgentContext.ts +3 -0
  26. package/src/graphs/Graph.ts +41 -36
  27. package/src/scripts/bedrock-content-aggregation-test.ts +265 -0
  28. package/src/scripts/bedrock-parallel-tools-test.ts +203 -0
  29. package/src/scripts/tools.ts +3 -12
  30. package/src/stream.ts +2 -1
  31. package/src/tools/ToolNode.ts +120 -14
  32. package/src/tools/__tests__/ToolNode.session.test.ts +465 -0
  33. package/src/tools/__tests__/handlers.test.ts +994 -0
  34. package/src/tools/handlers.ts +32 -13
  35. package/src/types/tools.ts +7 -1
@@ -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('post-reasoning');
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
- const newFiles = artifact?.files ?? [];
1114
- const hasNewFiles = newFiles.length > 0;
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
- * Merge files, preferring latest versions by name.
1138
- * If a file with the same name exists, replace it with the new version.
1139
- * This handles cases where files are edited/recreated in subsequent executions.
1140
- */
1141
- const newFileNames = new Set(filesWithSession.map((f) => f.name));
1142
- const filteredExisting = existingFiles.filter(
1143
- (f) => !newFileNames.has(f.name)
1144
- );
1145
-
1146
- this.sessions.set(Constants.EXECUTE_CODE, {
1147
- /** Keep latest session_id for reference/fallback */
1148
- session_id: artifact.session_id,
1149
- /** Accumulated files with latest versions preferred */
1150
- files: [...filteredExisting, ...filesWithSession],
1151
- lastUpdated: Date.now(),
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
+ });
@@ -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
- // console.dir(contentParts, { depth: null });
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