Skip to content

About

A turn-based debate orchestrator for AI CLIs and humans. Developed exclusively for experimental curiosity, not for real-world application.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Repository files navigation

Consilium

A turn-based debate orchestrator for AI CLIs and humans.

Consilium seats several AI participants — each running in its own CLI chat, with its own conversation — and one or more humans around a single table, and has them argue a topic in turns. A central Arbiter hands out turns and keeps an audit transcript, while each CLI chat keeps its own memory; a human moderator triggers the debate, steers it, and decides when it is over.

Status: early. Phase 0 runs, with a few Phase 1 controls prototyped in-process. There is no Arbiter, process isolation, socket protocol, TLS, dynamic seating or final synthesis yet. SPECIFICATION.md is normative for everything further along the roadmap.

A note on the spirit of this

This started partly as a game. I was curious what would actually happen if you sat several AI CLIs around a table and made them argue — whether anything worth reading would come out of it, or whether they would just be polite at each other for two hundred turns.

The specification ended up more thorough than the idea strictly required, because thinking it through properly turned out to be the enjoyable part. That is also why the design history is kept in full: the reasoning was the point, not just the conclusion.

Phase 0 exists precisely to find out whether the curiosity was justified. If the debates turn out to be boring, that is a result too — and a cheap one to reach.


Why this exists

Multi-agent orchestration frameworks already exist and solve turn-taking, shared context and speaker selection at the API level. Consilium is aimed at a narrower, different thing:

Make agentic CLIs debate each other — each with its own chat — rather than rebuilding their conversations from stateless API calls. Most seats can use subscription-backed CLIs; a CLI such as OpenCode can also use a metered provider while still keeping the chat locally.

That constraint drives most of the design. It is why the wrapper drives a CLI instead of an SDK, why transport is pluggable rather than fixed, and why "preserve the local histories" is a first-class objective rather than an implementation detail.

The second thing Consilium is aimed at is the part orchestration frameworks do not address at all: making the debate worth reading. Three capable models given the same prompt and no assigned stance converge toward polite agreement within a few turns. Most of the product layer in this specification exists to prevent that.


Core concepts

Concept What it is
Arbiter Independent central hub. Owns the roster, hands out turns with deadlines, enforces the cost cap, persists the audit transcript and tracks a per-seat delta cursor. It does not own the CLI chats' conversational memory.
Wrapper The trusted edge node, one per participant. Holds the credentials, talks the protocol, assembles context, and drives the participant through an adapter. The only component allowed on the control channel.
Adapter How a wrapper talks to its participant: PexpectAdapter keeps a TTY chat open, NativeAdapter resumes a CLI chat between headless invocations, and human adapters accept local input. The Arbiter never knows which is in use.
Seat A participant, defined by three independent dimensions: model, effort level, and point of view (role_prompt).
Role prompt A per-participant prompt fragment — sceptic, pragmatist, devil's advocate — appended to the shared debate rules. The countermeasure to consensus collapse.
Cold entry A participant joining mid-debate receives the topic and the current exchange, never the history — deliberately, so it judges without inherited preconceptions. Also the moderator's strongest steering move.
Transcript Append-only JSONL on the Arbiter's volume, carrying configured seat metadata and role information per turn. The only reading surface, and the thing that makes two debates comparable.

Architecture at a glance

[ AI Wrapper 1 ] ──┬── CONTROL channel ──► [ Arbiter ]
                   └── DATA channel ─────► [ Arbiter ]

[ AI Wrapper 2 ] ──┬── CONTROL channel ──► [ Arbiter ]
                   └── DATA channel ─────► [ Arbiter ]

[ Human Wrapper ] ─┬── CONTROL channel ──► [ Arbiter ]
                   └── DATA channel ─────► [ Arbiter ]

[ Admin Console ] ──── ADMIN channel ────► [ Arbiter ]   (outside the roster)

Inside each wrapper:

                              ┌── NativeAdapter    (non-interactive + resume)
[ Wrapper ] ── Adapter ───────┼── PexpectAdapter   (universal default, TTY)
                              └── HumanAdapter     (terminal or local web GUI)

Two channels, one rule. Commands travel on the control channel; debate content travels on the data channel. The two are physically separate, so a model that emits UNREG:someone into the conversation produces nothing but a strange sentence — the Arbiter has no path from dialogue to command, and therefore needs no semantic filter. Filtering happens in one place only: the wrapper's context assembly, where another participant's text becomes an agent's input.

A design walkthrough lives in SPECIFICATION.md §3.


Design principles

The wrapper is the trusted edge, but only if the CLI is isolated from it.
Transport belongs to the adapter, not to the architecture.
Every seat has a point of view, or the council becomes an echo.
The Arbiter keeps the audit transcript and delta cursors, not the chats' memory.
Each CLI chat keeps its own conversational memory.
Whoever joins later joins without preconceptions.
The AI never touches the control channel.
The human never types commands into the dialogue.
No participant holds privileges; the operator is not a participant.
Every session modifies only itself, within its own profile.
A skip is an empty TURN_END.
Reconnection goes through REG.
Automate what is measurable; the rest belongs to the moderator.
No debate runs without a cap.
An unexpected failure cannot authorise more turns; the operator decides.

Two of these deserve a note, because they are the ones people usually argue with:

"Automate what is measurable." Consilium does not try to detect when a debate has converged, and does not score debate quality. Neither is reliably computable, and a model's self-assessment of whether it said anything new is not usable data. Termination is a human decision. What is automated is mechanical: a cost cap so an unattended debate cannot run forever, and per-participant repetition detection so a returning moderator can see at a glance who is spinning in place.

"Every seat has a point of view." Assigning explicit, distinct stances is not a personalisation feature. Without it, models trained to be agreeable will agree — elegantly, at length, and uselessly — regardless of which brands are at the table.

"Each chat owns its memory." A normal headless CLI exits after responding, so its next invocation resumes the same saved chat. A TTY CLI remains open and needs no ordinary resume. Intentional chat closure ends participation. If a turn or connection fails unexpectedly, Phase 0 stops with ERROR; the planned Phase 2 pauses until an operator explicitly decides what to do. Neither silently starts another chat nor replays an uncertain turn. A raw stateless API, including one reached through OpenRouter, does not provide this chat memory on its own.


What the human moderator does

  1. Triggers the debate with a topic and an objective.
  2. Leaves, if they want to. The debate continues; away mode is a supported state, not a degraded one.
  3. Returns to a STATUS showing turns elapsed, cost consumed against the cap, and which participants are repeating themselves.
  4. Steers: injects a new element, changes phase, or seats a fresh participant carrying the point of view the discussion is missing.
  5. Ends it — or lets the cost cap end it, which the transcript records as a distinct outcome.

A debate may reach no acceptable conclusion. That is a valid result, and the final synthesis pass is specified to handle it: where the discussion got to, what was agreed, what was not, and what remains open.


Security posture

The threat model is declared explicitly in SPECIFICATION.md §14.1, including what is deliberately out of scope (the host operator; anyone able to write inside the wrapper image). Declaring it is what makes it possible to remove machinery, not only add it: with everything running locally in early phases, mTLS is not needed — while one thing is needed immediately.

The orchestrated CLIs are agents with shell, filesystem and network access. The production design requires OS isolation of the CLI from the wrapper's control endpoints and credentials. The default restricted harness limits tools and local customisation where the client permits it. An explicitly selected native harness retains the client's skills, plugins, MCP and permissions in an operator-selected project directory. It still requires the same isolation from the control plane; enabling tools changes the capability policy, not ownership of the debate. The in-process Version 0 implementation is not an OS sandbox, and neither profile promises one.

Phase 0 does not enforce that isolation. Native wrappers get an empty per-seat working directory, but still inherit the current user, environment and network; the TTY fallback may also inherit the caller's directory. Run this proof-of-value harness only in a disposable, unprivileged environment with no unrelated secrets; enforced OS isolation belongs to Phase 2.


Roadmap

Phase Content Gate
0 — Proof of value Single script, in-process. Adapter interface, role prompts, round-robin, JSONL transcript, cost cap. No sockets, no TLS, no Docker. Are the debates interesting? Do they add anything over asking one model? If no, the project ends here
1 — Product layer Role library, role_visible, cold entry as steering, phases, final synthesis, re-entry instruments, context sanitisation, pluggable speaker policy Is it usable by a moderator on a long debate without friction?
2 — Process separation Arbiter and wrappers as separate processes, dual channel, full protocol, profile capabilities, CLI OS isolation, admin plane, persistent transcript Does it survive crashes, hangs and misbehaving participants?
3 — Network Direct mTLS on both channels, certificate capability profiles, revocation, rate limiting, per-participant containers Only if remote participants are genuinely needed

The ordering is deliberate and inverts the project's own history: validate the value before building the perimeter. Phase 0 exists to make it cheap to discover that the idea does not work.

The experiment that justifies phase 0: run the same topic twice — once with three participants and no assigned roles, once with three roles. Read both.


Getting started (Phase 0)

git clone https://github.com/francescodifilippo/consiliumAI
cd consiliumAI
pip install -e ".[dev]"

cp config/debate.example.yaml config/debate.yaml   # edit topic, seats, roles
consilium roles                                    # list available role prompts
consilium wrappers                                 # list built-in wrappers
consilium run config/debate.yaml

The Python runtime dependencies are pexpect and PyYAML; supported AI seats also require their own CLI. Direct stateless API calls are not an active adapter, because that would put conversational history back in the wrapper.

The initial wrapper roster is deliberately data-driven: compatible headless CLIs reuse NativeAdapter; the human seats use local input adapters. There is no class per vendor.

Wrapper Executable / interface Restricted default / important limit
claude Claude Code headless tools disabled; resumes the seat-local session
codex Codex exec read-only sandbox and empty cwd; Codex has no strict no-tools flag
gemini Gemini CLI headless current personal OAuth plans are unsupported; enterprise/API-key use only
human terminal stdin waits by default; explicit timeout_s enables skip, /away continues voluntarily
human-with-web-gui local HTML form loopback-only, token-protected; waits unless timeout_s is explicit
opencode OpenCode run JSON output pins the exact chat ID; plan agent is not an OS security boundary
qwen-code Qwen Code headless safe mode plus write/shell tool exclusions
kimi current Moonshot Kimi Code CLI no strict headless no-tools switch
glm-code Claude Code against Z.AI requires ZAI_API_KEY; there is no separate glm-code executable
deepseek.coder OpenCode chat with DeepSeek provider exact chat ID is pinned; configure DeepSeek credentials

Seating a participant. A built-in wrapper supplies transport defaults; any explicit field overrides the preset:

- name: claude_sonnet_deep
  wrapper: claude
  role_prompt_file: roles/sceptic.txt
  role_label: sceptic

- name: moderator
  wrapper: human-with-web-gui
  port: 8765

Low-level adapter: native|pexpect|human|human_web configuration remains available for custom clients. deepseek.coder uses the same stateful OpenCode transport as opencode; OpenRouter may supply a model, but it is not itself a chat keeper.

Using the client's harness. All eight AI wrappers accept harness_profile: restricted|native. The default is restricted, retaining the existing private working directory and client-specific limiting flags. To let a client discover your project's skills, plugins, hooks and MCP configuration:

- name: claude_researcher
  wrapper: claude
  harness_profile: native
  cwd: .                     # relative to --root; omitted = --root
  role_prompt: Use the project's research skill to inform your arguments.
  role_label: researcher

native uses the chosen project directory, leaves harness loading and tool permissions to the client, and resumes the exact chat ID obtained from structured output. Several seats may use that directory while keeping separate chats; their project files are still shared. It adds no approval-bypass flags. Skills that require interactive questions or approvals remain subject to the client's headless limitations and trust settings. Both human wrappers are unaffected. See harness usage and client contracts for details, including Kimi's automatic headless permission policy.

adapter: native names the one-shot CLI transport; harness_profile: native names the opt-in harness configuration. They are separate settings. The current presets use that transport for both profiles; experimental Pexpect remains separately configured.

During your turn — Phase 0 stands in for the keybindings and the admin plane with a few slash commands, which never reach the debate content: /status, /phase REBUTTAL, /inject <text>, /away, /back, /end. An empty line skips your turn. Phase 2 replaces these with real keybindings and the ADMIN channel.

Phase 0 requires cap.max_rounds as a backstop even when token usage is absent; optional token/time caps can be stricter. An unexpected adapter failure or terminal EOF ends the run with ERROR and preserves the transcript. It does not advance to another seat. On POSIX, each invocation's process group is terminated on timeout, interruption or completion, including local child tools that remain in that group. CLI chat IDs live only for the current Consilium run; even with a project working directory, automatic restart of an interrupted debate is not implemented. Restricted OpenCode/DeepSeek and all native-profile AI presets parse structured output and pin the chat ID; the other restricted presets still use plain stdout and private-directory continuation. Offline tests do not certify real CLI/provider/plugin conformance; live checks remain pending.

The experiment worth running first: the same topic twice — once with three seats and no roles, once with three roles assigned. Compare the transcripts. That comparison is the whole point of Phase 0.

Transcripts land in transcripts/ as JSONL, one record per turn, carrying configured seat metadata and role information.


Repository contents

Path What it is
SPECIFICATION.md The v4.2 specification. Normative. Includes the full changelog, superseded decisions with rationale, the original v1 document (Appendix A), the version transitions (Appendix B), and the review outcome (Appendix C)
CONTRIBUTING.md How to argue with the design, and the ground rules for code
src/consilium/ In-process implementation: wrapper presets, adapters, debate loop, transcript, cap and status
roles/ Reusable role prompts — sceptic, pragmatist, devil's advocate, historian, constraint keeper, synthesist
config/debate.example.yaml Annotated example configuration
tests/ Phase 0 tests, using a stub adapter so they need no CLI, network or API key

The specification keeps its own history on purpose. §1.0 records the latest clarifications; §§1.1–1.4 and the appendices preserve the earlier choices, including those now superseded. If you are reading the design and something looks like an odd choice, the reason it is that way is usually written down.


Open questions

Tracked in SPECIFICATION.md §16. The two that can affect whether the project is viable at all:

  • Subscription terms of service for automated orchestration and for concurrent instances, per provider. This is a question to answer outside the code — it can invalidate the approach regardless of how good the architecture is.
  • CLI-side context compaction over long debates. Each CLI compacts its own context autonomously, which is a benefit rather than a problem, but after enough compactions a participant remembers the opening of the debate only in summarised form. To be measured, not pre-solved.

Naming

Consilium — a council convened to advise. The name is a reminder of the product objective: a council produces counsel, and a system that produces only a log has not finished its job.

Consilium is the project name; the Python package and command are consilium, while the repository slug is consiliumAI. The socket paths, group and environment variables retain the Consilium name. The former repository name CouncilAI and earlier project names DebateLoop and Disputatio are preserved in the design history and recorded evidence.


Contributing

The design is at the stage where argument is more valuable than code. If you disagree with a decision, Appendix C is the place to look first — the finding may already have been raised, accepted, downgraded or rejected, with the reasoning recorded. Issues that engage with that reasoning are welcome.

License

GNU General Public License v3.0.

You may use, study, modify and redistribute this work; derivative works must be released under the same licence. Note that this covers Consilium itself — the commercial AI CLIs it orchestrates carry their own terms, and whether a given subscription permits automated orchestration is a question to settle with that provider (see §16).

About

A turn-based debate orchestrator for AI CLIs and humans. Developed exclusively for experimental curiosity, not for real-world application.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages