Skip to content
Closed
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
34 changes: 31 additions & 3 deletions gateway/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -698,6 +698,34 @@ def _resolve_gateway_model(config: dict | None = None) -> str:
return ""


def _resolve_gateway_max_iterations(config: dict | None = None) -> int:
"""Resolve the agent loop limit for gateway-created agents.

``config.yaml`` is the documented source of truth for ``agent.max_turns``.
``HERMES_MAX_ITERATIONS`` remains supported for legacy setups, but a stale
``~/.hermes/.env`` value must not override the config value after the
long-lived gateway reloads dotenv between turns.
"""
cfg = config if config is not None else _load_gateway_config()
candidates: list[Any] = [
cfg_get(cfg, "agent", "max_turns"),
cfg_get(cfg, "max_turns"),
os.getenv("HERMES_MAX_ITERATIONS"),
]

for raw in candidates:
if raw in (None, ""):
continue
try:
value = int(raw)
except (TypeError, ValueError):
logger.debug("Ignoring invalid max iterations value: %r", raw)
continue
if value > 0:
return value
return 90


def _resolve_hermes_bin() -> Optional[list[str]]:
"""Resolve the Hermes update command as argv parts.

Expand Down Expand Up @@ -7135,7 +7163,7 @@ async def _run_background_task(
enabled_toolsets = sorted(_get_platform_tools(user_config, platform_key))

pr = self._provider_routing
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
max_iterations = _resolve_gateway_max_iterations(user_config)
reasoning_config = self._resolve_session_reasoning_config(source=source)
self._reasoning_config = reasoning_config
self._service_tier = self._load_service_tier()
Expand Down Expand Up @@ -10418,8 +10446,8 @@ def run_sync():
# (concurrency-safe). Keep os.environ as fallback for CLI/cron.
os.environ["HERMES_SESSION_KEY"] = session_key or ""

# Read from env var or use default (same as CLI)
max_iterations = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
# Resolve agent loop limit from config first, env fallback for legacy setups.
max_iterations = _resolve_gateway_max_iterations(user_config)

# Map platform enum to the platform hint key the agent understands.
# Platform.LOCAL ("local") maps to "cli"; others pass through as-is.
Expand Down
27 changes: 27 additions & 0 deletions tests/gateway/test_agent_max_turns_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""Gateway agent loop limit config tests."""

from gateway.run import _resolve_gateway_max_iterations


def test_resolve_gateway_max_iterations_prefers_agent_max_turns_over_env(monkeypatch):
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "200")

assert _resolve_gateway_max_iterations({"agent": {"max_turns": 600}}) == 600


def test_resolve_gateway_max_iterations_supports_legacy_top_level_max_turns(monkeypatch):
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "200")

assert _resolve_gateway_max_iterations({"max_turns": 600}) == 600


def test_resolve_gateway_max_iterations_supports_legacy_env(monkeypatch):
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "200")

assert _resolve_gateway_max_iterations({}) == 200


def test_resolve_gateway_max_iterations_falls_back_when_invalid(monkeypatch):
monkeypatch.setenv("HERMES_MAX_ITERATIONS", "not-a-number")

assert _resolve_gateway_max_iterations({"agent": {"max_turns": ""}}) == 90