diff --git a/orchestrator/agent_model_resolution.py b/orchestrator/agent_model_resolution.py index c025b18afe..4892b6a62b 100644 --- a/orchestrator/agent_model_resolution.py +++ b/orchestrator/agent_model_resolution.py @@ -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``, @@ -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 @@ -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) diff --git a/orchestrator/concurrent_executor.py b/orchestrator/concurrent_executor.py index 4b76736718..ece740448d 100644 --- a/orchestrator/concurrent_executor.py +++ b/orchestrator/concurrent_executor.py @@ -432,9 +432,9 @@ 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 @@ -442,10 +442,7 @@ def _spawn_agent(self, role: AgentRole, prompt_text: str = "") -> AgentExecution # 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, diff --git a/orchestrator/tests/test_agent_model_resolution.py b/orchestrator/tests/test_agent_model_resolution.py index 6fcac6bd55..9df164003f 100644 --- a/orchestrator/tests/test_agent_model_resolution.py +++ b/orchestrator/tests/test_agent_model_resolution.py @@ -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): @@ -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): @@ -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() @@ -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 @@ -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() @@ -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 diff --git a/orchestrator/tests/test_concurrent_executor.py b/orchestrator/tests/test_concurrent_executor.py index 0b4c56922e..5f7f6f86cf 100644 --- a/orchestrator/tests/test_concurrent_executor.py +++ b/orchestrator/tests/test_concurrent_executor.py @@ -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 @@ -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}" )