@@ -1183,6 +1183,98 @@ describe("agentLoop tool termination", () => {
11831183 expect ( events . at ( - 1 ) ) . toMatchObject ( { type : "agent_end" } ) ;
11841184 } ) ;
11851185
1186+ it ( "does not execute a prepared parallel tool after a later tool aborts the batch" , async ( ) => {
1187+ const controller = new AbortController ( ) ;
1188+ const paidExecute = vi . fn ( async ( ) : Promise < AgentToolResult < unknown > > => {
1189+ return {
1190+ content : [ { type : "text" , text : "charged" } ] ,
1191+ details : { charged : true } ,
1192+ } ;
1193+ } ) ;
1194+ const abortingExecute = vi . fn ( async ( ) : Promise < AgentToolResult < unknown > > => {
1195+ return {
1196+ content : [ { type : "text" , text : "should not run" } ] ,
1197+ details : { } ,
1198+ } ;
1199+ } ) ;
1200+ const paidTool : AgentTool = {
1201+ name : "paid_generation" ,
1202+ label : "paid_generation" ,
1203+ description : "Represents a paid side-effectful tool" ,
1204+ parameters : Type . Object ( { } , { additionalProperties : false } ) ,
1205+ execute : paidExecute ,
1206+ } ;
1207+ const abortingTool : AgentTool = {
1208+ name : "abort_gate" ,
1209+ label : "abort_gate" ,
1210+ description : "Aborts during preparation" ,
1211+ parameters : Type . Object ( { } , { additionalProperties : false } ) ,
1212+ execute : abortingExecute ,
1213+ } ;
1214+ const streamFn : StreamFn = ( ) => {
1215+ const stream = createAssistantMessageEventStream ( ) ;
1216+ queueMicrotask ( ( ) => {
1217+ const message = makeAssistantMessage ( [
1218+ { type : "toolCall" , id : "call-paid" , name : paidTool . name , arguments : { } } ,
1219+ { type : "toolCall" , id : "call-abort" , name : abortingTool . name , arguments : { } } ,
1220+ ] ) ;
1221+ stream . push ( { type : "done" , reason : "toolUse" , message } ) ;
1222+ stream . end ( ) ;
1223+ } ) ;
1224+ return stream ;
1225+ } ;
1226+ const events : AgentEvent [ ] = [ ] ;
1227+ const preparedToolNames : string [ ] = [ ] ;
1228+
1229+ const messages = await runAgentLoop (
1230+ [ { role : "user" , content : "abort mid batch" , timestamp : 1 } ] ,
1231+ {
1232+ systemPrompt : "" ,
1233+ messages : [ ] ,
1234+ tools : [ paidTool , abortingTool ] ,
1235+ } ,
1236+ {
1237+ ...config ,
1238+ beforeToolCall : async ( { toolCall } ) => {
1239+ preparedToolNames . push ( toolCall . name ) ;
1240+ if ( toolCall . name === abortingTool . name ) {
1241+ await Promise . resolve ( ) ;
1242+ controller . abort ( new Error ( "user aborted" ) ) ;
1243+ }
1244+ return undefined ;
1245+ } ,
1246+ } ,
1247+ ( event ) => {
1248+ events . push ( event ) ;
1249+ } ,
1250+ controller . signal ,
1251+ streamFn ,
1252+ ) ;
1253+
1254+ expect ( preparedToolNames ) . toEqual ( [ paidTool . name , abortingTool . name ] ) ;
1255+ expect ( paidExecute ) . not . toHaveBeenCalled ( ) ;
1256+ expect ( abortingExecute ) . not . toHaveBeenCalled ( ) ;
1257+ expect ( messages . map ( ( message ) => message . role ) ) . toEqual ( [
1258+ "user" ,
1259+ "assistant" ,
1260+ "toolResult" ,
1261+ "toolResult" ,
1262+ "assistant" ,
1263+ ] ) ;
1264+ expect ( messages . at ( - 1 ) ) . toMatchObject ( { role : "assistant" , stopReason : "aborted" } ) ;
1265+
1266+ const endEvents = events . filter (
1267+ ( event ) : event is Extract < AgentEvent , { type : "tool_execution_end" } > =>
1268+ event . type === "tool_execution_end" ,
1269+ ) ;
1270+ expect (
1271+ Object . fromEntries ( endEvents . map ( ( event ) => [ event . toolName , event . executionStarted ] ) ) ,
1272+ ) . toEqual ( {
1273+ [ paidTool . name ] : false ,
1274+ [ abortingTool . name ] : false ,
1275+ } ) ;
1276+ } ) ;
1277+
11861278 it ( "does not request another model turn when an async turn hook aborts the run" , async ( ) => {
11871279 const controller = new AbortController ( ) ;
11881280 let streamCalls = 0 ;
0 commit comments