agent-council
Run a structured multi-agent debate by spawning a panel of expert agents on any question, with convergence-aware iteration and typed synthesis output via the `agent-council` CLI. Use when a decision has genuine tradeoffs, high stakes, or hidden assumptions worth adversarial colla
Install
npx skills add https://github.com/magnus919/agent-skills/tree/main/agent-council
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install magnus919-agent-skills@llmmart
git clone https://github.com/magnus919/agent-skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole magnus919/agent-skills collection as a plugin from our marketplace. Git is the plain clone.
README
Agent Council
Multi-agent structured debate system — spawn a panel of expert agents to debate any question with convergence-aware iteration.
pip install agent-council
agent-council "Should we migrate from SQLite to Postgres?"
Quick Start
export AGENT_COUNCIL_API_KEY="sk-..."
export AGENT_COUNCIL_MODEL="openai:gpt-5.6-luna"
agent-council "Should we use WebSockets or SSE for real-time notifications?"
Features
- Structured debate protocol — compose, premortem, position, cross-examine (iterative), synthesis
- Convergence-aware iteration — the protocol measures confidence dispersion and stops when diminishing returns set in, not at a hardcoded round count
- Typed outputs — every phase produces validated Pydantic models, consumable as JSON or human-readable markdown
- Custom personas — supply your own agent definitions, or let the compose phase generate them from the question
- Convergence diagnostics — confidence dispersion, position overlap, argument novelty — surfaced in every synthesis report
- Cross-platform — works with any AI harness that supports agentskills.io skills (Claude Code, Cursor, Hermes Agent, OpenHands, etc.)
Installation
pip install pydantic-ai
pip install agent-council
Or install from the skill directory:
pip install -e /path/to/agent-council/
Pip and wheel installs include generated and user-supplied personas, but not the hermes-profiles library. To use real bundled profiles, start from a recursive source checkout.
Usage
# Quick debate (3 agents, 1 cross-examine round)
agent-council --mode quick "Should we use Postgres or SQLite?"
# Standard debate (5 agents, iterative cross-examination)
agent-council "What architecture should we choose for this service?"
# Deep debate (7 agents, full protocol with assumption mapping)
agent-council --mode deep --agents 7 "Should we migrate to microservices?"
# With custom personas
agent-council --persona-file personas.json "Evaluate our cloud strategy"
# JSON output for programmatic consumption
agent-council --json "Which cloud provider should we choose?"
Output
The synthesis report includes:
- Confidence dispersion table — agent-by-agent confidence before and after debate
- Shared risks — failure modes identified in the pre-mortem (before positional commitment)
- Shared concerns — what survived cross-examination as genuine shared risk
- Genuine disagreements — positions that remained unresolved after debate
- Assumptions per position — what would need to be true for each position to be correct
- Principal's path — narrative synthesis of the decision landscape
Configuration
| Env var | Required | Default | Description |
|---|---|---|---|
AGENT_COUNCIL_API_KEY |
Yes | — | API key for your LLM provider |
AGENT_COUNCIL_MODEL |
No | openai:gpt-5.6-luna |
Model string in provider:model format (e.g. deepseek:deepseek-v4-flash) |
AGENT_COUNCIL_BASE_URL |
No | Provider default | Custom API endpoint (e.g. https://api.deepseek.com/v1) |
License
MIT
Why Install This Skill
This skill packages practical, reusable guidance for this domain so you can move from a real task to a dependable result without rebuilding the workflow each time.
What You Get
A focused workflow in SKILL.md, with the referenced scripts, templates, and supporting material available when the task needs them.
Triggers
Use this skill for the task types and keywords described in its SKILL.md description.
Requirements
Check the compatibility requirements in SKILL.md before using commands or integrations from this skill.
Skill manifest
Agent Council
Spawn a panel of expert agents to debate any question. The council runs a structured protocol — compose, premortem, position, cross-examination (iterative), synthesis — and produces a decision landscape with convergence diagnostics.
When to Use
Invoke the council when any of these apply:
- The question has genuine tradeoffs with no clear correct answer
- You want multi-perspective analysis to surface hidden assumptions
- A decision would benefit from adversarial collaboration
- You want confidence diagnostics (not just a recommendation)
- The question has high stakes or irreversible consequences
Signal phrases: "Let's get multiple perspectives on this" / "Debate this: X" / "What would experts say about X" / "What are we missing?"
Quick Start
1. Install
# One-time setup
pip install pydantic-ai
pip install agent-council
# Or install from this skill directory:
python3 scripts/bootstrap.py
2. Configure
export AGENT_COUNCIL_API_KEY="sk-..."
export AGENT_COUNCIL_MODEL="openai:gpt-5.6-luna"
3. Run
agent-council "Should we use Postgres or SQLite for this service?"
Command Reference
agent-council [OPTIONS] <question>
Options:
--agents, -n {3,4,5,6,7} Number of agents (default: 5)
--mode, -m {quick,medium,deep} Debate depth (default: medium)
--profiles TEXT Comma-separated profile names from the hermes-profiles
library (e.g. "debugger,researcher,product-manager")
--persona-file PATH JSON file with custom agent personas
--json Output structured JSON instead of markdown
--verbose, -v Show phase-by-phase progress
--max-rounds INTEGER Max cross-examination rounds (default: 4)
--convergence FLOAT Convergence threshold (default: 0.10)
Mode Selection
| Mode | Agents | Rounds | When to use |
|---|---|---|---|
quick |
3 | 1 cross-examine round | Low-stakes check, fast answer needed |
medium (default) |
5 | Eval-driven, up to 4 rounds | Standard decisions |
deep |
7 | Eval-driven, up to 4 rounds | High-stakes, hidden assumptions |
Profile Selection
In a recursive source checkout, the council auto-selects relevant real professional profiles from the included hermes-profiles library. Each profile has a SOUL.md — an identity document with real methodology, values, and operating principles — rather than a fabricated persona. Pip and wheel installs do not bundle that library; use generated or user-supplied personas instead.
Auto-selection
When no --profiles flag is given, the council scores each profile's description against your question using keyword overlap. The top N most relevant profiles are selected. This works best for focused, single-domain questions.
Explicit selection
agent-council --profiles debugger,data-scientist,product-manager "What architecture should we choose?"
Comma-separated profile names. Available profiles include: ceo, cfo, cmo, coo, cpo, cto, curator, data-architect, data-engineer, data-scientist, debugger, editor, frontend-engineer, ml-engineer, orchestrator, product-manager, researcher, reviewer, security-engineer, site-reliability-engineer, technical-architect, technical-writer, ux-designer, verifier, wonderer, writer, and more.
Choosing a Profile Source
Three ways to populate the council, with different tradeoffs:
| Method | Best for | Diversity | Setup |
|---|---|---|---|
--profiles (auto-select) |
Single-domain questions with clear keywords | High — profiles have real SOUL.md methodology | Recursive source checkout required |
--profiles name1,name2 |
Targeted debates where you know the stakeholders | Highest — you pick specific methodological voices | Recursive source checkout and profile names |
--persona-file file.json |
Full control over agent identities, custom domains | Variable — depends on how you design them | Create a JSON file |
| Auto (no flag) | Default — uses profiles if available, falls back to generated | Good — varies with available profiles | No setup for generated personas; recursive source checkout for real profiles |
For most cases, let it auto-select or use --profiles with 3-5 names. Only use --persona-file when you need specific invented expertise that doesn't map to any existing profile.
Custom personas (fallback)
If the profile library is unavailable or you want full control, use --persona-file to supply your own persona definitions. If neither --profiles nor --persona-file is provided, the council auto-selects profiles from the library; if the library is missing, it falls back to LLM-generated personas.
How It Works
Pipeline
Compose ──► Premortem ──► Position ──► Cross-examine ──► [eval] ──► Synthesis
(1) (parallel) (parallel) (iterative loop) ↑ (1)
┌── converged ──────┐
├── diminishing_ret │
eval ───────────┼── genuine_disagr──┼──► Synthesis
└── continue ───────┘
↓
Cross-examine (next round)
Phases
| Phase | What happens | Method |
|---|---|---|
| Compose | A single LLM call generates N expert personas tuned to the question | 1 call |
| Premortem | Each agent independently imagines how the decision already failed — bypasses positional commitment bias | N parallel calls |
| Position | Each agent forms an independent position, referencing their own premortem | N parallel calls |
| Cross-examine | Each agent reads all other positions and responds — concedes, disagrees, updates confidence | N parallel calls per round |
| Eval | Convergence detection: measures dispersion, argument novelty, concession rate. Decides whether to loop or stop | Algorithmic |
| Synthesis | Collates all phases into a structured decision landscape with LLM-generated narrative | 1 call |
Convergence Detection
The council doesn't use a fixed number of rounds. After each cross-examination round, it measures:
- Confidence dispersion — standard deviation of agent confidence scores. Below threshold = converged.
- Argument novelty — new arguments not seen in prior rounds. Near zero = diminishing returns.
- Concession rate — points where agents shifted position. Zero + no new arguments = stalled.
Stopping conditions:
| Condition | Meaning |
|---|---|
converged |
Dispersion below threshold, confidence stable. Genuine agreement. |
diminishing_returns |
No new arguments or concessions. Nothing more to surface. |
genuine_disagreement |
Dispersion widened, positions hardened. Summary of irreducible tension. |
max_rounds |
Hard cap reached. Inconclusive — principal must decide. |
Bootstrapping
If agent-council is not available on PATH, the invoking agent should run:
python3 scripts/bootstrap.py
This installs the package from the skill directory using the current Python's pip, falling back to pipx. No PyPI dependency for the bootstrap path — the package ships inside the skill directory.
If bootstrap fails: Run one of these manually:
pip install pydantic-ai
pip install agent-council
# Or from this directory:
python3 -m pip install -e /path/to/agent-council/
Available Scripts
This skill bundles one script; there are no others to discover.
| Script | Purpose | Invocation |
|---|---|---|
scripts/bootstrap.py |
First-run installer: checks whether agent-council is already on PATH and, if not, installs the package from the skill directory using the current Python's pip, falling back to pipx. Run it whenever agent-council is not found on PATH (an invoking agent should run it automatically in that case); it exits 0 when the CLI is available and 1 with manual-install instructions when it could not install. If bootstrap fails, follow the manual steps above. |
python3 scripts/bootstrap.py |
Configuration
| Env var | Required | Default | Description |
|---|---|---|---|
AGENT_COUNCIL_API_KEY |
Yes | — | API key for your LLM provider |
AGENT_COUNCIL_MODEL |
No | openai:gpt-5.6-luna |
Model string (provider/model) |
AGENT_COUNCIL_BASE_URL |
No | Provider default | Custom API endpoint (OpenRouter, LiteLLM, etc.) |
You can set these as environment variables or create a .env file in the directory you run agent-council from:
# .env file
AGENT_COUNCIL_API_KEY=sk-...
AGENT_COUNCIL_MODEL=openai:gpt-5.6-luna
Environment variables take precedence over .env file values.
Model strings follow PydanticAI convention: openai:gpt-5.6-luna, anthropic:claude-sonnet-4-20250514, deepseek:deepseek-v4-flash, google:gemini-2.0-flash.
Output
The synthesis report is a structured decision landscape. In markdown mode it includes:
- Confidence dispersion table — per-round confidence metrics with diagnostic
- Shared risks — failure modes from the pre-mortem (pre-positional, uncontaminated)
- Shared concerns — what survived cross-examination as genuine shared risk
- Remaining disagreements — positions that did not resolve
- Assumptions per position — what must hold for each position to be valid
- Principal's path — narrative synthesis of the decision landscape
Use --json for programmatic consumption. The JSON output follows this structure:
{
"question": "string",
"mode": "quick|medium|deep",
"num_agents": 3,
"rounds_completed": 2,
"stopped_reason": "converged|max_rounds|diminishing_returns|genuine_disagreement",
"confidence_history": [
{"round": 1, "mean_confidence": 0.74, "dispersion": 0.061, "new_arguments": 20, "concessions_made": 17}
],
"shared_risks": [{"description": "...", "severity": "low|medium|high", "phase_discovered": "premortem"}],
"shared_concerns": ["..."],
"disagreements": [{"topic": "...", "positions": {"agent_a": "position_a", "agent_b": "position_b"}}],
"assumptions_per_position": {"agent_name": ["assumption1", "assumption2"]},
"principal_path": "narrative text"
}
Claims Verification
Every synthesis output includes a post-debate verification scan. A separate LLM call reads the narrative synthesis and identifies any claims about verifiable external facts (domain availability, package namespace status, pricing, statistics) that the debate could not have verified from its own reasoning. Flagged claims are appended as a ⚠️ Claims Not Verified section:
⚠️ Claims Not Verified
The following assertions in this synthesis could not be verified
by the council's own reasoning and should be checked before acting:
• "Dialekt passes all five checks..." — domain availability:
No evidence the council checked domain registries
This is not a rejection of the synthesis — it is a quality signal. Claims in this section should be treated as hypotheses to verify, not as facts.
Reading the Convergence Diagnostic
The confidence dispersion table tells you whether the debate was productive:
| Pattern | Meaning | What to do |
|---|---|---|
| Mean confidence DROPPED, dispersion WIDENED | Council surfaced genuine doubt — healthy debate | Trust the shared concerns; investigate the newly surfaced risks |
| Mean confidence ROSE, dispersion NARROWED | Genuine convergence — agents convinced each other | The strongest signal; highest-confidence path forward |
| Mean confidence STABLE, dispersion NARROWED | Possible false consensus — agents agreed before debating | Probe the assumptions section for shared blind spots |
| Mean confidence ROSE, dispersion WIDENED | Polarization — agents became more entrenched | The question may be genuinely irresolvable by argument alone; look for an experimental path |
stopped_reason: converged |
Dispersion fell below threshold | Good — run with the recommendation |
stopped_reason: max_rounds |
Hit hard cap before converging | The debate was cut off; consider a second run with --max-rounds higher or --mode quick for faster convergence |
stopped_reason: diminishing_returns |
No new arguments surfaced | The council exhausted what it could discover — make a call |
stopped_reason: genuine_disagreement |
Positions hardened, dispersion widened | The council could not resolve the tension. The output is valuable precisely because it maps irreconcilable disagreement — read the disagreements section carefully |
Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Debate fails with "Exceeded maximum output retries" | Model couldn't produce valid structured output for a phase | Retry the debate. If persistent, try a different model or add --verbose to see which agent failed. |
| Debate runs for 5+ minutes with no output | DeepSeek or slow model with many agents | Use --mode quick --agents 3 for fast turnarounds, or use --verbose to see progress in real time. |
| All agents agree immediately with high confidence | False consensus — same model shares blind spots | Check the dispersion diagnostic. Try --profiles with diverse identities to force methodological diversity. |
| "Profile X not found" warning | Typo in profile name | Run agent-council --profiles list (or check the profiles list above) for valid names. |
| Synthesis contains obvious factual errors | Agents fabricated claims during debate | Check the ⚠️ Claims Not Verified section. The guardrail reduces fabrication but cannot eliminate it. Verify any statistics, pricing, or availability claims before acting. |
Architecture Decision
Single-model debate: All agents share one LLM configuration. Diversity comes from persona definitions (system prompts with distinct backgrounds, analytical approaches, biases), not from different model instances. This minimizes setup friction — one API key, one endpoint, predictable cost.
Limitation: All agents share the model's knowledge cutoff and blind spots. The convergence diagnostics include a "possible false consensus" flag when confidence starts high and never shifts.
Reference Files
| File | Load when |
|---|---|
references/convergence.md |
Understanding the convergence detection algorithm |
references/debate-protocol.md |
Deep dive into phase structure and round design |
references/configuration.md |
Provider setup, troubleshooting, model strings |
Directory Structure
agent-council/
├── SKILL.md # This file — skill entry point
├── pyproject.toml # Pip package definition
├── README.md
├── LICENSE # MIT
├── agent_council/ # Python package
│ ├── cli.py # CLI entry point
│ ├── config.py # Env var loading
│ ├── state.py # Typed state + Pydantic models
│ ├── convergence.py # Convergence detection
│ ├── graph.py # Debate graph orchestration
│ └── phases/
│ ├── compose.py # Persona generation
│ ├── premortem.py # Failure pre-mortem
│ ├── position.py # Initial positions
│ ├── cross_examine.py # Iterative cross-examination
│ └── synthesis.py # Decision landscape
├── scripts/
│ └── bootstrap.py # First-run installation
├── templates/
│ └── personas.json # Example custom personas
└── references/
├── convergence.md
├── debate-protocol.md
└── configuration.md
Prerequisites
- Python 3.10+ with the
pydantic-aipackage; install viapip,pipx, orpython3 scripts/bootstrap.py(the package ships inside this skill directory, so bootstrap needs no PyPI access). - An LLM provider API key exported as
AGENT_COUNCIL_API_KEY(or set in a.envfile); optionallyAGENT_COUNCIL_MODELandAGENT_COUNCIL_BASE_URL. - The real professional profiles from the hermes-profiles library are available only in a recursive source checkout; pip and wheel installs use generated or user-supplied personas.
Limitations
- Single-model debate: all agents share one LLM configuration and therefore its knowledge cutoff and blind spots; diversity comes from persona definitions, not model instances (see Architecture Decision).
- Debates consume many parallel LLM calls per round — expect minutes on slow models or deep mode, and check ⚠️ Claims Not Verified in every synthesis before acting on verifiable external facts.
- Convergence diagnostics reduce fabrication and false consensus but cannot eliminate them;
max_roundsstops mean an inconclusive debate that the principal must resolve. - This is not a general agent-orchestration framework: it runs debates only — for state-machine orchestration beyond the debate protocol, route to langgraph.
Related Skills
- ai-frameworks — umbrella bundle for all AI framework skills. Load this when comparing agent-council against other multi-agent approaches (LangGraph, AutoGen, CrewAI).
- langgraph — for complex state-machine multi-agent orchestration beyond the debate protocol
- pydanticai — the underlying framework for type-safe agent definitions
- spec-driven-development — for building specs that agent-council can help you evaluate
- hermes-profiles — the 39-profile library that powers the profile selection system
Files (agent-skills)
-
agent_council
-
phases
-
compose.py 2 KB
"""Compose phase — generates agent personas from the question.""" from pydantic_ai import Agent from agent_council.state import AgentPersona from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL async def compose_personas(question: str, num_agents: int = 5) -> list[AgentPersona]: """Generate debate agent personas tailored to the question. Uses an LLM to compose personas with diverse backgrounds, analytical approaches, and biases. The compose agent is a single LLM call that outputs a structured list of AgentPersona definitions. """ cfg = load_config() compose_agent = Agent( cfg["model"], output_type=list[AgentPersona], retries=3, system_prompt=( "You are a council composition specialist. Your job is to design " "expert debating agents for a structured multi-perspective debate.\n\n" "Critical directive: Prioritize DIVERSITY OF INITIAL POSITION over " "diversity of expertise. Research shows that a group with four distinct " "approaches to a problem — none individually correct — outperforms a " "group with more expertise but shared framing.\n\n" "For each agent provide: name, one-paragraph career background, specific " "expertise, analytical approach, and what bias or experience they bring " f"to THIS question. Design exactly {num_agents} agents.\n\n" "At least one agent should be structurally skeptical (a light red-team " "role). At least one agent should approach the problem from a fundamentally " "different cognitive frame than the others. Design them to create productive " "friction — real disagreement grounded in real experience, not caricatures." f"{FACTUAL_CLAIM_GUARDRAIL}" ), ) result = await compose_agent.run( f"Design {num_agents} expert debating agents for the question: {question}" ) return result.output -
cross_examine.py 4 KB
"""Cross-examination phase — agents probe each other's positions.""" import asyncio from pydantic_ai import Agent from agent_council.state import ( CouncilState, Position, CrossExamination, ) from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL def _format_other_positions( my_name: str, positions: dict[str, Position], ) -> str: """Format other agents' positions for the prompt.""" lines = [] for name, pos in positions.items(): if name == my_name: continue lines.append(f"--- {name} ---") lines.append(f"Stance: {pos.stance}") lines.append(f"Reasoning: {'; '.join(pos.reasoning)}") lines.append(f"Confidence: {pos.confidence}") lines.append(f"Assumptions: {'; '.join(pos.key_assumptions)}") lines.append("") return "\n".join(lines) async def run_cross_examination( question: str, state: CouncilState, verbose: bool = False, ) -> dict[str, CrossExamination]: """Each agent reads all other positions and responds. Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() async def _cross( agent_id: str, identity_block: str, other_positions: str, round_context: str, ) -> tuple[str, CrossExamination]: system = ( f"{identity_block}\n\n" "You are in a structured debate. You have read every other agent's " "position on the question.\n\n" "Other agents' positions:\n" f"{other_positions}\n" f"{round_context}\n\n" "Respond to what you've read. For each point: concede where the " "other agent's reasoning is stronger, identify where you still " "disagree and why, and update your position if warranted. Be " "specific — do not hedge. If your confidence has changed, say so." f"{FACTUAL_CLAIM_GUARDRAIL}" ) agent = Agent(cfg["model"], output_type=CrossExamination, system_prompt=system, retries=3) result = await agent.run(question) output = result.output output.agent_name = agent_id return agent_id, output # Build list of agent identities agents = [] if state.profiles: for p in state.profiles: identity = ( f"You are {p.name}.\n\n" f"Your identity and operating principles:\n" f"{p.soul_content}\n\n" f"Description: {p.description}" ) agents.append((p.name, identity)) else: for p in state.personas: identity = ( f"You are {p.name}.\n" f"Background: {p.background}\n" f"Expertise: {p.expertise}\n" f"Approach: {p.approach}\n" f"Bias: {p.bias}" ) agents.append((p.name, identity)) prior_rounds = state.cross_examination_rounds tasks = [] for agent_id, identity_block in agents: other_positions = _format_other_positions(agent_id, state.positions) round_context = "" if prior_rounds: round_context = "\n\nPrevious round context:\n" for i, rnd in enumerate(prior_rounds): if agent_id in rnd: prev = rnd[agent_id] round_context += f"Round {i + 1} — your reflection: {prev.reflection}\n" if prev.concessions: round_context += f" You conceded: {'; '.join(prev.concessions)}\n" if prev.remaining_disagreements: round_context += ( f" Still in dispute: " f"{'; '.join(prev.remaining_disagreements)}\n" ) tasks.append(_cross(agent_id, identity_block, other_positions, round_context)) results = await asyncio.gather(*tasks) return dict(results) -
position.py 2.8 KB
"""Position phase — each agent forms an independent initial position.""" import asyncio from pydantic_ai import Agent from agent_council.state import CouncilState, Position, Premortem from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL async def run_positions( question: str, state: CouncilState, verbose: bool = False, ) -> dict[str, Position]: """Each agent forms an independent initial position. Agents see their own premortem (to maintain continuity) but NOT other agents' positions or premortems. Ensures independent thought. Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() async def _position( agent_id: str, identity_block: str, premortem: Premortem | None, ) -> tuple[str, Position]: system = ( f"{identity_block}\n\n" "You are in a structured debate. Your task: form your initial " "position on the question. Be specific about your stance, your " "reasoning, and what assumptions you're making." ) if premortem: system += ( f"\n\nYour pre-mortem identified these failure modes:\n" f" - Failure scenario: {premortem.failure_scenario}\n" f" - Root causes: {'; '.join(premortem.root_causes)}\n" f" - Warning signals: {'; '.join(premortem.early_warning_signals)}\n\n" "Your position should account for these risks." ) system += ( "\n\nReturn your position with a confidence score (0-1) and " "the key assumptions that must hold for your position to be correct." f"{FACTUAL_CLAIM_GUARDRAIL}" ) agent = Agent(cfg["model"], output_type=Position, system_prompt=system, retries=3) result = await agent.run(question) output = result.output output.agent_name = agent_id return agent_id, output tasks = [] if state.profiles: for p in state.profiles: identity = ( f"You are {p.name}.\n\n" f"Your identity and operating principles:\n" f"{p.soul_content}\n\n" f"Description: {p.description}" ) pm = state.premortems.get(p.name) tasks.append(_position(p.name, identity, pm)) else: for p in state.personas: identity = ( f"You are {p.name}.\n" f"Background: {p.background}\n" f"Expertise: {p.expertise}\n" f"Approach: {p.approach}\n" f"Bias: {p.bias}" ) pm = state.premortems.get(p.name) tasks.append(_position(p.name, identity, pm)) results = await asyncio.gather(*tasks) return dict(results) -
premortem.py 2.3 KB
"""Premortem phase — each agent envisions how the decision already failed.""" import asyncio from pydantic_ai import Agent from agent_council.state import CouncilState, Premortem from agent_council.config import load_config from agent_council.guardrails import FACTUAL_CLAIM_GUARDRAIL async def run_premortems( question: str, state: CouncilState, verbose: bool = False, ) -> dict[str, Premortem]: """Each agent independently writes a failure scenario. Agents do NOT see each other's premortems — this runs before any positions are formed, bypassing positional commitment bias. Uses real profiles if available, falls back to fabricated personas. """ cfg = load_config() async def _premortem(agent_id: str, identity_block: str) -> tuple[str, Premortem]: agent = Agent( cfg["model"], output_type=Premortem, retries=3, system_prompt=( f"{identity_block}\n\n" "You are in a structured debate. Your first task: write a " "pre-mortem — imagine it is 6 months in the future and the " "decision about to be discussed has ALREADY FAILED. Write " "the history of how it failed. What went wrong? What were the " "early warning signals nobody heeded? Be specific and draw on " "your expertise." f"{FACTUAL_CLAIM_GUARDRAIL}" ), ) result = await agent.run(question) return agent_id, result.output # Build agent identities tasks = [] if state.profiles: for p in state.profiles: identity = ( f"You are {p.name}.\n\n" f"Your identity and operating principles:\n" f"{p.soul_content}\n\n" f"Description: {p.description}" ) tasks.append(_premortem(p.name, identity)) else: for p in state.personas: identity = ( f"You are {p.name}.\n" f"Background: {p.background}\n" f"Expertise: {p.expertise}\n" f"Approach: {p.approach}\n" f"Bias: {p.bias}" ) tasks.append(_premortem(p.name, identity)) results = await asyncio.gather(*tasks) return dict(results) -
select.py 3.4 KB
"""Select phase — picks real profiles from the hermes-profiles library.""" import json import os import sys from pathlib import Path import yaml from agent_council.state import ProfileInfo # Path to the locally available profiles library within the skill directory. PROFILES_DIR = Path(__file__).resolve().parent.parent.parent / "profiles" / "profiles" def _list_available() -> list[str]: """List all available profile names.""" if not PROFILES_DIR.exists(): return [] return sorted( d.name for d in PROFILES_DIR.iterdir() if d.is_dir() and not d.name.startswith(".") ) def _load_profile(name: str) -> ProfileInfo | None: """Load a single profile's SOUL.md and profile.yaml.""" profile_dir = PROFILES_DIR / name soul_path = profile_dir / "SOUL.md" yaml_path = profile_dir / "profile.yaml" if not soul_path.exists(): return None soul_content = soul_path.read_text(encoding="utf-8") description = "" if yaml_path.exists(): try: with open(yaml_path) as f: data = yaml.safe_load(f) description = data.get("description", "") or "" except Exception: pass return ProfileInfo(name=name, description=description, soul_content=soul_content) def load_all() -> list[ProfileInfo]: """Load all locally available profiles.""" profiles = [] for name in _list_available(): p = _load_profile(name) if p: profiles.append(p) return profiles def select_by_names(names: list[str]) -> list[ProfileInfo]: """Load specific profiles by name.""" profiles = [] for name in names: name = name.strip().lower() p = _load_profile(name) if p: profiles.append(p) else: print( f"Warning: profile '{name}' not found. " f"Available: {', '.join(_list_available())}", file=sys.stderr, ) return profiles def select_by_question(question: str, count: int = 5) -> list[ProfileInfo]: """Auto-select the most relevant profiles for a question. Scores each profile by keyword overlap between the question and the profile's description. Returns the top N profiles. """ all_profiles = load_all() if not all_profiles: return [] question_lower = question.lower() question_words = set(question_lower.split()) scored = [] for p in all_profiles: desc_words = set(p.description.lower().split()) # Also score on profile name name_words = set(p.name.lower().replace("-", " ").split()) # Count overlapping words overlap = len(question_words & desc_words) + len(question_words & name_words) # Bonus for exact phrase matches if p.name.lower().replace("-", " ") in question_lower: overlap += 3 scored.append((overlap, p)) scored.sort(key=lambda x: -x[0]) # Pick top N, ensure diversity (skip if too similar description) selected = [] seen_descriptions = set() for _, p in scored: desc_key = p.description[:80] if desc_key not in seen_descriptions or len(selected) < 3: selected.append(p) seen_descriptions.add(desc_key) if len(selected) >= count: break # Fallback: if somehow empty, grab first N if not selected and all_profiles: selected = all_profiles[:count] return selected -
synthesis.py 8.8 KB
"""Synthesis phase — produces the final decision landscape.""" from typing import Literal from pydantic_ai import Agent from agent_council.state import ( CouncilState, Position, Premortem, CrossExamination, Synthesis, RiskVector, Disagreement, RoundMetrics, ) from agent_council.convergence import compute_round_metrics from agent_council.config import load_config from agent_council.guardrails import verify_synthesis def _collect_risks( premortems: dict[str, Premortem], positions: dict[str, Position], cross_rounds: list[dict[str, CrossExamination]], ) -> list[RiskVector]: """Collect all risks flagged across phases.""" risks: list[RiskVector] = [] # Heuristic severity: if 3+ agents flagged it independently, it's high. # Risks seen by 2 agents are medium, single-agent risks are low. def _severity(agent_count: int) -> Literal["low", "medium", "high"]: if agent_count >= 3: return "high" elif agent_count == 2: return "medium" return "low" # Track which risks were flagged by how many agents (dedup by description prefix) risk_counts: dict[str, set[str]] = {} def _record(description: str, agent: str, phase: str): key = description[:60] # group similar descriptions if key not in risk_counts: risk_counts[key] = set() risk_counts[key].add(agent) # From premortems (pre-positional) for p in premortems.values(): if p.root_causes: cause = "; ".join(p.root_causes[:3]) _record(cause, p.agent_name, "premortem") # From cross-examinations (post-positional) for rnd in cross_rounds: for ce in rnd.values(): if ce.remaining_disagreements: for d in ce.remaining_disagreements[:2]: _record(d, ce.agent_name, "cross_examine") # Build final risk list with computed severity premortem_agents = {p.agent_name for p in premortems.values()} for key, agents in risk_counts.items(): # Determine phase: if any premortem agent flagged it, origin is premortem phase = "premortem" if any(a in premortem_agents for a in agents) else "cross_examine" risks.append( RiskVector( description=key, agents_who_flagged=list(agents), severity=_severity(len(agents)), phase_discovered=phase, ) ) return risks async def synthesize(state: CouncilState) -> Synthesis: """Produce the final synthesis from all phase outputs. Combines algorithmic convergence metrics with an LLM-generated narrative synthesis of the decision landscape. """ cfg = load_config() # Collect all risks risks = _collect_risks( state.premortems, state.positions, state.cross_examination_rounds ) # Build convergence history history: list[RoundMetrics] = [] for i in range(len(state.cross_examination_rounds)): # Temporarily set round_number to replay metrics state.round_number = i + 1 metrics = compute_round_metrics(state) history.append(metrics) # Compute final metrics final_metrics = history[-1] if history else None first_metrics = history[0] if len(history) > 1 else final_metrics # Identify shared concerns from cross-examination shared_concerns = _extract_shared_concerns(state.cross_examination_rounds) # Identify disagreements disagreements = _extract_disagreements(state) # Build assumptions per position assumptions_per_position = { name: pos.key_assumptions for name, pos in state.positions.items() } # Generate narrative synthesis via LLM narrative = await _generate_synthesis_narrative(state, cfg) # Post-synthesis verification: scan for unsubstantiated factual claims verification = await verify_synthesis(narrative) verification_note = "" if verification.has_issues: items = "\n".join( f" • \"{c.quote[:120]}\" — {c.claim_type}: {c.explanation[:150]}" for c in verification.claims[:5] ) verification_note = ( f"\n\n---\n" f"⚠️ Claims Not Verified\n" f"The following assertions in this synthesis could not be verified " f"by the council's own reasoning and should be checked before acting:\n" f"{items}\n" ) if len(verification.claims) > 5: verification_note += ( f" ...and {len(verification.claims) - 5} more unsubstantiated " f"claim(s)." ) narrative += verification_note stopped_reason = "max_rounds" # will be overwritten by graph.py return Synthesis( question=state.question, mode=state.mode, num_agents=len(state.profiles) or len(state.personas), rounds_completed=len(state.cross_examination_rounds), stopped_reason=stopped_reason, # type: ignore confidence_history=history, final_dispersion=final_metrics.dispersion if final_metrics else 0.0, mean_confidence_delta=( (final_metrics.mean_confidence - first_metrics.mean_confidence) if final_metrics and first_metrics else 0.0 ), shared_risks=[r for r in risks if r.phase_discovered == "premortem"], shared_concerns=shared_concerns, disagreements=disagreements, assumptions_per_position=assumptions_per_position, risk_vectors=risks, principal_path=narrative, ) def _extract_shared_concerns( cross_rounds: list[dict[str, CrossExamination]], ) -> list[str]: """Find concerns raised by multiple agents across rounds.""" concern_counts: dict[str, int] = {} for rnd in cross_rounds: for ce in rnd.values(): for d in ce.remaining_disagreements: concern_counts[d] = concern_counts.get(d, 0) + 1 for c in ce.concessions: concern_counts[c] = concern_counts.get(c, 0) + 1 # Return concerns raised by more than one agent return [ concern for concern, count in sorted( concern_counts.items(), key=lambda x: -x[1] ) if count > 1 ][:10] def _extract_disagreements(state: CouncilState) -> list[Disagreement]: """Identify persistent disagreements from the last round.""" if not state.cross_examination_rounds: return [] last_round = state.cross_examination_rounds[-1] topic_positions: dict[str, dict[str, str]] = {} for ce in last_round.values(): for d in ce.remaining_disagreements: if d not in topic_positions: topic_positions[d] = {} topic_positions[d][ce.agent_name] = ce.updated_position or "maintains position" return [ Disagreement(topic=topic, positions=positions) for topic, positions in topic_positions.items() ][:8] async def _generate_synthesis_narrative( state: CouncilState, cfg: dict ) -> str: """Generate a narrative principal's path via LLM.""" # Build a summary of the debate for the LLM summary_parts = [f"# Debate: {state.question}\n"] summary_parts.append(f"Agents: {', '.join(p.name for p in state.personas)}\n") summary_parts.append("\n## Positions\n") for pos in state.positions.values(): summary_parts.append( f"- **{pos.agent_name}** (confidence {pos.confidence}): {pos.stance}\n" ) summary_parts.append("\n## Premortem Failure Scenarios\n") for pm in state.premortems.values(): summary_parts.append(f"- **{pm.agent_name}**: {pm.failure_scenario[:200]}\n") summary_parts.append("\n## Cross-Examination Rounds\n") for i, rnd in enumerate(state.cross_examination_rounds): summary_parts.append(f"\n### Round {i + 1}\n") for ce in rnd.values(): summary_parts.append(f"- **{ce.agent_name}**: {ce.reflection[:200]}\n") debate_summary = "".join(summary_parts) agent = Agent( cfg["model"], retries=3, system_prompt=( "You are a senior decision analyst. You have overseen a structured " "multi-agent debate on an important question. Your job: synthesize " "the debate into a clear 'principal's path' — a narrative that " "presents the decision landscape to someone who must make a call.\n\n" "Do NOT describe the debate process (rounds, phases, agents). " "Write as a single analyst presenting their findings. Structure: " "what's at stake, where the evidence is strongest, where it's weakest, " "what assumptions each path depends on, and your recommended path " "forward with associated risks.\n\n" "Keep it under 500 words. Be direct. No hedging." ), ) result = await agent.run(debate_summary) return result.output or "" -
__init__.py 18 B
"""Phase init."""
-
-
cli.py 7.4 KB
"""CLI entry point for agent-council.""" import argparse import asyncio import json import sys def main(): """Entry point for `agent-council` CLI.""" parser = argparse.ArgumentParser( description="Multi-agent structured debate system", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=( "Examples:\n" " agent-council \"Should we use Postgres or SQLite?\"\n" " agent-council --mode quick --agents 3 \"Quick check on this idea\"\n" " agent-council --json \"Output as machine-readable JSON\"\n" " agent-council --persona-file personas.json \"Custom agent lineup\"\n\n" "Environment:\n" " AGENT_COUNCIL_API_KEY API key (required)\n" " AGENT_COUNCIL_MODEL Model string (default: openai:gpt-5.6-luna)\n" " AGENT_COUNCIL_BASE_URL Custom API endpoint\n" ), ) parser.add_argument( "question", type=str, help="The question to debate", ) parser.add_argument( "--agents", "-n", type=int, default=5, choices=range(3, 8), help="Number of debate agents (3-7, default: 5)", ) parser.add_argument( "--mode", "-m", type=str, default="medium", choices=["quick", "medium", "deep"], help="Debate depth (default: medium)", ) parser.add_argument( "--persona-file", type=str, default=None, help="JSON file with custom agent persona definitions", ) parser.add_argument( "--json", action="store_true", help="Output as structured JSON instead of markdown", ) parser.add_argument( "--verbose", "-v", action="store_true", help="Show phase-by-phase progress", ) parser.add_argument( "--max-rounds", type=int, default=4, help="Maximum cross-examination rounds (default: 4)", ) parser.add_argument( "--convergence", type=float, default=0.10, help="Convergence threshold for confidence dispersion (default: 0.10)", ) parser.add_argument( "--profiles", type=str, default=None, help="Comma-separated profile names from the hermes-profiles library " "(e.g. 'debugger,researcher,product-manager'). " "Omit for auto-selection based on the question.", ) args = parser.parse_args() if not args.question.strip(): print("Error: Question cannot be empty.", file=sys.stderr) sys.exit(3) # Map mode to agent count agent_map = {"quick": 3, "medium": 5, "deep": 7} num_agents = args.agents or agent_map.get(args.mode, 5) # Import here so CLI help is fast even without pydantic-ai installed try: from agent_council.graph import run_debate except ImportError as e: print( f"Error: Could not import agent_council: {e}", file=sys.stderr, ) print( "Make sure pydantic-ai is installed: pip install pydantic-ai", file=sys.stderr, ) sys.exit(1) # Parse explicit profile list profile_names = None if args.profiles: profile_names = [n.strip() for n in args.profiles.split(",")] try: state = asyncio.run( run_debate( question=args.question, num_agents=num_agents, mode=args.mode, max_rounds=args.max_rounds, convergence_threshold=args.convergence, verbose=args.verbose, persona_file=args.persona_file, profile_names=profile_names, ) ) except ValueError as e: print(f"Configuration error: {e}", file=sys.stderr) sys.exit(1) except Exception as e: print(f"Debate failed: {e}", file=sys.stderr) sys.exit(2) synthesis = state.synthesis if not synthesis: print("Error: Debate completed but no synthesis was produced.", file=sys.stderr) sys.exit(1) if args.json: print(synthesis.model_dump_json(indent=2)) else: print(format_synthesis_markdown(synthesis)) def format_synthesis_markdown(synthesis) -> str: """Format synthesis as human-readable markdown.""" from agent_council.state import Synthesis lines = [] lines.append(f"# Council Synthesis") lines.append(f"") lines.append(f"**Question:** {synthesis.question}") lines.append(f"**Mode:** {synthesis.mode} ({synthesis.num_agents} agents, {synthesis.rounds_completed} rounds)") lines.append(f"**Stopped because:** {synthesis.stopped_reason}") lines.append(f"") # Confidence dispersion lines.append(f"## Confidence Dispersion") lines.append(f"") lines.append(f"| Round | Mean Confidence | Dispersion | New Args | Concessions |") lines.append(f"|-------|----------------|------------|----------|-------------|") for m in synthesis.confidence_history: lines.append( f"| {m.round} | {m.mean_confidence:.3f} | {m.dispersion:.3f} | " f"{m.new_arguments} | {m.concessions_made} |" ) lines.append(f"") lines.append(f"**Final dispersion:** {synthesis.final_dispersion:.3f}") lines.append(f"**Mean confidence delta:** {synthesis.mean_confidence_delta:+.3f}") lines.append(f"") # Diagnostic if synthesis.final_dispersion < 0.08: diag = "Confidence converged — agents reached alignment." elif synthesis.final_dispersion > 0.15: diag = "Confidence remained dispersed — genuine disagreement persisted." else: diag = "Moderate agreement with meaningful remaining tension." lines.append(f"> **Diagnostic:** {diag}") lines.append(f"") # Shared risks (from premortem) if synthesis.shared_risks: lines.append(f"## Shared Risks (Pre-Mortem)") lines.append(f"") for risk in synthesis.shared_risks: agents = ", ".join(risk.agents_who_flagged) lines.append(f"- **{risk.severity.upper()}** — {risk.description}") lines.append(f" *Flagged by: {agents}*") lines.append(f"") # Shared concerns if synthesis.shared_concerns: lines.append(f"## Shared Concerns (Confirmed by Debate)") lines.append(f"") for concern in synthesis.shared_concerns: lines.append(f"- {concern}") lines.append(f"") # Disagreements if synthesis.disagreements: lines.append(f"## Remaining Disagreements") lines.append(f"") for d in synthesis.disagreements: lines.append(f"- **{d.topic}**") for agent, pos in d.positions.items(): lines.append(f" - {agent}: {pos[:120]}") lines.append(f"") # Assumptions if synthesis.assumptions_per_position: lines.append(f"## Assumptions per Position") lines.append(f"") for agent, assumptions in synthesis.assumptions_per_position.items(): lines.append(f"- **{agent}**") for a in assumptions: lines.append(f" - {a}") lines.append(f"") # Principal's path if synthesis.principal_path: lines.append(f"## Principal's Path") lines.append(f"") lines.append(synthesis.principal_path) lines.append(f"") return "\n".join(lines) if __name__ == "__main__": main() -
config.py 3.2 KB
"""Configuration — env var loading with sensible defaults and .env support.""" import os from pathlib import Path def _load_dotenv(path: Path | None = None) -> None: """Load .env file using stdlib only. Looks for .env in cwd by default. Minimal implementation — no python-dotenv dependency. Handles: KEY=value KEY="quoted value" # comments export KEY=value (strips export prefix) """ dotenv_path = path or Path.cwd() / ".env" if not dotenv_path.exists(): return for line in dotenv_path.read_text().splitlines(): line = line.strip() if not line or line.startswith("#"): continue if line.startswith("export "): line = line[7:].strip() if "=" not in line: continue key, _, value = line.partition("=") key = key.strip() value = value.strip().strip("\"'") if key and key not in os.environ: os.environ[key] = value def load_config() -> dict: """Load configuration from environment variables and .env files. Checks for a .env file in the current working directory first, then falls back to environment variables. Env vars always take precedence over .env values. Also sets the provider-specific API key env var (e.g. OPENAI_API_KEY, DEEPSEEK_API_KEY, ANTHROPIC_API_KEY) from AGENT_COUNCIL_API_KEY so PydanticAI picks it up regardless of what's in the environment. Returns dict with keys: api_key, model, base_url. Raises ValueError if AGENT_COUNCIL_API_KEY is not set. """ _load_dotenv() api_key = os.environ.get("AGENT_COUNCIL_API_KEY") model = os.environ.get("AGENT_COUNCIL_MODEL", "openai:gpt-5.6-luna") base_url = os.environ.get("AGENT_COUNCIL_BASE_URL") if not api_key: raise ValueError( "AGENT_COUNCIL_API_KEY is not set. " "Set it via environment variable or create a .env file:\n" " export AGENT_COUNCIL_API_KEY='sk-...'\n" " export AGENT_COUNCIL_MODEL='openai:gpt-5.6-luna' # or your model\n\n" "Or create a .env file in the current directory:\n" " AGENT_COUNCIL_API_KEY=sk-...\n" " AGENT_COUNCIL_MODEL=openai:gpt-5.6-luna" ) # Map AGENT_COUNCIL_API_KEY to the provider-specific env var # that PydanticAI reads at Agent creation time. provider = model.split(":")[0] if ":" in model else "openai" provider_key_map = { "openai": "OPENAI_API_KEY", "deepseek": "DEEPSEEK_API_KEY", "anthropic": "ANTHROPIC_API_KEY", "google": "GOOGLE_API_KEY", "groq": "GROQ_API_KEY", "cohere": "COHERE_API_KEY", "mistral": "MISTRAL_API_KEY", "together": "TOGETHER_API_KEY", "xai": "XAI_API_KEY", "ollama": None, # no API key needed } env_var = provider_key_map.get(provider, "OPENAI_API_KEY") if env_var and not os.environ.get(env_var): os.environ[env_var] = api_key # Also set base URL if provided if base_url and not os.environ.get("OPENAI_BASE_URL"): os.environ["OPENAI_BASE_URL"] = base_url config = { "api_key": api_key, "model": model, } if base_url: config["base_url"] = base_url return config -
convergence.py 3.7 KB
"""Convergence detection — evaluates debate state to decide when to stop.""" import math from agent_council.state import CouncilState, RoundMetrics def compute_round_metrics(state: CouncilState) -> RoundMetrics: """Compute convergence metrics from the current round's data.""" if not state.cross_examination_rounds: return RoundMetrics( round=state.round_number, mean_confidence=0.0, dispersion=0.0, new_arguments=0, concessions_made=0, ) current_round = state.cross_examination_rounds[-1] confidences = [] concessions = 0 total_arguments_before = set() # Count arguments from all prior rounds for novelty detection for r in state.cross_examination_rounds[:-1]: for ce in r.values(): if ce.remaining_disagreements: total_arguments_before.update(ce.remaining_disagreements) if ce.new_evidence_needed: total_arguments_before.update(ce.new_evidence_needed) new_arguments = 0 for ce in current_round.values(): if ce.updated_confidence is not None: confidences.append(ce.updated_confidence) if ce.concessions: concessions += len(ce.concessions) if ce.remaining_disagreements: for arg in ce.remaining_disagreements: if arg not in total_arguments_before: new_arguments += 1 mean_conf = sum(confidences) / len(confidences) if confidences else 0.0 dispersion = ( math.sqrt(sum((c - mean_conf) ** 2 for c in confidences) / len(confidences)) if confidences else 0.0 ) return RoundMetrics( round=state.round_number, mean_confidence=round(mean_conf, 3), dispersion=round(dispersion, 3), new_arguments=new_arguments, concessions_made=concessions, ) def should_stop(state: CouncilState, metrics: RoundMetrics) -> str: """Evaluate whether the debate should stop. Returns one of: - "converged" — dispersion below threshold, confidence stable - "diminishing_returns" — nothing new is surfacing - "genuine_disagreement" — dispersion widened, positions hardened - "continue" — run another round """ # Hard cap if state.round_number >= state.max_rounds: return "max_rounds" # Need at least 2 rounds to compare if len(state.cross_examination_rounds) < 2: return "continue" prior = state.cross_examination_rounds[-2] prior_confs = [ ce.updated_confidence for ce in prior.values() if ce.updated_confidence is not None ] current_confs = [ ce.updated_confidence for ce in state.cross_examination_rounds[-1].values() if ce.updated_confidence is not None ] if not prior_confs or not current_confs: return "continue" prior_mean = sum(prior_confs) / len(prior_confs) current_mean = sum(current_confs) / len(current_confs) # Converged: dispersion below threshold if metrics.dispersion < state.convergence_threshold: # Still check if anything changed — settled means done if abs(current_mean - prior_mean) < 0.03: return "converged" return "continue" # Diminishing returns: no new arguments, no concessions if metrics.new_arguments == 0 and metrics.concessions_made == 0: return "diminishing_returns" # Genuine disagreement: dispersion widened and no one moved if ( metrics.dispersion > state.convergence_threshold * 1.5 and metrics.concessions_made == 0 and metrics.new_arguments == 0 ): return "genuine_disagreement" return "continue" -
graph.py 7.6 KB
"""Graph orchestration — runs the debate protocol as a state machine.""" import json import sys import time from datetime import datetime, timezone from pathlib import Path from agent_council.state import CouncilState from agent_council.phases.compose import compose_personas from agent_council.phases.select import select_by_names, select_by_question from agent_council.phases.premortem import run_premortems from agent_council.phases.position import run_positions from agent_council.phases.cross_examine import run_cross_examination from agent_council.phases.synthesis import synthesize from agent_council.convergence import should_stop, compute_round_metrics def _stream(msg: str, end: str = "\n") -> None: """Print a progress message immediately to stdout.""" print(msg, end=end, flush=True) def _run_dir() -> Path: """Create and return a timestamped run directory.""" ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S") path = Path(f"/tmp/agent-council/{ts}") path.mkdir(parents=True, exist_ok=True) return path def _identity_for( state: CouncilState, name: str, fallback_persona=None, ) -> str: """Build an identity block for a debate agent. If real profiles are loaded, uses the SOUL.md content. Otherwise falls back to fabricated persona fields. """ # Prefer real profiles for p in state.profiles: if p.name == name: return ( f"You are {name}.\n\n" f"Your identity and operating principles:\n" f"{p.soul_content}\n\n" f"Description: {p.description}" ) # Fallback to fabricated persona if fallback_persona: return ( f"You are {fallback_persona.name}.\n" f"Background: {fallback_persona.background}\n" f"Expertise: {fallback_persona.expertise}\n" f"Approach: {fallback_persona.approach}\n" f"Bias: {fallback_persona.bias}" ) return f"You are {name}." async def run_debate( question: str, num_agents: int = 5, mode: str = "medium", max_rounds: int = 4, convergence_threshold: float = 0.10, verbose: bool = False, persona_file: str | None = None, profile_names: list[str] | None = None, ) -> CouncilState: """Run the full debate protocol with live progress output. Phases: 1. Select/Compose — pick real profiles or generate personas 2. Premortem — each agent envisions failure 3. Position — each agent forms initial position 4. Cross-examine — iterative, convergence-checked rounds 5. Synthesis — produce decision landscape """ rundir = _run_dir() state = CouncilState( question=question, mode=mode, max_rounds=max_rounds, convergence_threshold=convergence_threshold, ) # Phase 1: Select or Compose agents _stream("🏛 Council assembling...") if profile_names: # Explicit profile selection state.profiles = select_by_names(profile_names) _stream(f" 📂 Loaded {len(state.profiles)} profiles (explicit)") else: # Try auto-selecting profiles from the library state.profiles = select_by_question(question, num_agents) if state.profiles: _stream(f" 📂 Auto-selected {len(state.profiles)} profiles from library") else: # Fallback: compose fabricated personas _stream(" ⚡ No profile library found, composing personas...") if persona_file: from agent_council.state import AgentPersona with open(persona_file) as f: data = json.load(f) state.personas = [AgentPersona(**p) for p in data] _stream(f" Loaded {len(state.personas)} personas from file") else: state.personas = await compose_personas(question, num_agents) if verbose: for p in state.profiles or state.personas: name = p.name if hasattr(p, 'name') else p _stream(f" 👤 {name}") # Write agent identities to run dir with open(rundir / "agents.json", "w") as f: agents = { "profiles": [ {"name": p.name, "description": p.description} for p in state.profiles ], "personas": [ {"name": p.name, "expertise": p.expertise} for p in state.personas ], } f.write(json.dumps(agents, indent=2, default=str)) _stream(f" ✅ {len(state.profiles or state.personas)} agents ready") # Phase 2: Premortem _stream(" 🔮 Pre-mortem phase...") t0 = time.time() state.premortems = await run_premortems(question, state, verbose) _stream(f" ✅ Pre-mortem complete ({len(state.premortems)} agents, {time.time()-t0:.0f}s)") with open(rundir / "premortems.json", "w") as f: f.write(json.dumps( {k: v.model_dump() for k, v in state.premortems.items()}, indent=2, default=str, )) # Phase 3: Position _stream(" 📋 Position phase...") t0 = time.time() state.positions = await run_positions(question, state, verbose) confidences = [p.confidence for p in state.positions.values()] avg_conf = sum(confidences) / len(confidences) if confidences else 0 _stream(f" ✅ Positions formed ({len(state.positions)} agents, avg confidence {avg_conf:.2f}, {time.time()-t0:.0f}s)") if verbose: for pos in state.positions.values(): _stream(f" {pos.agent_name}: {pos.stance[:80]}...") with open(rundir / "positions.json", "w") as f: f.write(json.dumps( {k: v.model_dump() for k, v in state.positions.items()}, indent=2, default=str, )) # Phase 4: Iterative cross-examination _stream(" 💬 Cross-examination rounds...") round_num = 0 while round_num < max_rounds: round_num += 1 state.round_number = round_num _stream(f" Round {round_num}... ", end="") t0 = time.time() cross_results = await run_cross_examination(question, state, verbose) state.cross_examination_rounds.append(cross_results) metrics = compute_round_metrics(state) stop_reason = should_stop(state, metrics) elapsed = time.time() - t0 _stream( f"dispersion={metrics.dispersion:.3f} " f"concessions={metrics.concessions_made} " f"new_args={metrics.new_arguments} " f"({elapsed:.0f}s) → {stop_reason}" ) with open(rundir / f"round_{round_num}.json", "w") as f: f.write(json.dumps( {k: v.model_dump() for k, v in cross_results.items()}, indent=2, default=str, )) if stop_reason != "continue": object.__setattr__(state, "_stopped_reason", stop_reason) break # Phase 5: Synthesis _stream(" 📝 Synthesizing final report...") t0 = time.time() state.synthesis = await synthesize(state) _stream(f" ✅ Synthesis complete ({time.time()-t0:.0f}s)") stop_reason = getattr(state, "_stopped_reason", "max_rounds") state.synthesis.stopped_reason = stop_reason # type: ignore with open(rundir / "synthesis.json", "w") as f: f.write(state.synthesis.model_dump_json(indent=2)) with open(rundir / "synthesis.md", "w") as f: from agent_council.cli import format_synthesis_markdown f.write(format_synthesis_markdown(state.synthesis)) _stream(f"\n📁 Full debate output: {rundir}/\n") return state -
guardrails.py 5.1 KB
"""Shared guardrails and eval utilities for the debate pipeline.""" from pydantic import BaseModel, Field from agent_council.config import load_config # ── Prompt guardrail injected into every debate agent ── FACTUAL_CLAIM_GUARDRAIL = ( "\n\n---\n" "CRITICAL RULE — Do NOT assert specific verifiable facts about the external " "world in your responses.\n" "This includes, but is not limited to:\n" " • Domain availability (\"example.com is available\")\n" " • Package namespace status (\"no collisions on PyPI\")\n" " • GitHub/trademark/registry availability\n" " • Pricing, statistics, dates, or third-party features\n" " • Whether a specific tool, library, or API exists or works in a specific way\n\n" "You have no ability to verify any of these claims. If you would need to " "check an external source to know something, say it needs to be verified " "rather than asserting it as fact.\n\n" "RIGHT: \"Dialekt would need to be checked against package registries and " "domain availability before committing.\"\n" "WRONG: \"Dialekt passes all five checks — no domain collisions, clear " "GitHub namespace, no PyPI conflicts.\"\n\n" "The second example fabricates specific facts the agent could not know. " "This undermines the entire debate. A recommendation based on fabricated " "premises is worse than no recommendation at all.\n" "---" ) # ── Post-synthesis verification eval ── class UnsubstantiatedClaim(BaseModel): """A claim in the synthesis that asserts a verifiable fact without evidence.""" quote: str = Field(description="The exact text making the claim") claim_type: str = Field( description="Type of claim, e.g. 'domain availability', " "'package namespace', 'trademark status', 'pricing', " "'statistic', 'third-party feature', 'date'" ) explanation: str = Field( description="Why this claim cannot be verified from the debate's own reasoning" ) class SynthesisVerification(BaseModel): """Result of scanning a synthesis for unsubstantiated factual claims.""" has_issues: bool = Field( description="True if any unsubstantiated claims were found" ) claims: list[UnsubstantiatedClaim] = Field(default_factory=list) summary: str = Field( description="One-line summary of the verification result" ) async def verify_synthesis(synthesis_text: str) -> SynthesisVerification: """Scan a council synthesis for unsupported factual claims. This is an LLM-based eval pass that identifies assertions the debate could not have verified from its own reasoning. Does not require search or external tools — just reads the output critically. """ from pydantic_ai import Agent cfg = load_config() verifier = Agent( cfg["model"], output_type=SynthesisVerification, retries=3, system_prompt=( "You are a verification quality-assurance agent. Your job is to " "read a council debate synthesis and identify any claims that " "assert specific, verifiable facts about the external world " "that the debate could not have verified.\n\n" "Flag claims about:\n" "- Domain availability or registration status\n" "- Package manager namespace collisions (PyPI, npm, crates.io, etc.)\n" "- GitHub namespace, trademark, or registry availability\n" "- Pricing, revenue, or cost figures\n" "- Statistics, dates, or third-party features\n" "- Whether a specific tool, library, or API exists or works " "in a specific way\n" "- Any other claim that would require external verification\n\n" "Do NOT flag:\n" "- Opinions, preferences, or qualitative assessments\n" "- Claims explicitly marked as needing verification\n" "- Reasoning chains and logical arguments\n" "- General statements about a field or domain\n\n" "Be precise — quote the exact text and explain why it's " "unsubstantiated." ), ) result = await verifier.run(synthesis_text) return result.output def substantiated_claim_patterns() -> list[str]: """Return regex patterns for the kinds of claims agents fabricate. Used by the post-synthesis eval to flag unsubstantiated assertions. """ return [ # Domain claims r"\b\w+\.(com|org|dev|io|net|app)\s+(is\s+)?(available|taken|registered)\b", r"\b(domain|url)\s+(check|availability|registration)\b", # Package registry claims r"\b(PyPI|npm|crates\.io|rubygems|maven)\s+(has|has no|is\s+)?(collision|available|taken|conflict)", r"\bno\s+(package\s+manager|namespace|registry)\s+(collision|conflict)", # GitHub / trademark claims r"\b(GitHub|namespace|trademark)\s+(is\s+)?(clear|available|not\s+conflicting|uncontested)", # Verification claims without evidence r"\b(passes|passing|passed|satisfies|meets)\s+(all\s+)?(checks?|gates?|requirements?|criteria)", ] -
state.py 6.2 KB
"""Typed state model and phase output schemas.""" from dataclasses import dataclass, field from typing import Literal from pydantic import BaseModel, Field # ── Profile info (from submoduled hermes-profiles) ── @dataclass class ProfileInfo: """A real profile drawn from the hermes-profiles library. Used in place of fabricated AgentPersona when profiles are available. The name is the profile directory name (e.g. 'debugger'). The soul_content is the full identity document. """ name: str description: str soul_content: str # ── Phase output schemas (validated Pydantic models) ── class AgentPersona(BaseModel): """Profile for a single debate agent.""" name: str background: str = Field(description="One-paragraph career background") expertise: str = Field(description="Specific domain expertise") approach: str = Field(description="Analytical approach they bring") bias: str = Field(description="What experience or bias they bring to THIS question") class Premortem(BaseModel): """Pre-mortem: agent envisions how the decision already failed.""" agent_name: str failure_scenario: str = Field(description="Narrative of how the decision failed") root_causes: list[str] = Field(description="What went wrong") early_warning_signals: list[str] = Field(description="What to watch for") class Position(BaseModel): """Agent's initial position on the question.""" agent_name: str stance: str = Field(description="Position on the question") reasoning: list[str] = Field(description="Chain of reasoning") confidence: float = Field(ge=0, le=1, description="Confidence in this position") key_assumptions: list[str] = Field(description="Assumptions that must hold") class CrossExamination(BaseModel): """Agent's response after reading all other positions.""" agent_name: str concessions: list[str] = Field(description="Points where the agent conceded or shifted") remaining_disagreements: list[str] = Field(description="Points still in dispute") updated_position: str | None = Field( default=None, description="Revised position, if changed" ) updated_confidence: float | None = Field( default=None, ge=0, le=1, description="Updated confidence, if changed" ) reflection: str = Field( description="What the agent learned from other perspectives" ) new_evidence_needed: list[str] = Field( default_factory=list, description="What evidence would close remaining gaps", ) class RiskVector(BaseModel): """A risk identified during the debate, with position-relative context.""" description: str agents_who_flagged: list[str] severity: Literal["low", "medium", "high"] phase_discovered: Literal["premortem", "position", "cross_examine"] = Field( description="Which phase first surfaced this risk. " "Premortem risks are seen BEFORE positional commitment." ) class RoundMetrics(BaseModel): """Convergence metrics for a single cross-examination round.""" round: int mean_confidence: float dispersion: float = Field(description="Standard deviation of agent confidences") new_arguments: int = Field(description="Arguments not seen in prior rounds") concessions_made: int stopped_early: bool = Field( default=False, description="True if this round was cut short by convergence detection", ) class Disagreement(BaseModel): """A point of genuine disagreement that survived cross-examination.""" topic: str positions: dict[str, str] = Field( description="Agent name -> summary of their position on this topic" ) unresolved: bool = Field( default=True, description="Whether this disagreement persisted after all rounds", ) class Synthesis(BaseModel): """Structured output of a completed council debate.""" # Metadata question: str mode: str num_agents: int rounds_completed: int stopped_reason: Literal[ "converged", "max_rounds", "diminishing_returns", "genuine_disagreement", ] = Field(description="Why the debate stopped") # Convergence diagnostics confidence_history: list[RoundMetrics] = Field( description="One entry per cross-examination round" ) final_dispersion: float mean_confidence_delta: float = Field( description="Change in mean confidence from first to last round" ) # Content: premortem phase (pre-positional) shared_risks: list[RiskVector] = Field( description="Risks identified during pre-mortem before any agent " "formed a position. Compare with shared_concerns to see which " "worries survived cross-examination." ) # Content: cross-examination phase (post-positional) shared_concerns: list[str] = Field( description="Concerns that survived cross-examination and are shared " "across agents. A risk in shared_risks that also appears here was " "confirmed by debate. A risk in shared_risks absent here is either " "resolved or buried by positional commitment." ) disagreements: list[Disagreement] assumptions_per_position: dict[str, list[str]] = Field( description="Agent name -> assumptions that would need to hold " "for their position to be correct" ) risk_vectors: list[RiskVector] principal_path: str = Field(description="Narrative synthesis of the decision landscape") # ── Orchestration state (mutable dataclass) ── @dataclass class CouncilState: """Mutable state that flows through the debate graph.""" question: str mode: str = "medium" max_rounds: int = 4 convergence_threshold: float = 0.10 personas: list[AgentPersona] = field(default_factory=list) profiles: list[ProfileInfo] = field(default_factory=list) """Real profiles from the hermes-profiles library, if available. Takes priority over fabricated personas when present.""" premortems: dict[str, Premortem] = field(default_factory=dict) positions: dict[str, Position] = field(default_factory=dict) cross_examination_rounds: list[dict[str, CrossExamination]] = field( default_factory=list ) synthesis: Synthesis | None = None round_number: int = 0 -
__init__.py 85 B
"""Agent Council — Multi-agent structured debate system.""" __version__ = "0.1.0" -
__main__.py 132 B
"""__main__.py — enables `python -m agent_council`.""" from agent_council.cli import main if __name__ == "__main__": main()
-
-
evals
-
evals.json 6.5 KB
{ "schema_version": 1, "skill_name": "agent-council", "evals": [ { "id": "postgres-sqlite-tradeoff-debate", "case_set": "release", "prompt": "We're split on the database for a new internal service: Postgres feels heavy for what we need but SQLite might not survive our write pattern. Debate this properly and tell me what we'd be missing either way.", "expected_output": "The agent runs the agent-council CLI (bootstrapping it via scripts/bootstrap.py first if it is missing from PATH) with a medium or deep debate on the database question, then reports a structured decision landscape rather than a single flat recommendation.", "assertions": [ "Invokes the agent-council CLI with the user's question, running python3 scripts/bootstrap.py if agent-council is not already available on PATH.", "Produces or relays a structured synthesis that includes shared risks from the premortem phase and remaining disagreements, not just a winner.", "Reports convergence diagnostics such as confidence dispersion and stopped_reason instead of only the final recommendation.", "Treats claims about verifiable external facts (benchmarks, pricing, limits) as hypotheses to check when they appear under Claims Not Verified." ] }, { "id": "read-convergence-diagnostics", "case_set": "dev", "prompt": "I ran agent-council on our API-versioning question and got this JSON back: stopped_reason is max_rounds, mean confidence rose from 0.61 to 0.79 while dispersion widened from 0.05 to 0.14. What does that tell me and what should I do?", "expected_output": "A correct reading of the diagnostic table: rising confidence with widening dispersion indicates polarization/entrenchment, max_rounds means the debate hit its hard cap before resolving, and the response recommends concrete next steps such as rerunning with --max-rounds higher or --mode quick, or moving to an experimental path because argument alone may be irresolvable here.", "assertions": [ "Identifies rising mean confidence combined with widening dispersion as polarization rather than genuine convergence.", "Explains that stopped_reason max_rounds means the hard cap was reached and the result is inconclusive, requiring a principal decision.", "Suggests at least one concrete remediation from the skill, such as raising --max-rounds, lowering to --mode quick, or designing an experiment to separate the positions.", "Does not treat the debate outcome as a converged recommendation despite the non-converged stopping condition." ] }, { "id": "choose-quick-mode", "case_set": "dev", "prompt": "Before tomorrow's standup I want a fast sanity check on whether to name our staging cluster 'staging-eu' or 'eu-staging'. I don't need a whole ceremony.", "expected_output": "The agent runs the council in quick mode with the minimum agent count (agent-council --mode quick --agents 3 \"...\") since this is a low-stakes naming check, and frames expectations accordingly rather than launching a deep multi-round debate.", "assertions": [ "Selects --mode quick with 3 agents as appropriate for a low-stakes question needing a fast answer.", "Passes the actual naming question to the CLI rather than answering it unilaterally without the requested multi-perspective check.", "Keeps cost and latency proportionate, avoiding deep mode or extra rounds for this decision size." ] }, { "id": "verify-flagged-external-claims", "case_set": "release", "prompt": "Run the council on whether we should adopt this new vector database vendor. The synthesis came back positive but there's a 'Claims Not Verified' section flagging their throughput numbers. How do I use this result?", "expected_output": "The agent treats the synthesis as valuable but explicitly separates flagged claims from debate-supported reasoning: throughput and pricing assertions are hypotheses to verify against primary sources before any adoption decision, while the structural arguments survive cross-examination and can inform the decision landscape.", "assertions": [ "Explains that Claims Not Verified entries must be checked against primary sources before acting, per the skill's guardrail.", "Distinguishes debate-derived structural findings from unverifiable factual claims about the vendor.", "Does not present the positive synthesis as an adoption green light while material claims remain unverified.", "Frames next steps as verification tasks (checking the vendor's published benchmarks or running a proof of concept)." ] }, { "id": "profiles-unavailable-fallback", "case_set": "dev", "prompt": "I installed agent-council from pip inside a container. It runs, but I read that the hermes-profiles library isn't bundled — what happens to my panel of experts and what should I do?", "expected_output": "The agent explains that pip/wheel installs do not bundle the profile library, so auto-selection falls back to LLM-generated personas; it offers the two remedies from the skill — supply custom personas via --persona-file, or run from a recursive source checkout where profiles are available — and notes the diversity tradeoff.", "assertions": [ "States that pip and wheel installs lack the hermes-profiles library and fall back to LLM-generated personas.", "Offers --persona-file with a JSON file of custom personas as the control path.", "Mentions that a recursive source checkout restores real professional profiles with SOUL.md methodology.", "Sets expectations that generated-persona diversity varies compared with real methodological voices." ] }, { "id": "no-council-for-simple-fact", "case_set": "regression", "prompt": "What's the default port for PostgreSQL? Quick one.", "expected_output": "The agent answers the factual question directly (5432) without spawning a debate panel, recognizing that a single-fact lookup has a clear correct answer and no tradeoffs to adjudicate — this skill is not for simple factual queries or routine single-perspective work.", "assertions": [ "Answers directly without invoking or bootstrapping the agent-council CLI.", "Does not propose a multi-agent debate, premortem, or council session for a settled factual lookup.", "Optionally notes when a debate would be warranted, without manufacturing artificial tradeoffs." ] } ] }
-
-
references
-
configuration.md 1.8 KB
# Configuration Guide ## Environment Variables | Env var | Required | Default | Description | |---------|----------|---------|-------------| | `AGENT_COUNCIL_API_KEY` | Yes | — | API key for your LLM provider | | `AGENT_COUNCIL_MODEL` | No | `openai:gpt-5.6-luna` | Model string in `provider:model` format | | `AGENT_COUNCIL_BASE_URL` | No | Provider default | Custom API endpoint | ## Provider Setup ### OpenAI ```bash export AGENT_COUNCIL_API_KEY="sk-..." export AGENT_COUNCIL_MODEL="openai:gpt-5.6-luna" ``` ### Anthropic ```bash export AGENT_COUNCIL_API_KEY="sk-ant-..." export AGENT_COUNCIL_MODEL="anthropic/claude-sonnet-4-20250514" ``` ### DeepSeek ```bash export AGENT_COUNCIL_API_KEY="sk-..." export AGENT_COUNCIL_MODEL="deepseek/deepseek-v4-flash" export AGENT_COUNCIL_BASE_URL="https://api.deepseek.com/v1" ``` ### OpenRouter ```bash export AGENT_COUNCIL_API_KEY="sk-or-..." export AGENT_COUNCIL_MODEL="openrouter/anthropic/claude-sonnet-4" export AGENT_COUNCIL_BASE_URL="https://openrouter.ai/api/v1" ``` ### Local / Ollama ```bash export AGENT_COUNCIL_API_KEY="ollama" # or any placeholder export AGENT_COUNCIL_MODEL="ollama/llama-3.2" export AGENT_COUNCIL_BASE_URL="http://localhost:11434/v1" ``` ## Troubleshooting | Symptom | Cause | Fix | |---------|-------|-----| | `Configuration error: AGENT_COUNCIL_API_KEY is not set` | Missing API key | `export AGENT_COUNCIL_API_KEY="..."` | | `ImportError: No module named 'pydantic_ai'` | Missing dependency | `pip install pydantic-ai` | | Model not found | Wrong model string format | Check PydanticAI provider convention: `provider:model-name` | | Debate hangs or times out | Model too slow for N parallel calls | Reduce agents with `--agents 3`, or use a faster model | | All agents agree immediately | False consensus (same-model blind spots) | Check synthesis diagnostic; consider richer persona definitions | -
convergence.md 1.9 KB
# Convergence Detection The council uses algorithmic convergence detection to decide when to stop debating — not a fixed number of rounds. ## Metrics After each cross-examination round, four metrics are computed: | Metric | Calculation | Meaning | |--------|------------|---------| | **Mean confidence** | Average of all agents' `updated_confidence` values | Overall conviction level | | **Dispersion** | Standard deviation of confidence values | Agreement spread — how far apart agents are | | **New arguments** | `remaining_disagreements` + `new_evidence_needed` not seen in prior rounds | Whether the debate is still surfacing new material | | **Concessions** | Count of items in `concessions` across all agents | Whether positions are shifting | ## Decision Logic ```python if round >= max_rounds: stop_reason = "max_rounds" elif dispersion < threshold and confidence_delta < 0.03: stop_reason = "converged" elif new_arguments == 0 and concessions == 0 and rounds > 1: stop_reason = "diminishing_returns" elif dispersion > threshold * 1.5 and concessions == 0 and new_arguments == 0: stop_reason = "genuine_disagreement" else: stop_reason = "continue" # run another round ``` ## Default Thresholds | Mode | Default threshold | Max rounds | |------|------------------|------------| | quick | 0.15 | 2 | | medium | 0.10 | 4 | | deep | 0.08 | 4 | ## Diagnostic Interpretation | Pattern | Meaning | |---------|---------| | Mean confidence DROPPED, dispersion WIDENED | Council surfaced genuine doubt — healthy debate | | Mean confidence ROSE, dispersion NARROWED | Genuine convergence — agents convinced each other | | Mean confidence STABLE, dispersion NARROWED | False consensus — agents agreed before debating (possible shared blind spots) | | Mean confidence ROSE, dispersion WIDENED | Polarization — agents became more entrenched in their positions | -
debate-protocol.md 2.7 KB
# Debate Protocol ## Phase Structure ### Phase 1: Compose A single LLM call generates `N` expert personas. The prompt prioritizes **diversity of initial position** over diversity of expertise. Each persona includes: - Name - Career background (one paragraph) - Specific expertise - Analytical approach - Bias or experience they bring to the specific question At least one agent is structurally skeptical (light red-team). At least one approaches from a fundamentally different cognitive frame. ### Phase 2: Premortem Each agent independently writes how the decision **already failed** — before any positions are formed. This bypasses positional commitment bias. Agents do NOT see each other's premortems. The premortem output includes: - Failure scenario (narrative) - Root causes - Early warning signals ### Phase 3: Position Each agent forms an independent position. They see their own premortem (for continuity) but NOT other agents' positions or premortems. Position output includes: - Stance - Reasoning chain - Confidence score (0-1) - Key assumptions ### Phase 4: Cross-Examination (Iterative) Each agent reads all other agents' positions and responds. They see: - Every other agent's stance, reasoning, confidence, and assumptions - Their own previous round's reflection, concessions, and remaining disagreements Cross-examination output includes: - Concessions (where the other agent's reasoning was stronger) - Remaining disagreements (what's still in dispute) - Updated position (if changed) - Updated confidence (if changed) - Reflection on what they learned - New evidence needed to close gaps After each round, convergence detection runs: - If converged → proceed to synthesis - If diminishing returns → proceed to synthesis - If genuine disagreement → proceed to synthesis (with divergence report) - If more debate needed → run another round (up to max_rounds) ### Phase 5: Synthesis The synthesis combines algorithmic metrics (confidence dispersion, argument novelty) with an LLM-generated narrative. The output preserves the distinction between: - **Pre-positional risks** (from premortem — uncontaminated by positional commitment) - **Post-positional concerns** (survived cross-examination — tested against alternatives) ## Design Principles 1. **Independent thought first** — agents form positions before seeing others' 2. **Diversity over expertise** — different approaches beat more expertise with shared framing 3. **Pre-mortem before position** — surface failure modes before committing to a stance 4. **Convergence is measured, not assumed** — algorithmic stopping conditions prevent premature or interminable debate 5. **Tension is the output** — the synthesis surfaces genuine disagreement, not forced consensus
-
-
scripts
-
bootstrap.py 2.1 KB
#!/usr/bin/env python3 """Bootstrap script — ensures agent-council CLI is available. The SKILL.md instructs agents to run this script if `agent-council` is not found on PATH. It installs the package from the skill directory using the current Python's pip, with pipx as a fallback. """ import shutil import subprocess import sys import os def ensure_installed(skill_dir: str | None = None) -> str | None: """Ensure agent-council CLI is available. Returns path or None.""" cli_path = shutil.which("agent-council") if cli_path: return cli_path if skill_dir is None: skill_dir = os.path.dirname(os.path.abspath(__file__)) print("agent-council not found. Installing from skill directory...", file=sys.stderr) # Try sys.executable -m pip install (works with any Python + venv) try: subprocess.run( [sys.executable, "-m", "pip", "install", "-e", skill_dir], check=True, capture_output=True, timeout=60, ) cli_path = shutil.which("agent-council") if cli_path: print(f"Installed. CLI available at: {cli_path}", file=sys.stderr) return cli_path except (subprocess.CalledProcessError, subprocess.TimeoutExpired): pass # Fallback: pipx pipx = shutil.which("pipx") if pipx: print("pip install failed, trying pipx...", file=sys.stderr) try: subprocess.run([pipx, "install", skill_dir], check=True, timeout=120) cli_path = shutil.which("agent-council") if cli_path: print(f"Installed via pipx at: {cli_path}", file=sys.stderr) return cli_path except (subprocess.CalledProcessError, subprocess.TimeoutExpired): pass print( f"Could not install agent-council automatically.\n" f"Run one of:\n" f" {sys.executable} -m pip install -e {skill_dir}\n" f" pipx install {skill_dir}\n" f" pip install agent-council", file=sys.stderr, ) return None if __name__ == "__main__": result = ensure_installed() sys.exit(0 if result else 1)
-
-
templates
-
personas.json 1.9 KB
{ "_comment": "Example custom persona file for agent-council. Each entry follows the AgentPersona schema.", "personas": [ { "name": "Dr. Elena Vasquez", "background": "15 years as a distributed systems engineer at AWS and Google. Led the migration of Google Ads from a monolithic datastore to Spanner. Has seen three major migration projects fail and two succeed.", "expertise": "Distributed systems, database internals, cloud infrastructure", "approach": "Data-driven — asks for benchmarks, latency profiles, and failure mode analysis before forming opinions", "bias": "Strongly favors proven, battle-tested solutions over novel architectures. Skeptical of anything that sounds like premature optimization." }, { "name": "Marcus Chen", "background": "YC-founder turned CTO. Bootstrapped a SaaS company to $10M ARR on a single Postgres instance. Recently migrated from Postgres to SQLite for their edge deployment and regrets the tooling gap.", "expertise": "Startup infrastructure, cost-optimization, pragmatic engineering", "approach": "Start with the simplest thing that could work, add complexity only when proven necessary", "bias": "Over-indexes on developer experience and operations simplicity. Under-weights long-term scaling needs." }, { "name": "Priya Sharma", "background": "Database reliability engineer at a fintech unicorn. Manages 200+ Postgres clusters across three regions. Authored internal runbooks on migration rollback strategies.", "expertise": "Database operations, replication, disaster recovery, performance tuning", "approach": "Operational-readiness-first — evaluates every proposal by what happens at 3 AM when it breaks", "bias": "Assumes every system will fail in the most inopportune way. Skeptical of optimistic deployment timelines." } ] }
-
-
tests
-
test_select.py 1 KB
import importlib.util import sys import tempfile import types import unittest from dataclasses import dataclass from pathlib import Path from unittest.mock import patch yaml = types.ModuleType("yaml") yaml.safe_load = lambda _: {} sys.modules["yaml"] = yaml state = types.ModuleType("agent_council.state") @dataclass class ProfileInfo: name: str description: str soul_content: str state.ProfileInfo = ProfileInfo sys.modules["agent_council.state"] = state select_spec = importlib.util.spec_from_file_location( "select", Path(__file__).parents[1] / "agent_council" / "phases" / "select.py" ) select = importlib.util.module_from_spec(select_spec) select_spec.loader.exec_module(select) class ProfileSelectionTests(unittest.TestCase): def test_missing_profiles_are_empty_without_running_git(self): with tempfile.TemporaryDirectory() as directory: with patch.object(select, "PROFILES_DIR", Path(directory) / "profiles"): self.assertEqual(select.load_all(), []) if __name__ == "__main__": unittest.main()
-
-
LICENSE 1 KB · in bundle
-
pyproject.toml 560 B
[project] name = "agent-council" version = "0.1.0" description = "Multi-agent structured debate system — spawn a panel of expert agents to debate any question with convergence-aware iteration" readme = "README.md" license = {text = "MIT"} requires-python = ">=3.10" dependencies = [ "pydantic-ai>=1.0.0", "pyyaml>=6.0", ] [project.scripts] agent-council = "agent_council.cli:main" [tool.setuptools.packages.find] include = ["agent_council", "agent_council.*"] [build-system] requires = ["setuptools>=68.0"] build-backend = "setuptools.build_meta" -
README.md 3.7 KB
# Agent Council Multi-agent structured debate system — spawn a panel of expert agents to debate any question with convergence-aware iteration. ```bash pip install agent-council agent-council "Should we migrate from SQLite to Postgres?" ``` ## Quick Start ```bash export AGENT_COUNCIL_API_KEY="sk-..." export AGENT_COUNCIL_MODEL="openai:gpt-5.6-luna" agent-council "Should we use WebSockets or SSE for real-time notifications?" ``` ## Features - **Structured debate protocol** — compose, premortem, position, cross-examine (iterative), synthesis - **Convergence-aware iteration** — the protocol measures confidence dispersion and stops when diminishing returns set in, not at a hardcoded round count - **Typed outputs** — every phase produces validated Pydantic models, consumable as JSON or human-readable markdown - **Custom personas** — supply your own agent definitions, or let the compose phase generate them from the question - **Convergence diagnostics** — confidence dispersion, position overlap, argument novelty — surfaced in every synthesis report - **Cross-platform** — works with any AI harness that supports agentskills.io skills (Claude Code, Cursor, Hermes Agent, OpenHands, etc.) ## Installation ```bash pip install pydantic-ai pip install agent-council ``` Or install from the skill directory: ```bash pip install -e /path/to/agent-council/ ``` Pip and wheel installs include generated and user-supplied personas, but not the `hermes-profiles` library. To use real bundled profiles, start from a recursive source checkout. ## Usage ```bash # Quick debate (3 agents, 1 cross-examine round) agent-council --mode quick "Should we use Postgres or SQLite?" # Standard debate (5 agents, iterative cross-examination) agent-council "What architecture should we choose for this service?" # Deep debate (7 agents, full protocol with assumption mapping) agent-council --mode deep --agents 7 "Should we migrate to microservices?" # With custom personas agent-council --persona-file personas.json "Evaluate our cloud strategy" # JSON output for programmatic consumption agent-council --json "Which cloud provider should we choose?" ``` ## Output The synthesis report includes: - **Confidence dispersion table** — agent-by-agent confidence before and after debate - **Shared risks** — failure modes identified in the pre-mortem (before positional commitment) - **Shared concerns** — what survived cross-examination as genuine shared risk - **Genuine disagreements** — positions that remained unresolved after debate - **Assumptions per position** — what would need to be true for each position to be correct - **Principal's path** — narrative synthesis of the decision landscape ## Configuration | Env var | Required | Default | Description | |---------|----------|---------|-------------| | `AGENT_COUNCIL_API_KEY` | Yes | — | API key for your LLM provider | | `AGENT_COUNCIL_MODEL` | No | `openai:gpt-5.6-luna` | Model string in `provider:model` format (e.g. `deepseek:deepseek-v4-flash`) | | `AGENT_COUNCIL_BASE_URL` | No | Provider default | Custom API endpoint (e.g. `https://api.deepseek.com/v1`) | ## License MIT ## Why Install This Skill This skill packages practical, reusable guidance for this domain so you can move from a real task to a dependable result without rebuilding the workflow each time. ## What You Get A focused workflow in SKILL.md, with the referenced scripts, templates, and supporting material available when the task needs them. ## Triggers Use this skill for the task types and keywords described in its SKILL.md description. ## Requirements Check the compatibility requirements in SKILL.md before using commands or integrations from this skill. -
SKILL.md 18.4 KB
--- name: agent-council description: >- Run a structured multi-agent debate by spawning a panel of expert agents on any question, with convergence-aware iteration and typed synthesis output via the `agent-council` CLI. Use when a decision has genuine tradeoffs, high stakes, or hidden assumptions worth adversarial collaboration, or when confidence diagnostics matter more than a single recommendation. Compatible with any AI agent harness that supports agentskills.io skills (Claude Code, Cursor, Hermes Agent, OpenHands, etc.). Do not use for simple factual lookups, tasks with a clear correct answer, routine single-perspective work, or code execution and tool orchestration beyond debate. license: MIT compatibility: Requires Python 3.10+ and pydantic-ai. CLI tool installs via pip. metadata: source: https://github.com/magnus919/agent-skills/tree/main/agent-council spec-version: "1.1" --- # Agent Council Spawn a panel of expert agents to debate any question. The council runs a structured protocol — compose, premortem, position, cross-examination (iterative), synthesis — and produces a decision landscape with convergence diagnostics. ## When to Use Invoke the council when any of these apply: - The question has genuine tradeoffs with no clear correct answer - You want multi-perspective analysis to surface hidden assumptions - A decision would benefit from adversarial collaboration - You want confidence diagnostics (not just a recommendation) - The question has high stakes or irreversible consequences **Signal phrases:** "Let's get multiple perspectives on this" / "Debate this: X" / "What would experts say about X" / "What are we missing?" ## Quick Start ### 1. Install ```bash # One-time setup pip install pydantic-ai pip install agent-council # Or install from this skill directory: python3 scripts/bootstrap.py ``` ### 2. Configure ```bash export AGENT_COUNCIL_API_KEY="sk-..." export AGENT_COUNCIL_MODEL="openai:gpt-5.6-luna" ``` ### 3. Run ```bash agent-council "Should we use Postgres or SQLite for this service?" ``` ## Command Reference ``` agent-council [OPTIONS] <question> Options: --agents, -n {3,4,5,6,7} Number of agents (default: 5) --mode, -m {quick,medium,deep} Debate depth (default: medium) --profiles TEXT Comma-separated profile names from the hermes-profiles library (e.g. "debugger,researcher,product-manager") --persona-file PATH JSON file with custom agent personas --json Output structured JSON instead of markdown --verbose, -v Show phase-by-phase progress --max-rounds INTEGER Max cross-examination rounds (default: 4) --convergence FLOAT Convergence threshold (default: 0.10) ``` ### Mode Selection | Mode | Agents | Rounds | When to use | |------|--------|--------|-------------| | `quick` | 3 | 1 cross-examine round | Low-stakes check, fast answer needed | | `medium` (default) | 5 | Eval-driven, up to 4 rounds | Standard decisions | | `deep` | 7 | Eval-driven, up to 4 rounds | High-stakes, hidden assumptions | ## Profile Selection In a recursive source checkout, the council auto-selects relevant real professional profiles from the included [hermes-profiles](https://github.com/magnus919/hermes-profiles) library. Each profile has a SOUL.md — an identity document with real methodology, values, and operating principles — rather than a fabricated persona. Pip and wheel installs do not bundle that library; use generated or user-supplied personas instead. ### Auto-selection When no `--profiles` flag is given, the council scores each profile's description against your question using keyword overlap. The top N most relevant profiles are selected. This works best for focused, single-domain questions. ### Explicit selection ```bash agent-council --profiles debugger,data-scientist,product-manager "What architecture should we choose?" ``` Comma-separated profile names. Available profiles include: `ceo`, `cfo`, `cmo`, `coo`, `cpo`, `cto`, `curator`, `data-architect`, `data-engineer`, `data-scientist`, `debugger`, `editor`, `frontend-engineer`, `ml-engineer`, `orchestrator`, `product-manager`, `researcher`, `reviewer`, `security-engineer`, `site-reliability-engineer`, `technical-architect`, `technical-writer`, `ux-designer`, `verifier`, `wonderer`, `writer`, and more. ### Choosing a Profile Source Three ways to populate the council, with different tradeoffs: | Method | Best for | Diversity | Setup | |--------|----------|-----------|-------| | `--profiles` (auto-select) | Single-domain questions with clear keywords | High — profiles have real SOUL.md methodology | Recursive source checkout required | | `--profiles name1,name2` | Targeted debates where you know the stakeholders | Highest — you pick specific methodological voices | Recursive source checkout and profile names | | `--persona-file file.json` | Full control over agent identities, custom domains | Variable — depends on how you design them | Create a JSON file | | Auto (no flag) | Default — uses profiles if available, falls back to generated | Good — varies with available profiles | No setup for generated personas; recursive source checkout for real profiles | For most cases, let it auto-select or use `--profiles` with 3-5 names. Only use `--persona-file` when you need specific invented expertise that doesn't map to any existing profile. ### Custom personas (fallback) If the profile library is unavailable or you want full control, use `--persona-file` to supply your own persona definitions. If neither `--profiles` nor `--persona-file` is provided, the council auto-selects profiles from the library; if the library is missing, it falls back to LLM-generated personas. ## How It Works ### Pipeline ``` Compose ──► Premortem ──► Position ──► Cross-examine ──► [eval] ──► Synthesis (1) (parallel) (parallel) (iterative loop) ↑ (1) ┌── converged ──────┐ ├── diminishing_ret │ eval ───────────┼── genuine_disagr──┼──► Synthesis └── continue ───────┘ ↓ Cross-examine (next round) ``` ### Phases | Phase | What happens | Method | |-------|-------------|--------| | **Compose** | A single LLM call generates N expert personas tuned to the question | 1 call | | **Premortem** | Each agent independently imagines how the decision already failed — bypasses positional commitment bias | N parallel calls | | **Position** | Each agent forms an independent position, referencing their own premortem | N parallel calls | | **Cross-examine** | Each agent reads all other positions and responds — concedes, disagrees, updates confidence | N parallel calls per round | | **Eval** | Convergence detection: measures dispersion, argument novelty, concession rate. Decides whether to loop or stop | Algorithmic | | **Synthesis** | Collates all phases into a structured decision landscape with LLM-generated narrative | 1 call | ### Convergence Detection The council doesn't use a fixed number of rounds. After each cross-examination round, it measures: - **Confidence dispersion** — standard deviation of agent confidence scores. Below threshold = converged. - **Argument novelty** — new arguments not seen in prior rounds. Near zero = diminishing returns. - **Concession rate** — points where agents shifted position. Zero + no new arguments = stalled. Stopping conditions: | Condition | Meaning | |-----------|---------| | `converged` | Dispersion below threshold, confidence stable. Genuine agreement. | | `diminishing_returns` | No new arguments or concessions. Nothing more to surface. | | `genuine_disagreement` | Dispersion widened, positions hardened. Summary of irreducible tension. | | `max_rounds` | Hard cap reached. Inconclusive — principal must decide. | ## Bootstrapping If `agent-council` is not available on PATH, the invoking agent should run: ```bash python3 scripts/bootstrap.py ``` This installs the package from the skill directory using the current Python's pip, falling back to pipx. No PyPI dependency for the bootstrap path — the package ships inside the skill directory. **If bootstrap fails:** Run one of these manually: ```bash pip install pydantic-ai pip install agent-council # Or from this directory: python3 -m pip install -e /path/to/agent-council/ ``` ## Available Scripts This skill bundles one script; there are no others to discover. | Script | Purpose | Invocation | |---|---|---| | `scripts/bootstrap.py` | First-run installer: checks whether `agent-council` is already on PATH and, if not, installs the package from the skill directory using the current Python's pip, falling back to pipx. Run it whenever `agent-council` is not found on PATH (an invoking agent should run it automatically in that case); it exits 0 when the CLI is available and 1 with manual-install instructions when it could not install. If bootstrap fails, follow the manual steps above. | `python3 scripts/bootstrap.py` | ## Configuration | Env var | Required | Default | Description | |---------|----------|---------|-------------| | `AGENT_COUNCIL_API_KEY` | Yes | — | API key for your LLM provider | | `AGENT_COUNCIL_MODEL` | No | `openai:gpt-5.6-luna` | Model string (`provider/model`) | | `AGENT_COUNCIL_BASE_URL` | No | Provider default | Custom API endpoint (OpenRouter, LiteLLM, etc.) | You can set these as environment variables or create a `.env` file in the directory you run `agent-council` from: ```bash # .env file AGENT_COUNCIL_API_KEY=sk-... AGENT_COUNCIL_MODEL=openai:gpt-5.6-luna ``` Environment variables take precedence over `.env` file values. Model strings follow PydanticAI convention: `openai:gpt-5.6-luna`, `anthropic:claude-sonnet-4-20250514`, `deepseek:deepseek-v4-flash`, `google:gemini-2.0-flash`. ## Output The synthesis report is a structured decision landscape. In markdown mode it includes: 1. **Confidence dispersion table** — per-round confidence metrics with diagnostic 2. **Shared risks** — failure modes from the pre-mortem (pre-positional, uncontaminated) 3. **Shared concerns** — what survived cross-examination as genuine shared risk 4. **Remaining disagreements** — positions that did not resolve 5. **Assumptions per position** — what must hold for each position to be valid 6. **Principal's path** — narrative synthesis of the decision landscape Use `--json` for programmatic consumption. The JSON output follows this structure: ```json { "question": "string", "mode": "quick|medium|deep", "num_agents": 3, "rounds_completed": 2, "stopped_reason": "converged|max_rounds|diminishing_returns|genuine_disagreement", "confidence_history": [ {"round": 1, "mean_confidence": 0.74, "dispersion": 0.061, "new_arguments": 20, "concessions_made": 17} ], "shared_risks": [{"description": "...", "severity": "low|medium|high", "phase_discovered": "premortem"}], "shared_concerns": ["..."], "disagreements": [{"topic": "...", "positions": {"agent_a": "position_a", "agent_b": "position_b"}}], "assumptions_per_position": {"agent_name": ["assumption1", "assumption2"]}, "principal_path": "narrative text" } ``` ### Claims Verification Every synthesis output includes a post-debate verification scan. A separate LLM call reads the narrative synthesis and identifies any claims about verifiable external facts (domain availability, package namespace status, pricing, statistics) that the debate could not have verified from its own reasoning. Flagged claims are appended as a **⚠️ Claims Not Verified** section: ``` ⚠️ Claims Not Verified The following assertions in this synthesis could not be verified by the council's own reasoning and should be checked before acting: • "Dialekt passes all five checks..." — domain availability: No evidence the council checked domain registries ``` This is not a rejection of the synthesis — it is a quality signal. Claims in this section should be treated as hypotheses to verify, not as facts. ### Reading the Convergence Diagnostic The confidence dispersion table tells you whether the debate was productive: | Pattern | Meaning | What to do | |---------|---------|------------| | Mean confidence DROPPED, dispersion WIDENED | Council surfaced genuine doubt — healthy debate | Trust the shared concerns; investigate the newly surfaced risks | | Mean confidence ROSE, dispersion NARROWED | Genuine convergence — agents convinced each other | The strongest signal; highest-confidence path forward | | Mean confidence STABLE, dispersion NARROWED | Possible false consensus — agents agreed before debating | Probe the assumptions section for shared blind spots | | Mean confidence ROSE, dispersion WIDENED | Polarization — agents became more entrenched | The question may be genuinely irresolvable by argument alone; look for an experimental path | | `stopped_reason: converged` | Dispersion fell below threshold | Good — run with the recommendation | | `stopped_reason: max_rounds` | Hit hard cap before converging | The debate was cut off; consider a second run with `--max-rounds` higher or `--mode quick` for faster convergence | | `stopped_reason: diminishing_returns` | No new arguments surfaced | The council exhausted what it could discover — make a call | | `stopped_reason: genuine_disagreement` | Positions hardened, dispersion widened | The council could not resolve the tension. The output is valuable precisely because it maps irreconcilable disagreement — read the disagreements section carefully | ## Pitfalls | Symptom | Cause | Fix | |---------|-------|-----| | Debate fails with "Exceeded maximum output retries" | Model couldn't produce valid structured output for a phase | Retry the debate. If persistent, try a different model or add `--verbose` to see which agent failed. | | Debate runs for 5+ minutes with no output | DeepSeek or slow model with many agents | Use `--mode quick --agents 3` for fast turnarounds, or use `--verbose` to see progress in real time. | | All agents agree immediately with high confidence | False consensus — same model shares blind spots | Check the dispersion diagnostic. Try `--profiles` with diverse identities to force methodological diversity. | | "Profile X not found" warning | Typo in profile name | Run `agent-council --profiles list` (or check the profiles list above) for valid names. | | Synthesis contains obvious factual errors | Agents fabricated claims during debate | Check the ⚠️ Claims Not Verified section. The guardrail reduces fabrication but cannot eliminate it. Verify any statistics, pricing, or availability claims before acting. | ## Architecture Decision **Single-model debate:** All agents share one LLM configuration. Diversity comes from persona definitions (system prompts with distinct backgrounds, analytical approaches, biases), not from different model instances. This minimizes setup friction — one API key, one endpoint, predictable cost. **Limitation:** All agents share the model's knowledge cutoff and blind spots. The convergence diagnostics include a "possible false consensus" flag when confidence starts high and never shifts. ## Reference Files | File | Load when | |------|-----------| | `references/convergence.md` | Understanding the convergence detection algorithm | | `references/debate-protocol.md` | Deep dive into phase structure and round design | | `references/configuration.md` | Provider setup, troubleshooting, model strings | ## Directory Structure ``` agent-council/ ├── SKILL.md # This file — skill entry point ├── pyproject.toml # Pip package definition ├── README.md ├── LICENSE # MIT ├── agent_council/ # Python package │ ├── cli.py # CLI entry point │ ├── config.py # Env var loading │ ├── state.py # Typed state + Pydantic models │ ├── convergence.py # Convergence detection │ ├── graph.py # Debate graph orchestration │ └── phases/ │ ├── compose.py # Persona generation │ ├── premortem.py # Failure pre-mortem │ ├── position.py # Initial positions │ ├── cross_examine.py # Iterative cross-examination │ └── synthesis.py # Decision landscape ├── scripts/ │ └── bootstrap.py # First-run installation ├── templates/ │ └── personas.json # Example custom personas └── references/ ├── convergence.md ├── debate-protocol.md └── configuration.md ``` ## Prerequisites - Python 3.10+ with the `pydantic-ai` package; install via `pip`, `pipx`, or `python3 scripts/bootstrap.py` (the package ships inside this skill directory, so bootstrap needs no PyPI access). - An LLM provider API key exported as `AGENT_COUNCIL_API_KEY` (or set in a `.env` file); optionally `AGENT_COUNCIL_MODEL` and `AGENT_COUNCIL_BASE_URL`. - The real professional profiles from the hermes-profiles library are available only in a recursive source checkout; pip and wheel installs use generated or user-supplied personas. ## Limitations - Single-model debate: all agents share one LLM configuration and therefore its knowledge cutoff and blind spots; diversity comes from persona definitions, not model instances (see Architecture Decision). - Debates consume many parallel LLM calls per round — expect minutes on slow models or deep mode, and check ⚠️ Claims Not Verified in every synthesis before acting on verifiable external facts. - Convergence diagnostics reduce fabrication and false consensus but cannot eliminate them; `max_rounds` stops mean an inconclusive debate that the principal must resolve. - This is not a general agent-orchestration framework: it runs debates only — for state-machine orchestration beyond the debate protocol, route to langgraph. ## Related Skills - **ai-frameworks** — umbrella bundle for all AI framework skills. Load this when comparing agent-council against other multi-agent approaches (LangGraph, AutoGen, CrewAI). - **langgraph** — for complex state-machine multi-agent orchestration beyond the debate protocol - **pydanticai** — the underlying framework for type-safe agent definitions - **spec-driven-development** — for building specs that agent-council can help you evaluate - **hermes-profiles** — the 39-profile library that powers the profile selection system
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.