use std::fmt::Write as _;
use crate::{ChatMessage, ChatRequest, ChatRequestMeta, Role, Workspace};
pub(crate) const DEFAULT_REVIEW_COUNT_LOW_CHURN: u64 = 500;
pub(crate) const DEFAULT_REVIEW_COUNT_HIGH_CHURN: u64 = 2000;
pub(crate) const MAX_BOUNCES: usize = 10;
pub(crate) struct JointVerdict<'a> {
pub agent_index: usize,
pub verdict: &'a crate::Verdict,
}
pub(crate) struct JointFailure {
pub agent_index: usize,
pub dump: String,
}
pub(crate) struct JointRound<'a> {
pub stage: &'a str,
pub dispatched: usize,
pub verdicts: Vec<JointVerdict<'a>>,
pub failures: Vec<JointFailure>,
pub header: String,
pub threshold: u8,
}
impl JointRound<'_> {
#[must_use]
pub fn n_valid(&self) -> usize {
self.verdicts.len()
}
}
#[must_use]
pub(crate) fn issues_by_agent(round: &JointRound<'_>) -> Vec<Vec<String>> {
let mut by_agent: Vec<Vec<String>> = vec![Vec::new(); round.dispatched];
for v in &round.verdicts {
by_agent[v.agent_index].clone_from(&v.verdict.issues_detected);
}
by_agent
}
const PIPELINE_GROUPING_MAX_TOKENS: u32 = 16_000;
fn synthesis_request(round: &JointRound<'_>, role: Role, ws: &Workspace) -> ChatRequest {
let system = format!(
"{}\n\n{}",
crate::prompt::load_prompt("synthesis/synthesis.md"),
crate::prompt::load_prompt("synthesis/grouping_contradictions.md"),
);
let material = crate::consensus::numbered_items_material(&issues_by_agent(round));
let user = format!(
"{}\n\nStage: {}\nAgent issues (id-numbered):\n{}",
crate::prompt::load_prompt("synthesis/synthesis_input.md"),
round.stage,
material,
);
let model = crate::config::CONFIG.role_model(role);
let routing = crate::config::CONFIG.model_routing(&model);
ChatRequest {
messages: vec![ChatMessage::system(&system), ChatMessage::user(&user)],
tools: None,
model,
allow_image_parts: false,
max_tokens: Some(PIPELINE_GROUPING_MAX_TOKENS),
reasoning_effort: Some(
crate::role::role_info(&role)
.default_reasoning_effort
.to_string(),
),
provider_order: routing.provider_order,
meta: Some(ChatRequestMeta {
purpose: "synthesis",
agent_id: format!("joint_verdict_{}", crate::generate_suffix()),
role: role.as_str().to_string(),
workspace: ws.name.clone(),
ticket_id: None,
}),
}
}
pub(crate) async fn build_joint_comment(
round: &JointRound<'_>,
role: Role,
ws: &Workspace,
ticket_id: &str,
ticket_title: &str,
) -> String {
let items = issues_by_agent(round);
let outcome = run_synthesis(round, role, ws, ticket_id, ticket_title).await;
render_joint_comment(round, &outcome, &crate::consensus::ItemTable::new(&items))
}
pub(crate) async fn run_synthesis(
round: &JointRound<'_>,
role: Role,
ws: &Workspace,
ticket_id: &str,
ticket_title: &str,
) -> crate::consensus::RepairOutcome {
let request = synthesis_request(round, role, ws);
let items = issues_by_agent(round);
crate::consensus::run_grouping_repair(
ws,
"synthesis",
request,
&items,
Some(crate::registry::ParentKey::Ticket(ticket_id.to_string())),
Some(ticket_title.to_string()),
)
.await
}
#[must_use]
pub(crate) fn render_joint_comment(
round: &JointRound<'_>,
outcome: &crate::consensus::RepairOutcome,
table: &crate::consensus::ItemTable<'_>,
) -> String {
let mut out = String::new();
if !round.header.is_empty() {
out.push_str(&round.header);
}
let has_issues = table.len() > 0;
if has_issues {
match outcome {
crate::consensus::RepairOutcome::Repaired { output, references } => {
for group in &output.groups {
let _ = write!(out, "\n\n**{}**", group.heading);
if group.contradiction {
out.push_str(" — DISPUTED");
}
for member in &group.members {
let _ = write!(out, "\n- {}", member_text(table, member));
}
}
out.push_str(&crate::consensus::render_ungrouped_section(
output,
references,
|member, disputed| member_text(table, member) + disputed,
));
}
crate::consensus::RepairOutcome::Fallback => {
out.push_str("\n\n**Issues**");
for id in 0..table.len() {
if let Some((_, text)) = table.resolve(id) {
let _ = write!(out, "\n- {text}");
}
}
}
}
}
match outcome {
crate::consensus::RepairOutcome::Repaired { output, .. } => {
out.push_str("\n\n### Summary");
let summary = output.summary.trim();
if summary.is_empty() {
out.push_str("\nLLM summary unavailable — deterministic member render.");
} else {
let _ = write!(out, "\n{summary}");
}
}
crate::consensus::RepairOutcome::Fallback => {
if has_issues {
out.push_str(
"\n\n### Summary\nLLM grouping unavailable — deterministic member dump only.",
);
} else {
let clean = round.failures.is_empty()
&& round
.verdicts
.iter()
.all(|v| v.verdict.score >= round.threshold);
let summary = if clean {
format!(
"\n\n### Summary\nNo issues found — all {} agents passed clean.",
round.n_valid()
)
} else if round.n_valid() > 0 {
"\n\n### Summary\nNo issues found by the responding agents.".to_string()
} else {
"\n\n### Summary\nNo issues to merge — no agents produced a verdict."
.to_string()
};
out.push_str(&summary);
}
}
}
if !round.failures.is_empty() {
out.push_str("\n\n### Agent failures");
for f in &round.failures {
let _ = write!(out, "\n- Agent {}: {}", f.agent_index + 1, f.dump);
}
}
crate::util::truncate_sandwich(
&crate::util::scrub_credentials(out.trim_start_matches('\n')),
crate::util::FAILURE_DETAIL_CAP,
"joint verdict comment",
)
}
fn member_text(
table: &crate::consensus::ItemTable<'_>,
member: &crate::consensus::GroupingMember,
) -> String {
table.resolve(member.id).map_or_else(
|| format!("<unknown item id {}>", member.id),
|(_, text)| text.to_string(),
)
}
#[must_use]
pub(crate) fn review_base_from_signals(total_churn: i64, low_churn: i64, high_churn: i64) -> usize {
if total_churn <= low_churn {
2
} else if total_churn > high_churn {
4
} else {
3
}
}
#[must_use]
pub(crate) fn review_agent_count(base: usize, priority: i64) -> usize {
if priority == 0 { base.max(3) } else { base }
}
#[must_use]
pub(crate) fn analysis_escalation_needed(
results: &[crate::management::ParallelVerdict],
dispatched: usize,
) -> bool {
results.len() == dispatched
&& results.iter().all(|r| {
matches!(
r,
crate::management::ParallelVerdict::Verdict(v)
if v.score < crate::management::ANALYST_PASS_THRESHOLD
)
})
}
#[cfg(test)]
#[path = "joint_verdict_tests.rs"]
mod tests;