@@ -767,6 +767,154 @@ describe("agentLoop tool termination", () => {
767767 expect ( events . filter ( ( event ) => event . type === "tool_execution_start" ) ) . toHaveLength ( 1 ) ;
768768 expect ( events . at ( - 1 ) ) . toMatchObject ( { type : "agent_end" } ) ;
769769 } ) ;
770+
771+ it ( "does not request another model turn after a tool aborts the run" , async ( ) => {
772+ const controller = new AbortController ( ) ;
773+ let streamCalls = 0 ;
774+ const streamFn : StreamFn = ( ) => {
775+ streamCalls += 1 ;
776+ if ( streamCalls > 1 ) {
777+ throw new Error ( "model was called after abort" ) ;
778+ }
779+ const stream = createAssistantMessageEventStream ( ) ;
780+ queueMicrotask ( ( ) => {
781+ const message = makeAssistantMessage ( [
782+ { type : "toolCall" , id : "call-abort" , name : "abort_tool" , arguments : { } } ,
783+ ] ) ;
784+ stream . push ( { type : "done" , reason : "toolUse" , message } ) ;
785+ stream . end ( ) ;
786+ } ) ;
787+ return stream ;
788+ } ;
789+ const abortTool : AgentTool = {
790+ name : "abort_tool" ,
791+ label : "abort_tool" ,
792+ description : "Abort the active run" ,
793+ parameters : Type . Object ( { } , { additionalProperties : false } ) ,
794+ execute : async ( ) => {
795+ controller . abort ( new Error ( "user aborted" ) ) ;
796+ return {
797+ content : [ { type : "text" , text : "aborted" } ] ,
798+ details : { aborted : true } ,
799+ } ;
800+ } ,
801+ } ;
802+ const events : AgentEvent [ ] = [ ] ;
803+
804+ const messages = await runAgentLoop (
805+ [ { role : "user" , content : "abort during tool" , timestamp : 1 } ] ,
806+ {
807+ systemPrompt : "" ,
808+ messages : [ ] ,
809+ tools : [ abortTool ] ,
810+ } ,
811+ config ,
812+ ( event ) => {
813+ events . push ( event ) ;
814+ } ,
815+ controller . signal ,
816+ streamFn ,
817+ ) ;
818+
819+ expect ( streamCalls ) . toBe ( 1 ) ;
820+ expect ( messages . map ( ( message ) => message . role ) ) . toEqual ( [
821+ "user" ,
822+ "assistant" ,
823+ "toolResult" ,
824+ "assistant" ,
825+ ] ) ;
826+ expect ( messages . at ( - 1 ) ) . toMatchObject ( { role : "assistant" , stopReason : "aborted" } ) ;
827+ expect ( events . map ( ( event ) => event . type ) ) . toEqual ( [
828+ "agent_start" ,
829+ "turn_start" ,
830+ "message_start" ,
831+ "message_end" ,
832+ "message_start" ,
833+ "message_end" ,
834+ "tool_execution_start" ,
835+ "tool_execution_end" ,
836+ "message_start" ,
837+ "message_end" ,
838+ "turn_end" ,
839+ "turn_start" ,
840+ "message_start" ,
841+ "message_end" ,
842+ "turn_end" ,
843+ "agent_end" ,
844+ ] ) ;
845+ expect ( events . at ( - 1 ) ) . toMatchObject ( { type : "agent_end" } ) ;
846+ } ) ;
847+
848+ it ( "does not request another model turn when an async turn hook aborts the run" , async ( ) => {
849+ const controller = new AbortController ( ) ;
850+ let streamCalls = 0 ;
851+ const streamFn : StreamFn = ( ) => {
852+ streamCalls += 1 ;
853+ if ( streamCalls > 1 ) {
854+ throw new Error ( "model was called after abort" ) ;
855+ }
856+ const stream = createAssistantMessageEventStream ( ) ;
857+ queueMicrotask ( ( ) => {
858+ const message = makeAssistantMessage ( [
859+ { type : "toolCall" , id : "call-hook-abort" , name : "hook_abort" , arguments : { } } ,
860+ ] ) ;
861+ stream . push ( { type : "done" , reason : "toolUse" , message } ) ;
862+ stream . end ( ) ;
863+ } ) ;
864+ return stream ;
865+ } ;
866+ const events : AgentEvent [ ] = [ ] ;
867+
868+ const messages = await runAgentLoop (
869+ [ { role : "user" , content : "abort from hook" , timestamp : 1 } ] ,
870+ {
871+ systemPrompt : "" ,
872+ messages : [ ] ,
873+ tools : [ makeTool ( "hook_abort" , [ ] ) ] ,
874+ } ,
875+ {
876+ ...config ,
877+ prepareNextTurn : async ( ) => {
878+ await Promise . resolve ( ) ;
879+ controller . abort ( new Error ( "user aborted" ) ) ;
880+ return undefined ;
881+ } ,
882+ } ,
883+ ( event ) => {
884+ events . push ( event ) ;
885+ } ,
886+ controller . signal ,
887+ streamFn ,
888+ ) ;
889+
890+ expect ( streamCalls ) . toBe ( 1 ) ;
891+ expect ( messages . map ( ( message ) => message . role ) ) . toEqual ( [
892+ "user" ,
893+ "assistant" ,
894+ "toolResult" ,
895+ "assistant" ,
896+ ] ) ;
897+ expect ( messages . at ( - 1 ) ) . toMatchObject ( { role : "assistant" , stopReason : "aborted" } ) ;
898+ expect ( events . map ( ( event ) => event . type ) ) . toEqual ( [
899+ "agent_start" ,
900+ "turn_start" ,
901+ "message_start" ,
902+ "message_end" ,
903+ "message_start" ,
904+ "message_end" ,
905+ "tool_execution_start" ,
906+ "tool_execution_end" ,
907+ "message_start" ,
908+ "message_end" ,
909+ "turn_end" ,
910+ "turn_start" ,
911+ "message_start" ,
912+ "message_end" ,
913+ "turn_end" ,
914+ "agent_end" ,
915+ ] ) ;
916+ expect ( events . at ( - 1 ) ) . toMatchObject ( { type : "agent_end" } ) ;
917+ } ) ;
770918} ) ;
771919
772920describe ( "agentLoop thinking state" , ( ) => {
0 commit comments