From 64e61656862b24776d5d7f5b87699097a16b9ec3 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Thu, 23 Apr 2026 13:56:26 -0700 Subject: [PATCH] fix(delegate): remove model-facing max_iterations override; config is authoritative (#14732) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously delegate_task exposed 'max_iterations' in its JSON schema and used `max_iterations or default_max_iter` — so a model guessing conservatively (or copy-pasting a docstring hint like 'Only set lower for simple tasks') could silently shrink a subagent's budget below the user's configured delegation.max_iterations. One such call this session capped a deep forensic audit at 40 iterations while the user's config was set to 250. Changes: - Drop 'max_iterations' from DELEGATE_TASK_SCHEMA['parameters']['properties']. Models can no longer emit it. - In delegate_task(): ignore any caller-supplied max_iterations, always use delegation.max_iterations from config. Log at debug if a stale schema or internal caller still passes one through. - Keep the Python kwarg on the function signature for internal callers (_build_child_agent tests pass it through the plumbing layer). - Update test_schema_valid to assert the param is now absent (intentional contract change, not a change-detector). --- tests/tools/test_delegate.py | 5 ++++- tools/delegate_tool.py | 20 ++++++++++++-------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/tools/test_delegate.py b/tests/tools/test_delegate.py index 356f72d88..9c93f05c7 100644 --- a/tests/tools/test_delegate.py +++ b/tests/tools/test_delegate.py @@ -69,7 +69,10 @@ class TestDelegateRequirements(unittest.TestCase): self.assertIn("tasks", props) self.assertIn("context", props) self.assertIn("toolsets", props) - self.assertIn("max_iterations", props) + # max_iterations is intentionally NOT exposed to the model — it's + # config-authoritative via delegation.max_iterations so users get + # predictable budgets. + self.assertNotIn("max_iterations", props) self.assertNotIn("maxItems", props["tasks"]) # removed — limit is now runtime-configurable diff --git a/tools/delegate_tool.py b/tools/delegate_tool.py index 7b0179c4c..2c35c7c7e 100644 --- a/tools/delegate_tool.py +++ b/tools/delegate_tool.py @@ -1558,7 +1558,18 @@ def delegate_task( # Load config cfg = _load_config() default_max_iter = cfg.get("max_iterations", DEFAULT_MAX_ITERATIONS) - effective_max_iter = max_iterations or default_max_iter + # Model-supplied max_iterations is ignored — the config value is authoritative + # so users get predictable budgets. The kwarg is retained for internal callers + # and tests; a model-emitted value here would only shrink the budget and + # surprise the user mid-run. Log and drop it if one slips through from a + # cached tool schema or a stale provider. + if max_iterations is not None and max_iterations != default_max_iter: + logger.debug( + "delegate_task: ignoring caller-supplied max_iterations=%s; " + "using delegation.max_iterations=%s from config", + max_iterations, default_max_iter, + ) + effective_max_iter = default_max_iter # Resolve delegation credentials (provider:model pair). # When delegation.provider is configured, this resolves the full credential @@ -2098,13 +2109,6 @@ DELEGATE_TASK_SCHEMA = { "When provided, top-level goal/context/toolsets are ignored." ), }, - "max_iterations": { - "type": "integer", - "description": ( - "Max tool-calling turns per subagent (default: 50). " - "Only set lower for simple tasks." - ), - }, "role": { "type": "string", "enum": ["leaf", "orchestrator"],