@@ -722,6 +722,225 @@ describe("wrapStreamFnTrimToolCallNames", () => {
722722 ] ) ;
723723 } ) ;
724724
725+ it ( "counts the final unknown-tool retry when streamed messages omit the tool name" , async ( ) => {
726+ const baseFn = vi . fn ( ( ) =>
727+ createFakeStream ( {
728+ events : [
729+ {
730+ type : "toolcall_delta" ,
731+ message : { role : "assistant" , content : [ { type : "toolCall" , name : "" } ] } ,
732+ } ,
733+ ] ,
734+ resultMessage : {
735+ role : "assistant" ,
736+ content : [ { type : "toolCall" , name : " exec " , arguments : { command : "echo retry" } } ] ,
737+ } ,
738+ } ) ,
739+ ) ;
740+ const wrappedFn = wrapStreamFnTrimToolCallNames ( baseFn as never , new Set ( [ "read" ] ) , {
741+ unknownToolThreshold : 1 ,
742+ } ) ;
743+
744+ const firstStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
745+ await firstStream . result ( ) ;
746+
747+ const secondStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
748+ for await ( const _item of secondStream ) {
749+ // drain
750+ }
751+ const secondResult = ( await secondStream . result ( ) ) as {
752+ role : string ;
753+ content : Array < { type : string ; text ?: string ; name ?: string } > ;
754+ } ;
755+
756+ expect ( secondResult . role ) . toBe ( "assistant" ) ;
757+ expect ( secondResult . content ) . toEqual ( [
758+ expect . objectContaining ( {
759+ type : "text" ,
760+ text : expect . stringContaining ( '"exec"' ) ,
761+ } ) ,
762+ ] ) ;
763+ } ) ;
764+
765+ it ( "resets a provisional streamed unknown-tool retry when later chunks resolve to an allowed tool" , async ( ) => {
766+ const baseFn = vi
767+ . fn ( )
768+ . mockImplementationOnce ( ( ) =>
769+ createFakeStream ( {
770+ events : [
771+ {
772+ type : "toolcall_delta" ,
773+ message : { role : "assistant" , content : [ { type : "toolCall" , name : " ex " } ] } ,
774+ } ,
775+ {
776+ type : "toolcall_delta" ,
777+ message : { role : "assistant" , content : [ { type : "toolCall" , name : " exec " } ] } ,
778+ } ,
779+ ] ,
780+ resultMessage : {
781+ role : "assistant" ,
782+ content : [ { type : "toolCall" , name : " exec " , arguments : { command : "echo ok" } } ] ,
783+ } ,
784+ } ) ,
785+ )
786+ . mockImplementationOnce ( ( ) =>
787+ createFakeStream ( {
788+ events : [ ] ,
789+ resultMessage : {
790+ role : "assistant" ,
791+ content : [ { type : "toolCall" , name : " ex " , arguments : { command : "echo retry" } } ] ,
792+ } ,
793+ } ) ,
794+ ) ;
795+ const wrappedFn = wrapStreamFnTrimToolCallNames ( baseFn as never , new Set ( [ "exec" ] ) , {
796+ unknownToolThreshold : 1 ,
797+ } ) ;
798+
799+ const firstStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
800+ for await ( const _item of firstStream ) {
801+ // drain
802+ }
803+ await firstStream . result ( ) ;
804+
805+ const secondStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
806+ const secondResult = ( await secondStream . result ( ) ) as {
807+ role : string ;
808+ content : Array < { type : string ; text ?: string ; name ?: string } > ;
809+ } ;
810+
811+ expect ( secondResult . role ) . toBe ( "assistant" ) ;
812+ expect ( secondResult . content ) . toEqual ( [
813+ expect . objectContaining ( {
814+ type : "toolCall" ,
815+ name : "ex" ,
816+ } ) ,
817+ ] ) ;
818+ } ) ;
819+
820+ it ( "keeps processing later streamed messages after one streamed unknown-tool retry was counted" , async ( ) => {
821+ const baseFn = vi
822+ . fn ( )
823+ . mockImplementationOnce ( ( ) =>
824+ createFakeStream ( {
825+ events : [
826+ {
827+ type : "toolcall_delta" ,
828+ message : { role : "assistant" , content : [ { type : "toolCall" , name : " re " } ] } ,
829+ } ,
830+ {
831+ type : "toolcall_delta" ,
832+ message : { role : "assistant" , content : [ { type : "toolCall" , name : " read " } ] } ,
833+ } ,
834+ ] ,
835+ resultMessage : {
836+ role : "assistant" ,
837+ content : [ { type : "text" , text : "resolved to allowed tool" } ] ,
838+ } ,
839+ } ) ,
840+ )
841+ . mockImplementationOnce ( ( ) =>
842+ createFakeStream ( {
843+ events : [ ] ,
844+ resultMessage : {
845+ role : "assistant" ,
846+ content : [ { type : "toolCall" , name : " re " , arguments : { command : "echo retry" } } ] ,
847+ } ,
848+ } ) ,
849+ ) ;
850+ const wrappedFn = wrapStreamFnTrimToolCallNames ( baseFn as never , new Set ( [ "read" ] ) , {
851+ unknownToolThreshold : 1 ,
852+ } ) ;
853+
854+ const firstStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
855+ for await ( const _item of firstStream ) {
856+ // drain
857+ }
858+ await firstStream . result ( ) ;
859+
860+ const secondStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
861+ const secondResult = ( await secondStream . result ( ) ) as {
862+ role : string ;
863+ content : Array < { type : string ; text ?: string ; name ?: string } > ;
864+ } ;
865+
866+ expect ( secondResult . role ) . toBe ( "assistant" ) ;
867+ expect ( secondResult . content ) . toEqual ( [
868+ expect . objectContaining ( {
869+ type : "toolCall" ,
870+ name : "re" ,
871+ } ) ,
872+ ] ) ;
873+ } ) ;
874+
875+ it ( "resets a stale unknown-tool streak when a streamed message mixes allowed and unknown tools" , async ( ) => {
876+ const baseFn = vi
877+ . fn ( )
878+ . mockImplementationOnce ( ( ) =>
879+ createFakeStream ( {
880+ events : [ ] ,
881+ resultMessage : {
882+ role : "assistant" ,
883+ content : [ { type : "toolCall" , name : " ex " , arguments : { command : "echo first" } } ] ,
884+ } ,
885+ } ) ,
886+ )
887+ . mockImplementationOnce ( ( ) =>
888+ createFakeStream ( {
889+ events : [
890+ {
891+ type : "toolcall_delta" ,
892+ message : {
893+ role : "assistant" ,
894+ content : [
895+ { type : "toolCall" , name : " exec " , arguments : { command : "echo allowed" } } ,
896+ { type : "toolCall" , name : " ex " , arguments : { command : "echo provisional" } } ,
897+ ] ,
898+ } ,
899+ } ,
900+ ] ,
901+ resultMessage : {
902+ role : "assistant" ,
903+ content : [ { type : "toolCall" , name : " exec " , arguments : { command : "echo ok" } } ] ,
904+ } ,
905+ } ) ,
906+ )
907+ . mockImplementationOnce ( ( ) =>
908+ createFakeStream ( {
909+ events : [ ] ,
910+ resultMessage : {
911+ role : "assistant" ,
912+ content : [ { type : "toolCall" , name : " ex " , arguments : { command : "echo retry" } } ] ,
913+ } ,
914+ } ) ,
915+ ) ;
916+ const wrappedFn = wrapStreamFnTrimToolCallNames ( baseFn as never , new Set ( [ "exec" ] ) , {
917+ unknownToolThreshold : 1 ,
918+ } ) ;
919+
920+ const firstStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
921+ await firstStream . result ( ) ;
922+
923+ const secondStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
924+ for await ( const _item of secondStream ) {
925+ // drain
926+ }
927+ await secondStream . result ( ) ;
928+
929+ const thirdStream = await Promise . resolve ( wrappedFn ( { } as never , { } as never , { } as never ) ) ;
930+ const thirdResult = ( await thirdStream . result ( ) ) as {
931+ role : string ;
932+ content : Array < { type : string ; text ?: string ; name ?: string } > ;
933+ } ;
934+
935+ expect ( thirdResult . role ) . toBe ( "assistant" ) ;
936+ expect ( thirdResult . content ) . toEqual ( [
937+ expect . objectContaining ( {
938+ type : "toolCall" ,
939+ name : "ex" ,
940+ } ) ,
941+ ] ) ;
942+ } ) ;
943+
725944 it ( "infers tool names from malformed toolCallId variants when allowlist is present" , async ( ) => {
726945 const partialToolCall = { type : "toolCall" , id : "functions.read:0" , name : "" } ;
727946 const finalToolCallA = { type : "toolCall" , id : "functionsread3" , name : "" } ;
@@ -1506,11 +1725,9 @@ describe("wrapStreamFnSanitizeMalformedToolCalls", () => {
15061725 ) ;
15071726
15081727 const wrapped = wrapStreamFnSanitizeMalformedToolCalls ( baseFn as never , new Set ( [ "read" ] ) ) ;
1509- const stream = wrapped (
1510- { api : "google-gemini" } as never ,
1511- { messages } as never ,
1512- { } as never ,
1513- ) as FakeWrappedStream | Promise < FakeWrappedStream > ;
1728+ const stream = wrapped ( { api : "google-gemini" } as never , { messages } as never , { } as never ) as
1729+ | FakeWrappedStream
1730+ | Promise < FakeWrappedStream > ;
15141731 await Promise . resolve ( stream ) ;
15151732
15161733 expect ( baseFn ) . toHaveBeenCalledTimes ( 1 ) ;
0 commit comments