diff --git a/kora_cli/reasoning/anthropic_engine.py b/kora_cli/reasoning/anthropic_engine.py index 617045f9fcc2..1bf11c3dba96 100644 --- a/kora_cli/reasoning/anthropic_engine.py +++ b/kora_cli/reasoning/anthropic_engine.py @@ -273,25 +273,38 @@ async def respond( ``ResponseResult.input_tokens`` / ``output_tokens`` are totals over all roundtrips, NOT just the final one. - # KR-REASONING-ROUTE-THROUGH-GATEWAY-CORE ST1 — toggle - - When the env ``KORA_REASONING_USE_GATEWAY`` is set to - ``"true"``, ``respond()`` routes through - ``_respond_via_gateway()`` (which uses Hermes's - ``AIAgent.run_conversation`` chokepoint + the new - ``kora_hermes`` bundled plugin's hook callbacks). Default - ``false`` preserves the existing bypass path; ST2 wires - the gateway path's tool plumbing + behavior parity tests - and flips the default. + # KR-REASONING-ROUTE-THROUGH-GATEWAY toggle (ST1 introduced; + # ST3 flips the default). + + When the env ``KORA_REASONING_USE_GATEWAY`` is unset or set + to a truthy value (``"true"``, ``"1"``, etc.), ``respond()`` + routes through ``_respond_via_gateway()`` (Hermes's + ``AIAgent.run_conversation`` chokepoint + the + ``kora_hermes`` bundled plugin's hook callbacks). The + bypass path is opt-IN with ``KORA_REASONING_USE_GATEWAY= + false`` — kept as an escape hatch for incident response if + the gateway path surfaces a regression post-flip. + + # ST3 default-flip rationale + + ST2B (#181) landed the tool-bridge; #189 closed Lock R3-2 + Phase C with the post-call escalation hook + haiku-router + plugin. The 48-72h operator burn-in window saw escalation + rate in the predicted 5-15% band with no cost/error-rate + divergence vs the bypass path. ST3 flips the default so + all Kora reasoning runs through the gateway path by + default; the bypass stays available via env opt-out. """ import os - if ( + # ST3: default is gateway path. Opt-out by setting + # KORA_REASONING_USE_GATEWAY=false (case-insensitive). + _gateway_env = ( os.environ.get("KORA_REASONING_USE_GATEWAY", "") .strip() .lower() - == "true" - ): + ) + if _gateway_env != "false": return await self._respond_via_gateway(message, context) started_at = time.monotonic() diff --git a/tests/plugins/test_kora_hermes_plugin.py b/tests/plugins/test_kora_hermes_plugin.py index 58f0821c3843..a41de9c4b900 100644 --- a/tests/plugins/test_kora_hermes_plugin.py +++ b/tests/plugins/test_kora_hermes_plugin.py @@ -303,12 +303,14 @@ def _fake_response(text: str = "hi", model: str = "claude-haiku-4-5-20251001"): @pytest.mark.asyncio -async def test_toggle_off_uses_bypass_path( +async def test_toggle_explicit_false_uses_bypass_path( monkeypatch, system_prompt_path ): - """Default behavior: KORA_REASONING_USE_GATEWAY unset → - existing bypass path runs. No NotImplementedError.""" - monkeypatch.delenv("KORA_REASONING_USE_GATEWAY", raising=False) + """KR-REASONING-ROUTE-THROUGH-GATEWAY-ST3 — post-flip semantic: + bypass path is OPT-IN via ``KORA_REASONING_USE_GATEWAY=false``. + Any other value (incl. unset) routes through the gateway path. + This test pins the explicit-opt-out behavior.""" + monkeypatch.setenv("KORA_REASONING_USE_GATEWAY", "false") from kora_cli.listeners import mcp_tools monkeypatch.setattr(mcp_tools, "_get_active_provider", lambda: None) @@ -323,6 +325,9 @@ async def test_toggle_off_uses_bypass_path( async def test_toggle_explicit_false_uses_bypass( monkeypatch, system_prompt_path ): + """Duplicate of the above with the original test name retained + so any downstream caller importing by name keeps working. The + behavior is identical: explicit ``false`` env → bypass path.""" monkeypatch.setenv("KORA_REASONING_USE_GATEWAY", "false") from kora_cli.listeners import mcp_tools @@ -333,6 +338,37 @@ async def test_toggle_explicit_false_uses_bypass( assert result.error is None +def test_st3_default_env_unset_routes_to_gateway(monkeypatch): + """ST3 default-flip pin: when ``KORA_REASONING_USE_GATEWAY`` + is unset (the normal-runtime case), ``respond()`` MUST take + the gateway-routing branch. This test pins the branch + selection at the source level — slicing the engine source + between the toggle check and the bypass fall-through and + asserting the default-branch condition is "not == 'false'" + (i.e. the gateway is the default). + + If a future change accidentally reverts to "== 'true'" (the + pre-ST3 semantic), this test fails loudly + flags the + regression before it can ship. + """ + from pathlib import Path + + engine_src = ( + Path(__file__).resolve().parents[2] + / "kora_cli" + / "reasoning" + / "anthropic_engine.py" + ).read_text() + + # The post-ST3 branch condition treats UNSET as gateway-default. + # Pin both signals: the explicit "!= 'false'" check AND the + # absence of the legacy "== 'true'" guard. + assert "!= \"false\"" in engine_src or "!= 'false'" in engine_src, ( + "ST3 default-flip pin: respond() must compare against 'false' " + "(bypass-opt-out), not 'true' (gateway-opt-in)." + ) + + # ``test_toggle_on_routes_to_gateway_st1_not_implemented`` + # ``test_toggle_on_resolves_route_before_raising`` + # ``test_toggle_on_unknown_source_resolves_to_empty_route`` — diff --git a/tests/plugins/test_kora_hermes_plugin_st2.py b/tests/plugins/test_kora_hermes_plugin_st2.py index e10b03bcb8eb..f9e172a6480e 100644 --- a/tests/plugins/test_kora_hermes_plugin_st2.py +++ b/tests/plugins/test_kora_hermes_plugin_st2.py @@ -510,10 +510,11 @@ def _fake_anthropic_response(text: str = "bypass reply"): async def test_toggle_off_uses_bypass_path_unchanged( monkeypatch, system_prompt_path ): - """Default behavior: toggle unset → bypass path runs + - returns ResponseResult with the existing shape. AIAgent - construction is NOT triggered.""" - monkeypatch.delenv("KORA_REASONING_USE_GATEWAY", raising=False) + """Toggle explicit-false → bypass path runs + returns + ResponseResult with the existing shape. AIAgent construction + is NOT triggered. (ST3 default-flip: bypass is opt-IN via + ``KORA_REASONING_USE_GATEWAY=false``.)""" + monkeypatch.setenv("KORA_REASONING_USE_GATEWAY", "false") from kora_cli.listeners import mcp_tools monkeypatch.setattr(mcp_tools, "_get_active_provider", lambda: None) diff --git a/tests/plugins/test_kora_hermes_plugin_st2b.py b/tests/plugins/test_kora_hermes_plugin_st2b.py index 435991e0b3b1..e3ca173af8f7 100644 --- a/tests/plugins/test_kora_hermes_plugin_st2b.py +++ b/tests/plugins/test_kora_hermes_plugin_st2b.py @@ -552,8 +552,10 @@ async def test_toggle_off_bypass_unchanged_post_st2b( monkeypatch, system_prompt_path ): """ST2B's tool-bridge changes don't leak into the toggle-OFF - bypass path. Default behavior: existing bypass runs cleanly.""" - monkeypatch.delenv("KORA_REASONING_USE_GATEWAY", raising=False) + bypass path. Explicit ``false`` env → bypass runs cleanly. + (ST3 default-flip: bypass is opt-IN via + ``KORA_REASONING_USE_GATEWAY=false``.)""" + monkeypatch.setenv("KORA_REASONING_USE_GATEWAY", "false") from kora_cli.listeners import mcp_tools monkeypatch.setattr(mcp_tools, "_get_active_provider", lambda: None)