feat(config): resolve_turn_limit — first-class 'none'/'unlimited' for agent.max_turns
Previously agent.max_turns only accepted positive integers. Setting it to
'none', 'unlimited', or 0 — all natural ways to say 'no limit' — either
crashed int() or was silently skipped by `or` checks, falling back to 90.
This adds resolve_turn_limit() in hermes_cli/config.py as the single
normalization point. It accepts:
- int/float → int(raw) (floats truncated)
- numeric string ('120') → int(raw)
- 'none'/'unlimited'/'infinite'/'∞'/'-1'/'0' (case-insensitive,
whitespace-tolerant) → sys.maxsize sentinel
- YAML None/null → default (90)
- bool/list/dict/garbage → default (with debug log)
All config-reading sites (cli.py, gateway/run.py, cron/scheduler.py) now
call this instead of bare int(), so agent.max_turns: none in config.yaml
becomes a first-class supported spelling of 'unlimited'.
The sentinel (sys.maxsize) survives the str()→int() round-trip through
the HERMES_MAX_ITERATIONS env-var bridge in gateway/run.py and works in
every <, >=, remaining = max - used comparison without requiring call
sites to learn about a special value.
Includes 38 tests covering the full spelling table, the str→int env-var
round-trip, and sentinel properties.
This commit is contained in:
@@ -5233,25 +5233,26 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
else:
|
||||
self.api_key = api_key or os.getenv("OPENAI_API_KEY") or os.getenv("OPENROUTER_API_KEY")
|
||||
# Max turns priority: CLI arg > config file > env var > default
|
||||
# All paths go through resolve_turn_limit() so that agent.max_turns
|
||||
# accepts "none"/"unlimited" (→ sys.maxsize) in addition to ints.
|
||||
# See hermes_cli.config.resolve_turn_limit for the full spelling table.
|
||||
from hermes_cli.config import resolve_turn_limit as _resolve_turn_limit
|
||||
if max_turns is not None: # CLI arg was explicitly set
|
||||
self.max_turns = max_turns
|
||||
elif CLI_CONFIG["agent"].get("max_turns"):
|
||||
self.max_turns = CLI_CONFIG["agent"]["max_turns"]
|
||||
elif CLI_CONFIG.get("max_turns"): # Backwards compat: root-level max_turns
|
||||
self.max_turns = _resolve_turn_limit(max_turns)
|
||||
elif CLI_CONFIG["agent"].get("max_turns") is not None:
|
||||
self.max_turns = _resolve_turn_limit(CLI_CONFIG["agent"]["max_turns"])
|
||||
elif CLI_CONFIG.get("max_turns") is not None: # Backwards compat: root-level max_turns
|
||||
# KEEP (evaluated for the v12 support-floor cleanup, July 2026):
|
||||
# no versioned config migration ever rewrote root-level max_turns
|
||||
# to agent.max_turns on disk — only load-time normalization
|
||||
# (_normalize_max_turns_config) folds it, and configs read through
|
||||
# other paths may bypass it. This fallback is therefore the only
|
||||
# safety net for configs that still carry the root key.
|
||||
self.max_turns = CLI_CONFIG["max_turns"]
|
||||
elif os.getenv("HERMES_MAX_ITERATIONS"):
|
||||
try:
|
||||
self.max_turns = int(os.getenv("HERMES_MAX_ITERATIONS", ""))
|
||||
except (TypeError, ValueError):
|
||||
self.max_turns = 500
|
||||
self.max_turns = _resolve_turn_limit(CLI_CONFIG["max_turns"])
|
||||
else:
|
||||
self.max_turns = 500
|
||||
# Env var bridge (set by gateway/run.py from config.yaml, or by the
|
||||
# user directly). Empty/unset → default (unlimited).
|
||||
self.max_turns = _resolve_turn_limit(os.getenv("HERMES_MAX_ITERATIONS"))
|
||||
|
||||
# Wall-clock run budget: CLI flag wins over config; both optional.
|
||||
# None keeps the feature fully off (AIAgent stays dormant).
|
||||
@@ -5259,7 +5260,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
|
||||
self.run_budget_seconds = run_budget
|
||||
else:
|
||||
self.run_budget_seconds = CLI_CONFIG["agent"].get("run_budget_seconds")
|
||||
|
||||
|
||||
# Parse and validate toolsets
|
||||
self.enabled_toolsets = toolsets
|
||||
from agent.skill_utils import parse_config_string_list
|
||||
|
||||
Reference in New Issue
Block a user