crabmate 0.5.0

Rust AI agent: OpenAI-compatible chat/completions, function calling, HTTP serve, ops CLI
Documentation
//! R 步:终答阶段(规划校验、是否继续外层循环等)。

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;

/// R:模型本轮若为最终文本(非 tool_calls),决定是否结束或追加重写提示。
pub(crate) enum ReflectOnAssistantOutcome {
    /// 结束 `run_agent_turn` 外层循环
    StopTurn,
    /// 已写入重写 user 消息,应继续外层循环再次请求模型
    ContinueOuterForPlanRewrite,
    /// 进入工具执行阶段
    ProceedToExecuteTools,
    /// 规划重写次数用尽(已尝试发 SSE 错误码 `plan_rewrite_exhausted` + `reason_code`)
    PlanRewriteExhausted { reason: PlanRewriteExhaustedReason },
    /// 侧向语义一致性 LLM 或主路径检测到用户取消
    UserCancelled,
}

/// 将 [`AfterFinalAssistant`] 映射为外环 [`ReflectOnAssistantOutcome`](**不**修改 `PerCoordinator` 计数;
/// 计数由 [`final_plan_gate::after_final_assistant`] 或语义路径上的
/// [`final_plan_gate::apply_plan_rewrite_count_from_gate`] 先行写入)。
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"
            );
            // 不应同步到达此分支;防御性回落为停轮,避免 panic。
            let _ = (plan, tool_digest);
            ReflectOnAssistantOutcome::StopTurn
        }
    }
}

/// 在已将 assistant 推入 `messages` 之后调用,决定是执行工具、终答结束还是规划重写。
///
/// **兼容**:部分 OpenAI 兼容实现在返回 `tool_calls` 时仍上报 `finish_reason: "stop"` 或空串。
/// 若仅判断 `finish_reason == "tool_calls"`,会误判为终答并 `StopTurn`,历史中留下未执行的
/// `tool_calls`、缺对应 `role: tool`,下一轮易 400,且本轮无任何工具执行。故 **非空 `tool_calls`**
/// 同样进入执行分支。
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),
    }
}

/// **`PendingSemanticLlm`**:侧向一致性 LLM → [`final_plan_gate::run_final_plan_gate_semantic_completed`] →
/// [`final_plan_gate::apply_plan_rewrite_count_from_gate`](与静态终答路径一致)→ 映射为外环结果。
///
/// **扩展点(观众角色,未实现)**:锚点 C — 静态规划通过 / 本侧向检查旁;挂此处即可,勿新开阶段机。
/// 见 `docs/design/audience_critic_role.md`、`docs/design/run_loop_state_ownership.md`。
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),
    }
}