Skip to content

feat(config): make codex reasoning effort configurable per role#17

Merged
TueVNguyen merged 4 commits into
Intelligent-Internet:mainfrom
Quigleybits:feat/per-role-reasoning-effort
Jul 20, 2026
Merged

feat(config): make codex reasoning effort configurable per role#17
TueVNguyen merged 4 commits into
Intelligent-Internet:mainfrom
Quigleybits:feat/per-role-reasoning-effort

Conversation

@Quigleybits

@Quigleybits Quigleybits commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Why

Every codex ACP role — worker, validator, terminal reviewer — currently launches with a hardcoded model_reasoning_effort="xhigh" (_augment_acp_command). Validators re-checking a single assertion and the terminal reviewer don't need frontier effort on every dispatch, and today there is no knob to dial any of it down. Effort tiering per role is the cheapest cost lever available: validation lanes typically run more often than work lanes, so that's where spend concentrates on long missions.

What

Three new env vars, following the exact shape of the existing per-role provider/command config:

  • ZENITH_WORKER_REASONING_EFFORT
  • ZENITH_VALIDATOR_REASONING_EFFORT
  • ZENITH_TERMINAL_REVIEWER_REASONING_EFFORT

Semantics:

  • Backwards compatible by default: fully unset config keeps the historical xhigh; nothing changes unless opted in.
  • Same inheritance chain as providers/commands: unset roles inherit terminal_reviewer → validator → worker, resolved in for_role() exactly like worker_provider_name / worker_acp_command.
  • Allowlist-validated at discovery (minimal | low | medium | high | xhigh | max): the resolved value is spliced into a shell command line, and a typo'd downgrade silently ignored would keep spending xhigh — so an invalid value fails fast at HarnessConfig.discover() instead. Codex's ultra is deliberately excluded — it is not a reasoning tier (codex downgrades the request to max on the wire) but a switch into proactive multi-agent mode; the allowlist comment documents this.
  • Claude/hermes paths are untouched (effort is a codex-acp -c override; other providers pass through unchanged).

Example — full-effort workers, cheaper validation lanes:

export ZENITH_VALIDATOR_REASONING_EFFORT=medium
export ZENITH_TERMINAL_REVIEWER_REASONING_EFFORT=high
# workers stay xhigh

Review round 1 (@TueVNguyen) — persistence through zenith init

Addressed in 3374e30: the three variables are forwarded through the (renamed) RUNTIME_ENV_FORWARD_ALLOWLIST, merged once above the format branch so both .mcp.json and .codex/config.toml persist them; .mcp.json output is byte-identical when the variables are unset; claude + codex init tests assert the values land in the generated server env.

Review round 2 (@stephanbrez) — max + init flags

Addressed in 698ace4 + e63462b:

  • max added to the allowlist (now protocol-complete against codex's ReasoningEffort, ultra excluded as above). Note: supported tiers vary per model; the adapter clamps unsupported values to the model default.
  • zenith init gains --worker-reasoning-effort / --validator-reasoning-effort / --terminal-reviewer-reasoning-effort — choice-validated, sugar for the env vars, a flag wins over a valid inherited shell setting, and an invalid value already in the environment still fails fast at discovery (pinned by test).

Known limitation (pre-existing on main, applies equally to the hardcoded xhigh): the npm codex-acp adapter ignores -c argv entirely, so effort values currently configure Zenith's intent without reaching the worker on that adapter — tracked in #27 with a proposed CODEX_CONFIG-based delivery fix that builds on this PR's per-role values.

Tests

  • Discovery: defaults to None (→ xhigh), per-role overrides (including max), invalid value rejected with the env var named in the error.
  • for_role() cascade: single worker-level setting reaches all three roles; explicit validator setting wins and feeds the terminal reviewer fallback.
  • _augment_acp_command: override lands in the codex command line, bypass flags unaffected, claude command untouched with or without an effort.
  • zenith init: claude + codex formats persist env-configured efforts; flags override valid inherited env; invalid inherited env fails fast despite a flag.

ruff check ., mypy src, and pytest -q all pass (Linux/py3.12, rebased on current main).

🤖 Generated with Claude Code

Every codex ACP role currently runs with a hardcoded
model_reasoning_effort="xhigh". Validators re-checking one assertion
and terminal reviewers do not need frontier effort on every dispatch,
and there was no way to dial any of it down.

Add ZENITH_WORKER_REASONING_EFFORT, ZENITH_VALIDATOR_REASONING_EFFORT,
and ZENITH_TERMINAL_REVIEWER_REASONING_EFFORT. Unset roles inherit down
the same chain the provider/command config already uses
(terminal_reviewer -> validator -> worker), and a fully unset config
keeps the historical xhigh default, so behavior is unchanged unless
opted in.

Values are validated against an allowlist at discovery: the resolved
value is spliced into a shell command line, and a typo'd downgrade
silently ignored would keep spending xhigh.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@TueVNguyen

Copy link
Copy Markdown
Collaborator

Thanks for this thoughtful addition. The per-role inheritance and early allowlist validation are especially nice touches.

One request before merge: preserve these settings through zenith init

The new values are read by HarnessConfig.discover(), but they are not currently written into the generated MCP configuration. zenith init builds its runtime environment from selection.env() and storage_env; the .mcp.json path only forwards variables in MCP_ENV_FORWARD_ALLOWLIST, and the three new reasoning-effort variables are not included there. The Codex config path does not merge that forwarded environment at all.

That means a user can export, for example:

export ZENITH_VALIDATOR_REASONING_EFFORT=medium
zenith init --agent claude

...but the generated .mcp.json will not retain that setting. If the MCP server is later launched without inheriting the original shell environment, it falls back to xhigh, which is a surprising outcome for a cost-control setting.

Could we:

  1. Add the three ZENITH_*_REASONING_EFFORT variables to the runtime environment allowlist.
  2. Merge forwarded runtime environment values before branching between .mcp.json and .codex/config.toml, so both generated formats receive them.
  3. Add zenith init tests for both Claude and Codex that assert the configured effort values are present in the generated server environment.

A small naming cleanup, such as renaming _forwarded_mcp_env() to _forwarded_runtime_env(), would make its broader use clearer too.

…figs

Review follow-up for Intelligent-Internet#17: the three ZENITH_*_REASONING_EFFORT values
were read by HarnessConfig.discover() but never written into the
generated server config, so an MCP server launched outside the original
shell silently fell back to xhigh.

- add the three variables to the runtime env forward allowlist
- merge forwarded runtime env before the .mcp.json / .codex/config.toml
  branch so both formats persist it; .mcp.json output is byte-identical
  when the variables are unset
- rename MCP_ENV_FORWARD_ALLOWLIST / _forwarded_mcp_env to
  RUNTIME_ENV_FORWARD_ALLOWLIST / _forwarded_runtime_env to match the
  broader use
- add claude + codex init tests asserting the configured values land in
  the generated server env

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Quigleybits

Copy link
Copy Markdown
Contributor Author

Thanks for the review — all four points addressed in 3374e30:

  • The three ZENITH_*_REASONING_EFFORT variables are now in the (renamed) RUNTIME_ENV_FORWARD_ALLOWLIST, and forwarded runtime env is merged once, above the format branch, so both .mcp.json and .codex/config.toml persist it. .mcp.json output is byte-identical when the variables are unset, and invalid values still fail closed (discover() runs before any config is written).
  • New tests test_claude_init_writes_reasoning_effort_env / test_codex_init_writes_reasoning_effort_env; both fail with KeyError without the fix.
  • _forwarded_mcp_env()_forwarded_runtime_env(), allowlist constant renamed to match.

One deliberate side effect worth flagging: .codex/config.toml now also persists the other forwarded keys (e.g. ANTHROPIC_API_KEY), matching what .mcp.json already did.

While verifying we hit two pre-existing issues in the workspace-scope codex writer — a second zenith init --agent codex emits invalid TOML, and env values are interpolated unescaped (a Windows --zenith-home path trips it today). #23's new user-scope writer already handles both patterns; happy to send a small follow-up porting that discipline once it lands.

@stephanbrez

stephanbrez commented Jul 17, 2026

Copy link
Copy Markdown

Great feature! Can I suggest you add the following as well:

  • Expose the role settings as cli flags on zenith init e.g. --worker-reasoning-effort
  • Expand the effort validation to include 'max'

Quigleybits and others added 2 commits July 19, 2026 16:21
Review follow-up for Intelligent-Internet#17 (suggested by @stephanbrez): codex's
ReasoningEffort enum now includes "max" (protocol/src/openai_models.rs),
so admit it to VALID_REASONING_EFFORTS.

"ultra" is deliberately excluded: it is not a reasoning tier. Codex
downgrades ultra requests to "max" on the wire (core/src/client.rs) and
instead flips the session into proactive multi-agent mode
(core/src/session/multi_agents.rs) — inside a Zenith lane that would
nest an agent swarm in a harness that already orchestrates and validates
per-lane work, with no reasoning gain over "max". The allowlist comment
records this so the omission doesn't read as a stale list.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review follow-up for Intelligent-Internet#17 (suggested by @stephanbrez): expose
--worker-reasoning-effort / --validator-reasoning-effort /
--terminal-reviewer-reasoning-effort on `zenith init`, completing
surface parity with the existing per-role provider/command flags.

Flags are sugar for the ZENITH_*_REASONING_EFFORT env vars:
choice-validated against VALID_REASONING_EFFORTS at parse time, they
win over valid inherited shell settings, and they merge into the
generated server env above the format branch so both .mcp.json and
.codex/config.toml persist them.

An invalid value already present in the environment still fails fast
at discover() rather than being masked by a flag — the same validation
would raise at server launch, so masking at init only defers the
failure. A test pins that behavior.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Quigleybits

Copy link
Copy Markdown
Contributor Author

Both suggestions are in — max in 698ace4, init flags in e63462b.

  • max: added to the allowlist. One caveat: supported tiers vary per model, and the codex-acp adapter clamps an unsupported value to the model's default rather than erroring.
  • ultra is deliberately excluded — it isn't a reasoning tier. Codex downgrades ultra requests to max on the wire (core/src/client.rs), and what the setting actually does is flip the session into proactive multi-agent mode (core/src/session/multi_agents.rs). Inside a Zenith lane that would nest an agent swarm in a harness that already orchestrates and validates per-lane work, with no reasoning gain over max. The allowlist comment documents the exclusion so it doesn't read as a stale list.
  • Flags: zenith init now takes --worker-reasoning-effort / --validator-reasoning-effort / --terminal-reviewer-reasoning-effort, choice-validated at parse time, matching the existing per-role provider/command flags. They're sugar for the env vars: a flag wins over a valid inherited shell setting, while an invalid value already in the environment still fails fast at discovery (masking it would only defer the same error to server launch) — both behaviors pinned by tests. They ride the runtime-env forwarding path, so no overlap with fix(cli): wire --terminal-reviewer-provider/acp-command through to config #26 — different hunks; either side rebases cleanly.

While verifying delivery end-to-end we found the codex -c overrides (including main's existing hardcoded xhigh and the sandbox/approval flags) are silently dropped by the npm codex-acp adapter — filed as #27 with a proposed CODEX_CONFIG-based fix that builds on this PR's per-role values.

@stephanbrez

Copy link
Copy Markdown

Thanks for that! I was wondering if the adapter was applying the overrides, glad you found it 👍

@TueVNguyen

Copy link
Copy Markdown
Collaborator

Looks good to me—approved and ready to merge. Thanks @Quigleybits

@TueVNguyen
TueVNguyen merged commit a21c071 into Intelligent-Internet:main Jul 20, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants