use super::*;
pub(super) fn prompt_command(prompt: String) -> RelayCommand {
RelayCommand::Prompt {
prompt: vec![agent_client_protocol::schema::v1::ContentBlock::Text(
agent_client_protocol::schema::v1::TextContent::new(prompt),
)],
}
}
pub(super) async fn reviewer_action(
control: &SessionManagerControl,
session_id: &str,
role: Option<String>,
action: ReviewerAction,
) -> Result<ReviewerOutcome, String> {
let handle: ManagedSessionHandle = control
.session(session_id.to_owned())
.await
.map_err(|error| format!("{error:#}"))?;
handle
.reviewer_as(role, action)
.await
.map_err(|error| format!("{error:#}"))
}
pub(super) async fn launch_role(
control: &SessionManagerControl,
environment: &Arc<dyn ReviewEnvironment>,
session_id: &str,
role: &str,
reviewer: &ReviewerIdentity,
generation: u64,
repositories: &[PathBuf],
) -> Result<(), String> {
let lane = mj_review::lanes::lane_by_id(role).is_some();
let mcp_servers = if role == INTENT_ROLE {
Vec::new()
} else {
mj_review::bifrost::review_mcp_servers(
repositories,
if lane {
mj_review::lanes::LANE_BIFROST_TOOLSET
} else {
mj_review::lanes::SUPERVISOR_BIFROST_TOOLSET
},
)
};
let dispatch_tool = role == SUPERVISOR_ROLE;
let staged = {
let session_id = session_id.to_owned();
let profile = reviewer.profile.clone();
let environment = environment.clone();
tokio::task::spawn_blocking(move || {
environment.stage(
&session_id,
&profile,
generation,
&mcp_servers,
dispatch_tool,
)
})
.await
.map_err(|error| format!("staging the reviewer stopped: {error}"))??
};
let mut config = staged;
config.model = reviewer.model.clone();
config.effort = reviewer.effort.clone();
match reviewer_action(
control,
session_id,
Some(role.to_owned()),
ReviewerAction::Start {
config: Box::new(config),
},
)
.await
{
Ok(ReviewerOutcome::Started(_)) => Ok(()),
other => Err(unexpected(other)),
}
}
pub(super) async fn prepare(
control: &SessionManagerControl,
environment: &Arc<dyn ReviewEnvironment>,
session_id: &str,
reviewer: &ReviewerIdentity,
tier: ReviewTier,
) -> Result<Prepared, StartRefusal> {
let profile = reviewer.profile.clone();
let session = session_id.to_owned();
let environment = environment.clone();
let checked = tokio::task::spawn_blocking(move || -> Result<TurnReviewState, String> {
environment.check(&session, &profile)?;
environment.load_state(&session)
})
.await
.map_err(|error| StartRefusal(format!("preparing the review stopped: {error}")))?;
let state = checked.map_err(StartRefusal)?;
let handle = control
.session(session_id.to_owned())
.await
.map_err(|error| StartRefusal(format!("{error:#}")))?;
match handle.reviewer(ReviewerAction::Status).await {
Ok(ReviewerOutcome::Status(state)) if state.active_prompt.is_some() => {
return Err(StartRefusal(
"the reviewer is busy with a second opinion".to_owned(),
));
}
Ok(_) => {}
Err(error) => return Err(StartRefusal(format!("{error:#}"))),
}
let view = handle.view();
if !view.connected {
return Err(StartRefusal("this session is not connected".to_owned()));
}
let Some(snapshot) = view.snapshot else {
return Err(StartRefusal(
"this session has no transcript yet".to_owned(),
));
};
if !matches!(
snapshot.materialized.execution,
MaterializedExecutionState::Idle
) {
return Err(StartRefusal(
"a review runs between turns; this one is still working".to_owned(),
));
}
if !snapshot.materialized.queued_prompts.is_empty() {
return Err(StartRefusal(
"prompts are queued; the review waits for them".to_owned(),
));
}
Ok(Prepared {
state,
reviewer: reviewer.clone(),
tier,
materialized: Box::new(snapshot.materialized),
resume_forward: None,
})
}
pub(super) async fn prepare_recovery(
control: &SessionManagerControl,
environment: &Arc<dyn ReviewEnvironment>,
session_id: &str,
) -> Result<Option<Prepared>, String> {
let session = session_id.to_owned();
let environment = environment.clone();
let state = tokio::task::spawn_blocking(move || environment.load_state(&session))
.await
.map_err(|error| format!("loading the pending review handoff stopped: {error}"))??;
let Some(pending) = state.pending_forward.clone() else {
return Ok(None);
};
let handle = control
.session(session_id.to_owned())
.await
.map_err(|error| format!("{error:#}"))?;
let view = handle.view();
if !view.connected {
return Err("the primary session is not connected".to_owned());
}
let Some(snapshot) = view.snapshot else {
return Err("the primary session has no transcript yet".to_owned());
};
if !matches!(
snapshot.materialized.execution,
MaterializedExecutionState::Idle
) {
return Err("the primary session is still working".to_owned());
}
if !snapshot.materialized.queued_prompts.is_empty() {
return Err("prompts are queued; the pending handoff waits for them".to_owned());
}
Ok(Some(Prepared {
state,
reviewer: ReviewerIdentity {
profile: String::new(),
model: None,
effort: None,
},
tier: ReviewTier::Quick,
materialized: Box::new(snapshot.materialized),
resume_forward: Some(pending),
}))
}
#[must_use]
pub fn resolution_notice(
phase: &TurnReviewPhase,
last_verdict: Option<&ReviewVerdict>,
) -> Option<String> {
let TurnReviewPhase::Resolved(resolution) = phase else {
return None;
};
Some(match resolution {
Resolution::Forwarded => "Review findings sent to the agent".to_owned(),
Resolution::Dismissed => match last_verdict {
Some(ReviewVerdict::Clean) => "Review complete: no material findings".to_owned(),
Some(ReviewVerdict::Failed { .. }) => {
"Review failed; the change stays unreviewed".to_owned()
}
_ => "Review dismissed".to_owned(),
},
Resolution::Cancelled => match last_verdict {
Some(ReviewVerdict::Failed { .. }) => {
"Review failed; the change stays unreviewed".to_owned()
}
_ => "Review cancelled".to_owned(),
},
Resolution::NothingToReview => "Nothing to review: the turn changed no files".to_owned(),
Resolution::CoverageStarted => {
"Review coverage starts here; the next completed turn is reviewed".to_owned()
}
})
}
pub(super) fn seed_from_session(
session: &MaterializedSession,
tier: ReviewTier,
state: &TurnReviewState,
_trigger: &str,
) -> TurnReviewSeed {
let reviewed_through = state.reviewed_through_ordinal;
let mut task = String::new();
let mut user_messages = Vec::new();
let mut initial_result = String::new();
let mut trajectory = Vec::new();
for item in &session.transcript {
match &item.body {
mj_core::state::TranscriptBody::User { content } => {
let text = mj_core::transcript::materialized_content_text(content);
let text = text.trim();
if text.is_empty() {
continue;
}
if mj_core::second_opinion::is_control_origin_prompt(text) {
continue;
}
task = text.to_owned();
user_messages.push(UserMessage::prompt(text));
if item.position > reviewed_through {
trajectory.push(format!("user: {text}"));
}
}
mj_core::state::TranscriptBody::Agent { chunks, .. } => {
if !item.is_nonempty_agent_message() {
continue;
}
let text = mj_core::transcript::materialized_chunks_text(chunks);
let text = text.trim();
if text.is_empty() {
continue;
}
initial_result = text.to_owned();
if item.position > reviewed_through {
trajectory.push(format!("agent: {text}"));
}
}
mj_core::state::TranscriptBody::Tool { call, .. } => {
let title = call
.get("title")
.and_then(serde_json::Value::as_str)
.unwrap_or_default()
.trim();
if item.position > reviewed_through && !title.is_empty() {
trajectory.push(format!("tool: {title}"));
}
}
_ => {}
}
}
TurnReviewSeed {
tier,
task,
user_messages,
initial_result,
trajectory: trajectory.join("\n"),
baselines: state.baselines.clone(),
through_ordinal: session.applied_event_ordinal,
prior_review: state.prior_review.clone(),
}
}