Architecture

Built for auditability and local control

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

Event Bus

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.

Event types

Lifecycle stage_start stage_end generation_start generation_end
Deliberation response pass critique status error
Operations 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

Bounded stages with explicit tool sets

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.

Tool availability by stage

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:

Recommendation

Council substantially agrees or one position dominates. Single clear guidance with reasoning, risks, and next steps.

Alternatives

Two or three materially different approaches remain viable. Presented with a default choice and decision rules — not a merged compromise.

Question

A single missing fact is decisive. The output is a sharp clarifying question with conditional recommendations for each likely answer.

Investigate

Disagreement depends on verifiable evidence. The output is an investigation plan with a provisional default while evidence is gathered.

Failure handling and graceful degradation

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

Persistent audit trail

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.

What is stored

  • Conversations — name, session ID, system instruction, timestamps
  • Messages — participant, content, attachments, and structured metadata including stage, action type, critique kind, target, and model ID
  • Attachments — files stored by UUID in ~/.council/attachments/ with MIME type and size tracking

Session 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

The Chair subsystem

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.

Why mediation matters

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.

Operation boundaries

  • Reads — workspace-wide, text files only, truncated at 50KB. Symlink targets are validated to prevent escape from the workspace boundary.
  • Writes — restricted to ./council-output/ directory only. Overwrites in interactive mode require explicit human confirmation.
  • Edits — find-and-replace with a unique match requirement, preventing ambiguous modifications. Output directory only.
  • Discovery — glob-based file search across the workspace, with automatic exclusion of generated directories (node_modules, __pycache__, .venv, dist, build).

The info request fallback chain

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:

Resolution order

  1. File heuristic — the Chair parses the question for file path references and attempts to read the relevant file directly.
  2. Workspace listing — if the question is about project structure, the Chair returns a directory listing automatically.
  3. Human operator — if neither heuristic applies, the Chair prompts the human for an answer in interactive mode. If no human is available, the participant is told to proceed with best judgment and state assumptions explicitly.

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 event lifecycle

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

Provider abstraction

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.

Provider interface

  • generate() — async text generation with system instruction
  • generate_structured() — structured JSON output for pass/contribute decisions
  • generate_with_tools() — agentic tool-calling loop with provider-native formats
  • format_tools() — translates internal ToolSpec to provider-native tool schemas
  • extract_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:

Included providers

  • Gemini — Google AI Studio (API key) or Vertex AI (ADC). Includes Google Search grounding for web-informed responses.
  • ChatGPT — OpenAI Responses API with web_search_preview for web-grounded responses.
  • Claude — Anthropic Messages API with native tool use support. Structured output via forced tool_choice.
  • Ollama — local models via HTTP API. Graceful degradation for models that do not support tool calling — falls back to prompt-based tool injection.

Implementation details worth noting

  • Gemini disables 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.
  • The OpenAI provider throttles reasoning effort to none when web search is active, preventing the model from spiraling into unbounded search loops.
  • The Claude provider handles strict user/assistant message alternation by merging consecutive same-role messages before sending to the API.
  • The Ollama provider attempts native tool calling first, then falls back to prompt-injected tool descriptions if the model returns a 200 with no tool calls — the most common local-model failure mode.
  • Binary file attachments (PDFs, images) are sent only on the most recent user message, with text placeholders substituted for historical messages to prevent token bloat across long conversations.

Interface

Interactive REPL

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.

REPL capabilities

  • Context-aware tab completion for commands, model names, file paths, and conversation names
  • Dynamic prompt showing current model, conversation, attachment count, and post-synthesis state
  • Slash commands: /attach, /files, /new, /resume, /rename, /discuss, /history, /save
  • Default Enter triggers full council protocol; @model prefix for single-model escape hatch
  • Mode-aware prompt that switches between council mode and lead follow-up mode after resolution

File attachments support text, code, PDFs, and images. Attached files are stored persistently and available to models via tool calls throughout the conversation.


Integration

Two integration surfaces

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.

Human surface: interactive REPL

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.

Automation surface: CLI and subprocess

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 askpromote flow lets lightweight one-shot queries become persistent named conversations when they turn out to be worth continuing.

Why this matters

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.