@@ -2609,6 +2609,148 @@ describe("SessionRunnerLLM", () => {
26092609 } ) ,
26102610 )
26112611
2612+ it . effect ( "returns policy-blocked tools to the model and continues" , ( ) =>
2613+ Effect . gen ( function * ( ) {
2614+ yield * setup
2615+ const session = yield * SessionV2 . Service
2616+ const registry = yield * ToolRegistry . Service
2617+ yield * registry . register ( {
2618+ blocked : Tool . make ( {
2619+ description : "Fail because policy blocked execution" ,
2620+ input : Schema . Struct ( { } ) ,
2621+ output : Schema . Struct ( { } ) ,
2622+ execute : ( ) =>
2623+ Effect . fail ( new PermissionV2 . BlockedError ( { rules : [ ] } ) ) . pipe (
2624+ Effect . mapError ( ( ) => new Tool . Failure ( { message : "Permission blocked" } ) ) ,
2625+ ) ,
2626+ } ) ,
2627+ } )
2628+ yield * session . prompt ( { sessionID, prompt : Prompt . make ( { text : "Call blocked" } ) , resume : false } )
2629+
2630+ requests . length = 0
2631+ responses = [
2632+ [
2633+ LLMEvent . stepStart ( { index : 0 } ) ,
2634+ LLMEvent . toolCall ( { id : "call-blocked" , name : "blocked" , input : { } } ) ,
2635+ LLMEvent . stepFinish ( { index : 0 , reason : "tool-calls" } ) ,
2636+ LLMEvent . finish ( { reason : "tool-calls" } ) ,
2637+ ] ,
2638+ [
2639+ LLMEvent . stepStart ( { index : 0 } ) ,
2640+ LLMEvent . stepFinish ( { index : 0 , reason : "stop" } ) ,
2641+ LLMEvent . finish ( { reason : "stop" } ) ,
2642+ ] ,
2643+ ]
2644+
2645+ yield * session . resume ( sessionID )
2646+
2647+ expect ( requests ) . toHaveLength ( 2 )
2648+ expect ( yield * session . context ( sessionID ) ) . toMatchObject ( [
2649+ { type : "user" , text : "Call blocked" } ,
2650+ {
2651+ type : "assistant" ,
2652+ content : [
2653+ { type : "tool" , id : "call-blocked" , state : { status : "error" , error : { message : "Permission blocked" } } } ,
2654+ ] ,
2655+ } ,
2656+ { type : "assistant" , finish : "stop" } ,
2657+ ] )
2658+ } ) ,
2659+ )
2660+
2661+ it . effect ( "interrupts runner continuation when permission approval is declined" , ( ) =>
2662+ Effect . gen ( function * ( ) {
2663+ yield * setup
2664+ const session = yield * SessionV2 . Service
2665+ const registry = yield * ToolRegistry . Service
2666+ yield * registry . register ( {
2667+ declined : Tool . make ( {
2668+ description : "Fail because the user declined approval" ,
2669+ input : Schema . Struct ( { } ) ,
2670+ output : Schema . Struct ( { } ) ,
2671+ execute : ( ) => Effect . die ( new PermissionV2 . DeclinedError ( ) ) ,
2672+ } ) ,
2673+ } )
2674+ yield * session . prompt ( { sessionID, prompt : Prompt . make ( { text : "Call declined" } ) , resume : false } )
2675+
2676+ requests . length = 0
2677+ response = [
2678+ LLMEvent . stepStart ( { index : 0 } ) ,
2679+ LLMEvent . toolCall ( { id : "call-declined" , name : "declined" , input : { } } ) ,
2680+ LLMEvent . stepFinish ( { index : 0 , reason : "tool-calls" } ) ,
2681+ LLMEvent . finish ( { reason : "tool-calls" } ) ,
2682+ ]
2683+
2684+ const exit = yield * session . resume ( sessionID ) . pipe ( Effect . exit )
2685+
2686+ expect ( exit . _tag ) . toBe ( "Failure" )
2687+ if ( exit . _tag === "Failure" ) expect ( Cause . hasInterruptsOnly ( exit . cause ) ) . toBe ( true )
2688+ expect ( requests ) . toHaveLength ( 1 )
2689+ expect ( yield * session . context ( sessionID ) ) . toMatchObject ( [
2690+ { type : "user" , text : "Call declined" } ,
2691+ {
2692+ type : "assistant" ,
2693+ content : [
2694+ {
2695+ type : "tool" ,
2696+ id : "call-declined" ,
2697+ state : { status : "error" , error : { message : "Tool execution interrupted" } } ,
2698+ } ,
2699+ ] ,
2700+ } ,
2701+ ] )
2702+ } ) ,
2703+ )
2704+
2705+ it . effect ( "returns permission corrections to the model and continues" , ( ) =>
2706+ Effect . gen ( function * ( ) {
2707+ yield * setup
2708+ const session = yield * SessionV2 . Service
2709+ const registry = yield * ToolRegistry . Service
2710+ yield * registry . register ( {
2711+ corrected : Tool . make ( {
2712+ description : "Fail with user correction feedback" ,
2713+ input : Schema . Struct ( { } ) ,
2714+ output : Schema . Struct ( { } ) ,
2715+ execute : ( ) =>
2716+ Effect . fail ( new PermissionV2 . CorrectedError ( { feedback : "Use another tool" } ) ) . pipe (
2717+ Effect . mapError ( ( ) => new Tool . Failure ( { message : "Use another tool" } ) ) ,
2718+ ) ,
2719+ } ) ,
2720+ } )
2721+ yield * session . prompt ( { sessionID, prompt : Prompt . make ( { text : "Call corrected" } ) , resume : false } )
2722+
2723+ requests . length = 0
2724+ responses = [
2725+ [
2726+ LLMEvent . stepStart ( { index : 0 } ) ,
2727+ LLMEvent . toolCall ( { id : "call-corrected" , name : "corrected" , input : { } } ) ,
2728+ LLMEvent . stepFinish ( { index : 0 , reason : "tool-calls" } ) ,
2729+ LLMEvent . finish ( { reason : "tool-calls" } ) ,
2730+ ] ,
2731+ [
2732+ LLMEvent . stepStart ( { index : 0 } ) ,
2733+ LLMEvent . stepFinish ( { index : 0 , reason : "stop" } ) ,
2734+ LLMEvent . finish ( { reason : "stop" } ) ,
2735+ ] ,
2736+ ]
2737+
2738+ yield * session . resume ( sessionID )
2739+
2740+ expect ( requests ) . toHaveLength ( 2 )
2741+ expect ( yield * session . context ( sessionID ) ) . toMatchObject ( [
2742+ { type : "user" , text : "Call corrected" } ,
2743+ {
2744+ type : "assistant" ,
2745+ content : [
2746+ { type : "tool" , id : "call-corrected" , state : { status : "error" , error : { message : "Use another tool" } } } ,
2747+ ] ,
2748+ } ,
2749+ { type : "assistant" , finish : "stop" } ,
2750+ ] )
2751+ } ) ,
2752+ )
2753+
26122754 it . effect ( "interrupts runner continuation when a question is dismissed" , ( ) =>
26132755 Effect . gen ( function * ( ) {
26142756 yield * setup
0 commit comments