summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYurenHao0426 <blackhao0426@gmail.com>2026-02-14 01:31:53 +0000
committerYurenHao0426 <blackhao0426@gmail.com>2026-02-14 01:31:53 +0000
commit51b5208b9b5ac51b7e531ddeb59093c1cb381491 (patch)
tree2a7800198d936b9384b9dfc017edc385ab8e5cf2
parentdbe8f68940e93e465f03b90f6b98e053f5a75391 (diff)
Rework debate prompts, add turn-based rounds and self-elimination convergence
- Round 1 parallel (form positions), Round 2+ sequential turn-based (each model sees current round's prior responses before speaking) - Prompts treat user question as open-ended, not as a debate proposition — models form their own thesis in Round 1 then defend it - Structured opposition: each model defends own unique position, not FOR/AGAINST - Self-convergence: each model judges if convinced after each round, eliminated models drop out, last one standing wins and provides final summary - Skip convergence/judge checks on Round 1 (still forming positions) - Auto-save now triggers on councilData/debateData updates so intermediate rounds persist across page refresh Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--backend/app/services/debate.py412
-rw-r--r--frontend/src/components/Sidebar.tsx32
-rw-r--r--frontend/src/store/flowStore.ts5
3 files changed, 331 insertions, 118 deletions
diff --git a/backend/app/services/debate.py b/backend/app/services/debate.py
index d409cb9..7ae25f3 100644
--- a/backend/app/services/debate.py
+++ b/backend/app/services/debate.py
@@ -16,6 +16,27 @@ def _sse_event(data: dict) -> str:
return f"data: {json.dumps(data)}\n\n"
+def _format_history(debate_history: List[Dict[str, Any]]) -> str:
+ """Format debate history into readable text."""
+ text = ""
+ for past_round in debate_history:
+ rn = past_round["round"]
+ text += f"\n--- Round {rn} ---\n"
+ for resp in past_round["responses"]:
+ text += f"\n[{resp['model']}]:\n{resp['response']}\n"
+ return text
+
+
+def _format_own_position(debate_history: List[Dict[str, Any]], model_name: str) -> str:
+ """Extract this model's own previous responses across rounds."""
+ parts = []
+ for past_round in debate_history:
+ for resp in past_round["responses"]:
+ if resp["model"] == model_name:
+ parts.append(f"Round {past_round['round']}:\n{resp['response']}")
+ return "\n\n".join(parts)
+
+
def build_debate_prompt(
user_query: str,
debate_history: List[Dict[str, Any]],
@@ -25,78 +46,121 @@ def build_debate_prompt(
custom_prompt: Optional[str] = None,
model_index: int = 0,
total_models: int = 2,
+ current_round_so_far: Optional[List[Dict[str, Any]]] = None,
) -> str:
- """Build the prompt for a debater based on format and history."""
- history_text = ""
- if debate_history:
- for past_round in debate_history:
- rn = past_round["round"]
- history_text += f"\n--- Round {rn} ---\n"
- for resp in past_round["responses"]:
- history_text += f"\n[{resp['model']}]:\n{resp['response']}\n"
+ """
+ Build the prompt for a debater based on format and history.
+
+ Key principle: The user's question is an open-ended question, NOT a debate
+ proposition. In Round 1, each model independently answers the question to
+ form their own thesis. In Round 2+, models see others' positions and debate
+ — defending their own viewpoint and critiquing others with evidence.
+
+ For Round 2+, models are queried sequentially. current_round_so_far contains
+ responses from models that have already spoken this round.
+ """
+ history_text = _format_history(debate_history) if debate_history else ""
+ own_position = _format_own_position(debate_history, model_name) if debate_history else ""
+
+ # Format current round's earlier responses (turn-based context)
+ current_round_text = ""
+ if current_round_so_far:
+ current_round_text = f"\n--- Round {round_num} (so far) ---\n"
+ for resp in current_round_so_far:
+ current_round_text += f"\n[{resp['model']}]:\n{resp['response']}\n"
if debate_format == DebateFormat.FREE_DISCUSSION:
if round_num == 1:
return (
- f"You are participating in a roundtable discussion about the following question:\n\n"
- f'"{user_query}"\n\n'
- f"Provide your perspective and answer to this question."
+ f"You are about to participate in a multi-model debate. "
+ f"First, independently answer the following question. "
+ f"Your answer will become your position in the debate.\n\n"
+ f'Question: "{user_query}"\n\n'
+ f"Provide a thorough, well-reasoned answer. This is your initial position."
)
return (
- f"You are participating in a roundtable discussion about the following question:\n\n"
- f'"{user_query}"\n\n'
- f"Here is the discussion so far:\n{history_text}\n\n"
- f"This is round {round_num}. Consider what others have said, respond to their points, "
- f"and refine or defend your position."
+ f'You are in a multi-model debate about the question:\n"{user_query}"\n\n'
+ f"Your position so far:\n{own_position}\n\n"
+ f"Previous rounds:\n{history_text}\n"
+ f"{current_round_text}\n"
+ f"This is round {round_num}. It is now your turn to speak. "
+ f"Review what the other participants have argued (including anyone who has "
+ f"already spoken this round). Defend your position where you believe you are "
+ f"right, acknowledge good points from others, and strengthen your argument "
+ f"with additional evidence or reasoning. "
+ f"You may refine your position but should not abandon it without strong justification."
)
if debate_format == DebateFormat.STRUCTURED_OPPOSITION:
- roles = ["FOR", "AGAINST", "DEVIL'S ADVOCATE", "MEDIATOR", "CRITIC", "SYNTHESIZER"]
- role = roles[model_index % len(roles)]
if round_num == 1:
return (
- f"You are arguing {role} the following position in a structured debate:\n\n"
- f'"{user_query}"\n\n'
- f"Present your strongest arguments from the {role} perspective."
+ f"You are Debater #{model_index + 1} in a structured multi-model debate. "
+ f"First, independently answer the following question. "
+ f"Your answer will be YOUR unique position that you must defend.\n\n"
+ f'Question: "{user_query}"\n\n'
+ f"Provide a thorough, well-reasoned answer. Take a clear, distinctive stance."
)
return (
- f"You are arguing {role} the following position in a structured debate:\n\n"
- f'"{user_query}"\n\n'
- f"Debate history:\n{history_text}\n\n"
- f"This is round {round_num}. Respond to the other participants' arguments "
- f"while maintaining your {role} position. Address their strongest points."
+ f'You are Debater #{model_index + 1} in a structured multi-model debate.\n'
+ f'Question: "{user_query}"\n\n'
+ f"Your position so far:\n{own_position}\n\n"
+ f"Previous rounds:\n{history_text}\n"
+ f"{current_round_text}\n"
+ f"This is round {round_num}. It is now your turn. Your task:\n"
+ f"1. Defend YOUR position with concrete arguments and evidence\n"
+ f"2. Directly critique each other debater's position — point out flaws, "
+ f"gaps, or weaker reasoning in what they have said so far\n"
+ f"3. Explain why your answer is superior to theirs\n"
+ f"Be persuasive and specific. Reference the other debaters' actual claims."
)
if debate_format == DebateFormat.ITERATIVE_IMPROVEMENT:
if round_num == 1:
return (
- f"You are participating in an iterative improvement exercise on the following question:\n\n"
- f'"{user_query}"\n\n'
- f"Provide your best answer."
+ f"You are participating in an iterative improvement exercise. "
+ f"First, independently answer the following question with your best effort.\n\n"
+ f'Question: "{user_query}"\n\n'
+ f"Provide a thorough, well-reasoned answer."
)
return (
- f"You are participating in an iterative improvement exercise on the following question:\n\n"
- f'"{user_query}"\n\n'
- f"Here are the previous answers from all participants:\n{history_text}\n\n"
- f"This is round {round_num}. Critique the other participants' answers, identify flaws or gaps, "
- f"and provide an improved answer that incorporates the best insights from everyone."
+ f'You are in an iterative improvement exercise.\n'
+ f'Question: "{user_query}"\n\n'
+ f"Your previous answer:\n{own_position}\n\n"
+ f"All participants' previous answers:\n{history_text}\n"
+ f"{current_round_text}\n"
+ f"This is round {round_num}. Review ALL other participants' answers carefully "
+ f"(including those who have already spoken this round). "
+ f"Identify their best ideas, strongest arguments, and any insights you missed. "
+ f"Also identify flaws or gaps in their reasoning. "
+ f"Now produce an improved version of YOUR answer that incorporates the best "
+ f"insights from everyone while fixing any weaknesses."
)
if debate_format == DebateFormat.CUSTOM and custom_prompt:
prompt = custom_prompt
prompt = prompt.replace("{history}", history_text or "(No history yet)")
+ prompt = prompt.replace("{own_position}", own_position or "(No position yet)")
+ prompt = prompt.replace("{current_round}", current_round_text or "(You are first to speak)")
prompt = prompt.replace("{round}", str(round_num))
prompt = prompt.replace("{model_name}", model_name)
prompt = prompt.replace("{question}", user_query)
+ prompt = prompt.replace("{debater_number}", str(model_index + 1))
return prompt
# Fallback to free discussion
if round_num == 1:
- return f'Provide your answer to the following question:\n\n"{user_query}"'
+ return (
+ f"Answer the following question thoroughly. "
+ f"Your answer will be your position in a multi-model debate.\n\n"
+ f'Question: "{user_query}"'
+ )
return (
f'Question: "{user_query}"\n\n'
- f"Previous discussion:\n{history_text}\n\n"
- f"Round {round_num}: Provide your updated response."
+ f"Your position so far:\n{own_position}\n\n"
+ f"Previous discussion:\n{history_text}\n"
+ f"{current_round_text}\n"
+ f"Round {round_num}: It is your turn. Defend and refine your position, "
+ f"responding to what others have argued."
)
@@ -113,13 +177,22 @@ async def debate_round(
openrouter_api_key: Optional[str] = None,
images: Optional[List[Dict[str, Any]]] = None,
) -> AsyncGenerator[Dict[str, Any], None]:
- """Query all debate models in parallel for one round, yielding as each completes."""
-
- async def _query_one(idx: int, config: LLMConfig) -> Dict[str, Any]:
+ """
+ Query debate models for one round.
+ Round 1: all models in parallel (independent initial positions).
+ Round 2+: sequential turn-based (each model sees prior models' responses
+ from the current round before responding).
+ """
+
+ async def _query_one(
+ idx: int, config: LLMConfig,
+ current_round_so_far: Optional[List[Dict[str, Any]]] = None,
+ ) -> Dict[str, Any]:
prompt = build_debate_prompt(
user_prompt, debate_history, config.model_name,
round_num, debate_format, custom_prompt,
model_index=idx, total_models=len(configs),
+ current_round_so_far=current_round_so_far,
)
atts = attachments_per_model[idx] if attachments_per_model else None
tls = tools_per_model[idx] if tools_per_model else None
@@ -128,20 +201,29 @@ async def debate_round(
context, prompt, config,
attachments=atts, tools=tls,
openrouter_api_key=openrouter_api_key,
- images=images if round_num == 1 else None, # Only send images in round 1
+ images=images if round_num == 1 else None,
)
return {"model": config.model_name, "response": response}
except Exception as e:
logger.error("Debate round %d failed for %s: %s", round_num, config.model_name, e)
return {"model": config.model_name, "response": f"[Error: {e}]"}
- tasks = {
- asyncio.ensure_future(_query_one(i, cfg)): i
- for i, cfg in enumerate(configs)
- }
- for coro in asyncio.as_completed(tasks.keys()):
- result = await coro
- yield result
+ if round_num == 1:
+ # Round 1: parallel — all models form positions independently
+ tasks = {
+ asyncio.ensure_future(_query_one(i, cfg)): i
+ for i, cfg in enumerate(configs)
+ }
+ for coro in asyncio.as_completed(tasks.keys()):
+ result = await coro
+ yield result
+ else:
+ # Round 2+: sequential turn-based — each model sees current round context
+ current_round_responses: List[Dict[str, Any]] = []
+ for i, cfg in enumerate(configs):
+ result = await _query_one(i, cfg, current_round_so_far=current_round_responses)
+ current_round_responses.append(result)
+ yield result
async def judge_evaluate_round(
@@ -160,15 +242,18 @@ async def judge_evaluate_round(
history_text += f"\n[{resp['model']}]:\n{resp['response']}\n"
prompt = (
- f"You are the judge of a multi-model debate on the following question:\n"
- f'"{user_query}"\n\n'
- f"Debate history (Round 1 to {last_round}):\n{history_text}\n\n"
- f"Evaluate whether the debate has reached a satisfactory conclusion.\n"
- f"Consider: Have the key points been thoroughly explored? Is there consensus?\n"
- f"Are there unresolved disagreements worth continuing?\n\n"
+ f"You are the judge of a multi-model debate. Each model answered the question "
+ f"independently and is now defending their position.\n\n"
+ f'Original question: "{user_query}"\n\n'
+ f"Debate transcript (Round 1 to {last_round}):\n{history_text}\n\n"
+ f"Evaluate whether continuing the debate would produce meaningful new insights.\n"
+ f"Consider:\n"
+ f"- Are the participants still raising substantive new arguments?\n"
+ f"- Has one position clearly emerged as strongest, or are there still valid competing views?\n"
+ f"- Would another round help clarify remaining disagreements?\n\n"
f"Respond with exactly one of:\n"
- f"CONTINUE - if the debate should go on (explain why briefly)\n"
- f"STOP - if a clear conclusion has been reached (explain why briefly)"
+ f"CONTINUE - if there are still productive arguments to be made (explain why briefly)\n"
+ f"STOP - if the debate has been thorough enough for a final verdict (explain why briefly)"
)
empty_context = Context(messages=[])
@@ -184,36 +269,88 @@ async def judge_evaluate_round(
return {"continue": False, "reasoning": f"[Judge error: {e}]"}
-async def check_self_convergence(
- configs: List[LLMConfig],
- round_responses: List[Dict[str, Any]],
+async def check_model_convinced(
+ config: LLMConfig,
+ debate_history: List[Dict[str, Any]],
+ user_query: str,
+ other_model_names: List[str],
openrouter_api_key: Optional[str] = None,
) -> Dict[str, Any]:
- """Check if debate responses have converged using the first available model."""
- responses_text = "\n\n".join(
- f"[{r['model']}]:\n{r['response']}" for r in round_responses
- )
+ """
+ Ask a single model whether it has been convinced by another participant.
+ Returns {"convinced": bool, "convinced_by": str|None, "reasoning": str}.
+ """
+ history_text = _format_history(debate_history)
+ own_position = _format_own_position(debate_history, config.model_name)
+
prompt = (
- f"Below are the responses from the latest round of a debate:\n\n"
- f"{responses_text}\n\n"
- f"Do all participants essentially agree on the answer? Respond ONLY with:\n"
- f"CONVERGED - if there is clear consensus\n"
- f"DIVERGENT - if there are still significant disagreements"
+ f"You are {config.model_name}, a participant in a multi-model debate.\n\n"
+ f'Original question: "{user_query}"\n\n'
+ f"Your position across rounds:\n{own_position}\n\n"
+ f"Full debate transcript:\n{history_text}\n\n"
+ f"After reviewing the debate so far, honestly evaluate:\n"
+ f"Has another participant made arguments strong enough to convince you "
+ f"that their answer is better than yours?\n\n"
+ f"Respond in EXACTLY this format:\n"
+ f"CONVINCED: <model name> - if you concede to another participant's position\n"
+ f"NOT CONVINCED - if you still believe your position is strongest\n"
+ f"Then briefly explain why.\n\n"
+ f"Other participants: {', '.join(other_model_names)}\n"
+ f"Be intellectually honest. If someone made a clearly stronger argument, "
+ f"acknowledge it."
)
empty_context = Context(messages=[])
- # Use the first config as the convergence checker
- check_config = configs[0]
try:
response = await query_model_full(
- empty_context, prompt, check_config,
+ empty_context, prompt, config,
openrouter_api_key=openrouter_api_key,
)
- converged = "CONVERGED" in response.upper().split("\n")[0]
- return {"converged": converged, "reasoning": response}
+ first_line = response.strip().split("\n")[0].upper()
+ if "CONVINCED:" in first_line or (first_line.startswith("CONVINCED") and "NOT" not in first_line):
+ # Parse who convinced them
+ convinced_by = None
+ raw = response.strip().split("\n")[0]
+ if ":" in raw:
+ candidate = raw.split(":", 1)[1].strip().rstrip(".")
+ for mn in other_model_names:
+ if mn.lower() in candidate.lower() or candidate.lower() in mn.lower():
+ convinced_by = mn
+ break
+ return {"convinced": True, "convinced_by": convinced_by, "reasoning": response}
+ return {"convinced": False, "convinced_by": None, "reasoning": response}
except Exception as e:
- logger.error("Convergence check failed: %s", e)
- return {"converged": False, "reasoning": f"[Convergence check error: {e}]"}
+ logger.error("Conviction check failed for %s: %s", config.model_name, e)
+ return {"convinced": False, "convinced_by": None,
+ "reasoning": f"[Check error: {e}]"}
+
+
+async def winner_final_summary(
+ winner_config: LLMConfig,
+ debate_history: List[Dict[str, Any]],
+ user_query: str,
+ openrouter_api_key: Optional[str] = None,
+) -> AsyncGenerator[str, None]:
+ """Stream a final summary from the winning model in self-convergence mode."""
+ history_text = _format_history(debate_history)
+
+ prompt = (
+ f"You participated in a multi-model debate and your position was judged "
+ f"the strongest.\n\n"
+ f'Original question: "{user_query}"\n\n'
+ f"Full debate transcript:\n{history_text}\n\n"
+ f"Now provide a comprehensive final answer to the original question. "
+ f"Incorporate the best insights and valid points raised by other participants "
+ f"during the debate, while maintaining the core of your position. "
+ f"This should be a polished, definitive answer — not a debate response."
+ )
+
+ empty_context = Context(messages=[])
+ async for chunk in llm_streamer(
+ empty_context, prompt, winner_config,
+ openrouter_api_key=openrouter_api_key,
+ ):
+ yield chunk
async def judge_final_verdict(
@@ -231,14 +368,17 @@ async def judge_final_verdict(
history_text += f"\n[{resp['model']}]:\n{resp['response']}\n"
prompt = (
- f"You are the judge of a multi-model debate. Below is the full debate transcript.\n\n"
- f'Question: "{user_query}"\n\n'
- f"{history_text}\n\n"
+ f"You are the judge of a multi-model debate. Each model independently answered "
+ f"the question and then debated to defend their position.\n\n"
+ f'Original question: "{user_query}"\n\n'
+ f"Full debate transcript:\n{history_text}\n\n"
f"As the judge, provide:\n"
- f"1. A summary of the key arguments from each participant\n"
- f"2. An evaluation of the strengths and weaknesses of each position\n"
- f"3. Your final verdict: the best, most accurate, and most comprehensive answer "
- f"to the original question, synthesizing the best insights from the debate."
+ f"1. Each participant's core position and how it evolved through the debate\n"
+ f"2. The strengths and weaknesses of each position, noting which arguments "
+ f"were effectively challenged and which stood up to scrutiny\n"
+ f"3. Your final verdict: synthesize the best answer to the original question, "
+ f"drawing from the strongest arguments and evidence presented across all participants. "
+ f"Clearly explain which positions or insights you drew from and why."
)
empty_context = Context(messages=[])
@@ -278,15 +418,25 @@ async def debate_event_stream(
debate_history: List[Dict[str, Any]] = []
+ # Track active participants (for self_convergence elimination)
+ active_indices = list(range(len(member_configs)))
+
for round_num in range(1, max_rounds + 1):
- yield _sse_event({"type": "round_start", "data": {"round": round_num}})
+ active_configs = [member_configs[i] for i in active_indices]
+ active_atts = [attachments_per_model[i] if attachments_per_model else None for i in active_indices] if attachments_per_model else None
+ active_tools = [tools_per_model[i] if tools_per_model else None for i in active_indices] if tools_per_model else None
+
+ yield _sse_event({"type": "round_start", "data": {
+ "round": round_num,
+ "active_models": [c.model_name for c in active_configs],
+ }})
round_responses: List[Dict[str, Any]] = []
async for result in debate_round(
- member_configs, context, user_prompt,
+ active_configs, context, user_prompt,
debate_history, round_num, debate_format, custom_format_prompt,
- attachments_per_model=attachments_per_model,
- tools_per_model=tools_per_model,
+ attachments_per_model=active_atts,
+ tools_per_model=active_tools,
openrouter_api_key=openrouter_api_key,
images=images,
):
@@ -310,8 +460,8 @@ async def debate_event_stream(
})
return
- # Check stop condition (skip on last round)
- if round_num < max_rounds:
+ # Check stop condition (skip Round 1 — still forming positions, and last round)
+ if round_num >= 2 and round_num < max_rounds:
if judge_mode == DebateJudgeMode.EXTERNAL_JUDGE and judge_config:
decision = await judge_evaluate_round(
judge_config, debate_history, user_prompt,
@@ -325,19 +475,57 @@ async def debate_event_stream(
break
elif judge_mode == DebateJudgeMode.SELF_CONVERGENCE:
- convergence = await check_self_convergence(
- member_configs, round_responses,
- openrouter_api_key=openrouter_api_key,
- )
+ # Ask each active model if it's been convinced
+ other_names = [c.model_name for c in active_configs]
+ conviction_tasks = []
+ for cfg in active_configs:
+ others = [n for n in other_names if n != cfg.model_name]
+ conviction_tasks.append(
+ check_model_convinced(
+ cfg, debate_history, user_prompt, others,
+ openrouter_api_key=openrouter_api_key,
+ )
+ )
+ results = await asyncio.gather(*conviction_tasks)
+
+ # Process eliminations
+ eliminated_this_round = []
+ for cfg, result in zip(active_configs, results):
+ if result["convinced"]:
+ eliminated_this_round.append(cfg.model_name)
+ yield _sse_event({
+ "type": "model_eliminated",
+ "data": {
+ "round": round_num,
+ "model": cfg.model_name,
+ "convinced_by": result.get("convinced_by"),
+ "reasoning": result["reasoning"],
+ },
+ })
+
+ # Remove eliminated models from active list
+ if eliminated_this_round:
+ active_indices = [
+ i for i in active_indices
+ if member_configs[i].model_name not in eliminated_this_round
+ ]
+
+ remaining = [member_configs[i].model_name for i in active_indices]
yield _sse_event({
- "type": "convergence_check",
- "data": {"round": round_num, **convergence},
+ "type": "convergence_status",
+ "data": {
+ "round": round_num,
+ "eliminated": eliminated_this_round,
+ "remaining": remaining,
+ },
})
- if convergence["converged"]:
+
+ # If only one model left, debate is over
+ if len(active_indices) <= 1:
break
# DISPLAY_ONLY: just continue to next round
- # Final synthesis
+ # === Final synthesis ===
if judge_mode == DebateJudgeMode.EXTERNAL_JUDGE and judge_config:
yield _sse_event({
"type": "final_start",
@@ -357,15 +545,27 @@ async def debate_event_stream(
"data": {"model": judge_config.model_name, "response": full_verdict},
})
- elif judge_mode == DebateJudgeMode.SELF_CONVERGENCE:
- # Use the last round's responses as the final answer
- last_responses = debate_history[-1]["responses"] if debate_history else []
- # Pick the longest response as the "best" convergent answer
- if last_responses:
- best = max(last_responses, key=lambda r: len(r.get("response", "")))
- yield _sse_event({
- "type": "final_complete",
- "data": {"model": best["model"], "response": best["response"]},
- })
+ elif judge_mode == DebateJudgeMode.SELF_CONVERGENCE and debate_history:
+ # Winner = last remaining model
+ winner_cfg = member_configs[active_indices[0]] if active_indices else member_configs[0]
+ winner_name = winner_cfg.model_name
+
+ yield _sse_event({
+ "type": "final_start",
+ "data": {"model": winner_name},
+ })
+
+ full_summary = ""
+ async for chunk in winner_final_summary(
+ winner_cfg, debate_history, user_prompt,
+ openrouter_api_key=openrouter_api_key,
+ ):
+ full_summary += chunk
+ yield _sse_event({"type": "final_chunk", "data": {"chunk": chunk}})
+
+ yield _sse_event({
+ "type": "final_complete",
+ "data": {"model": winner_name, "response": full_summary},
+ })
yield _sse_event({"type": "debate_complete"})
diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx
index aeb164b..8bb5fcb 100644
--- a/frontend/src/components/Sidebar.tsx
+++ b/frontend/src/components/Sidebar.tsx
@@ -810,20 +810,32 @@ const Sidebar: React.FC<SidebarProps> = ({ isOpen, onToggle, onInteract }) => {
}
break;
}
- case 'convergence_check': {
+ case 'model_eliminated': {
const lastRound2 = debateRounds[debateRounds.length - 1];
if (lastRound2) {
- lastRound2.converged = evt.data.converged;
- updateNodeData(runningNodeId, {
- debateData: {
- rounds: [...debateRounds],
- finalVerdict: null,
- config: { judgeMode, format: debateFormat, maxRounds },
- },
+ if (!lastRound2.eliminated) lastRound2.eliminated = [];
+ lastRound2.eliminated.push({
+ model: evt.data.model,
+ convincedBy: evt.data.convinced_by,
+ reasoning: evt.data.reasoning,
});
}
- if (evt.data.converged) {
- setDebateStage('Consensus reached!');
+ setDebateStage(`${evt.data.model} concedes to ${evt.data.convinced_by || 'another'}...`);
+ break;
+ }
+ case 'convergence_status': {
+ const remaining = evt.data.remaining as string[];
+ updateNodeData(runningNodeId, {
+ debateData: {
+ rounds: [...debateRounds],
+ finalVerdict: null,
+ config: { judgeMode, format: debateFormat, maxRounds },
+ },
+ });
+ if (remaining.length <= 1) {
+ setDebateStage(`${remaining[0] || 'Winner'} is the last one standing!`);
+ } else {
+ setDebateStage(`${remaining.length} models remaining...`);
}
break;
}
diff --git a/frontend/src/store/flowStore.ts b/frontend/src/store/flowStore.ts
index c1c8b99..5bb3e22 100644
--- a/frontend/src/store/flowStore.ts
+++ b/frontend/src/store/flowStore.ts
@@ -95,7 +95,7 @@ export interface DebateRound {
round: number;
responses: Array<{ model: string; response: string }>;
judgeDecision?: { continue: boolean; reasoning: string };
- converged?: boolean;
+ eliminated?: Array<{ model: string; convincedBy: string | null; reasoning: string }>;
}
export interface DebateData {
@@ -1063,7 +1063,8 @@ const useFlowStore = create<FlowState>((set, get) => {
}
// Trigger auto-save for important changes (not just userPrompt typing)
- if (data.response !== undefined || data.status !== undefined || data.attachedFileIds !== undefined) {
+ if (data.response !== undefined || data.status !== undefined || data.attachedFileIds !== undefined
+ || data.councilData !== undefined || data.debateData !== undefined) {
get().triggerAutoSave();
}
},