fix: honor configured max_turns in WebUI agents

Read agent.max_turns when constructing streaming WebUI AIAgent instances, pass it as max_iterations when supported, and include it in the per-session agent cache signature so budget changes take effect.

Add regression coverage for the config read, constructor kwarg, and cache key.
This commit is contained in:
Michael Lam
2026-05-08 01:04:27 -07:00
committed by nesquena-hermes
parent 773857d159
commit 01b9c82dc9
2 changed files with 56 additions and 0 deletions
+26
View File
@@ -2296,6 +2296,29 @@ def _run_agent_streaming(
import inspect as _inspect
_agent_params = set(_inspect.signature(_AIAgent.__init__).parameters)
# CLI-parity max-iteration budget: read config.yaml's
# agent.max_turns and pass it to AIAgent when supported. Without
# this WebUI-created agents silently use AIAgent's constructor
# default (90), so long browser-originated tasks hit the
# "maximum number of tool-calling iterations" summary path even
# after the operator raises Hermes' global turn budget.
_max_iterations_cfg = None
try:
_raw_max_iterations = None
_agent_cfg_for_iterations = _cfg.get('agent', {}) if isinstance(_cfg, dict) else {}
if isinstance(_agent_cfg_for_iterations, dict):
_raw_max_iterations = _agent_cfg_for_iterations.get('max_turns')
if _raw_max_iterations is None and isinstance(_cfg, dict):
# Back-compat for older Hermes config files that used a
# root-level max_turns key.
_raw_max_iterations = _cfg.get('max_turns')
if _raw_max_iterations is not None:
_parsed_max_iterations = int(_raw_max_iterations)
if _parsed_max_iterations > 0:
_max_iterations_cfg = _parsed_max_iterations
except Exception:
_max_iterations_cfg = None
# CLI-parity max output cap: read config.yaml's max_tokens and pass
# it to AIAgent when supported. Without this WebUI-created agents use
# provider-native output ceilings (e.g. Claude via OpenRouter can
@@ -2355,6 +2378,8 @@ def _run_agent_streaming(
_agent_kwargs['reasoning_config'] = _reasoning_config
if 'status_callback' in _agent_params:
_agent_kwargs['status_callback'] = _agent_status_callback
if 'max_iterations' in _agent_params and _max_iterations_cfg is not None:
_agent_kwargs['max_iterations'] = _max_iterations_cfg
if 'max_tokens' in _agent_params and _max_tokens_cfg is not None:
_agent_kwargs['max_tokens'] = _max_tokens_cfg
# Params added in newer hermes-agent — skip if not supported
@@ -2388,6 +2413,7 @@ def _run_agent_streaming(
_hashlib.sha256((resolved_api_key or '').encode()).hexdigest()[:16],
resolved_base_url or '',
resolved_provider or '',
_max_iterations_cfg or '',
_max_tokens_cfg or '',
_fallback_resolved or {},
sorted(_toolsets) if _toolsets else [],
+30
View File
@@ -0,0 +1,30 @@
"""Regression checks for WebUI AIAgent iteration-budget parity.
WebUI streaming agents must honor Hermes' configured agent.max_turns. Otherwise
browser-originated long-running tasks silently fall back to AIAgent's constructor
default and hit the "maximum number of tool-calling iterations" summary path even
when the operator raised the global Hermes budget.
"""
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
STREAMING_PY = (REPO / "api" / "streaming.py").read_text(encoding="utf-8")
def test_streaming_agent_reads_agent_max_turns_from_config():
assert "_agent_cfg_for_iterations" in STREAMING_PY
assert "_agent_cfg_for_iterations.get('max_turns')" in STREAMING_PY
assert "_cfg.get('max_turns')" in STREAMING_PY
def test_streaming_agent_passes_max_iterations_to_aiagent():
assert "if 'max_iterations' in _agent_params and _max_iterations_cfg is not None:" in STREAMING_PY
assert "_agent_kwargs['max_iterations'] = _max_iterations_cfg" in STREAMING_PY
def test_streaming_agent_cache_signature_includes_max_iterations():
sig_start = STREAMING_PY.index("_sig_blob = _json.dumps")
sig_block = STREAMING_PY[sig_start:STREAMING_PY.index("], sort_keys=True)", sig_start)]
assert "_max_iterations_cfg or ''" in sig_block