Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions orchestrator/agent_model_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@
:class:`AgentRole` at construction time.
2. ``repositories.yaml`` ``default_agent_model`` — repository-level
default surfaced via :func:`config.repo_config.get_default_agent_model`.
3. Built-in default — ``"fable"`` for the refine and plan phase roles
(:data:`_FABLE_DEFAULT_ROLES`), ``"opus"`` for everything else.
3. Built-in default — ``"opus"`` for all roles. Fable has been
disabled, so ``FABLE_DEFAULT_MODEL`` is now ``"opus"`` and the
refine/plan phase roles (:data:`_FABLE_DEFAULT_ROLES`) unify on opus
alongside everything else.

Classifier: a model string matching one of the recognised Claude
aliases (``opus``, ``opus[1m]``, ``sonnet``, ``sonnet[1m]``, ``haiku``,
Expand Down Expand Up @@ -53,13 +55,12 @@
# default in ``orchestrator/consensus_wrapper.py::build_consensus_wrapped_command``.
DEFAULT_AGENT_MODEL = "opus"

# Built-in default for the refine and plan phases. The drafting-heavy
# upstream phases (analysis, slice DAG, contract shape) run on the
# highest-capability tier; implement and downstream phases stay on
# DEFAULT_AGENT_MODEL. Both pipeline-level ``agent_models`` and the
# repo-level ``default_agent_model`` still override this (precedence
# unchanged — this only splits the tier-3 built-in by role).
FABLE_DEFAULT_MODEL = "fable"
# Built-in default for the refine and plan phases. Fable has been
# disabled, so the drafting-heavy upstream phases (analysis, slice DAG,
# contract shape) now run on opus alongside implement and downstream
# phases. Both pipeline-level ``agent_models`` and the repo-level
# ``default_agent_model`` still override this (precedence unchanged).
FABLE_DEFAULT_MODEL = "opus"

# Effort level pinned on fable-routed agents (threaded to ``--effort``
# on the ``python3 -m egg_agent`` command). Claude Code's built-in
Expand Down Expand Up @@ -347,8 +348,11 @@ def resolve_agent_model(
if repo_default:
return classify_model(repo_default)

# Tier 3: built-in default — refine/plan roles run the
# highest-capability tier, everything else stays on opus.
# Tier 3: built-in default — opus for all roles. Fable has been
# disabled, so FABLE_DEFAULT_MODEL is now "opus" and the refine/plan
# branch resolves to the same value as the default branch. The branch
# is retained so re-enabling fable for refine/plan is a one-line
# change to FABLE_DEFAULT_MODEL.
if role_value in _FABLE_DEFAULT_ROLES:
return classify_model(FABLE_DEFAULT_MODEL)
return classify_model(DEFAULT_AGENT_MODEL)
Expand Down
11 changes: 4 additions & 7 deletions orchestrator/concurrent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -432,20 +432,17 @@ def _spawn_agent(self, role: AgentRole, prompt_text: str = "") -> AgentExecution
# Per-agent model resolution (#2769 slice-2). The decision is a
# pure function over (role, pipeline_config, repo); when no
# override is configured the resolver returns a built-in
# Anthropic decision — ``fable`` for the refine/plan phase
# roles (``_FABLE_DEFAULT_ROLES``), ``opus`` for every other
# role — so the wire shape stays Anthropic-only.
# Anthropic decision — ``opus`` for every role now that fable has
# been disabled (refine/plan roles unify on opus) — so the wire
# shape stays Anthropic-only.
#
# Defensive wrap: a future regression in the resolver (e.g. a
# broken lazy import for the repo-default tier) would otherwise
# bring down agent spawn for every pipeline. Mirror the restart
# path's ``classify_model(DEFAULT_AGENT_MODEL)`` fallback at
# ``routes/pipelines.py:2683-2699`` so spawn degrades to the
# built-in opus / anthropic decision and logs the resolver
# failure rather than crashing. (The fallback stays on opus
# uniformly — refine/plan agents whose resolver call raised
# silently downgrade from fable to opus rather than failing
# to spawn at all.)
# failure rather than crashing.
try:
decision: AgentModelDecision = resolve_agent_model(
role=role,
Expand Down
36 changes: 23 additions & 13 deletions orchestrator/tests/test_agent_model_resolution.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@

1. ``pipeline_config.agent_models.get(role.value)`` — per-pipeline
2. ``get_default_agent_model(repo)`` — per-repo
3. Built-in default — ``"fable"`` for refine/plan phase roles,
``"opus"`` for everything else
3. Built-in default — ``"opus"`` for all roles. Fable has been
disabled, so ``FABLE_DEFAULT_MODEL`` is now ``"opus"`` and the
refine/plan phase roles unify on opus alongside everything else.

Classifier (model string → upstream):

Expand Down Expand Up @@ -756,9 +757,9 @@ def find_spec(self, fullname, path, target=None):

class TestDefaultPathRegression:
"""With ``agent_models={}`` and no repo default, EVERY role resolves
to an Anthropic-route built-in: ``fable`` for the refine/plan phase
roles, ``opus`` for everything else. ``upstream_model`` stays
``None`` on every default path — the slice-2 no-op invariant.
to an Anthropic-route built-in: ``opus`` for all roles now that fable
has been disabled (refine/plan unify on opus). ``upstream_model``
stays ``None`` on every default path — the slice-2 no-op invariant.
"""

def test_implement_phase_roles_default_to_anthropic_opus(self):
Expand Down Expand Up @@ -786,9 +787,13 @@ def test_implement_phase_roles_default_to_anthropic_opus(self):
f"regression: default config must be Anthropic-only"
)

def test_refine_and_plan_roles_default_to_anthropic_fable(self):
def test_refine_and_plan_roles_default_to_anthropic_opus(self):
"""Refine/plan producers and reviewers pick up the built-in
``fable`` default while staying on the Anthropic upstream."""
``opus`` default while staying on the Anthropic upstream.

Fable has been disabled, so ``FABLE_DEFAULT_MODEL`` is now
``"opus"`` — the refine/plan roles unify on opus alongside the
implement and downstream phases."""
resolve_agent_model = _resolver()
AgentRole = _agent_role()
config = _pipeline_config()
Expand All @@ -804,8 +809,8 @@ def test_refine_and_plan_roles_default_to_anthropic_fable(self):
AgentRole.REVIEWER_PLAN,
):
d = resolve_agent_model(role, config, "any/repo")
assert d.claude_code_alias == "fable", (
f"{role.value} should default to fable, got {d.claude_code_alias!r}"
assert d.claude_code_alias == "opus", (
f"{role.value} should default to opus, got {d.claude_code_alias!r}"
)
assert d.upstream == "anthropic"
assert d.upstream_model is None
Expand Down Expand Up @@ -870,9 +875,14 @@ def test_litellm_models_inherit_default_effort(self):

assert classify_model("qwen3-max").effort is None

def test_refine_plan_default_decision_carries_high_effort(self):
"""The built-in fable default for refine/plan roles flows through
``resolve_agent_model`` with the pinned effort attached."""
def test_refine_plan_default_decision_inherits_default_effort(self):
"""The built-in opus default for refine/plan roles flows through
``resolve_agent_model`` carrying ``effort=None``.

Fable has been disabled, so ``FABLE_DEFAULT_MODEL`` is ``"opus"``;
opus is not in ``_FABLE_ALIASES``, so the pinned ``FABLE_EFFORT``
no longer applies and these roles inherit Claude Code's per-model
default — matching the coder/implement roles."""
resolve_agent_model = _resolver()
AgentRole = _agent_role()
config = _pipeline_config()
Expand All @@ -881,5 +891,5 @@ def test_refine_plan_default_decision_carries_high_effort(self):
refiner = resolve_agent_model(AgentRole.REFINER, config, "any/repo")
coder = resolve_agent_model(AgentRole.CODER, config, "any/repo")

assert refiner.effort == "high"
assert refiner.effort is None
assert coder.effort is None
12 changes: 6 additions & 6 deletions orchestrator/tests/test_concurrent_executor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1047,14 +1047,14 @@ def _capture_command(prompt_text, **kwargs):
f"consensus wrapper; got {captured.get('model')!r}"
)

def test_default_config_passes_fable_for_refine_plan_role(self):
"""``agent_models == {}`` → refiner spawn passes ``model="fable"``
def test_default_config_passes_opus_for_refine_plan_role(self):
"""``agent_models == {}`` → refiner spawn passes ``model="opus"``
to ``build_consensus_wrapped_command``.

Symmetric to ``test_default_config_passes_opus_to_consensus_wrapper``
— guards the role→model wiring in ``_spawn_agent`` against a
future change that breaks the fable default for the refine/plan
roles (post-PR #3062 split of tier-3 built-in by role).
future change that breaks the opus default for the refine/plan
roles after fable was disabled.
"""
from concurrent_executor import ConcurrentPhaseExecutor
from egg_orchestrator.types import AgentRole
Expand All @@ -1076,8 +1076,8 @@ def _capture_command(prompt_text, **kwargs):
):
executor._spawn_agent(AgentRole.REFINER, prompt_text="run task")

assert captured.get("model") == "fable", (
f"Default-config refiner spawn MUST pass model='fable' to "
assert captured.get("model") == "opus", (
f"Default-config refiner spawn MUST pass model='opus' to "
f"the consensus wrapper; got {captured.get('model')!r}"
)

Expand Down
Loading