@@ -862,13 +862,15 @@ struct RuntimeStateSaveCall {
862862 conversation_id : String ,
863863 current_mode_id : Option < Option < String > > ,
864864 current_model_id : Option < Option < String > > ,
865+ config_selections_json : Option < Option < String > > ,
865866}
866867
867868#[ derive( Default ) ]
868869struct StubAcpSessionRepo {
869870 create_calls : Mutex < Vec < CreateAcpSessionCall > > ,
870871 runtime_state_saves : Mutex < Vec < RuntimeStateSaveCall > > ,
871872 session_id : Mutex < Option < String > > ,
873+ runtime_state : Mutex < Option < PersistedSessionState > > ,
872874}
873875
874876impl StubAcpSessionRepo {
@@ -881,6 +883,16 @@ impl StubAcpSessionRepo {
881883 create_calls : Mutex :: new ( Vec :: new ( ) ) ,
882884 runtime_state_saves : Mutex :: new ( Vec :: new ( ) ) ,
883885 session_id : Mutex :: new ( Some ( session_id. into ( ) ) ) ,
886+ runtime_state : Mutex :: new ( None ) ,
887+ }
888+ }
889+
890+ fn with_runtime_state ( state : PersistedSessionState ) -> Self {
891+ Self {
892+ create_calls : Mutex :: new ( Vec :: new ( ) ) ,
893+ runtime_state_saves : Mutex :: new ( Vec :: new ( ) ) ,
894+ session_id : Mutex :: new ( None ) ,
895+ runtime_state : Mutex :: new ( Some ( state) ) ,
884896 }
885897 }
886898
@@ -941,10 +953,12 @@ impl IAcpSessionRepository for StubAcpSessionRepo {
941953 Ok ( false )
942954 }
943955 async fn load_runtime_state ( & self , _conversation_id : & str ) -> Result < Option < PersistedSessionState > , DbError > {
944- Ok ( Some ( PersistedSessionState {
945- current_model_id : Some ( "deepseek-v4-pro" . to_owned ( ) ) ,
946- ..Default :: default ( )
947- } ) )
956+ Ok ( Some ( self . runtime_state . lock ( ) . unwrap ( ) . clone ( ) . unwrap_or (
957+ PersistedSessionState {
958+ current_model_id : Some ( "deepseek-v4-pro" . to_owned ( ) ) ,
959+ ..Default :: default ( )
960+ } ,
961+ ) ) )
948962 }
949963 async fn save_runtime_state (
950964 & self ,
@@ -955,6 +969,7 @@ impl IAcpSessionRepository for StubAcpSessionRepo {
955969 conversation_id : conversation_id. to_owned ( ) ,
956970 current_mode_id : params. current_mode_id . map ( |outer| outer. map ( ToOwned :: to_owned) ) ,
957971 current_model_id : params. current_model_id . map ( |outer| outer. map ( ToOwned :: to_owned) ) ,
972+ config_selections_json : params. config_selections_json . map ( |outer| outer. map ( ToOwned :: to_owned) ) ,
958973 } ) ;
959974 Ok ( true )
960975 }
@@ -2528,6 +2543,11 @@ impl MockAgent {
25282543 self
25292544 }
25302545
2546+ fn with_mode ( self , mode : & str ) -> Self {
2547+ * self . mode . lock ( ) . unwrap ( ) = mode. to_owned ( ) ;
2548+ self
2549+ }
2550+
25312551 fn with_set_config_option_response ( self , response : SetConfigOptionResponse ) -> Self {
25322552 * self . set_config_option_response . lock ( ) . unwrap ( ) = Some ( response) ;
25332553 self
@@ -2634,16 +2654,30 @@ impl IMockAgent for MockAgent {
26342654 . lock ( )
26352655 . unwrap ( )
26362656 . push ( ( option_id. to_owned ( ) , value. to_owned ( ) ) ) ;
2657+ if option_id == "mode" {
2658+ * self . mode . lock ( ) . unwrap ( ) = value. to_owned ( ) ;
2659+ }
26372660 if let Some ( error) = self . set_config_option_error . lock ( ) . unwrap ( ) . take ( ) {
26382661 return Err ( error) ;
26392662 }
26402663 if let Some ( response) = self . set_config_option_response . lock ( ) . unwrap ( ) . clone ( ) {
2664+ if let Some ( config_options) = response. config_options . as_ref ( ) {
2665+ let _ = self . event_tx . send ( AgentStreamEvent :: AcpConfigOption ( json ! ( {
2666+ "config_options" : config_options,
2667+ } ) ) ) ;
2668+ }
26412669 return Ok ( response) ;
26422670 }
2643- Ok ( SetConfigOptionResponse {
2671+ let response = SetConfigOptionResponse {
26442672 confirmation : ConfigOptionConfirmation :: Observed ,
26452673 config_options : Some ( self . config_options . lock ( ) . unwrap ( ) . clone ( ) ) ,
2646- } )
2674+ } ;
2675+ if let Some ( config_options) = response. config_options . as_ref ( ) {
2676+ let _ = self . event_tx . send ( AgentStreamEvent :: AcpConfigOption ( json ! ( {
2677+ "config_options" : config_options,
2678+ } ) ) ) ;
2679+ }
2680+ Ok ( response)
26472681 }
26482682}
26492683
@@ -3408,6 +3442,7 @@ async fn run_agent_turn_injects_conversation_runtime_context() {
34083442 content : "run scheduled task" . into ( ) ,
34093443 files : Vec :: new ( ) ,
34103444 inject_skills : Vec :: new ( ) ,
3445+ required_runtime_mode : None ,
34113446 persist_user_message : true ,
34123447 user_message_hidden : true ,
34133448 on_started : None ,
@@ -3583,6 +3618,90 @@ async fn set_config_option_returns_observed_confirmation() {
35833618 ) ;
35843619}
35853620
3621+ #[ tokio:: test]
3622+ async fn save_acp_runtime_mode_updates_runtime_mode_config_selection ( ) {
3623+ let acp_repo = Arc :: new ( StubAcpSessionRepo :: with_runtime_state ( PersistedSessionState {
3624+ current_mode_id : Some ( "read-only" . to_owned ( ) ) ,
3625+ current_model_id : Some ( "gpt-5.3-codex" . to_owned ( ) ) ,
3626+ config_selections_json : Some (
3627+ serde_json:: json!( {
3628+ "mode" : "read-only" ,
3629+ "model" : "gpt-5.3-codex" ,
3630+ "reasoning_effort" : "low"
3631+ } )
3632+ . to_string ( ) ,
3633+ ) ,
3634+ context_usage_json : None ,
3635+ } ) ) ;
3636+ let ( svc, _, _, _) = make_service_with_resolver_and_acp_session_repo (
3637+ Arc :: new ( FixedSkillResolver { names : vec ! [ ] } ) ,
3638+ acp_repo. clone ( ) ,
3639+ ) ;
3640+ svc. save_acp_runtime_mode ( "conv_1" , "full-access" ) . await . unwrap ( ) ;
3641+
3642+ let saves = acp_repo. runtime_state_saves ( ) ;
3643+ assert_eq ! ( saves. len( ) , 1 ) ;
3644+ assert_eq ! ( saves[ 0 ] . current_mode_id, Some ( Some ( "full-access" . to_owned( ) ) ) ) ;
3645+ let selections: serde_json:: Value =
3646+ serde_json:: from_str ( saves[ 0 ] . config_selections_json . as_ref ( ) . unwrap ( ) . as_ref ( ) . unwrap ( ) ) . unwrap ( ) ;
3647+ assert_eq ! ( selections[ "mode" ] , "full-access" ) ;
3648+ assert_eq ! ( selections[ "model" ] , "gpt-5.3-codex" ) ;
3649+ assert_eq ! ( selections[ "reasoning_effort" ] , "low" ) ;
3650+ }
3651+
3652+ #[ tokio:: test]
3653+ async fn run_agent_turn_applies_required_runtime_mode_after_stream_subscription ( ) {
3654+ let task_mgr = Arc :: new ( MockTaskManager :: new ( ) ) ;
3655+ let ( svc, broadcaster, _repo) = make_service_with_mock_task_manager ( task_mgr. clone ( ) ) ;
3656+ let conv = svc. create ( "user_1" , make_create_req ( ) ) . await . unwrap ( ) ;
3657+ broadcaster. take_events ( ) ;
3658+
3659+ let agent = Arc :: new (
3660+ MockAgent :: new ( & conv. id )
3661+ . with_mode ( "yolo" )
3662+ . with_config_options ( vec ! [ AcpConfigOptionDto {
3663+ id: "mode" . to_owned( ) ,
3664+ name: Some ( "Mode" . to_owned( ) ) ,
3665+ label: None ,
3666+ description: None ,
3667+ category: Some ( "mode" . to_owned( ) ) ,
3668+ option_type: "select" . to_owned( ) ,
3669+ current_value: Some ( "yolo" . to_owned( ) ) ,
3670+ options: Vec :: new( ) ,
3671+ } ] ) ,
3672+ ) ;
3673+ task_mgr. insert_agent ( & conv. id , AgentInstance :: Mock ( agent. clone ( ) ) ) ;
3674+
3675+ let outcome = svc
3676+ . run_agent_turn ( ConversationAgentTurnRequest {
3677+ user_id : "user_1" . to_owned ( ) ,
3678+ conversation_id : conv. id . clone ( ) ,
3679+ content : "run scheduled task" . to_owned ( ) ,
3680+ files : Vec :: new ( ) ,
3681+ inject_skills : Vec :: new ( ) ,
3682+ required_runtime_mode : Some ( "yolo" . to_owned ( ) ) ,
3683+ persist_user_message : true ,
3684+ user_message_hidden : true ,
3685+ on_started : None ,
3686+ } )
3687+ . await
3688+ . unwrap ( ) ;
3689+
3690+ assert_eq ! ( outcome. status, ConversationAgentTurnStatus :: Completed ) ;
3691+ assert_eq ! (
3692+ agent. set_config_option_calls. lock( ) . unwrap( ) . as_slice( ) ,
3693+ & [ ( "mode" . to_owned( ) , "yolo" . to_owned( ) ) ]
3694+ ) ;
3695+ let events = broadcaster. take_events ( ) ;
3696+ let config_event = events
3697+ . iter ( )
3698+ . find ( |event| event. name == "message.stream" && event. data [ "type" ] == "acp_config_option" )
3699+ . expect ( "runtime mode switch should broadcast config option snapshot" ) ;
3700+ assert_eq ! ( config_event. data[ "conversation_id" ] , conv. id) ;
3701+ assert_eq ! ( config_event. data[ "data" ] [ "config_options" ] [ 0 ] [ "id" ] , "mode" ) ;
3702+ assert_eq ! ( config_event. data[ "data" ] [ "config_options" ] [ 0 ] [ "current_value" ] , "yolo" ) ;
3703+ }
3704+
35863705#[ tokio:: test]
35873706async fn set_config_option_evicts_task_when_acp_protocol_is_not_connected ( ) {
35883707 let task_mgr = Arc :: new ( MockTaskManager :: new ( ) ) ;
@@ -4304,6 +4423,7 @@ async fn run_agent_turn_returns_error_message_when_agent_build_fails() {
43044423 content : "run scheduled task" . into ( ) ,
43054424 files : Vec :: new ( ) ,
43064425 inject_skills : Vec :: new ( ) ,
4426+ required_runtime_mode : None ,
43074427 persist_user_message : true ,
43084428 user_message_hidden : true ,
43094429 on_started : None ,
0 commit comments