@librechat/agents 3.1.87 → 3.1.89
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 +18 -1
- package/dist/cjs/graphs/Graph.cjs.map +1 -1
- package/dist/cjs/stream.cjs +120 -10
- package/dist/cjs/stream.cjs.map +1 -1
- package/dist/cjs/tools/ToolNode.cjs +24 -7
- package/dist/cjs/tools/ToolNode.cjs.map +1 -1
- package/dist/cjs/tools/toolOutputReferences.cjs +8 -0
- package/dist/cjs/tools/toolOutputReferences.cjs.map +1 -1
- package/dist/cjs/utils/events.cjs +3 -1
- package/dist/cjs/utils/events.cjs.map +1 -1
- package/dist/esm/graphs/Graph.mjs +18 -1
- package/dist/esm/graphs/Graph.mjs.map +1 -1
- package/dist/esm/stream.mjs +120 -10
- package/dist/esm/stream.mjs.map +1 -1
- package/dist/esm/tools/ToolNode.mjs +24 -7
- package/dist/esm/tools/ToolNode.mjs.map +1 -1
- package/dist/esm/tools/toolOutputReferences.mjs +8 -1
- package/dist/esm/tools/toolOutputReferences.mjs.map +1 -1
- package/dist/esm/utils/events.mjs +3 -1
- package/dist/esm/utils/events.mjs.map +1 -1
- package/dist/types/graphs/Graph.d.ts +8 -0
- package/dist/types/types/tools.d.ts +9 -0
- package/dist/types/utils/events.d.ts +1 -1
- package/package.json +1 -1
- package/src/__tests__/stream.eagerEventExecution.test.ts +1307 -342
- package/src/graphs/Graph.ts +20 -5
- package/src/specs/subagent.test.ts +87 -1
- package/src/stream.ts +168 -16
- package/src/tools/ToolNode.ts +134 -111
- package/src/tools/__tests__/ToolNode.eagerEventExecution.test.ts +278 -14
- package/src/types/tools.ts +9 -0
- package/src/utils/events.ts +4 -2
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
} from '@/common';
|
|
12
12
|
import { HandlerRegistry } from '@/events';
|
|
13
13
|
import * as events from '@/utils/events';
|
|
14
|
-
import { ChatModelStreamHandler } from '@/stream';
|
|
14
|
+
import { ChatModelStreamHandler, createContentAggregator } from '@/stream';
|
|
15
15
|
import {
|
|
16
16
|
STREAMED_TOOL_CALL_SEAL_METADATA_KEY,
|
|
17
17
|
STREAMED_TOOL_CALL_ADAPTER_METADATA_KEY,
|
|
@@ -71,7 +71,8 @@ function createGraph(overrides: Partial<StandardGraph> = {}): StandardGraph {
|
|
|
71
71
|
(details as t.StepDetails).type === StepTypes.TOOL_CALLS &&
|
|
72
72
|
Array.isArray((details as t.ToolCallsDetails).tool_calls)
|
|
73
73
|
) {
|
|
74
|
-
for (const toolCall of (details as t.ToolCallsDetails).tool_calls ??
|
|
74
|
+
for (const toolCall of (details as t.ToolCallsDetails).tool_calls ??
|
|
75
|
+
[]) {
|
|
75
76
|
if (toolCall.id != null && toolCall.id !== '') {
|
|
76
77
|
graph.toolCallStepIds.set(toolCall.id, id);
|
|
77
78
|
}
|
|
@@ -110,8 +111,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
110
111
|
it('prestarts a complete event-driven tool call from the stream', async () => {
|
|
111
112
|
const graph = createGraph();
|
|
112
113
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
113
|
-
jest
|
|
114
|
-
|
|
114
|
+
jest
|
|
115
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
116
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
115
117
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
116
118
|
return;
|
|
117
119
|
}
|
|
@@ -124,8 +126,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
124
126
|
content: 'sunny',
|
|
125
127
|
},
|
|
126
128
|
]);
|
|
127
|
-
}
|
|
128
|
-
);
|
|
129
|
+
});
|
|
129
130
|
|
|
130
131
|
await new ChatModelStreamHandler().handle(
|
|
131
132
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -162,11 +163,65 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
162
163
|
expect(graph.toolCallStepIds.has('call_weather')).toBe(true);
|
|
163
164
|
});
|
|
164
165
|
|
|
166
|
+
it('prestarts when subagent callback forwarding can execute tools without a handler registry', async () => {
|
|
167
|
+
const graph = createGraph({
|
|
168
|
+
handlerRegistry: undefined,
|
|
169
|
+
eventToolExecutionAvailable: true,
|
|
170
|
+
});
|
|
171
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
172
|
+
jest
|
|
173
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
174
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
175
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
179
|
+
toolExecuteCalls.push(batch);
|
|
180
|
+
batch.resolve([
|
|
181
|
+
{
|
|
182
|
+
toolCallId: 'call_weather',
|
|
183
|
+
status: 'success',
|
|
184
|
+
content: 'sunny',
|
|
185
|
+
},
|
|
186
|
+
]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
await new ChatModelStreamHandler().handle(
|
|
190
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
191
|
+
{
|
|
192
|
+
chunk: {
|
|
193
|
+
content: '',
|
|
194
|
+
tool_calls: [
|
|
195
|
+
{
|
|
196
|
+
id: 'call_weather',
|
|
197
|
+
name: 'weather',
|
|
198
|
+
args: { city: 'NYC' },
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
202
|
+
} as unknown as t.StreamChunk,
|
|
203
|
+
},
|
|
204
|
+
{ langgraph_node: 'agent' },
|
|
205
|
+
graph
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
209
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
210
|
+
id: 'call_weather',
|
|
211
|
+
name: 'weather',
|
|
212
|
+
args: { city: 'NYC' },
|
|
213
|
+
stepId: expect.stringMatching(/^step_/),
|
|
214
|
+
turn: 0,
|
|
215
|
+
});
|
|
216
|
+
expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
|
|
217
|
+
});
|
|
218
|
+
|
|
165
219
|
it('does not prestart parseable tool calls before a final tool-call signal', async () => {
|
|
166
220
|
const graph = createGraph();
|
|
167
221
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
168
|
-
jest
|
|
169
|
-
|
|
222
|
+
jest
|
|
223
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
224
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
170
225
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
171
226
|
return;
|
|
172
227
|
}
|
|
@@ -179,8 +234,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
179
234
|
content: 'sunny',
|
|
180
235
|
},
|
|
181
236
|
]);
|
|
182
|
-
}
|
|
183
|
-
);
|
|
237
|
+
});
|
|
184
238
|
|
|
185
239
|
const handler = new ChatModelStreamHandler();
|
|
186
240
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -248,8 +302,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
248
302
|
) as unknown as StandardGraph['getAgentContext'],
|
|
249
303
|
});
|
|
250
304
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
251
|
-
jest
|
|
252
|
-
|
|
305
|
+
jest
|
|
306
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
307
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
253
308
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
254
309
|
return;
|
|
255
310
|
}
|
|
@@ -262,8 +317,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
262
317
|
content: `${request.name} result`,
|
|
263
318
|
}))
|
|
264
319
|
);
|
|
265
|
-
}
|
|
266
|
-
);
|
|
320
|
+
});
|
|
267
321
|
|
|
268
322
|
await new ChatModelStreamHandler().handle(
|
|
269
323
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -308,7 +362,8 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
308
362
|
}),
|
|
309
363
|
]);
|
|
310
364
|
const weatherExecution = graph.eagerEventToolExecutions.get('call_weather');
|
|
311
|
-
const calendarExecution =
|
|
365
|
+
const calendarExecution =
|
|
366
|
+
graph.eagerEventToolExecutions.get('call_calendar');
|
|
312
367
|
expect(weatherExecution).toMatchObject({
|
|
313
368
|
toolCallId: 'call_weather',
|
|
314
369
|
toolName: 'weather',
|
|
@@ -325,8 +380,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
325
380
|
it('assigns same-tool eager turns in model emission order', async () => {
|
|
326
381
|
const graph = createGraph();
|
|
327
382
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
328
|
-
jest
|
|
329
|
-
|
|
383
|
+
jest
|
|
384
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
385
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
330
386
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
331
387
|
return;
|
|
332
388
|
}
|
|
@@ -339,8 +395,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
339
395
|
content: `${request.args.city} weather`,
|
|
340
396
|
}))
|
|
341
397
|
);
|
|
342
|
-
}
|
|
343
|
-
);
|
|
398
|
+
});
|
|
344
399
|
|
|
345
400
|
await new ChatModelStreamHandler().handle(
|
|
346
401
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -371,10 +426,12 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
371
426
|
0, 1,
|
|
372
427
|
]);
|
|
373
428
|
expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
|
|
374
|
-
expect(
|
|
375
|
-
.
|
|
376
|
-
|
|
377
|
-
|
|
429
|
+
expect(
|
|
430
|
+
graph.eagerEventToolExecutions.get('call_weather_1')?.request.turn
|
|
431
|
+
).toBe(0);
|
|
432
|
+
expect(
|
|
433
|
+
graph.eagerEventToolExecutions.get('call_weather_2')?.request.turn
|
|
434
|
+
).toBe(1);
|
|
378
435
|
});
|
|
379
436
|
|
|
380
437
|
it('scopes eager turn reservation by agent', async () => {
|
|
@@ -391,19 +448,21 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
391
448
|
const graph = createGraph({
|
|
392
449
|
getEagerEventToolUsageCount: jest.fn(getUsageCount),
|
|
393
450
|
getAgentContext: jest.fn(
|
|
394
|
-
(metadata?: Record<string, unknown>): AgentContext =>
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
451
|
+
(metadata?: Record<string, unknown>): AgentContext =>
|
|
452
|
+
({
|
|
453
|
+
provider: Providers.ANTHROPIC,
|
|
454
|
+
reasoningKey: 'reasoning',
|
|
455
|
+
toolDefinitions: [{ name: 'weather' }],
|
|
456
|
+
graphTools: [],
|
|
457
|
+
agentId:
|
|
458
|
+
metadata?.langgraph_node === 'agent_2' ? 'agent_2' : 'agent_1',
|
|
459
|
+
}) as unknown as AgentContext
|
|
402
460
|
),
|
|
403
461
|
});
|
|
404
462
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
405
|
-
jest
|
|
406
|
-
|
|
463
|
+
jest
|
|
464
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
465
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
407
466
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
408
467
|
return;
|
|
409
468
|
}
|
|
@@ -416,8 +475,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
416
475
|
content: 'ok',
|
|
417
476
|
}))
|
|
418
477
|
);
|
|
419
|
-
}
|
|
420
|
-
);
|
|
478
|
+
});
|
|
421
479
|
|
|
422
480
|
const handler = new ChatModelStreamHandler();
|
|
423
481
|
await handler.handle(
|
|
@@ -463,25 +521,27 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
463
521
|
]);
|
|
464
522
|
expect(usageByAgent.get('agent_1')?.get('weather')).toBe(1);
|
|
465
523
|
expect(usageByAgent.get('agent_2')?.get('weather')).toBe(1);
|
|
466
|
-
expect(
|
|
467
|
-
.
|
|
468
|
-
|
|
469
|
-
|
|
524
|
+
expect(
|
|
525
|
+
graph.eagerEventToolExecutions.get('call_agent_1_weather')?.request.turn
|
|
526
|
+
).toBe(0);
|
|
527
|
+
expect(
|
|
528
|
+
graph.eagerEventToolExecutions.get('call_agent_2_weather')?.request.turn
|
|
529
|
+
).toBe(0);
|
|
470
530
|
});
|
|
471
531
|
|
|
472
532
|
it('skips eager for the whole batch if any call is not request-plannable', async () => {
|
|
473
533
|
const graph = createGraph();
|
|
474
534
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
475
|
-
jest
|
|
476
|
-
|
|
535
|
+
jest
|
|
536
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
537
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
477
538
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
478
539
|
return;
|
|
479
540
|
}
|
|
480
541
|
const batch = data as t.ToolExecuteBatchRequest;
|
|
481
542
|
toolExecuteCalls.push(batch);
|
|
482
543
|
batch.resolve([]);
|
|
483
|
-
}
|
|
484
|
-
);
|
|
544
|
+
});
|
|
485
545
|
|
|
486
546
|
await new ChatModelStreamHandler().handle(
|
|
487
547
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -515,8 +575,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
515
575
|
it('records complete chunk-only tool calls after creating a tool step', async () => {
|
|
516
576
|
const graph = createGraph();
|
|
517
577
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
518
|
-
jest
|
|
519
|
-
|
|
578
|
+
jest
|
|
579
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
580
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
520
581
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
521
582
|
return;
|
|
522
583
|
}
|
|
@@ -529,8 +590,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
529
590
|
content: 'sunny',
|
|
530
591
|
},
|
|
531
592
|
]);
|
|
532
|
-
}
|
|
533
|
-
);
|
|
593
|
+
});
|
|
534
594
|
|
|
535
595
|
await new ChatModelStreamHandler().handle(
|
|
536
596
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -554,8 +614,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
554
614
|
expect(toolExecuteCalls).toHaveLength(0);
|
|
555
615
|
expect(graph.toolCallStepIds.has('call_weather')).toBe(true);
|
|
556
616
|
expect(
|
|
557
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
558
|
-
?.argsText
|
|
617
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
559
618
|
).toBe('{"city":"NYC"}');
|
|
560
619
|
});
|
|
561
620
|
|
|
@@ -572,8 +631,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
572
631
|
) as unknown as StandardGraph['getAgentContext'],
|
|
573
632
|
});
|
|
574
633
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
575
|
-
jest
|
|
576
|
-
|
|
634
|
+
jest
|
|
635
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
636
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
577
637
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
578
638
|
return;
|
|
579
639
|
}
|
|
@@ -586,8 +646,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
586
646
|
content: 'sunny',
|
|
587
647
|
},
|
|
588
648
|
]);
|
|
589
|
-
}
|
|
590
|
-
);
|
|
649
|
+
});
|
|
591
650
|
|
|
592
651
|
const handler = new ChatModelStreamHandler();
|
|
593
652
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -666,9 +725,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
666
725
|
stepId: expect.stringMatching(/^step_/),
|
|
667
726
|
turn: 0,
|
|
668
727
|
});
|
|
669
|
-
expect(
|
|
670
|
-
|
|
671
|
-
);
|
|
728
|
+
expect(
|
|
729
|
+
graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))
|
|
730
|
+
).toBe(false);
|
|
672
731
|
});
|
|
673
732
|
|
|
674
733
|
it('keeps OpenAI Chat Completions streamed chunks on the final tool_calls path', async () => {
|
|
@@ -684,8 +743,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
684
743
|
) as unknown as StandardGraph['getAgentContext'],
|
|
685
744
|
});
|
|
686
745
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
687
|
-
jest
|
|
688
|
-
|
|
746
|
+
jest
|
|
747
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
748
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
689
749
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
690
750
|
return;
|
|
691
751
|
}
|
|
@@ -698,8 +758,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
698
758
|
content: `ok ${call.name}`,
|
|
699
759
|
}))
|
|
700
760
|
);
|
|
701
|
-
}
|
|
702
|
-
);
|
|
761
|
+
});
|
|
703
762
|
|
|
704
763
|
const handler = new ChatModelStreamHandler();
|
|
705
764
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -786,8 +845,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
786
845
|
it('prestarts final tool calls even when the final chunk also has tool-call chunks', async () => {
|
|
787
846
|
const graph = createGraph();
|
|
788
847
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
789
|
-
jest
|
|
790
|
-
|
|
848
|
+
jest
|
|
849
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
850
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
791
851
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
792
852
|
return;
|
|
793
853
|
}
|
|
@@ -800,8 +860,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
800
860
|
content: 'sunny',
|
|
801
861
|
},
|
|
802
862
|
]);
|
|
803
|
-
}
|
|
804
|
-
);
|
|
863
|
+
});
|
|
805
864
|
|
|
806
865
|
await new ChatModelStreamHandler().handle(
|
|
807
866
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -843,8 +902,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
843
902
|
it('waits for final tool calls before prestarting streamed chunk calls', async () => {
|
|
844
903
|
const graph = createGraph();
|
|
845
904
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
846
|
-
jest
|
|
847
|
-
|
|
905
|
+
jest
|
|
906
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
907
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
848
908
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
849
909
|
return;
|
|
850
910
|
}
|
|
@@ -857,8 +917,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
857
917
|
content: 'sunny',
|
|
858
918
|
},
|
|
859
919
|
]);
|
|
860
|
-
}
|
|
861
|
-
);
|
|
920
|
+
});
|
|
862
921
|
|
|
863
922
|
const handler = new ChatModelStreamHandler();
|
|
864
923
|
|
|
@@ -1033,8 +1092,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1033
1092
|
it('preserves repeated adjacent argument deltas', async () => {
|
|
1034
1093
|
const graph = createGraph();
|
|
1035
1094
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
1036
|
-
jest
|
|
1037
|
-
|
|
1095
|
+
jest
|
|
1096
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1097
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1038
1098
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1039
1099
|
return;
|
|
1040
1100
|
}
|
|
@@ -1047,8 +1107,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1047
1107
|
content: 'ok',
|
|
1048
1108
|
},
|
|
1049
1109
|
]);
|
|
1050
|
-
}
|
|
1051
|
-
);
|
|
1110
|
+
});
|
|
1052
1111
|
|
|
1053
1112
|
const handler = new ChatModelStreamHandler();
|
|
1054
1113
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -1127,8 +1186,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1127
1186
|
|
|
1128
1187
|
expect(toolExecuteCalls).toHaveLength(0);
|
|
1129
1188
|
expect(
|
|
1130
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1131
|
-
?.argsText
|
|
1189
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1132
1190
|
).toBe('{"word":"book"}');
|
|
1133
1191
|
|
|
1134
1192
|
await handler.handle(
|
|
@@ -1202,8 +1260,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1202
1260
|
);
|
|
1203
1261
|
|
|
1204
1262
|
expect(
|
|
1205
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1206
|
-
?.argsText
|
|
1263
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1207
1264
|
).toBe('oo');
|
|
1208
1265
|
});
|
|
1209
1266
|
|
|
@@ -1249,16 +1306,16 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1249
1306
|
);
|
|
1250
1307
|
|
|
1251
1308
|
expect(
|
|
1252
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1253
|
-
?.argsText
|
|
1309
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1254
1310
|
).toBe('{"titl');
|
|
1255
1311
|
});
|
|
1256
1312
|
|
|
1257
1313
|
it('does not prestart from cumulative streamed args before final tool calls', async () => {
|
|
1258
1314
|
const graph = createGraph();
|
|
1259
1315
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
1260
|
-
jest
|
|
1261
|
-
|
|
1316
|
+
jest
|
|
1317
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1318
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1262
1319
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1263
1320
|
return;
|
|
1264
1321
|
}
|
|
@@ -1271,8 +1328,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1271
1328
|
content: 'sunny',
|
|
1272
1329
|
},
|
|
1273
1330
|
]);
|
|
1274
|
-
}
|
|
1275
|
-
);
|
|
1331
|
+
});
|
|
1276
1332
|
|
|
1277
1333
|
const handler = new ChatModelStreamHandler();
|
|
1278
1334
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -1353,8 +1409,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1353
1409
|
|
|
1354
1410
|
expect(toolExecuteCalls).toHaveLength(0);
|
|
1355
1411
|
expect(
|
|
1356
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1357
|
-
?.argsText
|
|
1412
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1358
1413
|
).toBe('{"city":"NYC","unit":"C"}');
|
|
1359
1414
|
|
|
1360
1415
|
await handler.handle(
|
|
@@ -1445,8 +1500,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1445
1500
|
);
|
|
1446
1501
|
|
|
1447
1502
|
expect(
|
|
1448
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1449
|
-
?.argsText
|
|
1503
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1450
1504
|
).toBe('{"title":"alpha","city":"NYC"}');
|
|
1451
1505
|
});
|
|
1452
1506
|
|
|
@@ -1504,8 +1558,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1504
1558
|
);
|
|
1505
1559
|
|
|
1506
1560
|
expect(
|
|
1507
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1508
|
-
?.argsText
|
|
1561
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1509
1562
|
).toBe('{"word":"book"}');
|
|
1510
1563
|
});
|
|
1511
1564
|
|
|
@@ -1590,8 +1643,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1590
1643
|
);
|
|
1591
1644
|
|
|
1592
1645
|
expect(
|
|
1593
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))
|
|
1594
|
-
?.argsText
|
|
1646
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 0))?.argsText
|
|
1595
1647
|
).toBe('{"city":"NYC"}');
|
|
1596
1648
|
});
|
|
1597
1649
|
|
|
@@ -1599,8 +1651,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1599
1651
|
const graphA = createGraph();
|
|
1600
1652
|
const graphB = createGraph();
|
|
1601
1653
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
1602
|
-
jest
|
|
1603
|
-
|
|
1654
|
+
jest
|
|
1655
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1656
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1604
1657
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1605
1658
|
return;
|
|
1606
1659
|
}
|
|
@@ -1613,8 +1666,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1613
1666
|
content: `${request.id} result`,
|
|
1614
1667
|
}))
|
|
1615
1668
|
);
|
|
1616
|
-
}
|
|
1617
|
-
);
|
|
1669
|
+
});
|
|
1618
1670
|
|
|
1619
1671
|
const handler = new ChatModelStreamHandler();
|
|
1620
1672
|
const sharedChunk = {
|
|
@@ -1660,8 +1712,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1660
1712
|
) as unknown as StandardGraph['getAgentContext'],
|
|
1661
1713
|
});
|
|
1662
1714
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
1663
|
-
jest
|
|
1664
|
-
|
|
1715
|
+
jest
|
|
1716
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1717
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1665
1718
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1666
1719
|
return;
|
|
1667
1720
|
}
|
|
@@ -1674,8 +1727,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1674
1727
|
content: `ok ${call.name}`,
|
|
1675
1728
|
}))
|
|
1676
1729
|
);
|
|
1677
|
-
}
|
|
1678
|
-
);
|
|
1730
|
+
});
|
|
1679
1731
|
|
|
1680
1732
|
const handler = new ChatModelStreamHandler();
|
|
1681
1733
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -1732,9 +1784,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1732
1784
|
]);
|
|
1733
1785
|
expect(graph.eagerEventToolExecutions.has('call_weather')).toBe(true);
|
|
1734
1786
|
expect(graph.eagerEventToolExecutions.has('call_stock')).toBe(false);
|
|
1735
|
-
expect(
|
|
1736
|
-
|
|
1737
|
-
);
|
|
1787
|
+
expect(
|
|
1788
|
+
graph.eagerEventToolCallChunks.has(chunkStateKey('step-key', 0))
|
|
1789
|
+
).toBe(false);
|
|
1738
1790
|
expect(
|
|
1739
1791
|
graph.eagerEventToolCallChunks.get(chunkStateKey('step-key', 1))?.argsText
|
|
1740
1792
|
).toBe('{"ticker":"C');
|
|
@@ -1793,7 +1845,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1793
1845
|
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
1794
1846
|
});
|
|
1795
1847
|
|
|
1796
|
-
it('
|
|
1848
|
+
it('emits a completed event when an eager streamed tool result settles', async () => {
|
|
1797
1849
|
const graph = createGraph({
|
|
1798
1850
|
getAgentContext: jest.fn(
|
|
1799
1851
|
(): Partial<AgentContext> => ({
|
|
@@ -1805,14 +1857,18 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1805
1857
|
})
|
|
1806
1858
|
) as unknown as StandardGraph['getAgentContext'],
|
|
1807
1859
|
});
|
|
1808
|
-
const
|
|
1809
|
-
jest
|
|
1810
|
-
|
|
1860
|
+
const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
|
|
1861
|
+
jest
|
|
1862
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1863
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1864
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
1865
|
+
completedEvents.push(data as { result: t.ToolEndEvent });
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1811
1868
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
1812
1869
|
return;
|
|
1813
1870
|
}
|
|
1814
1871
|
const batch = data as t.ToolExecuteBatchRequest;
|
|
1815
|
-
toolExecuteCalls.push(batch);
|
|
1816
1872
|
batch.resolve(
|
|
1817
1873
|
batch.toolCalls.map((call) => ({
|
|
1818
1874
|
toolCallId: call.id,
|
|
@@ -1820,8 +1876,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1820
1876
|
content: `ok ${call.name}`,
|
|
1821
1877
|
}))
|
|
1822
1878
|
);
|
|
1823
|
-
}
|
|
1824
|
-
);
|
|
1879
|
+
});
|
|
1825
1880
|
|
|
1826
1881
|
const handler = new ChatModelStreamHandler();
|
|
1827
1882
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -1851,10 +1906,6 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1851
1906
|
chunk: {
|
|
1852
1907
|
content: '',
|
|
1853
1908
|
tool_call_chunks: [
|
|
1854
|
-
{
|
|
1855
|
-
args: '',
|
|
1856
|
-
index: 0,
|
|
1857
|
-
},
|
|
1858
1909
|
{
|
|
1859
1910
|
id: 'call_stock',
|
|
1860
1911
|
name: 'stock',
|
|
@@ -1868,52 +1919,51 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1868
1919
|
graph
|
|
1869
1920
|
);
|
|
1870
1921
|
|
|
1871
|
-
|
|
1922
|
+
await graph.eagerEventToolExecutions.get('call_weather')?.promise;
|
|
1872
1923
|
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
} as unknown as t.StreamChunk,
|
|
1924
|
+
expect(completedEvents).toHaveLength(1);
|
|
1925
|
+
expect(completedEvents[0].result).toMatchObject({
|
|
1926
|
+
id: expect.stringMatching(/^step_/),
|
|
1927
|
+
type: 'tool_call',
|
|
1928
|
+
eager: true,
|
|
1929
|
+
tool_call: {
|
|
1930
|
+
id: 'call_weather',
|
|
1931
|
+
name: 'weather',
|
|
1932
|
+
args: '{"city":"NYC"}',
|
|
1933
|
+
output: 'ok weather',
|
|
1934
|
+
progress: 1,
|
|
1885
1935
|
},
|
|
1886
|
-
metadata,
|
|
1887
|
-
graph
|
|
1888
|
-
);
|
|
1889
|
-
|
|
1890
|
-
expect(toolExecuteCalls).toHaveLength(1);
|
|
1891
|
-
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
1892
|
-
id: 'call_weather',
|
|
1893
|
-
name: 'weather',
|
|
1894
|
-
args: { city: 'NYC' },
|
|
1895
1936
|
});
|
|
1937
|
+
expect(
|
|
1938
|
+
graph.eagerEventToolExecutions.get('call_weather')?.completionDispatched
|
|
1939
|
+
).toBe(true);
|
|
1896
1940
|
});
|
|
1897
1941
|
|
|
1898
|
-
it('
|
|
1899
|
-
const graph = createGraph(
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1942
|
+
it('does not emit a stale eager completion after the active execution is invalidated', async () => {
|
|
1943
|
+
const graph = createGraph({
|
|
1944
|
+
getAgentContext: jest.fn(
|
|
1945
|
+
(): Partial<AgentContext> => ({
|
|
1946
|
+
provider: Providers.ANTHROPIC,
|
|
1947
|
+
reasoningKey: 'reasoning',
|
|
1948
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
1949
|
+
graphTools: [],
|
|
1950
|
+
agentId: 'agent_1',
|
|
1951
|
+
})
|
|
1952
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
1953
|
+
});
|
|
1954
|
+
const completedEvents: Array<{ result: t.ToolEndEvent }> = [];
|
|
1955
|
+
let pendingBatch: t.ToolExecuteBatchRequest | undefined;
|
|
1956
|
+
jest
|
|
1957
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
1958
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
1959
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
1960
|
+
completedEvents.push(data as { result: t.ToolEndEvent });
|
|
1904
1961
|
return;
|
|
1905
1962
|
}
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
toolCallId: call.id,
|
|
1911
|
-
status: 'success',
|
|
1912
|
-
content: `ok ${call.args.city}`,
|
|
1913
|
-
}))
|
|
1914
|
-
);
|
|
1915
|
-
}
|
|
1916
|
-
);
|
|
1963
|
+
if (event === GraphEvents.ON_TOOL_EXECUTE) {
|
|
1964
|
+
pendingBatch = data as t.ToolExecuteBatchRequest;
|
|
1965
|
+
}
|
|
1966
|
+
});
|
|
1917
1967
|
|
|
1918
1968
|
const handler = new ChatModelStreamHandler();
|
|
1919
1969
|
const metadata = { langgraph_node: 'agent' };
|
|
@@ -1925,7 +1975,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1925
1975
|
content: '',
|
|
1926
1976
|
tool_call_chunks: [
|
|
1927
1977
|
{
|
|
1928
|
-
id: '
|
|
1978
|
+
id: 'call_weather',
|
|
1929
1979
|
name: 'weather',
|
|
1930
1980
|
args: '{"city":"NYC"}',
|
|
1931
1981
|
index: 0,
|
|
@@ -1936,7 +1986,6 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1936
1986
|
metadata,
|
|
1937
1987
|
graph
|
|
1938
1988
|
);
|
|
1939
|
-
|
|
1940
1989
|
await handler.handle(
|
|
1941
1990
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
1942
1991
|
{
|
|
@@ -1944,9 +1993,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1944
1993
|
content: '',
|
|
1945
1994
|
tool_call_chunks: [
|
|
1946
1995
|
{
|
|
1947
|
-
id: '
|
|
1948
|
-
name: '
|
|
1949
|
-
args: '{"
|
|
1996
|
+
id: 'call_stock',
|
|
1997
|
+
name: 'stock',
|
|
1998
|
+
args: '{"ticker":"C',
|
|
1950
1999
|
index: 1,
|
|
1951
2000
|
},
|
|
1952
2001
|
],
|
|
@@ -1956,90 +2005,56 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
1956
2005
|
graph
|
|
1957
2006
|
);
|
|
1958
2007
|
|
|
1959
|
-
|
|
1960
|
-
expect(
|
|
1961
|
-
|
|
1962
|
-
name: 'weather',
|
|
1963
|
-
args: { city: 'NYC' },
|
|
1964
|
-
turn: 0,
|
|
1965
|
-
});
|
|
1966
|
-
|
|
1967
|
-
await handler.handle(
|
|
1968
|
-
GraphEvents.CHAT_MODEL_STREAM,
|
|
1969
|
-
{
|
|
1970
|
-
chunk: {
|
|
1971
|
-
content: '',
|
|
1972
|
-
tool_call_chunks: [
|
|
1973
|
-
{
|
|
1974
|
-
args: 'oston"}',
|
|
1975
|
-
index: 1,
|
|
1976
|
-
},
|
|
1977
|
-
],
|
|
1978
|
-
} as unknown as t.StreamChunk,
|
|
1979
|
-
},
|
|
1980
|
-
metadata,
|
|
1981
|
-
graph
|
|
1982
|
-
);
|
|
2008
|
+
const staleRecord = graph.eagerEventToolExecutions.get('call_weather');
|
|
2009
|
+
expect(staleRecord).toBeDefined();
|
|
2010
|
+
expect(pendingBatch?.toolCalls).toHaveLength(1);
|
|
1983
2011
|
|
|
1984
|
-
|
|
1985
|
-
|
|
2012
|
+
graph.eagerEventToolExecutions.delete('call_weather');
|
|
2013
|
+
pendingBatch?.resolve([
|
|
1986
2014
|
{
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
{
|
|
1991
|
-
id: 'call_weather_1',
|
|
1992
|
-
name: 'weather',
|
|
1993
|
-
args: { city: 'NYC' },
|
|
1994
|
-
},
|
|
1995
|
-
{
|
|
1996
|
-
id: 'call_weather_2',
|
|
1997
|
-
name: 'weather',
|
|
1998
|
-
args: { city: 'Boston' },
|
|
1999
|
-
},
|
|
2000
|
-
],
|
|
2001
|
-
response_metadata: finalToolCallResponseMetadata,
|
|
2002
|
-
} as unknown as t.StreamChunk,
|
|
2015
|
+
toolCallId: 'call_weather',
|
|
2016
|
+
status: 'success',
|
|
2017
|
+
content: 'stale weather',
|
|
2003
2018
|
},
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
);
|
|
2019
|
+
]);
|
|
2020
|
+
await staleRecord?.promise;
|
|
2007
2021
|
|
|
2008
|
-
expect(
|
|
2009
|
-
expect(
|
|
2010
|
-
id: 'call_weather_2',
|
|
2011
|
-
name: 'weather',
|
|
2012
|
-
args: { city: 'Boston' },
|
|
2013
|
-
turn: 1,
|
|
2014
|
-
});
|
|
2015
|
-
expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
|
|
2022
|
+
expect(completedEvents).toHaveLength(0);
|
|
2023
|
+
expect(staleRecord?.completionDispatched).toBeUndefined();
|
|
2016
2024
|
});
|
|
2017
2025
|
|
|
2018
|
-
it('
|
|
2026
|
+
it('does not mark eager completion dispatched when event delivery is swallowed', async () => {
|
|
2019
2027
|
const graph = createGraph({
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2028
|
+
getAgentContext: jest.fn(
|
|
2029
|
+
(): Partial<AgentContext> => ({
|
|
2030
|
+
provider: Providers.ANTHROPIC,
|
|
2031
|
+
reasoningKey: 'reasoning',
|
|
2032
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
2033
|
+
graphTools: [],
|
|
2034
|
+
agentId: 'agent_1',
|
|
2035
|
+
})
|
|
2036
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2023
2037
|
});
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
async (event, data): Promise<void> => {
|
|
2027
|
-
if (event
|
|
2028
|
-
return;
|
|
2038
|
+
jest
|
|
2039
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2040
|
+
.mockImplementation(async (event, data): Promise<boolean | void> => {
|
|
2041
|
+
if (event === GraphEvents.ON_RUN_STEP_COMPLETED) {
|
|
2042
|
+
return false;
|
|
2029
2043
|
}
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2044
|
+
if (event === GraphEvents.ON_TOOL_EXECUTE) {
|
|
2045
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2046
|
+
batch.resolve([
|
|
2047
|
+
{
|
|
2048
|
+
toolCallId: 'call_weather',
|
|
2049
|
+
status: 'success',
|
|
2050
|
+
content: 'weather result',
|
|
2051
|
+
},
|
|
2052
|
+
]);
|
|
2053
|
+
}
|
|
2054
|
+
});
|
|
2041
2055
|
|
|
2042
2056
|
const handler = new ChatModelStreamHandler();
|
|
2057
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2043
2058
|
|
|
2044
2059
|
await handler.handle(
|
|
2045
2060
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
@@ -2048,18 +2063,17 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2048
2063
|
content: '',
|
|
2049
2064
|
tool_call_chunks: [
|
|
2050
2065
|
{
|
|
2051
|
-
id: '
|
|
2066
|
+
id: 'call_weather',
|
|
2052
2067
|
name: 'weather',
|
|
2053
|
-
args: '{"city":"
|
|
2068
|
+
args: '{"city":"NYC"}',
|
|
2054
2069
|
index: 0,
|
|
2055
2070
|
},
|
|
2056
2071
|
],
|
|
2057
2072
|
} as unknown as t.StreamChunk,
|
|
2058
2073
|
},
|
|
2059
|
-
|
|
2074
|
+
metadata,
|
|
2060
2075
|
graph
|
|
2061
2076
|
);
|
|
2062
|
-
|
|
2063
2077
|
await handler.handle(
|
|
2064
2078
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2065
2079
|
{
|
|
@@ -2067,18 +2081,808 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2067
2081
|
content: '',
|
|
2068
2082
|
tool_call_chunks: [
|
|
2069
2083
|
{
|
|
2070
|
-
id: '
|
|
2071
|
-
name: '
|
|
2072
|
-
args: '{"
|
|
2073
|
-
index:
|
|
2084
|
+
id: 'call_stock',
|
|
2085
|
+
name: 'stock',
|
|
2086
|
+
args: '{"ticker":"C',
|
|
2087
|
+
index: 1,
|
|
2088
|
+
},
|
|
2089
|
+
],
|
|
2090
|
+
} as unknown as t.StreamChunk,
|
|
2091
|
+
},
|
|
2092
|
+
metadata,
|
|
2093
|
+
graph
|
|
2094
|
+
);
|
|
2095
|
+
|
|
2096
|
+
const record = graph.eagerEventToolExecutions.get('call_weather');
|
|
2097
|
+
await record?.promise;
|
|
2098
|
+
|
|
2099
|
+
expect(record?.completionDispatched).toBeUndefined();
|
|
2100
|
+
});
|
|
2101
|
+
|
|
2102
|
+
it('does not overwrite a completed tool output with later streamed deltas', () => {
|
|
2103
|
+
const { contentParts, aggregateContent } = createContentAggregator();
|
|
2104
|
+
|
|
2105
|
+
aggregateContent({
|
|
2106
|
+
event: GraphEvents.ON_RUN_STEP,
|
|
2107
|
+
data: {
|
|
2108
|
+
id: 'step_weather',
|
|
2109
|
+
type: StepTypes.TOOL_CALLS,
|
|
2110
|
+
index: 0,
|
|
2111
|
+
stepDetails: {
|
|
2112
|
+
type: StepTypes.TOOL_CALLS,
|
|
2113
|
+
tool_calls: [
|
|
2114
|
+
{
|
|
2115
|
+
id: 'call_weather',
|
|
2116
|
+
name: 'weather',
|
|
2117
|
+
args: {},
|
|
2118
|
+
},
|
|
2119
|
+
],
|
|
2120
|
+
},
|
|
2121
|
+
usage: null,
|
|
2122
|
+
} as t.RunStep,
|
|
2123
|
+
});
|
|
2124
|
+
aggregateContent({
|
|
2125
|
+
event: GraphEvents.ON_RUN_STEP_COMPLETED,
|
|
2126
|
+
data: {
|
|
2127
|
+
result: {
|
|
2128
|
+
id: 'step_weather',
|
|
2129
|
+
index: 0,
|
|
2130
|
+
type: 'tool_call',
|
|
2131
|
+
tool_call: {
|
|
2132
|
+
id: 'call_weather',
|
|
2133
|
+
name: 'weather',
|
|
2134
|
+
args: '{"city":"NYC"}',
|
|
2135
|
+
output: 'sunny',
|
|
2136
|
+
progress: 1,
|
|
2137
|
+
} as t.ProcessedToolCall,
|
|
2138
|
+
},
|
|
2139
|
+
} as { result: t.ToolEndEvent },
|
|
2140
|
+
});
|
|
2141
|
+
aggregateContent({
|
|
2142
|
+
event: GraphEvents.ON_RUN_STEP_DELTA,
|
|
2143
|
+
data: {
|
|
2144
|
+
id: 'step_weather',
|
|
2145
|
+
delta: {
|
|
2146
|
+
type: StepTypes.TOOL_CALLS,
|
|
2147
|
+
tool_calls: [
|
|
2148
|
+
{
|
|
2149
|
+
id: 'call_weather',
|
|
2150
|
+
name: 'weather',
|
|
2151
|
+
args: '{"city":"NYC revised"}',
|
|
2152
|
+
},
|
|
2153
|
+
],
|
|
2154
|
+
},
|
|
2155
|
+
} as t.RunStepDeltaEvent,
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
expect(contentParts[0]).toMatchObject({
|
|
2159
|
+
type: ContentTypes.TOOL_CALL,
|
|
2160
|
+
tool_call: {
|
|
2161
|
+
id: 'call_weather',
|
|
2162
|
+
name: 'weather',
|
|
2163
|
+
args: '{"city":"NYC"}',
|
|
2164
|
+
output: 'sunny',
|
|
2165
|
+
progress: 1,
|
|
2166
|
+
},
|
|
2167
|
+
});
|
|
2168
|
+
});
|
|
2169
|
+
|
|
2170
|
+
it('does not use next-index sealing for Moonshot streamed tool chunks', async () => {
|
|
2171
|
+
const graph = createGraph({
|
|
2172
|
+
getAgentContext: jest.fn(
|
|
2173
|
+
(): Partial<AgentContext> => ({
|
|
2174
|
+
provider: Providers.MOONSHOT,
|
|
2175
|
+
reasoningKey: 'reasoning',
|
|
2176
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
2177
|
+
graphTools: [],
|
|
2178
|
+
agentId: 'agent_1',
|
|
2179
|
+
})
|
|
2180
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2181
|
+
});
|
|
2182
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2183
|
+
jest
|
|
2184
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2185
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2186
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2187
|
+
return;
|
|
2188
|
+
}
|
|
2189
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2190
|
+
toolExecuteCalls.push(batch);
|
|
2191
|
+
batch.resolve(
|
|
2192
|
+
batch.toolCalls.map((call) => ({
|
|
2193
|
+
toolCallId: call.id,
|
|
2194
|
+
status: 'success',
|
|
2195
|
+
content: `ok ${call.name}`,
|
|
2196
|
+
}))
|
|
2197
|
+
);
|
|
2198
|
+
});
|
|
2199
|
+
|
|
2200
|
+
const handler = new ChatModelStreamHandler();
|
|
2201
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2202
|
+
|
|
2203
|
+
await handler.handle(
|
|
2204
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2205
|
+
{
|
|
2206
|
+
chunk: {
|
|
2207
|
+
content: '',
|
|
2208
|
+
tool_call_chunks: [
|
|
2209
|
+
{
|
|
2210
|
+
id: 'call_weather',
|
|
2211
|
+
name: 'weather',
|
|
2212
|
+
args: '{"city":"NYC"}',
|
|
2213
|
+
index: 0,
|
|
2214
|
+
},
|
|
2215
|
+
],
|
|
2216
|
+
} as unknown as t.StreamChunk,
|
|
2217
|
+
},
|
|
2218
|
+
metadata,
|
|
2219
|
+
graph
|
|
2220
|
+
);
|
|
2221
|
+
await handler.handle(
|
|
2222
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2223
|
+
{
|
|
2224
|
+
chunk: {
|
|
2225
|
+
content: '',
|
|
2226
|
+
tool_call_chunks: [
|
|
2227
|
+
{
|
|
2228
|
+
id: 'call_stock',
|
|
2229
|
+
name: 'stock',
|
|
2230
|
+
args: '{"ticker":"C',
|
|
2231
|
+
index: 1,
|
|
2232
|
+
},
|
|
2233
|
+
],
|
|
2234
|
+
} as unknown as t.StreamChunk,
|
|
2235
|
+
},
|
|
2236
|
+
metadata,
|
|
2237
|
+
graph
|
|
2238
|
+
);
|
|
2239
|
+
|
|
2240
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
2241
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2242
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2243
|
+
|
|
2244
|
+
await handler.handle(
|
|
2245
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2246
|
+
{
|
|
2247
|
+
chunk: {
|
|
2248
|
+
content: '',
|
|
2249
|
+
tool_calls: [
|
|
2250
|
+
{
|
|
2251
|
+
id: 'call_weather',
|
|
2252
|
+
name: 'weather',
|
|
2253
|
+
args: { city: 'NYC revised' },
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
id: 'call_stock',
|
|
2257
|
+
name: 'stock',
|
|
2258
|
+
args: { ticker: 'CH' },
|
|
2259
|
+
},
|
|
2260
|
+
],
|
|
2261
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
2262
|
+
} as unknown as t.StreamChunk,
|
|
2263
|
+
},
|
|
2264
|
+
metadata,
|
|
2265
|
+
graph
|
|
2266
|
+
);
|
|
2267
|
+
|
|
2268
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
2269
|
+
expect(toolExecuteCalls[0].toolCalls).toEqual([
|
|
2270
|
+
expect.objectContaining({
|
|
2271
|
+
id: 'call_weather',
|
|
2272
|
+
name: 'weather',
|
|
2273
|
+
args: { city: 'NYC revised' },
|
|
2274
|
+
}),
|
|
2275
|
+
expect.objectContaining({
|
|
2276
|
+
id: 'call_stock',
|
|
2277
|
+
name: 'stock',
|
|
2278
|
+
args: { ticker: 'CH' },
|
|
2279
|
+
}),
|
|
2280
|
+
]);
|
|
2281
|
+
});
|
|
2282
|
+
|
|
2283
|
+
it('does not seal a streamed tool when the same chunk also carries its own index', async () => {
|
|
2284
|
+
const graph = createGraph({
|
|
2285
|
+
getAgentContext: jest.fn(
|
|
2286
|
+
(): Partial<AgentContext> => ({
|
|
2287
|
+
provider: Providers.ANTHROPIC,
|
|
2288
|
+
reasoningKey: 'reasoning',
|
|
2289
|
+
toolDefinitions: [{ name: 'weather' }, { name: 'stock' }],
|
|
2290
|
+
graphTools: [],
|
|
2291
|
+
agentId: 'agent_1',
|
|
2292
|
+
})
|
|
2293
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2294
|
+
});
|
|
2295
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2296
|
+
jest
|
|
2297
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2298
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2299
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2303
|
+
toolExecuteCalls.push(batch);
|
|
2304
|
+
batch.resolve(
|
|
2305
|
+
batch.toolCalls.map((call) => ({
|
|
2306
|
+
toolCallId: call.id,
|
|
2307
|
+
status: 'success',
|
|
2308
|
+
content: `ok ${call.name}`,
|
|
2309
|
+
}))
|
|
2310
|
+
);
|
|
2311
|
+
});
|
|
2312
|
+
|
|
2313
|
+
const handler = new ChatModelStreamHandler();
|
|
2314
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2315
|
+
|
|
2316
|
+
await handler.handle(
|
|
2317
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2318
|
+
{
|
|
2319
|
+
chunk: {
|
|
2320
|
+
content: '',
|
|
2321
|
+
tool_call_chunks: [
|
|
2322
|
+
{
|
|
2323
|
+
id: 'call_weather',
|
|
2324
|
+
name: 'weather',
|
|
2325
|
+
args: '{"city":"NYC"}',
|
|
2326
|
+
index: 0,
|
|
2327
|
+
},
|
|
2328
|
+
],
|
|
2329
|
+
} as unknown as t.StreamChunk,
|
|
2330
|
+
},
|
|
2331
|
+
metadata,
|
|
2332
|
+
graph
|
|
2333
|
+
);
|
|
2334
|
+
|
|
2335
|
+
await handler.handle(
|
|
2336
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2337
|
+
{
|
|
2338
|
+
chunk: {
|
|
2339
|
+
content: '',
|
|
2340
|
+
tool_call_chunks: [
|
|
2341
|
+
{
|
|
2342
|
+
args: '',
|
|
2343
|
+
index: 0,
|
|
2344
|
+
},
|
|
2345
|
+
{
|
|
2346
|
+
id: 'call_stock',
|
|
2347
|
+
name: 'stock',
|
|
2348
|
+
args: '{"ticker":"C',
|
|
2349
|
+
index: 1,
|
|
2350
|
+
},
|
|
2351
|
+
],
|
|
2352
|
+
} as unknown as t.StreamChunk,
|
|
2353
|
+
},
|
|
2354
|
+
metadata,
|
|
2355
|
+
graph
|
|
2356
|
+
);
|
|
2357
|
+
|
|
2358
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
2359
|
+
|
|
2360
|
+
await handler.handle(
|
|
2361
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2362
|
+
{
|
|
2363
|
+
chunk: {
|
|
2364
|
+
content: '',
|
|
2365
|
+
tool_call_chunks: [
|
|
2366
|
+
{
|
|
2367
|
+
args: 'H"}',
|
|
2368
|
+
index: 1,
|
|
2369
|
+
},
|
|
2370
|
+
],
|
|
2371
|
+
} as unknown as t.StreamChunk,
|
|
2372
|
+
},
|
|
2373
|
+
metadata,
|
|
2374
|
+
graph
|
|
2375
|
+
);
|
|
2376
|
+
|
|
2377
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
2378
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
2379
|
+
id: 'call_weather',
|
|
2380
|
+
name: 'weather',
|
|
2381
|
+
args: { city: 'NYC' },
|
|
2382
|
+
});
|
|
2383
|
+
});
|
|
2384
|
+
|
|
2385
|
+
it('preserves same-tool turns across per-call streamed eager starts', async () => {
|
|
2386
|
+
const graph = createGraph();
|
|
2387
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2388
|
+
jest
|
|
2389
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2390
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2391
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2392
|
+
return;
|
|
2393
|
+
}
|
|
2394
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2395
|
+
toolExecuteCalls.push(batch);
|
|
2396
|
+
batch.resolve(
|
|
2397
|
+
batch.toolCalls.map((call) => ({
|
|
2398
|
+
toolCallId: call.id,
|
|
2399
|
+
status: 'success',
|
|
2400
|
+
content: `ok ${call.args.city}`,
|
|
2401
|
+
}))
|
|
2402
|
+
);
|
|
2403
|
+
});
|
|
2404
|
+
|
|
2405
|
+
const handler = new ChatModelStreamHandler();
|
|
2406
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2407
|
+
|
|
2408
|
+
await handler.handle(
|
|
2409
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2410
|
+
{
|
|
2411
|
+
chunk: {
|
|
2412
|
+
content: '',
|
|
2413
|
+
tool_call_chunks: [
|
|
2414
|
+
{
|
|
2415
|
+
id: 'call_weather_1',
|
|
2416
|
+
name: 'weather',
|
|
2417
|
+
args: '{"city":"NYC"}',
|
|
2418
|
+
index: 0,
|
|
2419
|
+
},
|
|
2420
|
+
],
|
|
2421
|
+
} as unknown as t.StreamChunk,
|
|
2422
|
+
},
|
|
2423
|
+
metadata,
|
|
2424
|
+
graph
|
|
2425
|
+
);
|
|
2426
|
+
|
|
2427
|
+
await handler.handle(
|
|
2428
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2429
|
+
{
|
|
2430
|
+
chunk: {
|
|
2431
|
+
content: '',
|
|
2432
|
+
tool_call_chunks: [
|
|
2433
|
+
{
|
|
2434
|
+
id: 'call_weather_2',
|
|
2435
|
+
name: 'weather',
|
|
2436
|
+
args: '{"city":"B',
|
|
2437
|
+
index: 1,
|
|
2438
|
+
},
|
|
2439
|
+
],
|
|
2440
|
+
} as unknown as t.StreamChunk,
|
|
2441
|
+
},
|
|
2442
|
+
metadata,
|
|
2443
|
+
graph
|
|
2444
|
+
);
|
|
2445
|
+
|
|
2446
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
2447
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
2448
|
+
id: 'call_weather_1',
|
|
2449
|
+
name: 'weather',
|
|
2450
|
+
args: { city: 'NYC' },
|
|
2451
|
+
turn: 0,
|
|
2452
|
+
});
|
|
2453
|
+
|
|
2454
|
+
await handler.handle(
|
|
2455
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2456
|
+
{
|
|
2457
|
+
chunk: {
|
|
2458
|
+
content: '',
|
|
2459
|
+
tool_call_chunks: [
|
|
2460
|
+
{
|
|
2461
|
+
args: 'oston"}',
|
|
2462
|
+
index: 1,
|
|
2463
|
+
},
|
|
2464
|
+
],
|
|
2465
|
+
} as unknown as t.StreamChunk,
|
|
2466
|
+
},
|
|
2467
|
+
metadata,
|
|
2468
|
+
graph
|
|
2469
|
+
);
|
|
2470
|
+
|
|
2471
|
+
await handler.handle(
|
|
2472
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2473
|
+
{
|
|
2474
|
+
chunk: {
|
|
2475
|
+
content: '',
|
|
2476
|
+
tool_calls: [
|
|
2477
|
+
{
|
|
2478
|
+
id: 'call_weather_1',
|
|
2479
|
+
name: 'weather',
|
|
2480
|
+
args: { city: 'NYC' },
|
|
2481
|
+
},
|
|
2482
|
+
{
|
|
2483
|
+
id: 'call_weather_2',
|
|
2484
|
+
name: 'weather',
|
|
2485
|
+
args: { city: 'Boston' },
|
|
2486
|
+
},
|
|
2487
|
+
],
|
|
2488
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
2489
|
+
} as unknown as t.StreamChunk,
|
|
2490
|
+
},
|
|
2491
|
+
metadata,
|
|
2492
|
+
graph
|
|
2493
|
+
);
|
|
2494
|
+
|
|
2495
|
+
expect(toolExecuteCalls).toHaveLength(2);
|
|
2496
|
+
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
2497
|
+
id: 'call_weather_2',
|
|
2498
|
+
name: 'weather',
|
|
2499
|
+
args: { city: 'Boston' },
|
|
2500
|
+
turn: 1,
|
|
2501
|
+
});
|
|
2502
|
+
expect(graph.eagerEventToolUsageCount.get('weather')).toBe(2);
|
|
2503
|
+
});
|
|
2504
|
+
|
|
2505
|
+
it('scopes streamed chunk accumulation by step key', async () => {
|
|
2506
|
+
const graph = createGraph({
|
|
2507
|
+
getStepKey: jest.fn((metadata?: Record<string, unknown>) =>
|
|
2508
|
+
String(metadata?.langgraph_node ?? 'step-key')
|
|
2509
|
+
),
|
|
2510
|
+
});
|
|
2511
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2512
|
+
jest
|
|
2513
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2514
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2515
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2519
|
+
toolExecuteCalls.push(batch);
|
|
2520
|
+
batch.resolve(
|
|
2521
|
+
batch.toolCalls.map((call) => ({
|
|
2522
|
+
toolCallId: call.id,
|
|
2523
|
+
status: 'success',
|
|
2524
|
+
content: `ok ${call.name}`,
|
|
2525
|
+
}))
|
|
2526
|
+
);
|
|
2527
|
+
});
|
|
2528
|
+
|
|
2529
|
+
const handler = new ChatModelStreamHandler();
|
|
2530
|
+
|
|
2531
|
+
await handler.handle(
|
|
2532
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2533
|
+
{
|
|
2534
|
+
chunk: {
|
|
2535
|
+
content: '',
|
|
2536
|
+
tool_call_chunks: [
|
|
2537
|
+
{
|
|
2538
|
+
id: 'call_agent_a',
|
|
2539
|
+
name: 'weather',
|
|
2540
|
+
args: '{"city":"N',
|
|
2541
|
+
index: 0,
|
|
2542
|
+
},
|
|
2543
|
+
],
|
|
2544
|
+
} as unknown as t.StreamChunk,
|
|
2545
|
+
},
|
|
2546
|
+
{ langgraph_node: 'agent_a' },
|
|
2547
|
+
graph
|
|
2548
|
+
);
|
|
2549
|
+
|
|
2550
|
+
await handler.handle(
|
|
2551
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2552
|
+
{
|
|
2553
|
+
chunk: {
|
|
2554
|
+
content: '',
|
|
2555
|
+
tool_call_chunks: [
|
|
2556
|
+
{
|
|
2557
|
+
id: 'call_agent_b',
|
|
2558
|
+
name: 'weather',
|
|
2559
|
+
args: '{"city":"S',
|
|
2560
|
+
index: 0,
|
|
2561
|
+
},
|
|
2562
|
+
],
|
|
2563
|
+
} as unknown as t.StreamChunk,
|
|
2564
|
+
},
|
|
2565
|
+
{ langgraph_node: 'agent_b' },
|
|
2566
|
+
graph
|
|
2567
|
+
);
|
|
2568
|
+
|
|
2569
|
+
await handler.handle(
|
|
2570
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2571
|
+
{
|
|
2572
|
+
chunk: {
|
|
2573
|
+
content: '',
|
|
2574
|
+
tool_call_chunks: [
|
|
2575
|
+
{
|
|
2576
|
+
args: 'F"}',
|
|
2577
|
+
index: 0,
|
|
2578
|
+
},
|
|
2579
|
+
],
|
|
2580
|
+
} as unknown as t.StreamChunk,
|
|
2581
|
+
},
|
|
2582
|
+
{ langgraph_node: 'agent_b' },
|
|
2583
|
+
graph
|
|
2584
|
+
);
|
|
2585
|
+
|
|
2586
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
2587
|
+
|
|
2588
|
+
await handler.handle(
|
|
2589
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2590
|
+
{
|
|
2591
|
+
chunk: {
|
|
2592
|
+
content: '',
|
|
2593
|
+
tool_call_chunks: [
|
|
2594
|
+
{
|
|
2595
|
+
args: 'YC"}',
|
|
2596
|
+
index: 0,
|
|
2597
|
+
},
|
|
2598
|
+
],
|
|
2599
|
+
} as unknown as t.StreamChunk,
|
|
2600
|
+
},
|
|
2601
|
+
{ langgraph_node: 'agent_a' },
|
|
2602
|
+
graph
|
|
2603
|
+
);
|
|
2604
|
+
|
|
2605
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
2606
|
+
expect(
|
|
2607
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('agent_a', 0))?.argsText
|
|
2608
|
+
).toBe('{"city":"NYC"}');
|
|
2609
|
+
expect(
|
|
2610
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('agent_b', 0))?.argsText
|
|
2611
|
+
).toBe('{"city":"SF"}');
|
|
2612
|
+
|
|
2613
|
+
await handler.handle(
|
|
2614
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2615
|
+
{
|
|
2616
|
+
chunk: {
|
|
2617
|
+
content: '',
|
|
2618
|
+
tool_calls: [
|
|
2619
|
+
{
|
|
2620
|
+
id: 'call_agent_b',
|
|
2621
|
+
name: 'weather',
|
|
2622
|
+
args: { city: 'SF' },
|
|
2074
2623
|
},
|
|
2075
2624
|
],
|
|
2625
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
2076
2626
|
} as unknown as t.StreamChunk,
|
|
2077
2627
|
},
|
|
2078
2628
|
{ langgraph_node: 'agent_b' },
|
|
2079
2629
|
graph
|
|
2080
2630
|
);
|
|
2081
2631
|
|
|
2632
|
+
expect(
|
|
2633
|
+
graph.eagerEventToolCallChunks.has(chunkStateKey('agent_b', 0))
|
|
2634
|
+
).toBe(false);
|
|
2635
|
+
expect(
|
|
2636
|
+
graph.eagerEventToolCallChunks.get(chunkStateKey('agent_a', 0))?.argsText
|
|
2637
|
+
).toBe('{"city":"NYC"}');
|
|
2638
|
+
|
|
2639
|
+
await handler.handle(
|
|
2640
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2641
|
+
{
|
|
2642
|
+
chunk: {
|
|
2643
|
+
content: '',
|
|
2644
|
+
tool_calls: [
|
|
2645
|
+
{
|
|
2646
|
+
id: 'call_agent_a',
|
|
2647
|
+
name: 'weather',
|
|
2648
|
+
args: { city: 'NYC' },
|
|
2649
|
+
},
|
|
2650
|
+
],
|
|
2651
|
+
response_metadata: finalToolCallResponseMetadata,
|
|
2652
|
+
} as unknown as t.StreamChunk,
|
|
2653
|
+
},
|
|
2654
|
+
{ langgraph_node: 'agent_a' },
|
|
2655
|
+
graph
|
|
2656
|
+
);
|
|
2657
|
+
|
|
2658
|
+
expect(toolExecuteCalls).toHaveLength(2);
|
|
2659
|
+
expect(toolExecuteCalls[0].toolCalls[0]).toMatchObject({
|
|
2660
|
+
id: 'call_agent_b',
|
|
2661
|
+
name: 'weather',
|
|
2662
|
+
args: { city: 'SF' },
|
|
2663
|
+
});
|
|
2664
|
+
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
2665
|
+
id: 'call_agent_a',
|
|
2666
|
+
name: 'weather',
|
|
2667
|
+
args: { city: 'NYC' },
|
|
2668
|
+
});
|
|
2669
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2670
|
+
});
|
|
2671
|
+
|
|
2672
|
+
it('does not prestart when batch-sensitive hooks are configured', async () => {
|
|
2673
|
+
const graph = createGraph({
|
|
2674
|
+
hookRegistry: {} as StandardGraph['hookRegistry'],
|
|
2675
|
+
});
|
|
2676
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
2677
|
+
|
|
2678
|
+
await new ChatModelStreamHandler().handle(
|
|
2679
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2680
|
+
{
|
|
2681
|
+
chunk: {
|
|
2682
|
+
content: '',
|
|
2683
|
+
tool_calls: [
|
|
2684
|
+
{
|
|
2685
|
+
id: 'call_weather',
|
|
2686
|
+
name: 'weather',
|
|
2687
|
+
args: { city: 'NYC' },
|
|
2688
|
+
},
|
|
2689
|
+
],
|
|
2690
|
+
} as unknown as t.StreamChunk,
|
|
2691
|
+
},
|
|
2692
|
+
{ langgraph_node: 'agent' },
|
|
2693
|
+
graph
|
|
2694
|
+
);
|
|
2695
|
+
|
|
2696
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
2697
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
2698
|
+
expect.anything(),
|
|
2699
|
+
expect.anything()
|
|
2700
|
+
);
|
|
2701
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2702
|
+
});
|
|
2703
|
+
|
|
2704
|
+
it('does not buffer streamed chunks when eager execution is disabled', async () => {
|
|
2705
|
+
const graph = createGraph({
|
|
2706
|
+
eagerEventToolExecution: { enabled: false },
|
|
2707
|
+
} as Partial<StandardGraph>);
|
|
2708
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
2709
|
+
|
|
2710
|
+
await new ChatModelStreamHandler().handle(
|
|
2711
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2712
|
+
{
|
|
2713
|
+
chunk: {
|
|
2714
|
+
content: '',
|
|
2715
|
+
tool_call_chunks: [
|
|
2716
|
+
{
|
|
2717
|
+
id: 'call_weather',
|
|
2718
|
+
name: 'weather',
|
|
2719
|
+
args: '{"city":"NYC"}',
|
|
2720
|
+
index: 0,
|
|
2721
|
+
},
|
|
2722
|
+
],
|
|
2723
|
+
} as unknown as t.StreamChunk,
|
|
2724
|
+
},
|
|
2725
|
+
{ langgraph_node: 'agent' },
|
|
2726
|
+
graph
|
|
2727
|
+
);
|
|
2728
|
+
|
|
2729
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
2730
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
2731
|
+
expect.anything(),
|
|
2732
|
+
expect.anything()
|
|
2733
|
+
);
|
|
2734
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2735
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
it('does not prestart local-engine direct coding tools', async () => {
|
|
2739
|
+
const graph = createGraph({
|
|
2740
|
+
toolExecution: {
|
|
2741
|
+
engine: 'local',
|
|
2742
|
+
} as StandardGraph['toolExecution'],
|
|
2743
|
+
getAgentContext: jest.fn(
|
|
2744
|
+
(): Partial<AgentContext> => ({
|
|
2745
|
+
provider: Providers.OPENAI,
|
|
2746
|
+
reasoningKey: 'reasoning_content',
|
|
2747
|
+
toolDefinitions: [{ name: Constants.EXECUTE_CODE }],
|
|
2748
|
+
graphTools: [],
|
|
2749
|
+
agentId: 'agent_1',
|
|
2750
|
+
})
|
|
2751
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2752
|
+
});
|
|
2753
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
2754
|
+
|
|
2755
|
+
await new ChatModelStreamHandler().handle(
|
|
2756
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2757
|
+
{
|
|
2758
|
+
chunk: {
|
|
2759
|
+
content: '',
|
|
2760
|
+
tool_calls: [
|
|
2761
|
+
{
|
|
2762
|
+
id: 'call_code',
|
|
2763
|
+
name: Constants.EXECUTE_CODE,
|
|
2764
|
+
args: { code: 'print(1)' },
|
|
2765
|
+
},
|
|
2766
|
+
],
|
|
2767
|
+
} as unknown as t.StreamChunk,
|
|
2768
|
+
},
|
|
2769
|
+
{ langgraph_node: 'agent' },
|
|
2770
|
+
graph
|
|
2771
|
+
);
|
|
2772
|
+
|
|
2773
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
2774
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
2775
|
+
expect.anything(),
|
|
2776
|
+
expect.anything()
|
|
2777
|
+
);
|
|
2778
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2779
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2780
|
+
});
|
|
2781
|
+
|
|
2782
|
+
it('does not prestart streamed local-engine direct coding tools', async () => {
|
|
2783
|
+
const graph = createGraph({
|
|
2784
|
+
toolExecution: {
|
|
2785
|
+
engine: 'local',
|
|
2786
|
+
} as StandardGraph['toolExecution'],
|
|
2787
|
+
getAgentContext: jest.fn(
|
|
2788
|
+
(): Partial<AgentContext> => ({
|
|
2789
|
+
provider: Providers.OPENAI,
|
|
2790
|
+
reasoningKey: 'reasoning_content',
|
|
2791
|
+
toolDefinitions: [
|
|
2792
|
+
{ name: Constants.EXECUTE_CODE },
|
|
2793
|
+
{ name: 'weather' },
|
|
2794
|
+
],
|
|
2795
|
+
graphTools: [],
|
|
2796
|
+
agentId: 'agent_1',
|
|
2797
|
+
})
|
|
2798
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2799
|
+
});
|
|
2800
|
+
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
2801
|
+
const handler = new ChatModelStreamHandler();
|
|
2802
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2803
|
+
|
|
2804
|
+
await handler.handle(
|
|
2805
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2806
|
+
{
|
|
2807
|
+
chunk: {
|
|
2808
|
+
content: '',
|
|
2809
|
+
tool_call_chunks: [
|
|
2810
|
+
{
|
|
2811
|
+
id: 'call_weather',
|
|
2812
|
+
name: 'weather',
|
|
2813
|
+
args: '{"city":"NYC"}',
|
|
2814
|
+
index: 0,
|
|
2815
|
+
},
|
|
2816
|
+
],
|
|
2817
|
+
} as unknown as t.StreamChunk,
|
|
2818
|
+
},
|
|
2819
|
+
metadata,
|
|
2820
|
+
graph
|
|
2821
|
+
);
|
|
2822
|
+
await handler.handle(
|
|
2823
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
2824
|
+
{
|
|
2825
|
+
chunk: {
|
|
2826
|
+
content: '',
|
|
2827
|
+
tool_call_chunks: [
|
|
2828
|
+
{
|
|
2829
|
+
id: 'call_code',
|
|
2830
|
+
name: Constants.EXECUTE_CODE,
|
|
2831
|
+
args: '{"code":"print(1)"}',
|
|
2832
|
+
index: 1,
|
|
2833
|
+
},
|
|
2834
|
+
],
|
|
2835
|
+
} as unknown as t.StreamChunk,
|
|
2836
|
+
},
|
|
2837
|
+
metadata,
|
|
2838
|
+
graph
|
|
2839
|
+
);
|
|
2840
|
+
|
|
2841
|
+
expect(dispatchSpy).not.toHaveBeenCalledWith(
|
|
2842
|
+
GraphEvents.ON_TOOL_EXECUTE,
|
|
2843
|
+
expect.anything(),
|
|
2844
|
+
expect.anything()
|
|
2845
|
+
);
|
|
2846
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2847
|
+
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2848
|
+
});
|
|
2849
|
+
|
|
2850
|
+
it('prestarts streamed remote bash tools when the next Anthropic tool call begins', async () => {
|
|
2851
|
+
const graph = createGraph({
|
|
2852
|
+
getAgentContext: jest.fn(
|
|
2853
|
+
(): Partial<AgentContext> => ({
|
|
2854
|
+
provider: Providers.ANTHROPIC,
|
|
2855
|
+
reasoningKey: 'reasoning',
|
|
2856
|
+
toolDefinitions: [
|
|
2857
|
+
{ name: Constants.BASH_TOOL },
|
|
2858
|
+
{ name: Constants.READ_FILE },
|
|
2859
|
+
],
|
|
2860
|
+
graphTools: [],
|
|
2861
|
+
agentId: 'agent_1',
|
|
2862
|
+
})
|
|
2863
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2864
|
+
});
|
|
2865
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2866
|
+
jest
|
|
2867
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2868
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2869
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2870
|
+
return;
|
|
2871
|
+
}
|
|
2872
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2873
|
+
toolExecuteCalls.push(batch);
|
|
2874
|
+
batch.resolve(
|
|
2875
|
+
batch.toolCalls.map((call) => ({
|
|
2876
|
+
toolCallId: call.id,
|
|
2877
|
+
status: 'success',
|
|
2878
|
+
content: `ok ${call.name}`,
|
|
2879
|
+
}))
|
|
2880
|
+
);
|
|
2881
|
+
});
|
|
2882
|
+
|
|
2883
|
+
const handler = new ChatModelStreamHandler();
|
|
2884
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2885
|
+
|
|
2082
2886
|
await handler.handle(
|
|
2083
2887
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2084
2888
|
{
|
|
@@ -2086,13 +2890,15 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2086
2890
|
content: '',
|
|
2087
2891
|
tool_call_chunks: [
|
|
2088
2892
|
{
|
|
2089
|
-
|
|
2090
|
-
|
|
2893
|
+
id: 'toolu_env',
|
|
2894
|
+
name: Constants.BASH_TOOL,
|
|
2895
|
+
args: '{"command":"echo env"}',
|
|
2896
|
+
index: 2,
|
|
2091
2897
|
},
|
|
2092
2898
|
],
|
|
2093
2899
|
} as unknown as t.StreamChunk,
|
|
2094
2900
|
},
|
|
2095
|
-
|
|
2901
|
+
metadata,
|
|
2096
2902
|
graph
|
|
2097
2903
|
);
|
|
2098
2904
|
|
|
@@ -2105,135 +2911,256 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2105
2911
|
content: '',
|
|
2106
2912
|
tool_call_chunks: [
|
|
2107
2913
|
{
|
|
2108
|
-
|
|
2109
|
-
|
|
2914
|
+
id: 'toolu_net',
|
|
2915
|
+
name: Constants.BASH_TOOL,
|
|
2916
|
+
args: '{"command":"echo net"}',
|
|
2917
|
+
index: 3,
|
|
2110
2918
|
},
|
|
2111
2919
|
],
|
|
2112
2920
|
} as unknown as t.StreamChunk,
|
|
2113
2921
|
},
|
|
2114
|
-
|
|
2922
|
+
metadata,
|
|
2115
2923
|
graph
|
|
2116
2924
|
);
|
|
2117
2925
|
|
|
2118
|
-
expect(toolExecuteCalls).toHaveLength(
|
|
2119
|
-
expect(
|
|
2120
|
-
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2926
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
2927
|
+
expect(toolExecuteCalls[0].toolCalls).toEqual([
|
|
2928
|
+
expect.objectContaining({
|
|
2929
|
+
id: 'toolu_env',
|
|
2930
|
+
name: Constants.BASH_TOOL,
|
|
2931
|
+
args: { command: 'echo env' },
|
|
2932
|
+
stepId: expect.stringMatching(/^step_/),
|
|
2933
|
+
turn: 0,
|
|
2934
|
+
}),
|
|
2935
|
+
]);
|
|
2936
|
+
expect(graph.eagerEventToolExecutions.has('toolu_env')).toBe(true);
|
|
2937
|
+
expect(graph.eagerEventToolExecutions.has('toolu_net')).toBe(false);
|
|
2938
|
+
});
|
|
2939
|
+
|
|
2940
|
+
it('does not prestart streamed remote tools when graph tools may appear later', async () => {
|
|
2941
|
+
const graph = createGraph({
|
|
2942
|
+
getAgentContext: jest.fn(
|
|
2943
|
+
(): Partial<AgentContext> => ({
|
|
2944
|
+
provider: Providers.ANTHROPIC,
|
|
2945
|
+
reasoningKey: 'reasoning',
|
|
2946
|
+
toolDefinitions: [
|
|
2947
|
+
{ name: Constants.BASH_TOOL },
|
|
2948
|
+
{ name: Constants.READ_FILE },
|
|
2949
|
+
],
|
|
2950
|
+
graphTools: [
|
|
2951
|
+
{ name: 'transfer_to_researcher' } as unknown as t.GenericTool,
|
|
2952
|
+
],
|
|
2953
|
+
agentId: 'agent_1',
|
|
2954
|
+
})
|
|
2955
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
2956
|
+
});
|
|
2957
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2958
|
+
jest
|
|
2959
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
2960
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2961
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2962
|
+
return;
|
|
2963
|
+
}
|
|
2964
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
2965
|
+
toolExecuteCalls.push(batch);
|
|
2966
|
+
batch.resolve(
|
|
2967
|
+
batch.toolCalls.map((call) => ({
|
|
2968
|
+
toolCallId: call.id,
|
|
2969
|
+
status: 'success',
|
|
2970
|
+
content: `ok ${call.name}`,
|
|
2971
|
+
}))
|
|
2972
|
+
);
|
|
2973
|
+
});
|
|
2974
|
+
|
|
2975
|
+
const handler = new ChatModelStreamHandler();
|
|
2976
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2125
2977
|
|
|
2126
2978
|
await handler.handle(
|
|
2127
2979
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2128
2980
|
{
|
|
2129
2981
|
chunk: {
|
|
2130
2982
|
content: '',
|
|
2131
|
-
|
|
2983
|
+
tool_call_chunks: [
|
|
2132
2984
|
{
|
|
2133
|
-
id: '
|
|
2134
|
-
name:
|
|
2135
|
-
args: {
|
|
2985
|
+
id: 'toolu_env',
|
|
2986
|
+
name: Constants.BASH_TOOL,
|
|
2987
|
+
args: '{"command":"echo env"}',
|
|
2988
|
+
index: 2,
|
|
2136
2989
|
},
|
|
2137
2990
|
],
|
|
2138
|
-
response_metadata: finalToolCallResponseMetadata,
|
|
2139
2991
|
} as unknown as t.StreamChunk,
|
|
2140
2992
|
},
|
|
2141
|
-
|
|
2993
|
+
metadata,
|
|
2142
2994
|
graph
|
|
2143
2995
|
);
|
|
2144
2996
|
|
|
2145
|
-
expect(graph.eagerEventToolCallChunks.has(chunkStateKey('agent_b', 0))).toBe(
|
|
2146
|
-
false
|
|
2147
|
-
);
|
|
2148
|
-
expect(
|
|
2149
|
-
graph.eagerEventToolCallChunks.get(chunkStateKey('agent_a', 0))?.argsText
|
|
2150
|
-
).toBe('{"city":"NYC"}');
|
|
2151
|
-
|
|
2152
2997
|
await handler.handle(
|
|
2153
2998
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2154
2999
|
{
|
|
2155
3000
|
chunk: {
|
|
2156
3001
|
content: '',
|
|
2157
|
-
|
|
3002
|
+
tool_call_chunks: [
|
|
2158
3003
|
{
|
|
2159
|
-
id: '
|
|
2160
|
-
name:
|
|
2161
|
-
args: {
|
|
3004
|
+
id: 'toolu_net',
|
|
3005
|
+
name: Constants.BASH_TOOL,
|
|
3006
|
+
args: '{"command":"echo net"}',
|
|
3007
|
+
index: 3,
|
|
2162
3008
|
},
|
|
2163
3009
|
],
|
|
2164
|
-
response_metadata: finalToolCallResponseMetadata,
|
|
2165
3010
|
} as unknown as t.StreamChunk,
|
|
2166
3011
|
},
|
|
2167
|
-
|
|
3012
|
+
metadata,
|
|
2168
3013
|
graph
|
|
2169
3014
|
);
|
|
2170
3015
|
|
|
2171
|
-
expect(toolExecuteCalls).toHaveLength(
|
|
2172
|
-
expect(
|
|
2173
|
-
id: 'call_agent_b',
|
|
2174
|
-
name: 'weather',
|
|
2175
|
-
args: { city: 'SF' },
|
|
2176
|
-
});
|
|
2177
|
-
expect(toolExecuteCalls[1].toolCalls[0]).toMatchObject({
|
|
2178
|
-
id: 'call_agent_a',
|
|
2179
|
-
name: 'weather',
|
|
2180
|
-
args: { city: 'NYC' },
|
|
2181
|
-
});
|
|
3016
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
3017
|
+
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2182
3018
|
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2183
3019
|
});
|
|
2184
3020
|
|
|
2185
|
-
it('
|
|
2186
|
-
const graph = createGraph({
|
|
2187
|
-
|
|
3021
|
+
it('prestarts streamed remote bash tools when code output references are enabled', async () => {
|
|
3022
|
+
const graph = createGraph({
|
|
3023
|
+
toolOutputReferences: { enabled: true },
|
|
3024
|
+
getAgentContext: jest.fn(
|
|
3025
|
+
(): Partial<AgentContext> => ({
|
|
3026
|
+
provider: Providers.ANTHROPIC,
|
|
3027
|
+
reasoningKey: 'reasoning',
|
|
3028
|
+
toolDefinitions: [
|
|
3029
|
+
{ name: Constants.BASH_TOOL },
|
|
3030
|
+
{ name: Constants.READ_FILE },
|
|
3031
|
+
],
|
|
3032
|
+
graphTools: [],
|
|
3033
|
+
agentId: 'agent_1',
|
|
3034
|
+
})
|
|
3035
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
3036
|
+
});
|
|
3037
|
+
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
3038
|
+
jest
|
|
3039
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
3040
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
3041
|
+
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
3042
|
+
return;
|
|
3043
|
+
}
|
|
3044
|
+
const batch = data as t.ToolExecuteBatchRequest;
|
|
3045
|
+
toolExecuteCalls.push(batch);
|
|
3046
|
+
batch.resolve(
|
|
3047
|
+
batch.toolCalls.map((call) => ({
|
|
3048
|
+
toolCallId: call.id,
|
|
3049
|
+
status: 'success',
|
|
3050
|
+
content: `ok ${call.name}`,
|
|
3051
|
+
}))
|
|
3052
|
+
);
|
|
3053
|
+
});
|
|
2188
3054
|
|
|
2189
|
-
|
|
3055
|
+
const handler = new ChatModelStreamHandler();
|
|
3056
|
+
const metadata = { langgraph_node: 'agent' };
|
|
3057
|
+
|
|
3058
|
+
for (const args of ['{"command":"echo ', 'env && ', 'pwd"}']) {
|
|
3059
|
+
await handler.handle(
|
|
3060
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
3061
|
+
{
|
|
3062
|
+
chunk: {
|
|
3063
|
+
content: '',
|
|
3064
|
+
tool_call_chunks: [
|
|
3065
|
+
{
|
|
3066
|
+
id: 'toolu_env',
|
|
3067
|
+
name: Constants.BASH_TOOL,
|
|
3068
|
+
args,
|
|
3069
|
+
index: 2,
|
|
3070
|
+
},
|
|
3071
|
+
],
|
|
3072
|
+
} as unknown as t.StreamChunk,
|
|
3073
|
+
},
|
|
3074
|
+
metadata,
|
|
3075
|
+
graph
|
|
3076
|
+
);
|
|
3077
|
+
}
|
|
3078
|
+
|
|
3079
|
+
expect(toolExecuteCalls).toHaveLength(0);
|
|
3080
|
+
|
|
3081
|
+
await handler.handle(
|
|
2190
3082
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2191
3083
|
{
|
|
2192
3084
|
chunk: {
|
|
2193
3085
|
content: '',
|
|
2194
|
-
|
|
3086
|
+
tool_call_chunks: [
|
|
2195
3087
|
{
|
|
2196
|
-
id: '
|
|
2197
|
-
name:
|
|
2198
|
-
args: {
|
|
3088
|
+
id: 'toolu_net',
|
|
3089
|
+
name: Constants.BASH_TOOL,
|
|
3090
|
+
args: '{"command":"echo net"}',
|
|
3091
|
+
index: 3,
|
|
2199
3092
|
},
|
|
2200
3093
|
],
|
|
2201
3094
|
} as unknown as t.StreamChunk,
|
|
2202
3095
|
},
|
|
2203
|
-
|
|
3096
|
+
metadata,
|
|
2204
3097
|
graph
|
|
2205
3098
|
);
|
|
2206
3099
|
|
|
2207
|
-
expect(
|
|
2208
|
-
|
|
2209
|
-
expect.
|
|
2210
|
-
|
|
2211
|
-
|
|
2212
|
-
|
|
3100
|
+
expect(toolExecuteCalls).toHaveLength(1);
|
|
3101
|
+
expect(toolExecuteCalls[0].toolCalls).toEqual([
|
|
3102
|
+
expect.objectContaining({
|
|
3103
|
+
id: 'toolu_env',
|
|
3104
|
+
name: Constants.BASH_TOOL,
|
|
3105
|
+
args: { command: 'echo env && pwd' },
|
|
3106
|
+
}),
|
|
3107
|
+
]);
|
|
2213
3108
|
});
|
|
2214
3109
|
|
|
2215
|
-
it('does not
|
|
3110
|
+
it('does not prestart streamed code tools whose args contain output references', async () => {
|
|
2216
3111
|
const graph = createGraph({
|
|
2217
|
-
|
|
2218
|
-
|
|
3112
|
+
toolOutputReferences: { enabled: true },
|
|
3113
|
+
getAgentContext: jest.fn(
|
|
3114
|
+
(): Partial<AgentContext> => ({
|
|
3115
|
+
provider: Providers.ANTHROPIC,
|
|
3116
|
+
reasoningKey: 'reasoning',
|
|
3117
|
+
toolDefinitions: [
|
|
3118
|
+
{ name: Constants.BASH_TOOL },
|
|
3119
|
+
{ name: Constants.READ_FILE },
|
|
3120
|
+
],
|
|
3121
|
+
graphTools: [],
|
|
3122
|
+
agentId: 'agent_1',
|
|
3123
|
+
})
|
|
3124
|
+
) as unknown as StandardGraph['getAgentContext'],
|
|
3125
|
+
});
|
|
2219
3126
|
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
3127
|
+
const handler = new ChatModelStreamHandler();
|
|
3128
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2220
3129
|
|
|
2221
|
-
await
|
|
3130
|
+
await handler.handle(
|
|
2222
3131
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2223
3132
|
{
|
|
2224
3133
|
chunk: {
|
|
2225
3134
|
content: '',
|
|
2226
3135
|
tool_call_chunks: [
|
|
2227
3136
|
{
|
|
2228
|
-
id: '
|
|
2229
|
-
name:
|
|
2230
|
-
args: '{"
|
|
2231
|
-
index:
|
|
3137
|
+
id: 'toolu_ref',
|
|
3138
|
+
name: Constants.BASH_TOOL,
|
|
3139
|
+
args: '{"command":"cat <<EOF\\n{{tool0turn0}}\\nEOF"}',
|
|
3140
|
+
index: 2,
|
|
2232
3141
|
},
|
|
2233
3142
|
],
|
|
2234
3143
|
} as unknown as t.StreamChunk,
|
|
2235
3144
|
},
|
|
2236
|
-
|
|
3145
|
+
metadata,
|
|
3146
|
+
graph
|
|
3147
|
+
);
|
|
3148
|
+
await handler.handle(
|
|
3149
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
3150
|
+
{
|
|
3151
|
+
chunk: {
|
|
3152
|
+
content: '',
|
|
3153
|
+
tool_call_chunks: [
|
|
3154
|
+
{
|
|
3155
|
+
id: 'toolu_next',
|
|
3156
|
+
name: Constants.BASH_TOOL,
|
|
3157
|
+
args: '{"command":"echo next"}',
|
|
3158
|
+
index: 3,
|
|
3159
|
+
},
|
|
3160
|
+
],
|
|
3161
|
+
} as unknown as t.StreamChunk,
|
|
3162
|
+
},
|
|
3163
|
+
metadata,
|
|
2237
3164
|
graph
|
|
2238
3165
|
);
|
|
2239
3166
|
|
|
@@ -2242,42 +3169,63 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2242
3169
|
expect.anything(),
|
|
2243
3170
|
expect.anything()
|
|
2244
3171
|
);
|
|
2245
|
-
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2246
|
-
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2247
3172
|
});
|
|
2248
3173
|
|
|
2249
|
-
it('does not prestart
|
|
3174
|
+
it('does not prestart streamed tools when the next Anthropic tool call is a graph tool', async () => {
|
|
3175
|
+
const handoffToolName = `${Constants.LC_TRANSFER_TO_}researcher`;
|
|
2250
3176
|
const graph = createGraph({
|
|
2251
|
-
toolExecution: {
|
|
2252
|
-
engine: 'local',
|
|
2253
|
-
} as StandardGraph['toolExecution'],
|
|
2254
3177
|
getAgentContext: jest.fn(
|
|
2255
3178
|
(): Partial<AgentContext> => ({
|
|
2256
|
-
provider: Providers.
|
|
2257
|
-
reasoningKey: '
|
|
2258
|
-
toolDefinitions: [
|
|
2259
|
-
|
|
3179
|
+
provider: Providers.ANTHROPIC,
|
|
3180
|
+
reasoningKey: 'reasoning',
|
|
3181
|
+
toolDefinitions: [
|
|
3182
|
+
{ name: Constants.BASH_TOOL },
|
|
3183
|
+
{ name: handoffToolName },
|
|
3184
|
+
],
|
|
3185
|
+
graphTools: [{ name: handoffToolName } as unknown as t.GenericTool],
|
|
2260
3186
|
agentId: 'agent_1',
|
|
2261
3187
|
})
|
|
2262
3188
|
) as unknown as StandardGraph['getAgentContext'],
|
|
2263
3189
|
});
|
|
2264
3190
|
const dispatchSpy = jest.spyOn(events, 'safeDispatchCustomEvent');
|
|
3191
|
+
const handler = new ChatModelStreamHandler();
|
|
3192
|
+
const metadata = { langgraph_node: 'agent' };
|
|
2265
3193
|
|
|
2266
|
-
await
|
|
3194
|
+
await handler.handle(
|
|
2267
3195
|
GraphEvents.CHAT_MODEL_STREAM,
|
|
2268
3196
|
{
|
|
2269
3197
|
chunk: {
|
|
2270
3198
|
content: '',
|
|
2271
|
-
|
|
3199
|
+
tool_call_chunks: [
|
|
2272
3200
|
{
|
|
2273
|
-
id: '
|
|
2274
|
-
name: Constants.
|
|
2275
|
-
args: {
|
|
3201
|
+
id: 'toolu_env',
|
|
3202
|
+
name: Constants.BASH_TOOL,
|
|
3203
|
+
args: '{"command":"echo env"}',
|
|
3204
|
+
index: 2,
|
|
2276
3205
|
},
|
|
2277
3206
|
],
|
|
2278
3207
|
} as unknown as t.StreamChunk,
|
|
2279
3208
|
},
|
|
2280
|
-
|
|
3209
|
+
metadata,
|
|
3210
|
+
graph
|
|
3211
|
+
);
|
|
3212
|
+
|
|
3213
|
+
await handler.handle(
|
|
3214
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
3215
|
+
{
|
|
3216
|
+
chunk: {
|
|
3217
|
+
content: '',
|
|
3218
|
+
tool_call_chunks: [
|
|
3219
|
+
{
|
|
3220
|
+
id: 'toolu_handoff',
|
|
3221
|
+
name: handoffToolName,
|
|
3222
|
+
args: '{"message":"check this"}',
|
|
3223
|
+
index: 3,
|
|
3224
|
+
},
|
|
3225
|
+
],
|
|
3226
|
+
} as unknown as t.StreamChunk,
|
|
3227
|
+
},
|
|
3228
|
+
metadata,
|
|
2281
3229
|
graph
|
|
2282
3230
|
);
|
|
2283
3231
|
|
|
@@ -2287,20 +3235,20 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2287
3235
|
expect.anything()
|
|
2288
3236
|
);
|
|
2289
3237
|
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2290
|
-
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2291
3238
|
});
|
|
2292
3239
|
|
|
2293
|
-
it('does not prestart streamed
|
|
3240
|
+
it('does not prestart streamed tools after a graph tool appeared earlier in the same step', async () => {
|
|
3241
|
+
const handoffToolName = `${Constants.LC_TRANSFER_TO_}researcher`;
|
|
2294
3242
|
const graph = createGraph({
|
|
2295
|
-
toolExecution: {
|
|
2296
|
-
engine: 'local',
|
|
2297
|
-
} as StandardGraph['toolExecution'],
|
|
2298
3243
|
getAgentContext: jest.fn(
|
|
2299
3244
|
(): Partial<AgentContext> => ({
|
|
2300
|
-
provider: Providers.
|
|
2301
|
-
reasoningKey: '
|
|
2302
|
-
toolDefinitions: [
|
|
2303
|
-
|
|
3245
|
+
provider: Providers.ANTHROPIC,
|
|
3246
|
+
reasoningKey: 'reasoning',
|
|
3247
|
+
toolDefinitions: [
|
|
3248
|
+
{ name: Constants.BASH_TOOL },
|
|
3249
|
+
{ name: handoffToolName },
|
|
3250
|
+
],
|
|
3251
|
+
graphTools: [{ name: handoffToolName } as unknown as t.GenericTool],
|
|
2304
3252
|
agentId: 'agent_1',
|
|
2305
3253
|
})
|
|
2306
3254
|
) as unknown as StandardGraph['getAgentContext'],
|
|
@@ -2316,10 +3264,10 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2316
3264
|
content: '',
|
|
2317
3265
|
tool_call_chunks: [
|
|
2318
3266
|
{
|
|
2319
|
-
id: '
|
|
2320
|
-
name:
|
|
2321
|
-
args: '{"
|
|
2322
|
-
index:
|
|
3267
|
+
id: 'toolu_env',
|
|
3268
|
+
name: Constants.BASH_TOOL,
|
|
3269
|
+
args: '{"command":"echo env"}',
|
|
3270
|
+
index: 2,
|
|
2323
3271
|
},
|
|
2324
3272
|
],
|
|
2325
3273
|
} as unknown as t.StreamChunk,
|
|
@@ -2334,10 +3282,28 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2334
3282
|
content: '',
|
|
2335
3283
|
tool_call_chunks: [
|
|
2336
3284
|
{
|
|
2337
|
-
id: '
|
|
2338
|
-
name:
|
|
2339
|
-
args: '{"
|
|
2340
|
-
index:
|
|
3285
|
+
id: 'toolu_handoff',
|
|
3286
|
+
name: handoffToolName,
|
|
3287
|
+
args: '{"message":"partial',
|
|
3288
|
+
index: 3,
|
|
3289
|
+
},
|
|
3290
|
+
],
|
|
3291
|
+
} as unknown as t.StreamChunk,
|
|
3292
|
+
},
|
|
3293
|
+
metadata,
|
|
3294
|
+
graph
|
|
3295
|
+
);
|
|
3296
|
+
await handler.handle(
|
|
3297
|
+
GraphEvents.CHAT_MODEL_STREAM,
|
|
3298
|
+
{
|
|
3299
|
+
chunk: {
|
|
3300
|
+
content: '',
|
|
3301
|
+
tool_call_chunks: [
|
|
3302
|
+
{
|
|
3303
|
+
id: 'toolu_next',
|
|
3304
|
+
name: Constants.BASH_TOOL,
|
|
3305
|
+
args: '{"command":"echo next"}',
|
|
3306
|
+
index: 4,
|
|
2341
3307
|
},
|
|
2342
3308
|
],
|
|
2343
3309
|
} as unknown as t.StreamChunk,
|
|
@@ -2352,7 +3318,6 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2352
3318
|
expect.anything()
|
|
2353
3319
|
);
|
|
2354
3320
|
expect(graph.eagerEventToolExecutions.size).toBe(0);
|
|
2355
|
-
expect(graph.eagerEventToolCallChunks.size).toBe(0);
|
|
2356
3321
|
});
|
|
2357
3322
|
|
|
2358
3323
|
it('does not prestart event tools in a mixed direct-tool batch', async () => {
|
|
@@ -2411,8 +3376,9 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2411
3376
|
const graph = createGraph();
|
|
2412
3377
|
graph.eagerEventToolUsageCount.set('weather', 1);
|
|
2413
3378
|
const toolExecuteCalls: t.ToolExecuteBatchRequest[] = [];
|
|
2414
|
-
jest
|
|
2415
|
-
|
|
3379
|
+
jest
|
|
3380
|
+
.spyOn(events, 'safeDispatchCustomEvent')
|
|
3381
|
+
.mockImplementation(async (event, data): Promise<void> => {
|
|
2416
3382
|
if (event !== GraphEvents.ON_TOOL_EXECUTE) {
|
|
2417
3383
|
return;
|
|
2418
3384
|
}
|
|
@@ -2425,8 +3391,7 @@ describe('ChatModelStreamHandler eager event tool execution', () => {
|
|
|
2425
3391
|
content: 'sunny',
|
|
2426
3392
|
},
|
|
2427
3393
|
]);
|
|
2428
|
-
}
|
|
2429
|
-
);
|
|
3394
|
+
});
|
|
2430
3395
|
|
|
2431
3396
|
await new ChatModelStreamHandler().handle(
|
|
2432
3397
|
GraphEvents.CHAT_MODEL_STREAM,
|