@@ -267,6 +267,112 @@ public void TruncateArgsSummary_WithinLimit_ReturnsAsIs()
267267 Assert . AreEqual ( s , AgentLoopRunner . TruncateArgsSummary ( s ) ) ;
268268 }
269269
270+ [ TestMethod ]
271+ [ Timeout ( 10_000 ) ]
272+ public async Task Trim_OversizeRetrievalResult_IsNotReStashedAndDoesNotSpin ( )
273+ {
274+ // Regression for the 2026-06-10 communications-briefing runaway: an explicit
275+ // GetFromWorkingMemory retrieval returned an oversized result, which the trim
276+ // re-stashed under the *retrieval* call's id and advertised back to the model.
277+ // The model re-fetched the new key, got a larger reference, which was re-stashed
278+ // again — a retrieve→re-stash→retrieve loop that burned the whole subagent budget.
279+ // Explicit working-memory reads must be left intact.
280+ var wm = new TestWorkingMemory ( ) ;
281+ var runner = NewRunner ( wm ) ;
282+ var stashState = new AgentLoopStashContext . State { SessionId = "sess-1" } ;
283+
284+ var bigRetrieval = new string ( 'R' , 4000 ) ;
285+ var messages = new List < ChatMessage >
286+ {
287+ new ( ChatRole . System , "system prompt" ) ,
288+ new ( ChatRole . User , "do the thing" ) ,
289+ BuildAssistantWithCall ( "GetFromWorkingMemory" , "call-1" ) ,
290+ new ( ChatRole . Tool , [ new FunctionResultContent ( "call-1" , bigRetrieval ) ] ) ,
291+ } ;
292+
293+ await runner . TrimLargeToolResultsAsync ( messages , maxTokens : 200 , "sess-1" , stashState ) ;
294+
295+ var frc = ( FunctionResultContent ) messages [ 3 ] . Contents [ 0 ] ;
296+ Assert . AreEqual ( bigRetrieval , frc . Result ? . ToString ( ) ,
297+ "An explicit GetFromWorkingMemory retrieval must be left intact, not head+tail trimmed." ) ;
298+ Assert . IsTrue ( stashState . Registry . IsEmpty ,
299+ "A retrieval result must never be re-stashed (that mints a fresh key and loops the model)." ) ;
300+ Assert . AreEqual ( 0 , wm . WriteCount , "Nothing should be written to working memory for a retrieval result." ) ;
301+ }
302+
303+ [ TestMethod ]
304+ [ Timeout ( 10_000 ) ]
305+ public async Task Trim_RetrievalAndNormalResult_TrimsNormalAndSkipsRetrieval ( )
306+ {
307+ // When both an exempt retrieval and a normal oversized result are over budget,
308+ // the trim must skip the retrieval and reclaim space from the normal result.
309+ var wm = new TestWorkingMemory ( ) ;
310+ var runner = NewRunner ( wm ) ;
311+ var stashState = new AgentLoopStashContext . State { SessionId = "sess-1" } ;
312+
313+ var bigRetrieval = new string ( 'R' , 4000 ) ;
314+ var biggerNormal = new string ( 'N' , 5000 ) + "NORMAL-TAIL" ;
315+ var messages = new List < ChatMessage >
316+ {
317+ new ( ChatRole . System , "system prompt" ) ,
318+ new ( ChatRole . User , "do the thing" ) ,
319+ BuildAssistantWithCall ( "GetFromWorkingMemory" , "call-ret" ) ,
320+ new ( ChatRole . Tool , [ new FunctionResultContent ( "call-ret" , bigRetrieval ) ] ) ,
321+ BuildAssistantWithCall ( "fetch_url" , "call-norm" ) ,
322+ new ( ChatRole . Tool , [ new FunctionResultContent ( "call-norm" , biggerNormal ) ] ) ,
323+ } ;
324+
325+ await runner . TrimLargeToolResultsAsync ( messages , maxTokens : 200 , "sess-1" , stashState ) ;
326+
327+ var retrieval = ( FunctionResultContent ) messages [ 3 ] . Contents [ 0 ] ;
328+ Assert . AreEqual ( bigRetrieval , retrieval . Result ? . ToString ( ) ,
329+ "The retrieval result must be untouched." ) ;
330+
331+ var normal = ( FunctionResultContent ) messages [ 5 ] . Contents [ 0 ] ;
332+ StringAssert . Contains ( normal . Result ? . ToString ( ) ?? string . Empty , ElisionMarkerPrefix ,
333+ "The normal result must be head+tail trimmed to reclaim space." ) ;
334+
335+ Assert . AreEqual ( 1 , stashState . Registry . Snapshot ( ) . Count ,
336+ "Only the normal result should be stashed." ) ;
337+ Assert . AreEqual ( "call-norm" , stashState . Registry . Snapshot ( ) [ 0 ] . CallId ) ;
338+ }
339+
340+ [ TestMethod ]
341+ public async Task CapToolResult_RetrievalTool_ReturnsUnchangedWithoutStashing ( )
342+ {
343+ var wm = new TestWorkingMemory ( ) ;
344+ var stashState = new AgentLoopStashContext . State { SessionId = "sess-1" } ;
345+ var big = new string ( 'R' , 4000 ) ;
346+
347+ var capped = await AgentLoopRunner . CapToolResultAsync (
348+ big , callId : "call-1" , toolName : "GetFromWorkingMemory" ,
349+ workingMemory : wm , stashState : stashState ,
350+ maxChars : 1000 , headRatio : 0.6 , ttl : TimeSpan . FromMinutes ( 60 ) ,
351+ logger : NullLogger < AgentLoopRunner > . Instance ) ;
352+
353+ Assert . AreEqual ( big , capped , "An explicit retrieval must be returned in full, not capped." ) ;
354+ Assert . IsTrue ( stashState . Registry . IsEmpty , "A retrieval result must not be stashed." ) ;
355+ Assert . AreEqual ( 0 , wm . WriteCount , "A retrieval result must not be written back to working memory." ) ;
356+ }
357+
358+ [ TestMethod ]
359+ public async Task CapToolResult_NormalTool_CapsAndStashes ( )
360+ {
361+ var wm = new TestWorkingMemory ( ) ;
362+ var stashState = new AgentLoopStashContext . State { SessionId = "sess-1" } ;
363+ var big = new string ( 'N' , 4000 ) ;
364+
365+ var capped = await AgentLoopRunner . CapToolResultAsync (
366+ big , callId : "call-1" , toolName : "fetch_url" ,
367+ workingMemory : wm , stashState : stashState ,
368+ maxChars : 1000 , headRatio : 0.6 , ttl : TimeSpan . FromMinutes ( 60 ) ,
369+ logger : NullLogger < AgentLoopRunner > . Instance ) ;
370+
371+ Assert . IsTrue ( capped . Length < big . Length , "A normal oversized result must be capped." ) ;
372+ StringAssert . Contains ( capped , ElisionMarkerPrefix ) ;
373+ Assert . AreEqual ( 1 , stashState . Registry . Snapshot ( ) . Count , "A normal capped result must be stashed." ) ;
374+ }
375+
270376 // ── Helpers ──────────────────────────────────────────────────────────────
271377
272378 private static AgentLoopRunner NewRunner ( IWorkingMemory workingMemory )
0 commit comments