roundtable-cli 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
roundtable/config.py ADDED
@@ -0,0 +1,265 @@
1
+ """Roundtable configuration: which backend, which model/agent per role, runtime knobs.
2
+
3
+ Loaded from ``roundtable.config.yaml`` at the project root.
4
+
5
+ Four backends (``provider``):
6
+
7
+ * ``pi`` — drive the `pi` coding agent for every role. Task agents run with
8
+ pi's file tools (they edit the repo); every other role runs
9
+ ``pi --no-tools`` as a pure completion. Connectivity, auth and
10
+ model routing are handled by pi/pi-ai. The recommended backend
11
+ when `pi` is installed — it reports exact token usage and cost.
12
+ * ``cli`` — run other LLMs through their terminal CLIs (claude, codex, gemini,
13
+ aider, llm, ollama, ...). Each role's "model" names an entry in
14
+ ``agents``. Agents act on the real project files via their own
15
+ tools. This is what roundtable falls back to without pi.
16
+ * ``litellm`` — direct API calls; "model" is a litellm model string.
17
+ * ``scripted``— deterministic offline backend (demo/tests).
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from pathlib import Path
23
+
24
+ import yaml
25
+ from pydantic import BaseModel, Field
26
+
27
+ from .models import AgentRef
28
+
29
+ CONFIG_FILENAME = "roundtable.config.yaml"
30
+
31
+
32
+ def _ref(agent: str) -> AgentRef:
33
+ return AgentRef(agent=agent)
34
+
35
+
36
+ class ModelRoles(BaseModel):
37
+ """Default (agent, model) assignment per role.
38
+
39
+ Each value is an :class:`AgentRef` — accepts an object ``{agent, model}`` or
40
+ the shorthand string ``agent:model`` in YAML. Per-phase / per-task ``runner``
41
+ entries in the generated plan override these.
42
+ """
43
+
44
+ planner: AgentRef = Field(default_factory=lambda: _ref("claude"))
45
+ main: AgentRef = Field(default_factory=lambda: _ref("claude"))
46
+ phase: AgentRef = Field(default_factory=lambda: _ref("claude"))
47
+ task: AgentRef = Field(default_factory=lambda: _ref("claude"))
48
+
49
+
50
+ class AgentSpec(BaseModel):
51
+ """A terminal command that talks to an LLM.
52
+
53
+ ``command`` is an argv list (no shell). Tokens may contain ``{prompt}``,
54
+ ``{system}`` and ``{model}`` placeholders. ``{model}`` is replaced with the
55
+ role/task's chosen model so one agent command serves many models. If
56
+ ``{system}`` is absent, the system text is prepended to the prompt. If
57
+ ``stdin`` is true, the prompt is piped on stdin instead of into ``{prompt}``.
58
+
59
+ ``models_command`` is an optional argv that lists the models this CLI offers
60
+ (e.g. ``["opencode", "models"]``, ``["agy", "models"]``, ``["ollama",
61
+ "list"]``); ``roundtable init`` / ``roundtable agents`` runs it to show you what you
62
+ can assign. Omit it for tools with no enumeration command (claude, codex).
63
+ """
64
+
65
+ command: list[str]
66
+ stdin: bool = False
67
+ pty: bool = False # run in a pseudo-terminal so the CLI sees a real TTY
68
+ models_command: list[str] | None = None
69
+
70
+
71
+ class Defaults(BaseModel):
72
+ temperature: float = 0.2
73
+ max_concurrency: int = Field(default=1, ge=1)
74
+ max_retries: int = Field(default=1, ge=0)
75
+ timeout: int = Field(default=900, ge=1) # per agent call, seconds
76
+ hitl_timeout: int = Field(default=0, ge=0) # seconds to wait for HITL approval; 0 = infinite
77
+ validate_timeout: int = Field(default=120, ge=1) # seconds per task/phase validate_command
78
+
79
+
80
+ # No-config fallback agents. Kept model-less so the bare-agent role defaults
81
+ # (ModelRoles -> agent only) work without an empty {model} argument. The richer
82
+ # {model}-templated commands live in DEFAULT_CONFIG_YAML that `roundtable init` writes.
83
+ DEFAULT_AGENTS: dict[str, AgentSpec] = {
84
+ # Claude Code, non-interactive print mode.
85
+ "claude": AgentSpec(command=["claude", "-p", "{prompt}"]),
86
+ # OpenAI Codex CLI, non-interactive exec mode.
87
+ "codex": AgentSpec(command=["codex", "exec", "{prompt}"]),
88
+ # Gemini CLI.
89
+ "gemini": AgentSpec(command=["gemini", "-p", "{prompt}"]),
90
+ }
91
+
92
+
93
+ class PiOptions(BaseModel):
94
+ """Options for ``provider: pi`` — drive a pi-family coding agent for every role.
95
+
96
+ Two flavors are supported (both share the same CLI contract — ``--mode json``,
97
+ ``--no-tools``, ``--model``, stdin prompt, and identical usage/cost JSON):
98
+
99
+ * ``pi`` — upstream `pi` (`earendil-works/pi`), binary ``pi``.
100
+ * ``omp`` — `oh-my-pi` (`can1357/oh-my-pi`), binary ``omp``; a batteries-included
101
+ fork (LSP/DAP/subagents). It has no ``--no-context-files`` and gates edits
102
+ behind approval, so task agents get ``--auto-approve`` for autonomous runs.
103
+
104
+ Only the task role runs with file tools (so task agents edit the repo); every
105
+ other role (planner, phase, main, map) runs ``--no-tools`` as a pure completion.
106
+ Each role's ``model`` is passed to ``--model`` (a ``provider/id`` string, glob,
107
+ or fuzzy pattern — ``<tool> --list-models`` shows what's available). Auth is the
108
+ tool's: set a provider key in the environment (``ANTHROPIC_API_KEY`` /
109
+ ``OPENAI_API_KEY`` / ``GEMINI_API_KEY`` / ...) or use the tool's own login.
110
+ """
111
+
112
+ flavor: str = "pi" # "pi" (upstream) | "omp" (oh-my-pi)
113
+ command: list[str] = Field(default_factory=list) # override binary; [] -> flavor default (["pi"]/["omp"])
114
+ extra_args: list[str] = Field(default_factory=list) # appended to every invocation
115
+ worker_extra_args: list[str] = Field(default_factory=list) # extra flags for the task (worker) role
116
+ orchestrator_extra_args: list[str] = Field(default_factory=list) # extra flags for non-worker roles
117
+ # (pi flavor only) orchestrator roles ignore the repo's AGENTS.md/CLAUDE.md by
118
+ # default (roundtable injects its own context); set true to let them load those.
119
+ orchestrator_context_files: bool = False
120
+
121
+
122
+ class Config(BaseModel):
123
+ provider: str = "cli" # "pi" | "cli" | "litellm" | "scripted"
124
+ models: ModelRoles = Field(default_factory=ModelRoles)
125
+ agents: dict[str, AgentSpec] = Field(default_factory=lambda: dict(DEFAULT_AGENTS))
126
+ pi: PiOptions = Field(default_factory=PiOptions)
127
+ defaults: Defaults = Field(default_factory=Defaults)
128
+ project_context: str = "" # optional project context injected into agent prompts
129
+
130
+
131
+ DEFAULT_CONFIG_YAML = """\
132
+ # roundtable configuration.
133
+
134
+ # Backend:
135
+ # cli -> reach other LLMs through their terminal CLIs (default)
136
+ # litellm -> direct API calls (model = litellm string, needs API keys)
137
+ # scripted -> deterministic offline backend (demo/tests, no network)
138
+ provider: cli
139
+
140
+ # Choosable (agent, model) per role. `agent` names an entry in `agents` below;
141
+ # `model` is passed to that agent's {model} placeholder. Mix CLIs and models
142
+ # freely per role -- e.g. main on claude/opus, phase on antigravity/gemini,
143
+ # tasks on opencode. Per-phase and per-task `runner` entries in the generated
144
+ # plan override these. (With provider: litellm, `model` is the litellm string
145
+ # -- openai/gpt-4o, anthropic/claude-3-5-sonnet-latest, ... -- and `agent` is
146
+ # ignored.) Shorthand `agent:model` strings work too.
147
+ models:
148
+ planner: { agent: claude, model: opus }
149
+ main: { agent: claude, model: opus }
150
+ phase: { agent: antigravity, model: gemini-3.5-flash }
151
+ task: { agent: opencode, model: opencode/mimo-v2.5-free }
152
+
153
+ # Terminal commands for provider: cli. argv lists (no shell). Tokens may use
154
+ # {prompt}, {system} and {model}; {model} is replaced with the role/task's
155
+ # chosen model so one command serves many models. If {system} is absent it is
156
+ # prepended to the prompt. Set stdin: true to pipe the prompt on stdin instead
157
+ # of via {prompt}. Add whatever flags your tool needs to run non-interactively
158
+ # and edit files (these flags are illustrative -- check each tool's own docs).
159
+ # Optional models_command lists that CLI's models; `roundtable init` / `roundtable
160
+ # agents` runs it so you can see what to assign above.
161
+ agents:
162
+ claude:
163
+ command: ["claude", "-p", "{prompt}", "--model", "{model}"]
164
+ codex:
165
+ command: ["codex", "exec", "--model", "{model}", "{prompt}"]
166
+ # Antigravity CLI (formerly the Gemini CLI; binary is `agy`), runs Gemini models.
167
+ antigravity:
168
+ command: ["agy", "-p", "{prompt}", "--model", "{model}"]
169
+ models_command: ["agy", "models"]
170
+ opencode:
171
+ command: ["opencode", "run", "--model", "{model}", "{prompt}"]
172
+ models_command: ["opencode", "models"]
173
+ ollama:
174
+ command: ["ollama", "run", "{model}"]
175
+ models_command: ["ollama", "list"]
176
+ stdin: true
177
+
178
+ defaults:
179
+ temperature: 0.2
180
+ max_concurrency: 1 # raise to run independent tasks in a phase concurrently
181
+ # (keep at 1 when file-editing agents might touch the same files)
182
+ max_retries: 1 # retries on a failing/timed-out agent call
183
+ timeout: 900 # seconds per agent call
184
+ validate_timeout: 120 # seconds per task/phase validate_command
185
+ """
186
+
187
+
188
+ # Recommended backend when a pi-family CLI is installed: it drives every role and
189
+ # handles LLM connectivity/auth itself. `roundtable init` writes this when it finds
190
+ # `pi` or `omp` on PATH. Model defaults are a mixed strong+cheap start -- edit freely.
191
+ def pi_config_yaml(flavor: str = "pi") -> str:
192
+ tool = "omp" if flavor == "omp" else "pi"
193
+ other = "pi" if flavor == "omp" else "omp"
194
+ models_hint = "`pi --list-models`" if tool == "pi" else "`omp --help` / omp's model config"
195
+ return f"""\
196
+ # roundtable configuration ({tool} backend -- recommended).
197
+
198
+ # Backend:
199
+ # pi -> drive a pi-family coding agent ({tool}) for every role (this file).
200
+ # It handles LLM connectivity + auth; task agents edit files,
201
+ # orchestrator roles run `{tool} --no-tools`. Exact usage & cost reported.
202
+ # flavor `pi` = upstream `pi`; flavor `omp` = oh-my-pi (binary `omp`).
203
+ # cli -> reach other LLMs through their terminal CLIs (claude, codex, ...)
204
+ # litellm -> direct API calls (model = litellm string, needs API keys)
205
+ # scripted -> deterministic offline backend (demo/tests, no network)
206
+ provider: pi
207
+
208
+ # Per-role model (passed to `{tool} --model`: a `provider/id` string, glob, or fuzzy
209
+ # pattern -- see {models_hint} for what you can use). `agent` is ignored
210
+ # on this backend. Mixed strong+cheap defaults: strong models plan/orchestrate, a
211
+ # cheap/fast model does the task work. Per-phase/per-task `runner` entries in the
212
+ # generated plan override these.
213
+ models:
214
+ planner: {{ model: anthropic/claude-opus-4-1 }} # strong: breaks the goal into phases/tasks
215
+ main: {{ model: anthropic/claude-opus-4-1 }} # strong: keeps project docs coherent
216
+ phase: {{ model: anthropic/claude-sonnet-4-5 }} # mid: defines & summarizes each phase
217
+ task: {{ model: anthropic/claude-haiku-4-5 }} # cheap/fast: does the actual task work
218
+ # Cheaper/free task work? Point `task` (and `phase`) at another provider the tool
219
+ # supports, e.g. `openrouter/...` or `opencode/...` -- see {models_hint}.
220
+
221
+ # Connect {tool} to your LLMs (pick one per provider you use):
222
+ # * env key: export ANTHROPIC_API_KEY=... (or OPENAI_API_KEY / GEMINI_API_KEY / ...)
223
+ # * or use {tool}'s own login (e.g. `pi-ai login anthropic` for pi)
224
+
225
+ # Options for the pi backend (all optional).
226
+ pi:
227
+ flavor: {flavor} # "pi" (upstream) or "omp" (oh-my-pi); switch to use `{other}`
228
+ command: ["{tool}"] # base binary; use ["npx", "{tool}"] if not installed globally
229
+ extra_args: [] # appended to every call, e.g. ["--thinking", "medium"]
230
+ worker_extra_args: [] # extra flags for the task role{_omp_worker_note(flavor)}
231
+ orchestrator_extra_args: [] # extra flags for planner/main/phase/map roles
232
+ orchestrator_context_files: false # (pi flavor) true -> orchestrator roles read AGENTS.md/CLAUDE.md
233
+
234
+ defaults:
235
+ temperature: 0.2
236
+ max_concurrency: 1 # raise to run independent tasks in a phase concurrently
237
+ # ({tool} has no per-action permission gate and tasks share the
238
+ # repo dir, so keep at 1 unless you isolate tasks yourself)
239
+ max_retries: 1 # retries on a failing/timed-out {tool} call
240
+ timeout: 900 # seconds per {tool} call
241
+ validate_timeout: 120 # seconds per task/phase validate_command
242
+ """
243
+
244
+
245
+ def _omp_worker_note(flavor: str) -> str:
246
+ if flavor == "omp":
247
+ return " (omp task agents already get --auto-approve)"
248
+ return ""
249
+
250
+
251
+ def load_config(project_dir: Path) -> Config:
252
+ path = Path(project_dir) / CONFIG_FILENAME
253
+ if not path.exists():
254
+ return Config()
255
+ raw = yaml.safe_load(path.read_text()) or {}
256
+ return Config.model_validate(raw)
257
+
258
+
259
+ def write_default_config(project_dir: Path, *, backend: str = "cli", flavor: str = "pi") -> Path:
260
+ """Write ``roundtable.config.yaml``. ``backend='pi'`` writes the recommended
261
+ pi-family template for ``flavor`` ('pi' or 'omp'); anything else writes the CLI
262
+ template."""
263
+ path = Path(project_dir) / CONFIG_FILENAME
264
+ path.write_text(pi_config_yaml(flavor) if backend == "pi" else DEFAULT_CONFIG_YAML)
265
+ return path