Architecture
Council is not a prompt chain. It is a bounded protocol with explicit stages, typed events, sandboxed operations, and a persistent audit trail. This page describes the system properties that make it trustworthy for high-stakes use.
Core abstraction
All orchestrator operations emit typed events to a sink. The protocol produces events; renderers consume them. This decoupling means the deliberation logic does not depend on any specific UI surface.
Events carry structured fields such as participant, stage, elapsed time, critique kind, and target, along with auxiliary metadata like resolution type or source. The same event stream can drive a terminal renderer, a web board, a log file, or downstream tooling.
stage_start stage_end generation_start generation_end response pass critique status error operator_request chair_request chair_response write_proposed write_approved write_denied Two renderers ship with Council: a plain-text renderer for pipes and non-interactive contexts (CI, Claude Code), and a Rich-based terminal renderer for interactive sessions with color-coded participants, markdown rendering, and timing information.
Protocol design
Each stage of the protocol has a distinct purpose and a distinct set of available tools. Models cannot write files during proposals. They cannot propose during critique. Tool availability is enforced structurally by the orchestrator — each stage receives a different tool set. Output formats and interaction norms (proposal structure, critique categories, resolution types) are guided by stage-specific system prompts.
| Stage | Available tools |
|---|---|
Proposals | list_attachments, read_attachment, read_file, find_files, request_info |
Critique | All proposal tools + call_pass, call_contribute(kind, message, target) |
Resolution | No tools — structured JSON generation only (resolution type + markdown) |
Follow-up | All proposal tools + write_file, edit_file, call_council (reconvene) |
The critique stage uses a structured vocabulary. Participants must categorize every contribution as a challenge, alternative, refinement, or question, with an explicit target. Participants can also pass — signaling that they have no material objection. Silence is treated as approval.
Synthesis produces one of four resolution types:
Council substantially agrees or one position dominates. Single clear guidance with reasoning, risks, and next steps.
Two or three materially different approaches remain viable. Presented with a default choice and decision rules — not a merged compromise.
A single missing fact is decisive. The output is a sharp clarifying question with conditional recommendations for each likely answer.
Disagreement depends on verifiable evidence. The output is an investigation plan with a provisional default while evidence is gathered.
The protocol handles real-world failures at every stage. Provider calls time out after 180 seconds. The tool-calling loop in proposals and critique is capped at 3 iterations, after which the participant defaults to a pass. If the resolver returns invalid JSON, the system falls back to a plain-text recommendation.
Resolution types interact with the rest of the system. A Question resolution prompts the human for a clarifying answer and re-runs resolution with the new input. An Investigate resolution triggers the Chair to gather evidence from the workspace, then re-resolves with findings. Single-model sessions skip critique and resolution entirely.
Persistence
Every deliberation is persisted in a local SQLite database with WAL journaling. Proposals, critiques, tool actions, synthesis results, and follow-up exchanges are stored as structured records with full metadata.
~/.council/attachments/ with MIME type and size trackingSession tagging groups conversations logically. You can list, review, and extend past councils. The same conversation name can exist in different sessions, supporting per-project or per-day organization.
Security boundary
Models can request file reads, searches, and writes through their available tools — but every one of those operations routes through a mediation layer called the Chair. The Chair validates paths, enforces boundaries, logs every operation to the event stream, and falls back to the human operator when it cannot resolve a request autonomously.
The metaphor is an advisory board: participants can ask for documents, but the chair decides what to hand over and where deliverables can be filed. Read access is broad; write access is confined.
When you give AI models file access, the natural instinct is to either give them full access (dangerous) or no access (useless). The Chair provides a middle path: broad read access for context, narrow write access for deliverables, and explicit human approval for anything destructive. Every operation is recorded in the event stream, so there are no silent side effects.
./council-output/ directory only. Overwrites in interactive mode require explicit human confirmation.
When a participant needs information that isn't a straightforward file read, they
make an request_info call. The Chair resolves it through a three-step
fallback:
This chain means participants get answers fast when the workspace has what they need, and gracefully degrade to asking the human only when necessary. Every resolution — whether from a file, the workspace, or the operator — is recorded with its source for auditability.
Write operations follow an explicit lifecycle tracked in the event stream:
write_proposed when a participant requests a write,
write_approved when the Chair permits it, and
write_denied when it is rejected (either by policy or by the
human operator). This makes every file modification inspectable after the fact.
Extensibility
Council defines a provider protocol that any LLM backend can implement. Each provider handles its own message formatting, tool schema translation, and response parsing. The orchestrator operates against the protocol interface, not against specific APIs.
generate() — async text generation with system instructiongenerate_structured() — structured JSON output for pass/contribute decisionsgenerate_with_tools() — agentic tool-calling loop with provider-native formatsformat_tools() — translates internal ToolSpec to provider-native tool schemasextract_tool_calls() — parses tool calls from provider-specific response formats
A provider registry checks availability at runtime based on configured
credentials, initializes providers lazily on first use, and caches instances.
All stage executions run providers in parallel via asyncio.gather,
so adding more models does not increase wall-clock latency.
Ships with four providers:
web_search_preview for web-grounded responses.AutomaticFunctionCallingConfig so the orchestrator retains strict control over the tool execution loop — the model proposes tool calls, but the orchestrator decides whether to execute them.none when web search is active, preventing the model from spiraling into unbounded search loops.Interface
Running council with no arguments launches an interactive session
built on prompt_toolkit. The REPL maintains a single async event loop for the
entire session, allowing seamless task cancellation with Ctrl+C without exiting.
/attach, /files, /new, /resume, /rename, /discuss, /history, /save@model prefix for single-model escape hatchFile attachments support text, code, PDFs, and images. Attached files are stored persistently and available to models via tool calls throughout the conversation.
Integration
Council was designed from the start for two distinct consumers: humans in an interactive terminal and automation running Council through CLI commands and subprocesses. These are not an afterthought bolted onto a single interface — they are separate, first-class surfaces with different renderers, different interaction models, and different output expectations.
The REPL provides a persistent async session with rich terminal output, color-coded participants, pause points between stages, and post-synthesis follow-up. Default Enter runs the full council protocol. Humans can interrupt, review intermediate proposals, and steer the conversation interactively.
council chat, council discuss, and council ask
run as non-interactive commands that emit plain-text output suitable for
subprocess consumption. An AI coding assistant can invoke Council as a subprocess,
pass a question that benefits from multi-model input, and parse the structured
response — all without human intervention. The ask →
promote flow lets lightweight one-shot queries become persistent
named conversations when they turn out to be worth continuing.
Most multi-model tools assume a human is always in the loop. Council does not. The event bus architecture means the same deliberation can be rendered as a rich terminal session for a human or as structured plain text for an agent — the protocol is identical either way.
This makes Council composable. An AI coding assistant can convene a council for an architecture decision, read the synthesis, and continue its work. A CI pipeline could run a council review on a pull request. A research workflow could batch-run deliberations and compare synthesis quality across model combinations. The deliberation protocol is the stable primitive; how it gets consumed is up to the caller.