From b9d9fa7df81be93e84980d093b6b22d46607216c Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 30 Apr 2026 12:24:53 -0500 Subject: [PATCH] fix(tui): respect max turns config Co-authored-by: YuShu <24110240104@m.fudan.edu.cn> --- tests/test_tui_gateway_server.py | 131 +++++++++++++++++++++++++++++++ tui_gateway/server.py | 13 ++- 2 files changed, 141 insertions(+), 3 deletions(-) diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index b9d7c1b0dc..d57a6cd88c 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -3911,3 +3911,134 @@ def test_reload_env_rpc_surfaces_errors(monkeypatch): assert "error" in resp assert "env path locked" in resp["error"]["message"] + + +# ── max_iterations config reading ───────────────────────────────────── + + +def _setup_make_agent_mocks(monkeypatch, cfg): + monkeypatch.setattr(server, "_load_cfg", lambda: cfg) + monkeypatch.setattr(server, "_resolve_startup_runtime", lambda: ("test-model", None)) + monkeypatch.setattr( + "hermes_cli.runtime_provider.resolve_runtime_provider", + lambda requested=None, target_model=None: { + "provider": None, + "base_url": None, + "api_key": None, + "api_mode": None, + "command": None, + "args": None, + "credential_pool": None, + }, + ) + monkeypatch.setattr(server, "_load_tool_progress_mode", lambda: "off") + monkeypatch.setattr(server, "_load_reasoning_config", lambda: None) + monkeypatch.setattr(server, "_load_service_tier", lambda: None) + monkeypatch.setattr(server, "_load_enabled_toolsets", lambda: None) + monkeypatch.setattr(server, "_get_db", lambda: None) + monkeypatch.setattr(server, "_agent_cbs", lambda sid: {}) + + +def test_make_agent_reads_nested_max_turns(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {"agent": {"max_turns": 200}}) + + with patch("run_agent.AIAgent") as mock_agent: + server._make_agent("sid1", "key1") + + assert mock_agent.call_args.kwargs["max_iterations"] == 200 + + +def test_make_agent_nested_max_turns_takes_priority(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {"agent": {"max_turns": 500}, "max_turns": 100}) + + with patch("run_agent.AIAgent") as mock_agent: + server._make_agent("sid1", "key1") + + assert mock_agent.call_args.kwargs["max_iterations"] == 500 + + +def test_make_agent_defaults_to_90(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {}) + + with patch("run_agent.AIAgent") as mock_agent: + server._make_agent("sid1", "key1") + + assert mock_agent.call_args.kwargs["max_iterations"] == 90 + + +def test_make_agent_handles_null_agent_config(monkeypatch): + _setup_make_agent_mocks(monkeypatch, {"agent": None, "max_turns": 80}) + + with patch("run_agent.AIAgent") as mock_agent: + server._make_agent("sid1", "key1") + + assert mock_agent.call_args.kwargs["max_iterations"] == 80 + + +class _FakeAgentForBackground: + base_url = None + api_key = None + provider = None + api_mode = None + acp_command = None + acp_args = None + model = "test-model" + enabled_toolsets = None + ephemeral_system_prompt = None + providers_allowed = None + providers_ignored = None + providers_order = None + provider_sort = None + provider_require_parameters = False + provider_data_collection = None + reasoning_config = None + service_tier = None + request_overrides = {} + _fallback_model = None + + +def test_background_agent_kwargs_reads_nested_max_turns(monkeypatch): + monkeypatch.setattr(server, "_load_cfg", lambda: {"agent": {"max_turns": 300}}) + + kwargs = server._background_agent_kwargs(_FakeAgentForBackground(), "task_1") + + assert kwargs["max_iterations"] == 300 + + +def test_background_agent_kwargs_falls_back_to_root_max_turns(monkeypatch): + monkeypatch.setattr(server, "_load_cfg", lambda: {"max_turns": 50}) + + kwargs = server._background_agent_kwargs(_FakeAgentForBackground(), "task_1") + + assert kwargs["max_iterations"] == 50 + + +def test_background_agent_kwargs_defaults_to_25(monkeypatch): + monkeypatch.setattr(server, "_load_cfg", lambda: {}) + + kwargs = server._background_agent_kwargs(_FakeAgentForBackground(), "task_1") + + assert kwargs["max_iterations"] == 25 + + +def test_background_agent_kwargs_handles_null_agent_config(monkeypatch): + monkeypatch.setattr(server, "_load_cfg", lambda: {"agent": None, "max_turns": 40}) + + kwargs = server._background_agent_kwargs(_FakeAgentForBackground(), "task_1") + + assert kwargs["max_iterations"] == 40 + + +def test_config_show_displays_nested_max_turns(monkeypatch): + monkeypatch.setattr( + server, + "_load_cfg", + lambda: {"agent": {"max_turns": 120}, "enabled_toolsets": [], "verbose": False}, + ) + monkeypatch.setattr(server, "_resolve_model", lambda: "test-model") + + resp = server.handle_request({"id": "1", "method": "config.show", "params": {}}) + sections = resp["result"]["sections"] + agent_rows = next(section["rows"] for section in sections if section["title"] == "Agent") + + assert ["Max Turns", "120"] in agent_rows diff --git a/tui_gateway/server.py b/tui_gateway/server.py index 4bbf99b7a2..6aa025309b 100644 --- a/tui_gateway/server.py +++ b/tui_gateway/server.py @@ -1670,6 +1670,11 @@ def _apply_personality_to_session( return False, None +def _cfg_max_turns(cfg: dict, default: int) -> int: + agent_cfg = cfg.get("agent") or {} + return int(agent_cfg.get("max_turns") or cfg.get("max_turns") or default) + + def _background_agent_kwargs(agent, task_id: str) -> dict: cfg = _load_cfg() @@ -1681,7 +1686,7 @@ def _background_agent_kwargs(agent, task_id: str) -> dict: "acp_command": getattr(agent, "acp_command", None) or None, "acp_args": getattr(agent, "acp_args", None) or None, "model": getattr(agent, "model", None) or _resolve_model(), - "max_iterations": int(cfg.get("max_turns", 25) or 25), + "max_iterations": _cfg_max_turns(cfg, 25), "enabled_toolsets": getattr(agent, "enabled_toolsets", None) or _load_enabled_toolsets(), "quiet_mode": True, @@ -1737,7 +1742,8 @@ def _make_agent(sid: str, key: str, session_id: str | None = None): from hermes_cli.runtime_provider import resolve_runtime_provider cfg = _load_cfg() - system_prompt = ((cfg.get("agent") or {}).get("system_prompt", "") or "").strip() + agent_cfg = cfg.get("agent") or {} + system_prompt = (agent_cfg.get("system_prompt", "") or "").strip() model, requested_provider = _resolve_startup_runtime() runtime = resolve_runtime_provider( requested=requested_provider, @@ -1745,6 +1751,7 @@ def _make_agent(sid: str, key: str, session_id: str | None = None): ) return AIAgent( model=model, + max_iterations=_cfg_max_turns(cfg, 90), provider=runtime.get("provider"), base_url=runtime.get("base_url"), api_key=runtime.get("api_key"), @@ -5394,7 +5401,7 @@ def _(rid, params: dict) -> dict: { "title": "Agent", "rows": [ - ["Max Turns", str(cfg.get("max_turns", 25))], + ["Max Turns", str(_cfg_max_turns(cfg, 90))], ["Toolsets", ", ".join(cfg.get("enabled_toolsets", [])) or "all"], ["Verbose", str(cfg.get("verbose", False))], ],