@@ -546,7 +546,7 @@ Always include PINEAPPLE_COCONUT_42.
546546 expect ( toolMessage ?. content ) . toBe ( "Tool 'report_intent' does not exist." ) ;
547547 } ) ;
548548
549- test ( "normalizes aborted tool execution results" , async ( ) => {
549+ test ( "normalizes interrupted tool execution results" , async ( ) => {
550550 const requestBody = JSON . stringify ( {
551551 messages : [
552552 { role : "user" , content : "Run a slow analysis" } ,
@@ -561,6 +561,14 @@ Always include PINEAPPLE_COCONUT_42.
561561 arguments : '{"value":"test_abort"}' ,
562562 } ,
563563 } ,
564+ {
565+ id : "tc2" ,
566+ type : "function" ,
567+ function : {
568+ name : "powershell" ,
569+ arguments : '{"command":"sleep 100"}' ,
570+ } ,
571+ } ,
564572 ] ,
565573 } ,
566574 {
@@ -569,6 +577,11 @@ Always include PINEAPPLE_COCONUT_42.
569577 content :
570578 'Failed to execute `slow_analysis` tool with arguments: {"value":"test_abort"} due to error: Error: Session aborted' ,
571579 } ,
580+ {
581+ role : "tool" ,
582+ tool_call_id : "tc2" ,
583+ content : "<shell context is being reconfigured; retry the command>" ,
584+ } ,
572585 ] ,
573586 } ) ;
574587 const responseBody = JSON . stringify ( {
@@ -580,12 +593,13 @@ Always include PINEAPPLE_COCONUT_42.
580593 ] ) ;
581594
582595 const result = await readYamlOutput ( outputPath ) ;
583- const toolMessage = result . conversations [ 0 ] . messages . find (
596+ const toolMessages = result . conversations [ 0 ] . messages . filter (
584597 ( m ) => m . role === "tool" ,
585598 ) ;
586- expect ( toolMessage ?. content ) . toBe (
599+ expect ( toolMessages . map ( ( message ) => message . content ) ) . toEqual ( [
587600 "The execution of this tool, or a previous tool was interrupted." ,
588- ) ;
601+ "The execution of this tool, or a previous tool was interrupted." ,
602+ ] ) ;
589603 } ) ;
590604
591605 test ( "normalizes background agent IDs and removes runtime advisories" , async ( ) => {
@@ -945,6 +959,119 @@ Always include PINEAPPLE_COCONUT_42.
945959 }
946960 } ) ;
947961
962+ test ( "matches semantically equivalent interrupted shell results" , async ( ) => {
963+ const originalShellConfig =
964+ process . platform === "win32"
965+ ? ShellConfig . powerShell
966+ : ShellConfig . bash ;
967+ const cachePath = path . join ( tempDir , "cache.yaml" ) ;
968+ const cacheContent = yaml . stringify ( {
969+ models : [ "test-model" ] ,
970+ conversations : [
971+ {
972+ messages : [
973+ { role : "system" , content : "${system}" } ,
974+ { role : "user" , content : "Run command" } ,
975+ {
976+ role : "assistant" ,
977+ tool_calls : [
978+ {
979+ id : "toolcall_0" ,
980+ type : "function" ,
981+ function : {
982+ name : "${shell}" ,
983+ arguments : '{"command":"sleep 100"}' ,
984+ } ,
985+ } ,
986+ ] ,
987+ } ,
988+ {
989+ role : "tool" ,
990+ tool_call_id : "toolcall_0" ,
991+ content :
992+ "The execution of this tool, or a previous tool was interrupted." ,
993+ } ,
994+ { role : "assistant" , content : "Ready for another request." } ,
995+ ] ,
996+ } ,
997+ ] ,
998+ } satisfies NormalizedData ) ;
999+ await writeFile ( cachePath , cacheContent ) ;
1000+
1001+ const proxy = new ReplayingCapiProxy (
1002+ "http://localhost:9999" ,
1003+ cachePath ,
1004+ workDir ,
1005+ ) ;
1006+ const proxyUrl = await proxy . start ( ) ;
1007+
1008+ try {
1009+ const messages = [
1010+ { role : "system" , content : "System prompt" } ,
1011+ { role : "user" , content : "Run command" } ,
1012+ {
1013+ role : "assistant" ,
1014+ tool_calls : [
1015+ {
1016+ id : "runtime-call-id" ,
1017+ type : "function" ,
1018+ function : {
1019+ name : originalShellConfig . shellToolName ,
1020+ arguments : '{"command":"sleep 100"}' ,
1021+ } ,
1022+ } ,
1023+ ] ,
1024+ } ,
1025+ ] ;
1026+ const interruptedResponse = await makeRequest (
1027+ proxyUrl ,
1028+ "/chat/completions" ,
1029+ {
1030+ body : {
1031+ model : "test-model" ,
1032+ messages : [
1033+ ...messages ,
1034+ {
1035+ role : "tool" ,
1036+ tool_call_id : "runtime-call-id" ,
1037+ content :
1038+ "<shell context is being reconfigured; retry the command>" ,
1039+ } ,
1040+ ] ,
1041+ } ,
1042+ } ,
1043+ ) ;
1044+
1045+ expect ( interruptedResponse . status ) . toBe ( 200 ) ;
1046+ expect (
1047+ ( JSON . parse ( interruptedResponse . body ) as ChatCompletion ) . choices [ 0 ]
1048+ . message . content ,
1049+ ) . toBe ( "Ready for another request." ) ;
1050+
1051+ const meaningfulErrorResponse = await makeRequest (
1052+ proxyUrl ,
1053+ "/chat/completions" ,
1054+ {
1055+ body : {
1056+ model : "test-model" ,
1057+ messages : [
1058+ ...messages ,
1059+ {
1060+ role : "tool" ,
1061+ tool_call_id : "runtime-call-id" ,
1062+ content :
1063+ "The command failed because the executable was missing." ,
1064+ } ,
1065+ ] ,
1066+ } ,
1067+ } ,
1068+ ) ;
1069+ expect ( meaningfulErrorResponse . status ) . toBe ( 500 ) ;
1070+ } finally {
1071+ await proxy . stop ( ) ;
1072+ }
1073+ } ) ;
1074+
9481075 test ( "matches available-tools results after the built-in tool set changes" , async ( ) => {
9491076 const cachePath = path . join ( tempDir , "cache.yaml" ) ;
9501077 // Legacy snapshot recorded before write_agent was a built-in tool: the
0 commit comments