Skip to main content

Convergence Detection

This document describes how Aragora detects when agent positions have converged, enabling early termination of debates.

Overview​

Convergence detection identifies when further debate rounds would provide diminishing returns. It uses semantic similarity between consecutive rounds combined with advanced metrics for nuanced analysis.

Similarity Backends​

The system uses a 3-tier fallback for computing text similarity:

BackendAccuracyDependencySpeed
SentenceTransformerBestsentence-transformersSlower
TF-IDFGoodscikit-learnFast
JaccardBasicNoneFastest

Backend Selection​

Automatic selection (best available):

detector = ConvergenceDetector()  # Auto-selects

Manual override via environment:

export ARAGORA_CONVERGENCE_BACKEND=tfidf  # or: jaccard, sentence

SentenceTransformer Backend​

Uses all-MiniLM-L6-v2 model for semantic embeddings:

  • Captures meaning, not just word overlap
  • Handles paraphrasing and synonyms
  • Caches embeddings per debate (scoped by debate_id)

TF-IDF Backend​

Uses term frequency-inverse document frequency:

  • Fast for medium-length texts
  • Good for technical content with consistent terminology
  • No GPU required

Jaccard Backend​

Simple token overlap:

similarity = len(tokens_a & tokens_b) / len(tokens_a | tokens_b)
  • Always available (zero dependencies)
  • Works well for short, keyword-dense text

Convergence Thresholds​

StatusMin SimilarityInterpretation
converged>= 85%Agents have reached agreement
refining40-85%Still improving positions
diverging< 40%Positions are splitting

Consecutive Rounds​

Convergence requires consecutive_rounds_needed stable rounds (default: 1).

Convergence Result​

@dataclass
class ConvergenceResult:
converged: bool # Should debate terminate?
status: str # "converged", "refining", "diverging"
min_similarity: float # Lowest agent similarity
avg_similarity: float # Average across agents
per_agent_similarity: dict # Per-agent breakdown
consecutive_stable_rounds: int # Stability counter

Algorithm​

For each round (after min_rounds_before_check):

  1. Get common agents between current and previous round
  2. Compute pairwise similarity for each agent's responses
  3. Calculate min and avg similarity scores
  4. Determine status:
    • If min_similarity >= convergence_threshold: increment stable counter
    • If stable counter >= consecutive_rounds_needed: converged = True
    • If min_similarity < divergence_threshold: reset counter, diverging
    • Otherwise: reset counter, refining
def check_convergence(current, previous, round_num):
if round_num <= min_rounds_before_check:
return None

common_agents = current.keys() & previous.keys()
similarities = {
agent: backend.compute_similarity(current[agent], previous[agent])
for agent in common_agents
}

min_sim = min(similarities.values())

if min_sim >= 0.85:
consecutive_stable_count += 1
if consecutive_stable_count >= consecutive_rounds_needed:
return ConvergenceResult(converged=True, status="converged", ...)
elif min_sim < 0.40:
consecutive_stable_count = 0
return ConvergenceResult(converged=False, status="diverging", ...)
else:
consecutive_stable_count = 0
return ConvergenceResult(converged=False, status="refining", ...)

Advanced Metrics​

For deeper analysis, AdvancedConvergenceAnalyzer computes additional signals:

Argument Diversity​

Measures how different the arguments are across agents:

@dataclass
class ArgumentDiversityMetric:
unique_arguments: int # Arguments with &lt;70% similarity to others
total_arguments: int
diversity_score: float # unique / total (0-1)

Interpretation:

  • High diversity (>0.7): Agents exploring different points
  • Low diversity (<0.3): Arguments converging

Evidence Convergence​

Measures citation overlap between agents:

@dataclass
class EvidenceConvergenceMetric:
shared_citations: int # Citations used by 2+ agents
total_citations: int
overlap_score: float # shared / total (0-1)

Interpretation:

  • High overlap (>0.6): Agents citing same sources (strong agreement)
  • Low overlap: Different evidence bases

Stance Volatility​

Tracks how often agents change positions:

@dataclass
class StanceVolatilityMetric:
stance_changes: int # Position flips across rounds
total_responses: int
volatility_score: float # changes / responses (0-1)

Stances detected: support, oppose, neutral, mixed

Interpretation:

  • Low volatility (<0.2): Stable positions
  • High volatility: Ongoing disagreement

Overall Convergence Score​

The weighted combination:

def compute_overall_score():
weights = {
"semantic": 0.4,
"diversity": 0.2,
"evidence": 0.2,
"stability": 0.2
}

score = semantic_similarity * 0.4
score += (1 - diversity_score) * 0.2 # Lower diversity = more converged
score += evidence_overlap_score * 0.2
score += (1 - volatility_score) * 0.2 # Lower volatility = more converged

return clamp(score, 0, 1)

Configuration​

detector = ConvergenceDetector(
convergence_threshold=0.85, # When to consider converged
divergence_threshold=0.40, # When to consider diverging
min_rounds_before_check=1, # Don't check too early
consecutive_rounds_needed=1, # Stable rounds required
debate_id="debate-123" # For scoped embedding cache
)

Embedding Cache​

For performance, embeddings are cached per debate:

# Scoped cache (recommended)
cache = get_scoped_embedding_cache(debate_id)

# Cleanup after debate
cleanup_embedding_cache(debate_id)

# Global reset (for testing)
reset_embedding_cache()

Usage Example​

from aragora.debate.convergence import ConvergenceDetector

detector = ConvergenceDetector(debate_id="debate-001")

# After each round
result = detector.check_convergence(
current_responses={"claude": "...", "gpt4": "..."},
previous_responses={"claude": "...", "gpt4": "..."},
round_number=3
)

if result and result.converged:
print(f"Debate converged! Avg similarity: {result.avg_similarity:.2%}")