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:
Chris Fontes
2026-07-19 21:38:13 +00:00
committed by Teknium
parent d3e124601f
commit 5046282867
5 changed files with 236 additions and 19 deletions
+13 -12
View File
@@ -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