@@ -2,6 +2,7 @@ import { Anthropic } from "@anthropic-ai/sdk"
22import { TelemetryService } from "@roo-code/telemetry"
33import {
44 validateAndFixToolResultIds ,
5+ validateMessageHistoryBeforeSend ,
56 ToolResultIdMismatchError ,
67 MissingToolResultError ,
78} from "../validateToolResultIds"
@@ -995,3 +996,225 @@ describe("validateAndFixToolResultIds", () => {
995996 } )
996997 } )
997998} )
999+
1000+ describe ( "validateMessageHistoryBeforeSend" , ( ) => {
1001+ beforeEach ( ( ) => {
1002+ vi . clearAllMocks ( )
1003+ } )
1004+
1005+ it ( "should return the same array reference when all tool_use blocks have matching tool_results" , ( ) => {
1006+ const messages : Anthropic . Messages . MessageParam [ ] = [
1007+ {
1008+ role : "user" ,
1009+ content : [ { type : "text" , text : "Hello" } ] ,
1010+ } ,
1011+ {
1012+ role : "assistant" ,
1013+ content : [
1014+ { type : "tool_use" , id : "tool_1" , name : "read_file" , input : { path : "foo.ts" } } ,
1015+ { type : "tool_use" , id : "tool_2" , name : "read_file" , input : { path : "bar.ts" } } ,
1016+ ] ,
1017+ } ,
1018+ {
1019+ role : "user" ,
1020+ content : [
1021+ { type : "tool_result" , tool_use_id : "tool_1" , content : "file contents 1" } ,
1022+ { type : "tool_result" , tool_use_id : "tool_2" , content : "file contents 2" } ,
1023+ ] ,
1024+ } ,
1025+ ]
1026+
1027+ const result = validateMessageHistoryBeforeSend ( messages )
1028+ expect ( result ) . toBe ( messages ) // Same reference = no modification
1029+ expect ( TelemetryService . instance . captureException ) . not . toHaveBeenCalled ( )
1030+ } )
1031+
1032+ it ( "should inject placeholder tool_results for missing tool_use IDs" , ( ) => {
1033+ const messages : Anthropic . Messages . MessageParam [ ] = [
1034+ {
1035+ role : "user" ,
1036+ content : [ { type : "text" , text : "Hello" } ] ,
1037+ } ,
1038+ {
1039+ role : "assistant" ,
1040+ content : [
1041+ { type : "tool_use" , id : "tool_1" , name : "read_file" , input : { path : "foo.ts" } } ,
1042+ { type : "tool_use" , id : "tool_2" , name : "read_file" , input : { path : "bar.ts" } } ,
1043+ { type : "tool_use" , id : "tool_3" , name : "read_file" , input : { path : "baz.ts" } } ,
1044+ { type : "tool_use" , id : "tool_4" , name : "read_file" , input : { path : "qux.ts" } } ,
1045+ ] ,
1046+ } ,
1047+ {
1048+ role : "user" ,
1049+ content : [
1050+ { type : "tool_result" , tool_use_id : "tool_1" , content : "result 1" } ,
1051+ // tool_2, tool_3, tool_4 are missing
1052+ ] ,
1053+ } ,
1054+ ]
1055+
1056+ const result = validateMessageHistoryBeforeSend ( messages )
1057+
1058+ // Should have the same number of messages
1059+ expect ( result . length ) . toBe ( 3 )
1060+
1061+ // The patched user message should contain placeholders for tool_2, tool_3, tool_4
1062+ const patchedUser = result [ 2 ]
1063+ expect ( patchedUser . role ) . toBe ( "user" )
1064+ const content = patchedUser . content as Anthropic . Messages . ContentBlockParam [ ]
1065+
1066+ // 3 placeholders + 1 existing tool_result = 4
1067+ expect ( content . length ) . toBe ( 4 )
1068+
1069+ const toolResults = content . filter ( ( b ) : b is Anthropic . ToolResultBlockParam => b . type === "tool_result" )
1070+ expect ( toolResults . length ) . toBe ( 4 )
1071+
1072+ const toolResultIds = toolResults . map ( ( r ) => r . tool_use_id )
1073+ expect ( toolResultIds ) . toContain ( "tool_1" )
1074+ expect ( toolResultIds ) . toContain ( "tool_2" )
1075+ expect ( toolResultIds ) . toContain ( "tool_3" )
1076+ expect ( toolResultIds ) . toContain ( "tool_4" )
1077+
1078+ // Should report to telemetry
1079+ expect ( TelemetryService . instance . captureException ) . toHaveBeenCalledTimes ( 1 )
1080+ const capturedError = ( TelemetryService . instance . captureException as ReturnType < typeof vi . fn > ) . mock . calls [ 0 ] [ 0 ]
1081+ expect ( capturedError ) . toBeInstanceOf ( MissingToolResultError )
1082+ expect ( capturedError . missingToolUseIds ) . toEqual ( [ "tool_2" , "tool_3" , "tool_4" ] )
1083+ } )
1084+
1085+ it ( "should insert a synthetic user message when no following user message exists" , ( ) => {
1086+ const messages : Anthropic . Messages . MessageParam [ ] = [
1087+ {
1088+ role : "user" ,
1089+ content : [ { type : "text" , text : "Hello" } ] ,
1090+ } ,
1091+ {
1092+ role : "assistant" ,
1093+ content : [ { type : "tool_use" , id : "tool_1" , name : "read_file" , input : { path : "foo.ts" } } ] ,
1094+ } ,
1095+ // No following user message
1096+ ]
1097+
1098+ const result = validateMessageHistoryBeforeSend ( messages )
1099+
1100+ // Should now have 3 messages (synthetic user message added)
1101+ expect ( result . length ) . toBe ( 3 )
1102+ expect ( result [ 2 ] . role ) . toBe ( "user" )
1103+
1104+ const content = result [ 2 ] . content as Anthropic . ToolResultBlockParam [ ]
1105+ expect ( content . length ) . toBe ( 1 )
1106+ expect ( content [ 0 ] . type ) . toBe ( "tool_result" )
1107+ expect ( content [ 0 ] . tool_use_id ) . toBe ( "tool_1" )
1108+ expect ( content [ 0 ] . content ) . toBe ( "Tool execution was interrupted before completion." )
1109+ } )
1110+
1111+ it ( "should handle multiple assistant messages with tool_use blocks" , ( ) => {
1112+ const messages : Anthropic . Messages . MessageParam [ ] = [
1113+ {
1114+ role : "user" ,
1115+ content : [ { type : "text" , text : "Hello" } ] ,
1116+ } ,
1117+ {
1118+ role : "assistant" ,
1119+ content : [ { type : "tool_use" , id : "tool_1" , name : "read_file" , input : { path : "foo.ts" } } ] ,
1120+ } ,
1121+ {
1122+ role : "user" ,
1123+ content : [ { type : "tool_result" , tool_use_id : "tool_1" , content : "result 1" } ] ,
1124+ } ,
1125+ {
1126+ role : "assistant" ,
1127+ content : [
1128+ { type : "tool_use" , id : "tool_2" , name : "read_file" , input : { path : "bar.ts" } } ,
1129+ { type : "tool_use" , id : "tool_3" , name : "read_file" , input : { path : "baz.ts" } } ,
1130+ ] ,
1131+ } ,
1132+ {
1133+ role : "user" ,
1134+ content : [
1135+ // Only tool_2 has a result, tool_3 is missing
1136+ { type : "tool_result" , tool_use_id : "tool_2" , content : "result 2" } ,
1137+ ] ,
1138+ } ,
1139+ ]
1140+
1141+ const result = validateMessageHistoryBeforeSend ( messages )
1142+
1143+ expect ( result . length ) . toBe ( 5 )
1144+
1145+ // First pair should be untouched
1146+ const firstUserContent = result [ 2 ] . content as Anthropic . Messages . ContentBlockParam [ ]
1147+ expect ( firstUserContent . length ) . toBe ( 1 )
1148+
1149+ // Second pair should have placeholder for tool_3
1150+ const secondUserContent = result [ 4 ] . content as Anthropic . Messages . ContentBlockParam [ ]
1151+ const toolResults = secondUserContent . filter (
1152+ ( b ) : b is Anthropic . ToolResultBlockParam => b . type === "tool_result" ,
1153+ )
1154+ expect ( toolResults . length ) . toBe ( 2 )
1155+ expect ( toolResults . map ( ( r ) => r . tool_use_id ) . sort ( ) ) . toEqual ( [ "tool_2" , "tool_3" ] )
1156+ } )
1157+
1158+ it ( "should not modify messages without tool_use blocks" , ( ) => {
1159+ const messages : Anthropic . Messages . MessageParam [ ] = [
1160+ {
1161+ role : "user" ,
1162+ content : [ { type : "text" , text : "Hello" } ] ,
1163+ } ,
1164+ {
1165+ role : "assistant" ,
1166+ content : [ { type : "text" , text : "Hi there!" } ] ,
1167+ } ,
1168+ {
1169+ role : "user" ,
1170+ content : [ { type : "text" , text : "Thanks" } ] ,
1171+ } ,
1172+ ]
1173+
1174+ const result = validateMessageHistoryBeforeSend ( messages )
1175+ expect ( result ) . toBe ( messages ) // Same reference
1176+ } )
1177+
1178+ it ( "should handle assistant messages with string content" , ( ) => {
1179+ const messages : Anthropic . Messages . MessageParam [ ] = [
1180+ {
1181+ role : "user" ,
1182+ content : "Hello" ,
1183+ } ,
1184+ {
1185+ role : "assistant" ,
1186+ content : "Hi there!" ,
1187+ } ,
1188+ ]
1189+
1190+ const result = validateMessageHistoryBeforeSend ( messages )
1191+ expect ( result ) . toBe ( messages )
1192+ } )
1193+
1194+ it ( "should handle the next message being an assistant (not user)" , ( ) => {
1195+ const messages : Anthropic . Messages . MessageParam [ ] = [
1196+ {
1197+ role : "user" ,
1198+ content : [ { type : "text" , text : "Hello" } ] ,
1199+ } ,
1200+ {
1201+ role : "assistant" ,
1202+ content : [ { type : "tool_use" , id : "tool_1" , name : "read_file" , input : { path : "foo.ts" } } ] ,
1203+ } ,
1204+ {
1205+ role : "assistant" ,
1206+ content : [ { type : "text" , text : "Continuing..." } ] ,
1207+ } ,
1208+ ]
1209+
1210+ const result = validateMessageHistoryBeforeSend ( messages )
1211+
1212+ // Should insert a synthetic user message between the two assistant messages
1213+ expect ( result . length ) . toBe ( 4 )
1214+ expect ( result [ 2 ] . role ) . toBe ( "user" )
1215+ const syntheticContent = result [ 2 ] . content as Anthropic . ToolResultBlockParam [ ]
1216+ expect ( syntheticContent [ 0 ] . tool_use_id ) . toBe ( "tool_1" )
1217+ // The original second assistant message should still be there
1218+ expect ( result [ 3 ] . role ) . toBe ( "assistant" )
1219+ } )
1220+ } )
0 commit comments