feat: raise default tool-calling iteration limit from 90 to 500

The default max_iterations/agent.max_turns budget was set when long
agentic runs were rare; complex tasks now routinely exceed 90 tool
calls. Raise the default to 500 across every surface that hardcodes
the fallback: AIAgent constructor, DEFAULT_CONFIG, CLI resolution
chain, gateway env bridge, cron scheduler, and TUI gateway. Explicit
user config values are unaffected (deep-merge preserves them; no
_config_version bump needed).

Docs (en + zh-Hans), CLI help text, tips, and pinned tests updated
to match.
This commit is contained in:
teknium1 2026-07-26 12:42:53 -07:00 committed by Teknium
parent 4a9f67acfb
commit 1b081e4891
23 changed files with 48 additions and 48 deletions

View file

@ -325,7 +325,7 @@ class AIAgent:
provider: str = None,
api_mode: str = None, # "chat_completions" | "codex_responses" | ...
model: str = "", # empty → resolved from config/provider later
max_iterations: int = 90, # tool-calling iterations (shared with subagents)
max_iterations: int = 500, # tool-calling iterations (shared with subagents)
enabled_toolsets: list = None,
disabled_toolsets: list = None,
quiet_mode: bool = False,

View file

@ -455,7 +455,7 @@ def init_agent(
command: str = None,
args: list[str] | None = None,
model: str = "",
max_iterations: int = 90, # Default tool-calling iterations (shared with subagents)
max_iterations: int = 500, # Default tool-calling iterations (shared with subagents)
tool_delay: float = 1.0,
enabled_toolsets: List[str] = None,
disabled_toolsets: List[str] = None,
@ -529,7 +529,7 @@ def init_agent(
requested_provider (str): Original provider identity before runtime canonicalization
api_mode (str): API mode override: "chat_completions" or "codex_responses"
model (str): Model name to use (default: "anthropic/claude-opus-4.6")
max_iterations (int): Maximum number of tool calling iterations (default: 90)
max_iterations (int): Maximum number of tool calling iterations (default: 500)
tool_delay (float): Delay between tool calls in seconds (default: 1.0)
enabled_toolsets (List[str]): Only enable tools from these toolsets (optional)
disabled_toolsets (List[str]): Disable tools from these toolsets (optional)

View file

@ -2,7 +2,7 @@
Extracted from ``run_agent.py``. Each ``AIAgent`` instance (parent or
subagent) holds an :class:`IterationBudget`; the parent's cap comes from
``max_iterations`` (default 90), each subagent's cap comes from
``max_iterations`` (default 500), each subagent's cap comes from
``delegation.max_iterations`` (default 50).
``run_agent`` re-exports ``IterationBudget`` so existing
@ -18,7 +18,7 @@ class IterationBudget:
"""Thread-safe iteration counter for an agent.
Each agent (parent or subagent) gets its own ``IterationBudget``.
The parent's budget is capped at ``max_iterations`` (default 90).
The parent's budget is capped at ``max_iterations`` (default 500).
Each subagent gets an independent budget capped at
``delegation.max_iterations`` (default 50) this means total
iterations across parent + subagents can exceed the parent's cap.

12
cli.py
View file

@ -471,7 +471,7 @@ def load_cli_config() -> Dict[str, Any]:
"min_tail_user_messages": 1, # Real user messages guaranteed in the tail (1 = existing single anchor)
},
"agent": {
"max_turns": 90, # Default max tool-calling iterations (shared with subagents)
"max_turns": 500, # Default max tool-calling iterations (shared with subagents)
"verbose": False,
"system_prompt": "",
"prefill_messages_file": "",
@ -4099,7 +4099,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
provider: Inference provider ("auto", "openrouter", "nous", "openai-codex", "zai", "kimi-coding", "minimax", "minimax-cn")
api_key: API key (default: from environment)
base_url: API base URL (default: OpenRouter)
max_turns: Maximum tool-calling iterations shared with subagents (default: 90)
max_turns: Maximum tool-calling iterations shared with subagents (default: 500)
verbose: Enable verbose logging
compact: Use compact display mode
resume: Session ID to resume (restores conversation history from SQLite)
@ -4272,9 +4272,9 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
try:
self.max_turns = int(os.getenv("HERMES_MAX_ITERATIONS", ""))
except (TypeError, ValueError):
self.max_turns = 90
self.max_turns = 500
else:
self.max_turns = 90
self.max_turns = 500
# Parse and validate toolsets
self.enabled_toolsets = toolsets
@ -13128,8 +13128,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# Notify when iteration budget was hit
if result and not result.get("completed") and not result.get("interrupted"):
_api_calls = result.get("api_calls", 0)
if _api_calls >= getattr(self.agent, "max_iterations", 90):
_max_iter = getattr(self.agent, "max_iterations", 90)
if _api_calls >= getattr(self.agent, "max_iterations", 500):
_max_iter = getattr(self.agent, "max_iterations", 500)
_cprint(
f"\n{_DIM}⚠ Iteration budget reached "
f"({_api_calls}/{_max_iter}) — "

View file

@ -3254,7 +3254,7 @@ def run_job(
prefill_messages = None
# Max iterations
max_iterations = _cfg.get("agent", {}).get("max_turns") or _cfg.get("max_turns") or 90
max_iterations = _cfg.get("agent", {}).get("max_turns") or _cfg.get("max_turns") or 500
# Provider routing
pr = _cfg.get("provider_routing") or {}

View file

@ -1667,9 +1667,9 @@ def _current_max_iterations() -> int:
"""Return the current per-turn iteration budget after runtime env refresh."""
_reload_runtime_env_preserving_config_authority()
try:
return int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
return int(os.getenv("HERMES_MAX_ITERATIONS", "500"))
except (TypeError, ValueError):
return 90
return 500
from contextlib import contextmanager as _contextmanager
@ -7877,10 +7877,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
# config.yaml → env bridge did the right thing at a glance (instead
# of silently running at a stale .env value for weeks).
try:
_effective_max_iter = int(os.getenv("HERMES_MAX_ITERATIONS", "90"))
_effective_max_iter = int(os.getenv("HERMES_MAX_ITERATIONS", "500"))
logger.info(
"Agent budget: max_iterations=%d (agent.max_turns from config.yaml, "
"or HERMES_MAX_ITERATIONS from .env, or default 90)",
"or HERMES_MAX_ITERATIONS from .env, or default 500)",
_effective_max_iter,
)
except Exception:

View file

@ -382,7 +382,7 @@ def build_top_level_parser():
type=int,
default=None,
metavar="N",
help="Maximum tool-calling iterations per conversation turn (default: 90, or agent.max_turns in config)",
help="Maximum tool-calling iterations per conversation turn (default: 500, or agent.max_turns in config)",
)
_inherited_flag(
chat_parser,

View file

@ -945,7 +945,7 @@ DEFAULT_CONFIG = {
# pressure. Reopening one re-resumes it from disk. 0/null disables.
"max_live_sessions": 16,
"agent": {
"max_turns": 90,
"max_turns": 500,
# Inactivity timeout for gateway agent execution (seconds).
# The agent can run indefinitely as long as it's actively calling
# tools or receiving API responses. Only fires when the agent has

View file

@ -69,7 +69,7 @@ TIPS = [
"hermes chat -t web,terminal enables only specific toolsets for a focused session.",
"hermes chat -s github-pr-workflow preloads a skill at launch.",
"hermes chat -q \"query\" runs a single non-interactive query and exits.",
"hermes chat --max-turns 200 overrides the default 90-iteration limit per turn.",
"hermes chat --max-turns 1000 overrides the default 500-iteration limit per turn.",
"hermes chat --checkpoints enables filesystem snapshots before every destructive file change.",
"hermes --yolo bypasses all dangerous command approval prompts for the entire session.",
"hermes chat --source telegram tags the session for filtering in hermes sessions list.",
@ -112,7 +112,7 @@ TIPS = [
"Set display.busy_input_mode: queue to queue messages instead of interrupting the agent, or steer to inject them mid-run via /steer.",
"Set display.resume_display: minimal to skip the full conversation recap on session resume.",
"Set compression.threshold: 0.50 to control when auto-compression fires (default: 50% of context).",
"Set agent.max_turns: 200 to let the agent take more tool-calling steps per turn.",
"Set agent.max_turns: 1000 to let the agent take more tool-calling steps per turn.",
"Set file_read_max_chars: 200000 to increase the max content per read_file call.",
"Set approvals.mode: smart to let an LLM auto-approve safe commands and auto-deny dangerous ones.",
"Set fallback_model in config.yaml to automatically fail over to a backup provider.",

View file

@ -437,7 +437,7 @@ class AIAgent:
command: str = None,
args: list[str] | None = None,
model: str = "",
max_iterations: int = 90, # Default tool-calling iterations (shared with subagents)
max_iterations: int = 500, # Default tool-calling iterations (shared with subagents)
tool_delay: float = 1.0,
enabled_toolsets: List[str] = None,
disabled_toolsets: List[str] = None,

View file

@ -60,7 +60,7 @@ class TestMaxTurnsResolution:
def test_default_max_turns_is_integer(self):
cli = _make_cli()
assert isinstance(cli.max_turns, int)
assert cli.max_turns == 90
assert cli.max_turns == 500
def test_explicit_max_turns_honored(self):
cli = _make_cli(max_turns=25)
@ -69,7 +69,7 @@ class TestMaxTurnsResolution:
def test_none_max_turns_gets_default(self):
cli = _make_cli(max_turns=None)
assert isinstance(cli.max_turns, int)
assert cli.max_turns == 90
assert cli.max_turns == 500
def test_env_var_max_turns(self):
"""Env var is used when config file doesn't set max_turns."""
@ -79,7 +79,7 @@ class TestMaxTurnsResolution:
def test_invalid_env_var_max_turns_falls_back_to_default(self):
"""Invalid env values should not crash CLI init."""
cli_obj = _make_cli(env_overrides={"HERMES_MAX_ITERATIONS": "not-a-number"})
assert cli_obj.max_turns == 90
assert cli_obj.max_turns == 500
def test_legacy_root_max_turns_is_used_when_agent_key_exists_without_value(self):
cli_obj = _make_cli(config_overrides={"agent": {}, "max_turns": 77})
@ -88,7 +88,7 @@ class TestMaxTurnsResolution:
def test_max_turns_never_none_for_agent(self):
"""The value passed to AIAgent must never be None (causes TypeError in run_conversation)."""
cli = _make_cli()
assert isinstance(cli.max_turns, int) and cli.max_turns == 90
assert isinstance(cli.max_turns, int) and cli.max_turns == 500
class TestVerboseAndToolProgress:

View file

@ -11656,22 +11656,22 @@ def test_make_agent_waits_for_shared_mcp_discovery(monkeypatch):
def test_make_agent_nested_max_turns_takes_priority(monkeypatch):
_setup_make_agent_mocks(
monkeypatch, {"agent": {"max_turns": 500}, "max_turns": 100}
monkeypatch, {"agent": {"max_turns": 400}, "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
assert mock_agent.call_args.kwargs["max_iterations"] == 400
def test_make_agent_defaults_to_90(monkeypatch):
def test_make_agent_defaults_to_500(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
assert mock_agent.call_args.kwargs["max_iterations"] == 500
def test_make_agent_uses_session_runtime_overrides(monkeypatch):

View file

@ -5751,7 +5751,7 @@ def _make_agent(
_pr = _load_provider_routing()
return AIAgent(
model=model,
max_iterations=_cfg_max_turns(cfg, 90),
max_iterations=_cfg_max_turns(cfg, 500),
provider=runtime.get("provider"),
base_url=runtime.get("base_url"),
api_key=runtime.get("api_key"),
@ -17560,7 +17560,7 @@ def _(rid, params: dict) -> dict:
{
"title": "Agent",
"rows": [
["Max Turns", str(_cfg_max_turns(cfg, 90))],
["Max Turns", str(_cfg_max_turns(cfg, 500))],
["Toolsets", ", ".join(cfg.get("enabled_toolsets", [])) or "all"],
["Verbose", str(cfg.get("verbose", False))],
],

View file

@ -181,7 +181,7 @@ These tools modify agent state directly and return synthetic tool results withou
The agent tracks iterations via `IterationBudget`:
- Default: 90 iterations (configurable via `agent.max_turns`)
- Default: 500 iterations (configurable via `agent.max_turns`)
- Each agent gets its own budget. Subagents get independent budgets capped at `delegation.max_iterations` (default 50) — total iterations across parent + subagents can exceed the parent's cap
- At 100%, the agent stops and returns a summary of work done

View file

@ -309,7 +309,7 @@ print(review)
| `disabled_toolsets` | `List[str]` | `None` | Blacklist specific toolsets |
| `save_trajectories` | `bool` | `False` | Save conversations to JSONL |
| `ephemeral_system_prompt` | `str` | `None` | Custom system prompt (not saved to trajectories) |
| `max_iterations` | `int` | `90` | Max tool-calling iterations per conversation |
| `max_iterations` | `int` | `500` | Max tool-calling iterations per conversation |
| `skip_context_files` | `bool` | `False` | Skip loading AGENTS.md files |
| `skip_memory` | `bool` | `False` | Disable persistent memory read/write |
| `api_key` | `str` | `None` | API key (falls back to env vars) |
@ -329,5 +329,5 @@ print(review)
:::warning
- **Thread safety**: Create one `AIAgent` per thread or task. Never share an instance across concurrent calls.
- **Resource cleanup**: The agent automatically cleans up resources (terminal sessions, browser instances) when a conversation ends. If you're running in a long-lived process, ensure each conversation completes normally.
- **Iteration limits**: The default `max_iterations=90` is generous. For simple Q&A use cases, consider lowering it (e.g., `max_iterations=10`) to prevent runaway tool-calling loops and control costs.
- **Iteration limits**: The default `max_iterations=500` is generous. For simple Q&A use cases, consider lowering it (e.g., `max_iterations=10`) to prevent runaway tool-calling loops and control costs.
:::

View file

@ -120,7 +120,7 @@ Common options:
| `--ignore-rules` | Skip auto-injection of `AGENTS.md`, `SOUL.md`, `.cursorrules`, persistent memory, and preloaded skills. Combine with `--ignore-user-config` for a fully isolated run. |
| `--safe-mode` | Troubleshooting mode: disable ALL customizations — user config, rules/memory injection, plugins, shell hooks, and MCP servers (implies `--ignore-user-config` and `--ignore-rules`). Use to isolate whether a problem comes from your setup or from Hermes itself. |
| `--source <tag>` | Session source tag for filtering (default: `cli`). Use `tool` for third-party integrations that should not appear in user session lists. |
| `--max-turns <N>` | Maximum tool-calling iterations per conversation turn (default: 90, or `agent.max_turns` in config). |
| `--max-turns <N>` | Maximum tool-calling iterations per conversation turn (default: 500, or `agent.max_turns` in config). |
Examples:

View file

@ -733,7 +733,7 @@ Advanced per-platform knobs for throttling the outbound message batcher. Most us
| Variable | Description |
|----------|-------------|
| `HERMES_MAX_ITERATIONS` | Max tool-calling iterations per conversation (default: 90) |
| `HERMES_MAX_ITERATIONS` | Max tool-calling iterations per conversation (default: 500) |
| `HERMES_INFERENCE_MODEL` | Override model name at process level (takes priority over `config.yaml` for the session). Also settable via `-m`/`--model` flag. |
| `HERMES_YOLO_MODE` | Set to `1` to bypass dangerous-command approval prompts. Equivalent to `--yolo`. |
| `HERMES_ACCEPT_HOOKS` | Auto-approve any unseen shell hooks declared in `config.yaml` without a TTY prompt. Equivalent to `--accept-hooks` or `hooks_auto_accept: true`. |

View file

@ -854,13 +854,13 @@ See [Memory Providers](/user-guide/features/memory-providers) for the analogous
## Iteration Budget
When the agent is working on a complex task with many tool calls, it can burn through its iteration budget (default: 90 turns). Hermes does **not** inject mid-task pressure warnings — earlier builds warned the model at 70%/90% budget, which caused models to abandon complex tasks prematurely and was removed in April 2026.
When the agent is working on a complex task with many tool calls, it can burn through its iteration budget (default: 500 turns). Hermes does **not** inject mid-task pressure warnings — earlier builds warned the model at 70%/90% budget, which caused models to abandon complex tasks prematurely and was removed in April 2026.
Instead, when the budget is actually exhausted (90/90), Hermes injects one message asking the model to wrap up and allows a single **grace call** so it can deliver a final response. If that grace call still doesn't produce text, the agent is asked to summarise what it accomplished.
Instead, when the budget is actually exhausted (500/500), Hermes injects one message asking the model to wrap up and allows a single **grace call** so it can deliver a final response. If that grace call still doesn't produce text, the agent is asked to summarise what it accomplished.
```yaml
agent:
max_turns: 90 # Max iterations per conversation turn (default: 90)
max_turns: 500 # Max iterations per conversation turn (default: 500)
api_max_retries: 3 # Retries per provider before fallback engages (default: 3)
```

View file

@ -181,7 +181,7 @@ for each tool_call in response.tool_calls:
agent 通过 `IterationBudget` 追踪迭代次数:
- 默认:90 次迭代(可通过 `agent.max_turns` 配置)
- 默认:500 次迭代(可通过 `agent.max_turns` 配置)
- 每个 agent 拥有独立预算。子 agent 获得独立预算,上限为 `delegation.max_iterations`(默认 50——父 agent 与子 agent 的总迭代次数可超过父 agent 的上限
- 达到 100% 时agent 停止并返回已完成工作的摘要

View file

@ -309,7 +309,7 @@ print(review)
| `disabled_toolsets` | `List[str]` | `None` | 黑名单指定工具集 |
| `save_trajectories` | `bool` | `False` | 将对话保存为 JSONL |
| `ephemeral_system_prompt` | `str` | `None` | 自定义系统 prompt不保存到轨迹文件 |
| `max_iterations` | `int` | `90` | 每次对话的最大工具调用迭代次数 |
| `max_iterations` | `int` | `500` | 每次对话的最大工具调用迭代次数 |
| `skip_context_files` | `bool` | `False` | 跳过加载 AGENTS.md 文件 |
| `skip_memory` | `bool` | `False` | 禁用持久化内存的读写 |
| `api_key` | `str` | `None` | API 密钥(回退到环境变量) |
@ -329,5 +329,5 @@ print(review)
:::warning
- **线程安全**:每个线程或任务创建一个 `AIAgent` 实例。切勿在并发调用中共享同一实例。
- **资源清理**agent 在对话结束时会自动清理资源(终端会话、浏览器实例)。若在长期运行的进程中使用,请确保每次对话正常结束。
- **迭代限制**:默认的 `max_iterations=90` 较为宽松。对于简单的问答场景,建议适当降低该值(如 `max_iterations=10`),以防止工具调用循环失控并控制成本。
- **迭代限制**:默认的 `max_iterations=500` 较为宽松。对于简单的问答场景,建议适当降低该值(如 `max_iterations=10`),以防止工具调用循环失控并控制成本。
:::

View file

@ -108,7 +108,7 @@ hermes chat [options]
| `--ignore-user-config` | 忽略 `~/.hermes/config.yaml`,使用内置默认值。`.env` 中的凭据仍会加载。适用于隔离的 CI 运行、可复现的 bug 报告和第三方集成。 |
| `--ignore-rules` | 跳过 `AGENTS.md``SOUL.md``.cursorrules`、持久 memory 和预加载 skill 的自动注入。与 `--ignore-user-config` 组合可实现完全隔离的运行。 |
| `--source <tag>` | 用于过滤的会话来源标签(默认:`cli`)。对于不应出现在用户会话列表中的第三方集成,使用 `tool`。 |
| `--max-turns <N>` | 每个对话轮次的最大工具调用迭代次数(默认:90或 config 中的 `agent.max_turns`)。 |
| `--max-turns <N>` | 每个对话轮次的最大工具调用迭代次数(默认:500或 config 中的 `agent.max_turns`)。 |
示例:

View file

@ -526,7 +526,7 @@ Graph 事件Teams 会议、日历、聊天等)的入站变更通知监听
| 变量 | 描述 |
|----------|-------------|
| `HERMES_MAX_ITERATIONS` | 每次对话的最大工具调用迭代次数(默认:90 |
| `HERMES_MAX_ITERATIONS` | 每次对话的最大工具调用迭代次数(默认:500 |
| `HERMES_INFERENCE_MODEL` | 在进程级别覆盖模型名称(优先于本次会话的 `config.yaml`)。也可通过 `-m`/`--model` 标志设置。 |
| `HERMES_YOLO_MODE` | 设为 `1` 可绕过危险命令审批提示。等同于 `--yolo`。 |
| `HERMES_ACCEPT_HOOKS` | 无需 TTY 提示自动批准 `config.yaml` 中声明的任何未见过的 shell hook。等同于 `--accept-hooks``hooks_auto_accept: true`。 |

View file

@ -637,17 +637,17 @@ context:
## 迭代预算
当 agent 在处理具有许多工具调用的复杂任务时,它可能会耗尽其迭代预算(默认:90 轮。Hermes **不会**在任务中途注入压力警告 —— 早期版本会在预算达到 70%/90% 时警告模型,这会导致模型过早放弃复杂任务,该机制已于 2026 年 4 月移除。
当 agent 在处理具有许多工具调用的复杂任务时,它可能会耗尽其迭代预算(默认:500 轮。Hermes **不会**在任务中途注入压力警告 —— 早期版本会在预算达到 70%/90% 时警告模型,这会导致模型过早放弃复杂任务,该机制已于 2026 年 4 月移除。
取而代之的是,当预算真正耗尽(90/90Hermes 注入一条消息要求模型收尾,并允许一次**宽限调用**以便其给出最终响应。如果该宽限调用仍未产生文本,则会要求 agent 总结已完成的工作。
取而代之的是,当预算真正耗尽(500/500Hermes 注入一条消息要求模型收尾,并允许一次**宽限调用**以便其给出最终响应。如果该宽限调用仍未产生文本,则会要求 agent 总结已完成的工作。
```yaml
agent:
max_turns: 90 # 每次对话轮次的最大迭代次数默认90
max_turns: 500 # 每次对话轮次的最大迭代次数默认500
api_max_retries: 3 # 回退启动前每个 provider 的重试次数默认3
```
当迭代预算完全耗尽时CLI 向用户显示通知:`⚠ Iteration budget reached (90/90) — response may be incomplete`。
当迭代预算完全耗尽时CLI 向用户显示通知:`⚠ Iteration budget reached (500/500) — response may be incomplete`。
`agent.api_max_retries` 控制 Hermes 在回退 provider 切换启动**之前**对瞬时错误速率限制、连接断开、5xx重试 provider API 调用的次数。默认为 `3` —— 总共四次尝试。如果您配置了[回退 providers](/user-guide/features/fallback-providers) 并希望更快地故障转移,请将其降至 `0`,这样主 provider 上的第一个瞬时错误会立即切换到回退,而不是对不稳定的端点进行重试。