mj_controller/review_host/
environment.rs1use super::*;
2
3pub trait ReviewEnvironment: Send + Sync {
10 fn check(&self, session_id: &str, profile: &str) -> Result<(), String>;
13
14 fn stage(
17 &self,
18 session_id: &str,
19 profile: &str,
20 generation: u64,
21 mcp_servers: &[mj_core::worker_launch::ReviewMcpServer],
22 dispatch_tool: bool,
23 ) -> Result<mj_core::worker_launch::ReviewerLaunchConfig, String>;
24
25 fn load_state(&self, session_id: &str) -> Result<TurnReviewState, String>;
28
29 fn save_state(&self, session_id: &str, state: &TurnReviewState) -> Result<(), String>;
33
34 fn clear_interrupted(&self) -> Result<Vec<String>, String>;
39}
40
41#[derive(Debug, Default)]
47pub struct ControllerEnvironment;
48
49impl ReviewEnvironment for ControllerEnvironment {
50 fn check(&self, session_id: &str, profile: &str) -> Result<(), String> {
51 let controller =
52 crate::controller::Controller::load().map_err(|error| format!("{error:#}"))?;
53 let Some(reviewer) = controller.config.profiles.get(profile) else {
54 return Err(format!(
55 "turn review needs a reviewer: [review] profile {profile:?} is not a profile in config.toml"
56 ));
57 };
58 if !reviewer.enabled {
59 return Err(format!(
60 "turn review needs an enabled reviewer: [review] profile {profile:?} is disabled"
61 ));
62 }
63 validate_reviewer_assignment(
64 session_id,
65 controller.state.sessions.get(session_id),
66 profile,
67 )
68 }
69
70 fn stage(
71 &self,
72 session_id: &str,
73 profile: &str,
74 generation: u64,
75 mcp_servers: &[mj_core::worker_launch::ReviewMcpServer],
76 dispatch_tool: bool,
77 ) -> Result<mj_core::worker_launch::ReviewerLaunchConfig, String> {
78 let controller =
79 crate::controller::Controller::load().map_err(|error| format!("{error:#}"))?;
80 controller
81 .stage_reviewer_profile_with_mcp(
82 session_id,
83 profile,
84 generation,
85 mcp_servers,
86 dispatch_tool,
87 )
88 .map_err(|error| format!("{error:#}"))
89 }
90
91 fn load_state(&self, session_id: &str) -> Result<TurnReviewState, String> {
92 crate::database::turn_review_state(session_id).map_err(|error| format!("{error:#}"))
93 }
94
95 fn save_state(&self, session_id: &str, state: &TurnReviewState) -> Result<(), String> {
96 crate::database::save_turn_review_state(session_id, state)
97 .map_err(|error| format!("{error:#}"))
98 }
99
100 fn clear_interrupted(&self) -> Result<Vec<String>, String> {
101 crate::database::clear_interrupted_turn_reviews().map_err(|error| format!("{error:#}"))
102 }
103}
104
105pub(crate) fn validate_reviewer_assignment(
106 session_id: &str,
107 session: Option<&mj_core::state::SessionRecord>,
108 profile: &str,
109) -> Result<(), String> {
110 let Some(session) = session else {
111 return Err(format!(
112 "session {session_id:?} is not in the controller store"
113 ));
114 };
115 if session.archived {
116 return Err("this session is archived".to_owned());
117 }
118 if session.last_profile == profile {
119 return Err(format!(
120 "turn review profile {profile:?} is also this session's primary profile; choose a different [review] profile"
121 ));
122 }
123 Ok(())
124}