@@ -870,6 +870,28 @@ describe("SSE event builders", () => {
870870 expect ( JSON . parse ( delta . arguments as string ) ) . toEqual ( { city : "NYC" } ) ;
871871 } ) ;
872872
873+ it ( "assigns sequential step indices for multiple tool calls (SDK 2.x)" , ( ) => {
874+ const events = buildInteractionsToolCallSSEEvents (
875+ [
876+ { name : "get_weather" , arguments : '{"city":"NYC"}' , id : "call_1" } ,
877+ { name : "get_time" , arguments : '{"tz":"UTC"}' , id : "call_2" } ,
878+ ] ,
879+ "aimock-int-0" ,
880+ logger ,
881+ ) ;
882+ const stepStarts = events . filter ( ( e ) => e . event_type === "step.start" ) ;
883+ expect ( stepStarts . map ( ( e ) => e . index ) ) . toEqual ( [ 0 , 1 ] ) ;
884+ expect ( ( stepStarts [ 0 ] . step as Record < string , unknown > ) . name ) . toBe ( "get_weather" ) ;
885+ expect ( ( stepStarts [ 1 ] . step as Record < string , unknown > ) . name ) . toBe ( "get_time" ) ;
886+ // Each call's arguments_delta is keyed to its own index.
887+ const argDeltas = events . filter (
888+ ( e ) =>
889+ e . event_type === "step.delta" &&
890+ ( e . delta as Record < string , unknown > ) . type === "arguments_delta" ,
891+ ) ;
892+ expect ( argDeltas . map ( ( e ) => e . index ) ) . toEqual ( [ 0 , 1 ] ) ;
893+ } ) ;
894+
873895 it ( "builds content+tools SSE with correct indices (SDK 2.x)" , ( ) => {
874896 const events = buildInteractionsContentWithToolCallsSSEEvents (
875897 "Text" ,
@@ -1349,6 +1371,58 @@ describe("collapseGeminiInteractionsSSE", () => {
13491371 expect ( result . toolCalls ! [ 0 ] . id ) . toBe ( "call_1" ) ;
13501372 } ) ;
13511373
1374+ it ( "collapses multiple interleaved tool calls in step-index order (SDK 2.x)" , ( ) => {
1375+ const sse = [
1376+ 'data: {"event_type":"step.start","index":1,"step":{"type":"function_call","id":"call_a","name":"get_weather","arguments":{}},"event_id":"evt_1"}' ,
1377+ 'data: {"event_type":"step.start","index":2,"step":{"type":"function_call","id":"call_b","name":"get_time","arguments":{}},"event_id":"evt_2"}' ,
1378+ // Interleaved argument fragments for both calls.
1379+ 'data: {"event_type":"step.delta","index":2,"delta":{"type":"arguments_delta","arguments":"{\\"tz\\":"},"event_id":"evt_3"}' ,
1380+ 'data: {"event_type":"step.delta","index":1,"delta":{"type":"arguments_delta","arguments":"{\\"city\\":\\"NYC\\"}"},"event_id":"evt_4"}' ,
1381+ 'data: {"event_type":"step.delta","index":2,"delta":{"type":"arguments_delta","arguments":"\\"UTC\\"}"},"event_id":"evt_5"}' ,
1382+ 'data: {"event_type":"step.stop","index":1,"event_id":"evt_6"}' ,
1383+ 'data: {"event_type":"step.stop","index":2,"event_id":"evt_7"}' ,
1384+ ] . join ( "\n\n" ) ;
1385+ const result = collapseGeminiInteractionsSSE ( sse ) ;
1386+ expect ( result . toolCalls ) . toHaveLength ( 2 ) ;
1387+ // Sorted by step index regardless of step.stop / delta arrival order.
1388+ expect ( result . toolCalls ! [ 0 ] ) . toEqual ( {
1389+ name : "get_weather" ,
1390+ arguments : '{"city":"NYC"}' ,
1391+ id : "call_a" ,
1392+ } ) ;
1393+ expect ( result . toolCalls ! [ 1 ] ) . toEqual ( {
1394+ name : "get_time" ,
1395+ arguments : '{"tz":"UTC"}' ,
1396+ id : "call_b" ,
1397+ } ) ;
1398+ } ) ;
1399+
1400+ it ( "flags assembled arguments_delta that is not valid JSON by step.stop" , ( ) => {
1401+ const sse = [
1402+ 'data: {"event_type":"step.start","index":0,"step":{"type":"function_call","id":"call_1","name":"fn","arguments":{}},"event_id":"evt_1"}' ,
1403+ // Truncated/interrupted stream — fragment never closes into valid JSON.
1404+ 'data: {"event_type":"step.delta","index":0,"delta":{"type":"arguments_delta","arguments":"{\\"city\\":\\"NY"},"event_id":"evt_2"}' ,
1405+ 'data: {"event_type":"step.stop","index":0,"event_id":"evt_3"}' ,
1406+ ] . join ( "\n\n" ) ;
1407+ const result = collapseGeminiInteractionsSSE ( sse ) ;
1408+ // The (corrupt) call is still surfaced, but flagged so the recorder warns.
1409+ expect ( result . toolCalls ) . toHaveLength ( 1 ) ;
1410+ expect ( result . toolCalls ! [ 0 ] . arguments ) . toBe ( '{"city":"NY' ) ;
1411+ expect ( result . droppedChunks ) . toBe ( 1 ) ;
1412+ expect ( result . firstDroppedSample ) . toMatch ( / n o t v a l i d J S O N / ) ;
1413+ } ) ;
1414+
1415+ it ( "preserves identity of a function_call step.start that arrives without an index" , ( ) => {
1416+ const sse = [
1417+ 'data: {"event_type":"step.start","step":{"type":"function_call","id":"call_1","name":"fn","arguments":{"x":1}},"event_id":"evt_1"}' ,
1418+ 'data: {"event_type":"step.stop","event_id":"evt_2"}' ,
1419+ ] . join ( "\n\n" ) ;
1420+ const result = collapseGeminiInteractionsSSE ( sse ) ;
1421+ expect ( result . toolCalls ) . toHaveLength ( 1 ) ;
1422+ expect ( result . toolCalls ! [ 0 ] . name ) . toBe ( "fn" ) ;
1423+ expect ( result . toolCalls ! [ 0 ] . arguments ) . toBe ( '{"x":1}' ) ;
1424+ } ) ;
1425+
13521426 it ( "uses step.start arguments object when no arguments_delta streams (SDK 2.x)" , ( ) => {
13531427 const sse = [
13541428 'data: {"event_type":"step.start","index":0,"step":{"type":"function_call","id":"call_1","name":"fn","arguments":{"x":1}},"event_id":"evt_1"}' ,
0 commit comments