use crate::agent::per_coord::final_plan_gate;
use crate::agent::per_coord::{AfterFinalAssistant, PerCoordinator, PlanRewriteExhaustedReason};
use crate::agent::per_plan_semantic_check::{self, PlanSemanticLlmCtx};
use crate::types::Message;
use crate::agent::agent_turn::params::RunLoopParams;
pub(crate) enum ReflectOnAssistantOutcome {
StopTurn,
ContinueOuterForPlanRewrite,
ProceedToExecuteTools,
PlanRewriteExhausted { reason: PlanRewriteExhaustedReason },
UserCancelled,
}
fn reflect_finish_from_after_final_assistant(
p: &mut RunLoopParams<'_>,
after: AfterFinalAssistant,
) -> ReflectOnAssistantOutcome {
match after {
AfterFinalAssistant::StopTurn => ReflectOnAssistantOutcome::StopTurn,
AfterFinalAssistant::RequestPlanRewrite(m) => {
p.turn.push_message(m);
ReflectOnAssistantOutcome::ContinueOuterForPlanRewrite
}
AfterFinalAssistant::StopTurnPlanRewriteExhausted { reason } => {
ReflectOnAssistantOutcome::PlanRewriteExhausted { reason }
}
AfterFinalAssistant::StopTurnPendingPlanConsistencyLlm { plan, tool_digest } => {
log::error!(
target: "crabmate::agent_turn",
"StopTurnPendingPlanConsistencyLlm reached reflect_finish_from_after_final_assistant; \
scheduling semantic check inline"
);
let _ = (plan, tool_digest);
ReflectOnAssistantOutcome::StopTurn
}
}
}
pub(crate) async fn per_reflect_after_assistant(
p: &mut RunLoopParams<'_>,
per_coord: &mut PerCoordinator,
finish_reason: &str,
msg: &Message,
) -> ReflectOnAssistantOutcome {
p.turn.sub_phase = crate::agent::agent_turn::AgentTurnSubPhase::Reflect;
if finish_reason == "tool_calls" || msg.tool_calls.as_ref().is_some_and(|c| !c.is_empty()) {
return ReflectOnAssistantOutcome::ProceedToExecuteTools;
}
match per_coord.after_final_assistant(
msg,
p.turn.messages(),
p.ctx.core.cfg.as_ref(),
p.ctx.core.workspace_is_set,
) {
AfterFinalAssistant::StopTurnPendingPlanConsistencyLlm { plan, tool_digest } => {
reflect_pending_semantic_consistency_llm(p, per_coord, plan, tool_digest).await
}
other => reflect_finish_from_after_final_assistant(p, other),
}
}
async fn reflect_pending_semantic_consistency_llm(
p: &mut RunLoopParams<'_>,
per_coord: &mut PerCoordinator,
plan: crate::agent::plan_artifact::AgentReplyPlanV1,
tool_digest: Option<String>,
) -> ReflectOnAssistantOutcome {
let plan_json = per_plan_semantic_check::agent_reply_plan_json_compact(&plan);
let outcome = per_plan_semantic_check::evaluate_plan_consistency_with_recent_tools_llm(
PlanSemanticLlmCtx {
llm_backend: p.ctx.core.llm_backend,
client: p.ctx.core.client,
api_key: p.ctx.core.api_key,
cfg: p.ctx.core.cfg.as_ref(),
out: p.ctx.io.control.out,
no_stream: p.ctx.io.no_stream,
cancel: p.ctx.io.cancel.as_deref(),
request_chrome_trace: p.ctx.obs.request_chrome_trace.clone(),
temperature_override: p.turn.temperature_override,
model_override: p.turn.model_override.clone(),
seed_override: p.turn.seed_override,
max_tokens: p
.ctx
.core
.cfg
.per_plan_policy
.final_plan_semantic_check_max_tokens,
turn_budget: Some(&p.turn.turn_budget),
},
plan_json.as_str(),
tool_digest.as_deref(),
)
.await;
if outcome.user_cancelled {
return ReflectOnAssistantOutcome::UserCancelled;
}
let sem_outcome = final_plan_gate::run_final_plan_gate_semantic_completed(
&outcome,
per_coord.plan_rewrite_attempts_snapshot(),
per_coord.plan_rewrite_max_attempts_limit(),
);
tracing::debug!(
target: "crabmate::agent_turn",
gate_route = ?sem_outcome.route,
gate_phase = ?final_plan_gate::FinalPlanGatePhase::PendingSemanticLlm,
sub_phase = "reflect",
"final_plan_gate semantic transition"
);
final_plan_gate::apply_plan_rewrite_count_from_gate(per_coord, &sem_outcome);
match sem_outcome.after {
AfterFinalAssistant::StopTurnPendingPlanConsistencyLlm { .. } => {
log::error!(
target: "crabmate::agent_turn",
"run_final_plan_gate_semantic_completed returned StopTurnPendingPlanConsistencyLlm; \
stopping turn defensively"
);
ReflectOnAssistantOutcome::StopTurn
}
other => reflect_finish_from_after_final_assistant(p, other),
}
}