[FEATURE] Subagent model policy: relative bias + cap/floor bounds applied after the resolution cascade

Status Open
Maintainer reply None cached
Activity 0 comments · opened Jul 25, 2026

Preflight Checklist

  • [x] I have searched existing requests and this feature hasn't been requested yet
  • [x] This is a single feature request (not multiple features)

Problem Statement

Every existing subagent-model knob works by injecting an absolute value into the resolution cascade, so using any of them destroys the information below it:

  • CLAUDE_CODE_SUBAGENT_MODEL sits at the top of the cascade. Setting it (e.g. to opus to keep subagents off a Fable 5 main session) also clobbers deliberate per-invocation downgrades (model: haiku for mechanical fan-out) and model: frontmatter pins on custom agents.
  • Frontmatter and per-invocation model are absolute pins with no way to express "relative to my main model."
  • availableModels is global, so it can't express "premium for the main session, cheaper for subagents" (see #74788).

There is no way to say any of:

  • "Subagents should default one tier below my main model" (main = Fable → defaults land on Opus), while keeping Haiku routings and frontmatter pins intact.
  • "Never spend above Sonnet on subagents, no matter who chose the model" (budget mode).
  • "Never go below Opus on subagents" (quality mode).

The root cause: the current system conflates selection (which mechanism picks the model) with policy (what the user is willing to spend). The fix is to leave the cascade alone and apply user policy as a transform on its output.

Proposed Solution

One rule

Bias shifts defaults. Bounds constrain everything.

Every resolved model carries a provenance: explicit (deliberately chosen — per-invocation model parameter, or non-inherit frontmatter) or default (fell through to "inherit the main model"). Bias only touches defaults; cap/floor apply regardless of provenance. This one distinction preserves all the existing desirable mechanics.

Configuration

// settings.json
"subagentModels": {
  "bias": -1,              // tier shift applied to *defaulted* models
  "cap": "opus",           // ceiling for *all* subagents, however chosen
  "floor": "haiku",        // floor for *all* subagents, however chosen
  "overrides": {           // optional per-agent-type policy
    "Explore": { "bias": -2 }
  }
}

Env-var equivalents for scripting: CLAUDE_CODE_SUBAGENT_BIAS, CLAUDE_CODE_SUBAGENT_CAP, CLAUDE_CODE_SUBAGENT_FLOOR. All fields optional; an empty policy is byte-for-byte today's behavior.

Tier ladder: haiku(0) < sonnet(1) < opus(2) < fable(3). Full model IDs map to their family's tier. Bias saturates at the ladder ends rather than erroring. floor > cap is a config-load error.

Resolution algorithm

1. Resolve candidate + provenance (today's cascade, unchanged):
     a. per-invocation model param        → explicit
     b. frontmatter model (≠ "inherit")   → explicit
     c. inherit → main conversation model → default

2. if provenance == default:
     candidate = shift(candidate, bias)          // saturating

3. candidate = clamp(candidate, floor, cap)      // all provenances
   built-in bounds compose here (Explore's Opus cap on the Claude API
   is an implicit cap; effective cap = min(user cap, built-in cap))

4. availableModels allowlist check (existing behavior), except the
   fallback re-enters at step 2 instead of raw-inheriting, so policy
   still applies to the fallback

5. Freeze the resolved model on the spawned agent, so resume/follow-up
   keeps it (matching v2.1.211 semantics)

CLAUDE_CODE_SUBAGENT_MODEL stays supported as an absolute pre-empt for back-compat, with a deprecation warning when both it and a policy are set (legacy wins, so nothing breaks silently).

Worked scenarios

Main on Fable 5, bias: -1 (the motivating case):

  • Plan / general-purpose / unpinned custom agents: inherit Fable → default → biased to Opus
  • Per-invocation model: haiku for a mechanical task: explicit → bias skipped → Haiku
  • Custom agent pinned model: sonnet in frontmatter: explicitSonnet
  • Explore: inherit Fable → biased to Opus → built-in Opus cap composes as a no-op → Opus
  • Fork: unchanged — always parent model, policy skipped (it inherits the full conversation context, so a model switch isn't safe). The one documented exception.

Budget mode, cap: "sonnet": everything — including pins and per-invocation choices — lands at Sonnet or below. This is the honest version of what CLAUDE_CODE_SUBAGENT_MODEL gets misused for today, and directly addresses the premium-lane-exhaustion failure in #74788 (nested unpinned spawns are default provenance, so both bias and cap apply to them).

Quality mode, floor: "opus" vs bias: +1: floor promotes even deliberate Haiku routings; bias alone promotes only the defaults. The user picks which semantic they mean — today neither is expressible.

Edge cases

  • Unknown model IDs (custom Bedrock/Vertex mappings with no known family): unbiasable and unclampable — pass through with a one-time warning rather than guessing a tier.
  • Nested spawns: a subagent spawning children resolves each child through the same policy; unpinned children are default provenance relative to the main session's model, so they get biased/clamped — closing the silent-premium-inheritance hole from #74788.
  • Observability: task list and /agents show the resolution, e.g. opus (fable → bias −1), so a surprising model choice is diagnosable at a glance (complements #76370 / #72287).

Non-goals (deliberately excluded)

  • Per-invocation bias syntax — the per-invocation model param already covers that case exactly.
  • Biasing forks — model-switching mid-context is a correctness question, not a cost preference.
  • Effort couplingeffort is a separate axis (frontmatter already treats it as one); bundling would recreate the same inflexibility this removes. (Effort-scoped control is #79135.)

Relationship to existing requests

  • #78217 (managed default for subagent model): the bias-applies-to-defaults-only half of this proposal, expressed as an absolute value; a relative bias additionally tracks /model changes mid-week without reconfiguration.
  • #74788 (subagent model allowlist below the main session): the cap half of this proposal.
  • #73552 (per-built-in-agent model override): subsumed by overrides without forking prompts.

This proposal unifies those into one mechanism with a single learnable rule (bias vs. bounds), is fully additive (empty policy = current behavior), and keeps the legacy env var working.

View original on GitHub ↗