Skip to content

fix(delegation): add per-turn spawn cap to prevent runaway delegate_task loops - #56447

Open
Slobaka wants to merge 1 commit into
NousResearch:mainfrom
Slobaka:fix/delegate-task-per-turn-spawn-limit-v2
Open

Slobaka wants to merge 1 commit into
NousResearch:mainfrom
Slobaka:fix/delegate-task-per-turn-spawn-limit-v2

Conversation

@Slobaka

@Slobaka Slobaka commented Jul 1, 2026

Copy link
Copy Markdown
Contributor

Problem

delegate_task has max_concurrent_children (bounds batch size per call) and max_spawn_depth (bounds nesting), but neither limits how many times the parent agent calls delegate_task in a single user turn. A model stuck in a loop can spawn thousands of sequential subagents, exhausting tokens and API budget.

Fix

Add delegation.max_spawns_per_turn config key (default: 10). Track _turn_spawn_count on the parent agent. Each delegate_task call checks current_turn_spawn_count + n_tasks > max_spawns_per_turn and returns a tool_error directing the model to synthesize a response or split work across turns.

Files changed

  • tools/delegate_tool.py_get_max_spawns_per_turn(), check in delegate_task(), _DEFAULT_MAX_SPAWNS_PER_TURN = 10
  • agent/conversation_loop.py — reset _turn_spawn_count at turn start
  • tests/tools/test_delegate_per_turn_spawn_limit.py — full test coverage

Config

delegation:
  max_spawns_per_turn: 10  # default; raise deliberately

Like max_concurrent_children there is no upper ceiling — but a high value multiplies API cost linearly.

Verification

Rebased against main (9be292f, 2026-07-01). Confirmed upstream does not contain max_spawns_per_turn or _turn_spawn_count.

…ask loops

A model can call delegate_task dozens of times in a single user turn,
spawning 44+ sequential subagent sessions over 53 minutes with no
guardrail. max_concurrent_children limits batch size per individual
delegate_task call, and max_spawn_depth prevents nested delegation.
But neither limits how many times the parent calls delegate_task in
a single turn.

Fix: add a per-turn spawn counter (_turn_spawn_count) on the parent
agent, reset to 0 at the start of each user turn, and enforce a cap
via delegation.max_spawns_per_turn (default 10, floor 1). When
exceeded, returns an error directing the model to stop delegating
and synthesize a response from collected results.
@alt-glitch alt-glitch added type/feature New feature or request P2 Medium — degraded but workaround exists comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint tool/delegate Subagent delegation labels Jul 1, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for identifying a real remaining delegation budget gap. Current main caps calls only within each assistant response (agent/conversation_loop.py:4734, run_agent.py:3863-3891) and validates each tasks batch independently (tools/delegate_tool.py:2465-2474), so cumulative same-turn fan-out is still possible.

Problems

  • tests/tools/test_delegate_per_turn_spawn_limit.py:50-82 does not call delegate_task; it only asserts local arithmetic. It therefore does not verify rejection, state mutation, or turn reset.
  • The proposed config surface is absent from hermes_cli/config.py:2242-2301 and the delegation docs at website/docs/user-guide/configuration.md:1989-2011.
  • Existing direct-call tests construct a MagicMock parent without _turn_spawn_count (tests/tools/test_delegate.py:41-61) and invoke delegate_task (:231-243); the new counter path needs that fixture/state handled explicitly.

Suggested changes

  • Add the default, docs, and dynamic schema disclosure for the cap.
  • Add executable multi-call and reset tests through patched delegate_task, and seed the existing parent fixture with an integer counter.

The PR predates the async-concurrency refactor in 6e369a376, so salvaging the helper into current tools/delegate_tool.py needs conflict-aware transplantation rather than a clean cherry-pick.

Automated hermes-sweeper review.

agent = self._make_mock_agent(spawn_count=0)
# We can't fully run delegate_task without heavy mocking, but we can
# verify the cap check logic by testing the condition directly
max_spawns = 10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only reimplements the comparison locally; delegate_task() is never invoked. Please test sequential patched calls through the tool and assert the second call returns its tool error without constructing children, then verify a new conversation turn resets the counter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P2 Medium — degraded but workaround exists sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants