@nicknisi/pi-llm-council
LLM Council — multiple models answer independently, chairman synthesizes
Package details
Install @nicknisi/pi-llm-council from npm and Pi will load the resources declared by the package manifest.
$ pi install npm:@nicknisi/pi-llm-council- Package
@nicknisi/pi-llm-council- Version
0.3.0- Published
- Sep 12, 2026
- Downloads
- 449/mo · 216/wk
- Author
- nicknisi
- License
- MIT
- Types
- extension
- Size
- 194.5 KB
- Dependencies
- 2 dependencies · 2 peers
Pi manifest JSON
{
"extensions": [
"./index.ts"
]
}Security note
Pi packages can execute code and influence agent behavior. Review the source before installing third-party packages.
README
llm-council
An LLM Council tool for pi: multiple models answer the same question independently, in parallel, as in-process child agent sessions (via @nicknisi/pi-shared's subagent runtime), then a chairman model synthesizes their (anonymized) answers into one unified response. Useful for questions that benefit from multiple perspectives or cross-checking — divergent answers flag uncertainty. Not for simple factual questions or routine tasks. Progress streams inline in the tool result with animated spinners, per-member status, cumulative token usage, and elapsed times; expanding the result shows the full markdown of every member response plus the chairman's synthesis.
Install
pi install /Users/nicknisi/Developer/pi-extensions/packages/llm-council
What it adds
- Tool:
llm_council(label "LLM Council"), with optional per-callmodelsandchairmanoverrides. - Command:
/council <question>runs immediately with the current conversation as context. Bare/councilasks for a question and runs on Enter./council settingsedits the lineup without running./council resetreturns this session to configured defaults. - Command results:
llm-council-resultmessages display the synthesis in chat and keep it available to subsequent turns. Expand the result for individual responses and errors. - Session state:
llm-council-selectioncustom entries remember the lineup and thinking levels across reloads, resumes, forks, and branch navigation. These entries are not sent to the LLM. A footer status shows the session's selected lineup. - Custom
renderCall/renderResultfor the tool: live member/chairman tree with spinner, status icons, elapsed times, and an expanded view rendering full member + chairman markdown. Expand/collapse uses the standardapp.tools.expandkeybinding (defaultctrl+o).
Tool parameters
| Parameter | Type | Description |
|---|---|---|
question |
string |
The question to pose to the council |
models |
string[], optional |
Replace the members for this call only. At least one distinct model is required. |
chairman |
string, optional |
Replace the chairman for this call only. |
Use exact provider/model IDs or unambiguous names. Qualified IDs must match exactly, so a missing model is never silently replaced by a newer variant. Ambiguous references open a provider/model selection dialog in interactive or RPC mode. Headless calls return an error listing the matches. Unknown models, duplicate members, and cancelled selections fail before any member starts. Per-call overrides never change session or global defaults.
Prompt guidance registered with the tool tells the agent to use it for complex questions that benefit from multiple perspectives, and not for simple factual questions.
How it works
- Members — each council member receives the same question and answers independently, in parallel (
Promise.all). Each runs as a hermetic in-process child session spawned through pi's SDK (createAgentSession), shared via@nicknisi/pi-shared'screateSubagentRuntime; the answer is the child's final assistant message. - Chairman — receives the question plus all successful member answers (labeled Member A/B/C) and synthesizes a unified answer. If
chairman.exposePersonasistrue, each member's system prompt is included as(persona: "..."). The chairman's text is the tool's final content. - If every member fails, the tool returns an error result; the chairman never runs. If the chairman fails, its error is returned and any partial synthesis is labeled incomplete.
Exec config → spawn options
The tools / thinking / extensions / skills / contextFiles options on member and chairman map onto the shared runtime's spawn options:
| Option | Value | Effect |
|---|---|---|
tools |
null/[] |
No tools |
tools |
[...] |
Exactly those built-in tools (allowlist) |
thinking |
null |
(pi default) |
thinking |
"..." |
Thinking level (off/minimal/low/medium/high/xhigh/max) |
extensions |
null/[] |
(none — children are hermetic) |
extensions |
[name] |
Load ~/.pi/agent/extensions/<name>/src/index.ts (per name, containment-checked) |
skills |
null/[] |
(none — children are hermetic) |
skills |
[name] |
Load ~/.pi/agent/skills/<name>/SKILL.md (per name, containment-checked) |
contextFiles |
false |
No AGENTS.md / project context files |
contextFiles |
true |
Context files load |
Behavior change from the subprocess era:
extensions: null/skills: nullused to mean "inherit pi defaults" (ambient extensions/skills loaded into the child). Children are now hermetic by construction —nulland[]both mean none; only explicitly named resources load.
System prompts are appended to pi's default system prompt through the child's resource loader (no temp files). The runtime honors the ecosystem recursion guard: when PI_SUBAGENT_DEPTH/PI_SUBAGENT_CHILD are set (i.e. the council itself is running inside a pi-subagents child), spawns are refused with a typed crashed result.
Default council
The built-in lineup assumes models enabled in ~/.pi/agent/settings.json enabledModels:
| Role | Model | Label |
|---|---|---|
| Member | fireworks/accounts/fireworks/models/glm-5p2 |
Member A |
| Member | fireworks/accounts/fireworks/models/kimi-k3 |
Member B |
| Member | anthropic/claude-fable-5 |
Member C |
| Chairman | anthropic/claude-opus-5 |
Chairman |
Members run with read-only built-in tools (read, grep, find, ls), no extensions, no skills, thinking: medium, and no project context files. The chairman has no tools — it only synthesizes.
Usage
Run /council <question> to start immediately with the current lineup. Bare /council opens one question field with the members and synthesizer shown underneath. Enter runs the council without another confirmation. Progress appears while the members work, followed by the synthesized answer in chat. Escape cancels the question prompt or aborts a running council.
The command sends the current branch's conversation context to every member and the synthesizer. Compaction summaries are included, rather than discarded history or other branches. Pi's text serializer shortens tool outputs and omits image data. No extra summarization call is made. Very long context can still exceed a selected model's context window.
Runs reuse the selected or configured lineup without changing it or switching the main chat model. Missing or ambiguous models must be resolved before any member starts. Use /council settings to edit the lineup without running models.
Open Members to edit a searchable checklist. Every model available through your configured providers is searchable immediately, regardless of the session's model scope. Selected models appear first with [x], followed by scoped models and the rest of the catalog. Type a model name, provider, or exact ID to filter. Space toggles the highlighted member, even while searching. Enter keeps your choices and returns to settings. Escape returns without changing the checklist's original selection.
Open Synthesizer to choose the model that combines the member answers, called chairman in tool arguments and config. It can also be a member. Type to search and press Enter to choose it. Both lists show readable names and providers, with the highlighted model's full ID below the list. Missing or ambiguous configured selections stay visible so you can replace or remove them.
Member and synthesizer thinking levels can be changed separately. default uses Pi's default. Pi adjusts thinking to each model's capabilities when it runs.
In /council settings, Save lineup saves the draft for this session without running models. Save as global default asks for confirmation, updates the global config while preserving unrelated settings and existing member personas, then applies the lineup to this session. Escape from settings discards the draft. A council must have at least one member. These commands require TUI mode while the agent is idle.
You can still ask a question normally in chat, for example:
Which approach is better for X: A or B? Convene the council.
Or target models for one question: "Ask Fable 5.1 and Astra to compare these two designs, with Fable as chairman." The agent supplies the tool overrides. Exact IDs avoid ambiguity:
{
"question": "Compare these two designs.",
"models": ["anthropic/claude-fable-5-1", "openai-codex/gpt-6-astra"],
"chairman": "anthropic/claude-fable-5-1"
}
Model availability depends on your Pi catalog and provider credentials. Catalog presence does not verify that a token is still valid. Use /login to reconnect a provider if a call fails authentication.
Or steer it directly: "use llm_council to compare these two designs". The tool result shows the chairman's synthesis; press the tools-expand key (ctrl+o) on the tool block to see every member's full response.
Configuration
Lineup precedence is per-call overrides > session selection > project config > global config > built-in defaults. /council reset removes the session selection. A global save does not overwrite project overrides or other sessions' saved selections.
Two config files:
- Global:
~/.pi/agent/configs/llm-council.json— copyllm-council.example.json. Execution settings reload per call. Display settings load once at module load, and this is the only source forsharedsettings. The path follows pi's agent dir, so it moves withPI_CODING_AGENT_DIRif you set it. - Project-local:
<cwd>/.pi/configs/llm-council.json— copyllm-council.project.example.json. Deep-merged over the global file per tool call, so only differing keys are needed — typicallymember.councilandchairman.modelto give a work project a different lineup. Display (shared) settings do not apply from the project file.
No environment variables are read for configuration. (PI_SUBAGENT_DEPTH is set internally to block recursion.)
member
| Key | Type | Default | Description |
|---|---|---|---|
council |
object[] |
(3 members, above) | Each entry: model (required), label (default: "1", "2", …), displayName, systemPrompt (both optional) |
defaultSystemPrompt |
string |
(built-in; see config.ts) |
System prompt for members without their own. The built-in default forbids spawning subprocesses |
display.labelColor |
string |
"accent" |
Member label color |
display.modelColor |
string |
"dim" |
Model name color |
tools |
string[] | null |
["read","grep","find","ls"] |
Tool allowlist for member child sessions (null/[] → no tools) |
thinking |
string | null |
"medium" |
Thinking level (null → pi default) |
extensions |
string[] | null |
[] |
Extension names, resolved to ~/.pi/agent/extensions/<name>/src/index.ts (null → pi defaults) |
skills |
string[] | null |
[] |
Skill names, resolved to ~/.pi/agent/skills/<name>/SKILL.md (null → pi defaults) |
contextFiles |
boolean |
false |
false → --no-context-files |
chairman
| Key | Type | Default | Description |
|---|---|---|---|
model |
string |
"anthropic/claude-opus-5" |
Chairman model |
displayName |
string |
"Claude Opus 5" |
Human-readable name shown in the UI |
systemPrompt |
string |
(built-in; see config.ts) |
Chairman system prompt (treats member answers as anonymous) |
exposePersonas |
boolean |
true |
Include each member's system prompt as a persona in chairman input |
display.icon |
string |
"" |
Icon prefix before the "Synthesizer" label |
display.labelColor |
string |
"accent" |
Chairman label color |
display.modelColor |
string |
"dim" |
Chairman model name color |
tools |
string[] | null |
[] |
Chairman tool allowlist (none by default) |
thinking |
string | null |
"medium" |
Thinking level |
extensions |
string[] | null |
[] |
Extensions (null → pi defaults) |
skills |
string[] | null |
[] |
Skills (null → pi defaults) |
contextFiles |
boolean |
false |
Context files for chairman |
shared (display — global config only)
| Key | Default | Description |
|---|---|---|
spinner.prefixChars |
["·","✢","✳","✶","✻","✽"] |
Spinner frames (played forward then reverse) |
spinner.interval |
80 |
Frame interval, ms |
spinner.color |
"muted" |
Spinner color |
successPrefix.prefix/color |
"✓" / "success" |
Success icon |
errorPrefix.prefix/color |
"✗" / "error" |
Error icon |
branch.prefix/color |
"└─" / "separator" |
Sub-line branch prefix |
status.doneLabel/doneColor |
"Done" / "success" |
Completed-status label/color |
status.errorLabel/errorColor |
"Error" / "error" |
Error-status label/color |
status.workingLabel/workingColor |
"Working..." / "dim" |
In-progress label/color |
status.waitingIcon/waitingIconColor |
"↪" / "muted" |
Pending member icon/color |
status.synthesizingLabel |
"Synthesising..." |
Chairman in-progress label |
status.waitingLabel |
"Waiting for members..." |
Chairman pending label |
status.elapsedColor |
"dim" |
Elapsed-time color |
toolHeader.titleColor/summaryColor |
"toolTitle" / "dim" |
Tool call header colors |
expandHint.color |
"dim" |
"ctrl+o to expand" hint color |
questionPreview.maxLength |
40 |
Chars of the question shown in the header |
Color values
Any color field accepts a pi theme token ("text", "accent", "success", "error", "muted", "dim", "separator", "toolTitle", …) or a 6-digit hex string ("#ff6600", rendered as a 24-bit ANSI fg). Unknown tokens fall back to uncolored text.
Dependencies
@earendil-works/pi-coding-agent(peer) —ExtensionAPI(pi.registerTool),Theme/ThemeColor,getMarkdownTheme.@earendil-works/pi-tui(peer) —MarkdownandTextrender components,getKeybindings(for the expand-hint key label).typebox— tool parameter schema (Type.Object).@nicknisi/pi-shared(workspace) — the in-process subagent runtime (createSubagentRuntime) that members and the chairman spawn through.- No
pibinary requirement: children are in-process SDK sessions, not subprocesses.
Caveats
- Extension resolution path is hardcoded.
extensions: ["name"]resolves to~/.pi/agent/extensions/<name>/src/index.ts— only directory-style extensions with that layout work. Single-file.tsextensions and npm-package extensions don't match; the code comments recommend keepingextensions: []for members. Same forskills→~/.pi/agent/skills/<name>/SKILL.md. - Depends on pi's SDK surface:
createAgentSession,DefaultResourceLoader(itsnoExtensions/additionalExtensionPathssemantics),SessionManager.inMemory,SettingsManager.inMemory,ModelRuntime/resolveCliModel. These are pi internals that could change across versions; the runtime is version-matched at runtime because pi aliases@earendil-works/*imports to the host, but type-level drift would surface at extension load. - Pi internals: the spinner relies on the
renderCall/renderResultctx.statebag andctx.invalidate(). A module-levelliveDetailsbridgesonUpdate→renderCallas a workaround for anisPartialbug (per code comment); only one council can render live at a time. - Recursion guard: the shared runtime refuses to spawn when
PI_SUBAGENT_DEPTH/PI_SUBAGENT_CHILDare set — this tool won't work if invoked from inside a pi-subagents child session. - Global and project execution settings are re-read on every tool call. Display changes still require
/reloador a restart. Session selections override model and thinking settings until/council reset. - Members and chairman run with the current working directory as
cwd;contextFiles: falsekeeps CLAUDE.md/AGENTS.md out of member context by default.