Adversarial multi-model debate engine -- pit LLMs against each other, detect hollow consensus, and produce cryptographic decision receipts. Zero required dependencies.
Project description
aragora-debate
Adversarial multi-model debate engine with decision receipts. Zero required dependencies.
One model's opinion isn't enough when the decision matters. aragora-debate
orchestrates structured adversarial debates across multiple LLM providers,
detects consensus, tracks dissent, and produces cryptographic decision receipts.
Part of the Aragora Decision Integrity Platform. This standalone package gives you the debate engine with no framework lock-in.
Why adversarial debate?
| Problem | What debate does |
|---|---|
| LLMs fail at compositional reasoning (Song et al., 2026) | Multiple models cross-check each other's reasoning |
| Models are overconfident when wrong | Structured critique surfaces hidden weaknesses |
| Agreement between models ≠ correctness | Heterogeneous models surface genuine uncertainty |
| No audit trail for AI-assisted decisions | Cryptographic decision receipts with dissent tracking |
Multi-agent debate achieves +13.8 percentage points accuracy over single-model baselines (Harrasse et al., 2024) and significantly reduces hallucinations (Du et al., 2023).
Install
pip install aragora-debate
# With provider SDKs
pip install aragora-debate[anthropic] # Claude
pip install aragora-debate[openai] # GPT
pip install aragora-debate[mistral] # Mistral
pip install aragora-debate[gemini] # Gemini
pip install aragora-debate[all] # All providers
Quick start
import asyncio
from aragora_debate import Arena, Agent, DebateConfig, Critique, Vote, Message
# 1. Implement agents by wrapping your preferred LLM
class MyAgent(Agent):
async def generate(self, prompt: str, context: list[Message] | None = None) -> str:
return await call_my_llm(self.model, prompt) # your LLM call
async def critique(self, proposal: str, task: str, **kw) -> Critique:
resp = await call_my_llm(self.model, f"Critique this proposal:\n{proposal}")
return Critique(agent=self.name, target_agent=kw.get("target_agent", ""),
target_content=proposal, issues=[resp], severity=5.0)
async def vote(self, proposals: dict[str, str], task: str) -> Vote:
best = await call_my_llm(self.model, f"Pick the best:\n{proposals}")
return Vote(agent=self.name, choice=best, reasoning="Most thorough analysis")
# 2. Run a debate
async def main():
agents = [
MyAgent("claude", model="claude-sonnet-4-5-20250929"),
MyAgent("gpt4", model="gpt-4o"),
MyAgent("mistral", model="mistral-large-latest"),
]
arena = Arena(
question="Should we migrate from REST to GraphQL?",
agents=agents,
config=DebateConfig(rounds=3, consensus_method="supermajority"),
)
result = await arena.run()
print(result.summary())
print(result.receipt.to_markdown())
asyncio.run(main())
How it works
Round 1 Round 2 Round 3
┌──────────┐ ┌──────────┐ ┌──────────┐
│ PROPOSE │ │ PROPOSE │ │ PROPOSE │
│ All agents│ │ Address │ │ Final │
│ respond │ │ critiques │ │ positions │
├──────────┤ ├──────────┤ ├──────────┤
│ CRITIQUE │ │ CRITIQUE │ │ CRITIQUE │
│ Challenge │ │ Deeper │ │ Last │
│ each other│ │ analysis │ │ challenges│
├──────────┤ ├──────────┤ ├──────────┤
│ VOTE │ │ VOTE │ │ VOTE │
│ Weighted │ │ May stop │ │ Final │
│ votes │ │ if agreed │ │ tally │
└──────────┘ └──────────┘ └──────────┘
│
┌─────▼──────┐
│ RECEIPT │
│ Consensus, │
│ dissent, │
│ signature │
└────────────┘
Each round has three phases:
- Propose — Every agent generates an independent response
- Critique — Each agent challenges every other agent's reasoning
- Vote — Agents vote on which proposal is strongest
Early stopping kicks in when consensus is reached before all rounds complete. The final decision receipt captures who agreed, who dissented and why, the confidence level, and a cryptographic signature for audit purposes.
Evidence quality & hollow consensus detection
Not all agreement is meaningful. aragora-debate detects when models agree
without substantive evidence — a pattern called hollow consensus.
from aragora_debate import EvidenceQualityAnalyzer, HollowConsensusDetector
analyzer = EvidenceQualityAnalyzer()
score = analyzer.analyze("According to a 2024 Gartner report, 73% of enterprises...")
print(f"Evidence quality: {score.overall:.2f}") # 0.0–1.0
# Detect hollow consensus across agents
detector = HollowConsensusDetector()
alert = detector.check(
responses={"claude": "I agree, this is great", "gpt4": "I also think it's great"},
convergence_similarity=0.95,
)
if alert.detected:
print(f"Hollow consensus: {alert.challenge}")
The Trickster system uses this automatically during debates to inject challenge prompts when it detects agents converging without evidence:
from aragora_debate import Debate, create_agent
debate = Debate(
"Should we adopt microservices?",
enable_trickster=True, # Inject challenges on hollow consensus
enable_convergence=True, # Track proposal similarity across rounds
trickster_sensitivity=0.7, # Higher = more interventions
)
debate.add_agent(create_agent("mock", name="analyst"))
debate.add_agent(create_agent("mock", name="critic"))
result = await debate.run()
print(f"Trickster interventions: {result.trickster_interventions}")
print(f"Convergence detected: {result.convergence_detected}")
Events & callbacks
Monitor debates in real-time with the event system:
from aragora_debate import EventEmitter, EventType
def on_event(event):
print(f"[{event.event_type.value}] round={event.round_num} {event.data}")
debate = Debate("Should we use Kafka?", on_event=on_event)
# Events: debate_start, round_start, proposal, critique, vote,
# consensus_check, convergence_detected, trickster_intervention,
# round_end, debate_end
Cross-proposal analysis
Analyze evidence patterns across all proposals:
from aragora_debate import CrossProposalAnalyzer
analyzer = CrossProposalAnalyzer()
analysis = analyzer.analyze({
"claude": "According to Smith (2024), Kafka handles 1M msgs/sec...",
"gpt4": "RabbitMQ has better delivery guarantees per Jones (2023)...",
})
print(f"Shared evidence: {len(analysis.shared_evidence)}")
print(f"Contradictions: {len(analysis.contradictions)}")
print(f"Evidence gaps: {len(analysis.evidence_gaps)}")
print(f"Weakest agent: {analysis.weakest_agent}")
Decision receipts
Every debate produces a DecisionReceipt — an auditable artifact:
# Decision Receipt DR-20260211-a3f8c2
**Question:** Should we migrate from REST to GraphQL?
**Verdict:** Approved With Conditions
**Confidence:** 78%
**Consensus:** Reached (supermajority, 75% agreement)
**Agents:** claude, gpt4, mistral
## Dissenting Views
**mistral:**
- Caching complexity underestimated for existing CDN infrastructure
> REST+BFF achieves 80% of benefits with 20% of migration risk
Export as Markdown, JSON, or HTML. Sign with HMAC-SHA256 for tamper detection.
from aragora_debate import ReceiptBuilder
# Sign for audit compliance
ReceiptBuilder.sign_hmac(result.receipt, key="your-signing-key")
# Export
print(ReceiptBuilder.to_json(result.receipt))
with open("receipt.html", "w") as f:
f.write(ReceiptBuilder.to_html(result.receipt))
Consensus methods
| Method | Threshold | Use when |
|---|---|---|
majority |
>50% | General decisions |
supermajority |
>66.7% | Important decisions |
unanimous |
100% | Safety-critical decisions |
weighted |
Configurable | When agent reliability varies |
judge |
N/A | One agent decides after hearing debate |
Configuration
DebateConfig(
rounds=3, # Number of debate rounds
consensus_method="supermajority", # How consensus is determined
consensus_threshold=0.6, # For weighted consensus
early_stopping=True, # Stop when consensus reached
early_stop_threshold=0.85, # Confidence to trigger early stop
min_rounds=1, # Minimum rounds before early stop
timeout_seconds=300, # Overall timeout (0 = none)
require_reasoning=True, # Agents must explain votes
)
When to use adversarial debate
Good fit:
- Architecture decisions ("Kafka vs RabbitMQ?")
- Compliance reviews ("Does this meet SOC 2?")
- Risk assessments ("What are the risks of this vendor?")
- Strategy decisions ("Should we enter market X?")
- Security reviews ("Is this auth model sound?")
Not a good fit:
- Simple lookups ("What's the capital of France?")
- Creative generation ("Write me a poem")
- Real-time responses (debate takes seconds to minutes)
Rule of thumb: If the decision is worth a meeting, it's worth a debate.
Built-in providers
| Provider | Class | Install | Default model |
|---|---|---|---|
| Anthropic | ClaudeAgent |
pip install aragora-debate[anthropic] |
claude-sonnet-4-5-20250929 |
| OpenAI | OpenAIAgent |
pip install aragora-debate[openai] |
gpt-4o |
| Mistral | MistralAgent |
pip install aragora-debate[mistral] |
mistral-large-latest |
GeminiAgent |
pip install aragora-debate[gemini] |
gemini-2.0-flash |
|
| Mock | MockAgent |
(included) | N/A |
Use the factory for quick setup:
from aragora_debate import create_agent
agents = [
create_agent("anthropic", name="analyst"),
create_agent("openai", name="challenger"),
create_agent("mistral", name="devil-advocate"),
]
Extending
Custom agents
Implement Agent.generate(), Agent.critique(), and Agent.vote():
class ClaudeAgent(Agent):
def __init__(self, name: str = "claude"):
super().__init__(name, model="claude-sonnet-4-5-20250929")
import anthropic
self.client = anthropic.AsyncAnthropic()
async def generate(self, prompt, context=None):
resp = await self.client.messages.create(
model=self.model,
max_tokens=2048,
system=self.system_prompt or "You are a careful analyst.",
messages=[{"role": "user", "content": prompt}],
)
return resp.content[0].text
async def critique(self, proposal, task, **kw):
prompt = f"Task: {task}\n\nProposal to critique:\n{proposal}\n\nIdentify issues and suggest improvements."
resp = await self.generate(prompt)
return Critique(
agent=self.name,
target_agent=kw.get("target_agent", "unknown"),
target_content=proposal,
issues=[resp],
severity=5.0,
)
async def vote(self, proposals, task):
formatted = "\n\n".join(f"**{name}:** {text}" for name, text in proposals.items())
prompt = f"Task: {task}\n\nProposals:\n{formatted}\n\nWhich is strongest? Reply with just the agent name."
choice = await self.generate(prompt)
return Vote(agent=self.name, choice=choice.strip(), reasoning="Best analysis")
Accessing debate history
result = await arena.run()
# Full message history
for msg in result.messages:
print(f"[Round {msg.round}] {msg.agent} ({msg.role}): {msg.content[:100]}")
# All critiques
for c in result.critiques:
print(f"{c.agent} → {c.target_agent}: severity {c.severity}/10")
# Vote breakdown
for v in result.votes:
print(f"{v.agent} voted for {v.choice} (confidence: {v.confidence})")
Regulatory compliance
Decision receipts help satisfy:
- EU AI Act Art. 12 — Automatic record-keeping
- EU AI Act Art. 13 — Transparent, interpretable output
- EU AI Act Art. 14 — Human oversight (review before acting)
- SOC 2 CC6.1 — Audit logging
- HIPAA 164.312(b) — Audit controls
See EU_AI_ACT_COMPLIANCE.md for the full mapping.
Full platform
aragora-debate is the standalone debate engine extracted from the
Aragora Decision Integrity Platform.
The full platform adds 30+ agent types, knowledge management, enterprise auth,
compliance frameworks, and the Nomic Loop for autonomous self-improvement.
License
MIT -- see LICENSE for details.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aragora_debate-0.2.0.tar.gz.
File metadata
- Download URL: aragora_debate-0.2.0.tar.gz
- Upload date:
- Size: 76.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
246d06167b5d167e30a5c975e8d373d9469bba6ed92338042d1ae5c97638a317
|
|
| MD5 |
6d2212e52ba6823a4373cd6ffa21b0b4
|
|
| BLAKE2b-256 |
1984b85376cb781c97ce840efbb5755cfdda98458d55dd068b272315f18713f0
|
File details
Details for the file aragora_debate-0.2.0-py3-none-any.whl.
File metadata
- Download URL: aragora_debate-0.2.0-py3-none-any.whl
- Upload date:
- Size: 51.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
43733820af0ca1dbd4dc4e68d5b9b6d852378352f4e94281feaf2c6c3e4982e1
|
|
| MD5 |
5fced8dd58eed612e8841e1d941e9d31
|
|
| BLAKE2b-256 |
a2f5f6f4d8e0efa99dba985e6a569160d1f110aeeb33fe00a64617872a2f4219
|