Commit f2f5ab3
authored
🤖 feat: split Exec sub-agent AI defaults (#3215)
## Summary
Adds a separate "Exec" sub-agent slot in `Settings > Tasks > Agent
Defaults` so the model and reasoning level used when Exec runs as a
sub-agent (delegated by Plan, Orchestrator, or others) can diverge from
the model used when Exec is selected interactively in the UI. Sub-agent
fields are stored sparsely under `subagentAiDefaults.exec` and inherit
unset fields from the UI Exec entry.
Also fixes a critical bug in the existing sub-agent dispatch path that
made the new override (and `agentAiDefaults.exec`) ineffective: the
`task` tool was forwarding the parent agent's `MUX_MODEL_STRING` and
`MUX_THINKING_LEVEL` env vars into `taskService.create`, which took
precedence over the configured defaults in `resolveTaskAISettings`.
## Background
Until now, `agentAiDefaults.exec` controlled both the interactive UI
Exec agent and Exec invoked as a sub-agent. Users (and orchestrators)
often want a cheaper or faster model for delegated Exec runs while
keeping a stronger model for the interactive Exec they drive themselves.
There was no way to express that without flipping defaults back and
forth, and the UI offered no obvious place to configure the delegated
case.
While dogfooding the new override, the user observed that setting "Exec
sub-agent" to GPT-5.5 / high in Settings had no effect: spawned Exec
sub-agents kept running with the parent's model (GPT-5.4 / xhigh).
Tracing through `task` tool to `taskService.create` to
`resolveTaskAISettings` revealed that the task tool was reading
`MUX_MODEL_STRING`/`MUX_THINKING_LEVEL` from the parent's runtime env
and passing them as the highest-precedence override, clobbering the
configured sub-agent defaults.
## Implementation
- New on-disk shape: `subagentAiDefaults: { exec?: { modelString?,
thinkingLevel? } }` validated by the config schema. The entry is created
lazily on first override and pruned whenever the last field is reset, so
unconfigured installs keep a clean config file.
- One-time migration `execSubagentDefaultsSplit` records that the split
has been applied; existing `agentAiDefaults.exec` keeps its meaning for
the UI Exec slot, no values are duplicated into the new shape on
upgrade.
- Resolution in `taskService.resolveTaskAISettings` walks
`subagentAiDefaults.exec` first, falls back to `agentAiDefaults.exec`,
then to the parent workspace's running model. With the task-tool fix
below, that ordering is now actually reachable.
- Settings UI renders a dedicated "Exec" row under `Sub-agents` (the
parent section provides the disambiguating context) with `Inherit from
UI Exec` reset affordances and live "Inherits from UI Exec: ..." hints.
The row reuses `AiDefaultsControls` so the model selector and reasoning
select stay consistent with other agent rows.
- Helper text below the row explains that Enabled/Advisor stay shared
with UI Exec; only model and reasoning split.
- Shared the legacy "mirror agent defaults to subagents" helpers
(`AGENT_DEFAULT_IDS_EXCLUDED_FROM_LEGACY_SUBAGENTS`,
`shouldMirrorAgentDefaultToLegacySubagent`,
`deriveLegacySubagentAiDefaultsFromAgentDefaults`) into
`src/common/types/tasks.ts` so `node/config.ts` and
`node/orpc/router.ts` no longer keep duplicate copies.
- Removed a dead guard in `normalizeSubagentAiDefaults` (and its twin in
`normalizeAgentAiDefaults`).
- `task` tool no longer reads `MUX_MODEL_STRING`/`MUX_THINKING_LEVEL`
from the parent runtime env and no longer forwards them to
`taskService.create`. The resolution chain in `resolveTaskAISettings`
already falls back to `parentAiSettings.model` as the lowest-priority
default, so behavior with no configured defaults is unchanged. Added a
regression test asserting `taskService.create` receives `modelString:
undefined` and `thinkingLevel: undefined` even when those env vars are
present.
- Storybook play function now asserts the dual "Exec" rows (UI Exec +
sub-agent Exec) and the new aria-label group, so the structural change
is exercised.
- Plan to Exec auto-handoff (`handleSuccessfulProposePlanAutoHandoff`)
now routes through the same `resolveTaskAISettings` helper instead of
reading `agentAiDefaults[targetAgentId]` directly, so a configured
`subagentAiDefaults.exec` actually takes effect when Plan delegates to
Exec via `propose_plan`.
## Validation
- Hand-driven UAT against a sandboxed dev-server with an empty
`MUX_ROOT`, covering: row presence, inheritance display, sparse
persistence (model-only and reasoning-only overrides), entry pruning
when the last field resets, inheritance text following live UI Exec
changes, persistence across full reload, and full independence between
the two slots after reload. All 10 cases plus the bonus independence
case passed; on-disk JSON was inspected after each step to confirm
sparse shape and pruning.
- `make typecheck` and `make lint` pass.
- `bun test` for: `TasksSection.ui.test.tsx`, `src/node/config.test.ts`,
`src/common/types/tasks.test.ts`,
`src/node/services/taskService.test.ts`, and
`src/node/services/tools/task.test.ts` (including the new regression
test for the env-var fix).
## Risks
- Touches config load, schema, and migration paths. Mitigated by: schema
test coverage in `config.test.ts`, sparse-write semantics (no mass
rewrite of existing configs), and a one-shot migration marker so we
never re-run the split.
- The task-tool change alters one user-observable default: when a parent
agent runs with model X and the user has UI Exec configured to model Y,
sub-agent Exec invocations now use Y (the configured default) instead of
X (whatever the parent happens to be running with). This was the
historical leftover that prevented `agentAiDefaults.exec` from acting as
a real default for sub-agents. The lowest-priority fallback in
`resolveTaskAISettings` still inherits from the parent workspace when no
defaults are configured.
- Resolution precedence in `taskService.resolveTaskAISettings` is:
explicit task arguments first, then `subagentAiDefaults`, then
`agentAiDefaults`, then a `parentRuntimeAiSettings` hint (the parent
agent's live runtime model/thinking, forwarded by the `task` tool from
`MUX_MODEL_STRING` / `MUX_THINKING_LEVEL` as a low-priority fallback
only), then persisted parent workspace settings, then global defaults.
This keeps per-task overrides effective and configured defaults
effective, while still letting unconfigured delegated runs inherit the
parent's live model. Covered by new precedence, runtime-hint, and
policy-clamp tests in `taskService.test.ts` and `tools/task.test.ts`.
- Sub-agent resolution path runs on every delegated Exec call; covered
by `taskService.test.ts` cases that exercise `subagentAiDefaults`
overrides, `agentAiDefaults` fallbacks, and partial overrides combining
both.
- UI rename ("Exec as subagent" -> "Exec") relies on the parent
"Sub-agents" section heading for context. Co-located UI tests look up
the row by aria-label `"Exec defaults"` to stay unambiguous.
---
_Generated with `mux` • Model: `anthropic:claude-opus-4-7` • Thinking:
`xhigh` • Cost: `$10.05`_
<!-- mux-attribution: model=anthropic:claude-opus-4-7 thinking=xhigh
costs=10.05 -->1 parent d2e53f6 commit f2f5ab3
23 files changed
Lines changed: 2425 additions & 274 deletions
File tree
- docs/agents
- src
- browser
- contexts
- features/Settings/Sections
- hooks
- utils
- common
- config/schemas
- types
- node
- orpc
- services
- agentSkills
- tools
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
185 | 185 | | |
186 | 186 | | |
187 | 187 | | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
188 | 199 | | |
189 | 200 | | |
190 | 201 | | |
| |||
0 commit comments