@@ -1068,30 +1068,57 @@ export function fitRequestToContext(
10681068 let total = estimateRequestTokens ( req ) ;
10691069 if ( total <= budget ) return req ;
10701070
1071- const messages = [ ... req . messages ] ;
1071+ const messages = req . messages ;
10721072 // Indices we must never drop: all system messages and the final message.
10731073 const lastIdx = messages . length - 1 ;
10741074 const protectedIdx = new Set < number > ( [ lastIdx ] ) ;
10751075 messages . forEach ( ( m , i ) => {
10761076 if ( m . role === 'system' ) protectedIdx . add ( i ) ;
10771077 } ) ;
10781078
1079- // Walk from oldest to newest, removing droppable messages until we fit.
1079+ // Group messages into atomic drop units: an assistant `tool_use` turn and
1080+ // its immediately-following `tool_result` reply must be dropped (or kept)
1081+ // together. Trimming just one half leaves the other as an orphan and
1082+ // upstream rejects the request with a 400 ("tool_use ids were found
1083+ // without tool_result blocks" / "unexpected tool_use_id in tool_result").
1084+ const units : Array < {
1085+ indices : number [ ] ;
1086+ tokens : number ;
1087+ protected : boolean ;
1088+ } > = [ ] ;
1089+ for ( let i = 0 ; i < messages . length ; ) {
1090+ const m = messages [ i ] ;
1091+ const pairsWithNext =
1092+ m . role === 'assistant' &&
1093+ ! ! m . toolCalls ?. length &&
1094+ ! ! messages [ i + 1 ] ?. toolResults ?. length ;
1095+ const indices = pairsWithNext ? [ i , i + 1 ] : [ i ] ;
1096+ const tokens = indices . reduce (
1097+ ( sum , idx ) => sum + estimateMessageTokens ( messages [ idx ] ) ,
1098+ 0 ,
1099+ ) ;
1100+ units . push ( {
1101+ indices,
1102+ tokens,
1103+ protected : indices . some ( ( idx ) => protectedIdx . has ( idx ) ) ,
1104+ } ) ;
1105+ i += indices . length ;
1106+ }
1107+
1108+ // Walk from oldest to newest, removing droppable units until we fit.
10801109 let removed = 0 ;
1081- for ( let i = 0 ; i < messages . length && total > budget ; i ++ ) {
1082- if ( protectedIdx . has ( i ) ) continue ;
1083- if ( ! messages [ i ] ) continue ;
1084- total -= estimateMessageTokens ( messages [ i ] ) ;
1085- // Tombstone; filtered out below to keep indices stable during the loop.
1086- ( messages as Array < NormMessage | null > ) [ i ] = null ;
1087- removed ++ ;
1110+ const dropped = new Set < number > ( ) ;
1111+ for ( const unit of units ) {
1112+ if ( total <= budget ) break ;
1113+ if ( unit . protected ) continue ;
1114+ total -= unit . tokens ;
1115+ for ( const idx of unit . indices ) dropped . add ( idx ) ;
1116+ removed += unit . indices . length ;
10881117 }
10891118
10901119 if ( removed === 0 ) return req ;
10911120
1092- const trimmed = ( messages as Array < NormMessage | null > ) . filter (
1093- ( m ) : m is NormMessage => m !== null ,
1094- ) ;
1121+ const trimmed = messages . filter ( ( _ , i ) => ! dropped . has ( i ) ) ;
10951122 log (
10961123 `context safeguard: trimmed ${ removed } oldest message(s); ` +
10971124 `~${ total } est tokens <= ${ budget } budget (limit ${ limit } )` ,
0 commit comments