fix(delegation): align child prompts with depth-derived roles

This commit is contained in:
funky-xamarin
2026-09-18 16:13:02 +08:00
committed by Teknium
parent c7e165a068
commit a915be8bd7
2 changed files with 83 additions and 4 deletions

View File

@@ -0,0 +1,79 @@
"""Child-facing nesting guidance must agree with depth-derived capability."""
import json
import threading
from unittest.mock import MagicMock, patch
import pytest
from tools import delegate_tool
from tools.registry import registry
@pytest.mark.parametrize("legacy_role", [None, "leaf", "orchestrator"])
@pytest.mark.parametrize(
"parent_depth,max_depth,enabled",
[(1, 5, True), (1, 3, True), (2, 3, True), (1, 5, False)],
)
def test_dispatched_child_prompt_matches_depth_capability(
tmp_path, monkeypatch, legacy_role, parent_depth, max_depth, enabled,
):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
parent = MagicMock()
parent._delegate_depth = parent_depth
parent._active_children = []
parent._active_children_lock = threading.Lock()
parent._session_db = None
parent.enabled_toolsets = ["terminal", "file", "delegation"]
parent.disabled_toolsets = []
parent.request_overrides = {}
parent.provider = "custom"
parent.base_url = "https://fixture.invalid/v1"
parent.api_key = "fixture-only"
parent.api_mode = "chat_completions"
parent.model = "fixture"
parent.platform = "cli"
parent._print_fn = None
parent.tool_progress_callback = None
parent.thinking_callback = None
child = MagicMock()
child._credential_pool = None
child._delegate_saved_tool_names = []
child.session_prompt_tokens = 0
child.session_completion_tokens = 0
child.model = "fixture"
child.run_conversation.return_value = {
"final_response": "done", "completed": True, "api_calls": 1, "messages": [],
}
config = {"max_spawn_depth": max_depth, "orchestrator_enabled": enabled}
args = {"tasks": [{"goal": "Inspect fixture"}], "background": False}
if legacy_role is not None:
args["role"] = legacy_role
with (
patch.object(delegate_tool, "_load_config", return_value=config),
patch.object(delegate_tool, "_resolve_workspace_hint", return_value=str(tmp_path)),
patch("run_agent.AIAgent", return_value=child) as agent_class,
):
result = json.loads(registry.get_entry("delegate_task").handler(args, parent_agent=parent))
child.run_conversation.assert_called_once()
assert "error" not in result
kwargs = agent_class.call_args.kwargs
prompt = kwargs["ephemeral_system_prompt"]
depth = parent_depth + 1
can_delegate = enabled and depth < max_depth
assert child._delegate_depth == depth
assert child._delegate_role == ("orchestrator" if can_delegate else "leaf")
assert ("delegation" in kwargs["enabled_toolsets"]) == can_delegate
assert ("CAN spawn your own subagents" in prompt) == can_delegate
# Neither sibling note may teach a nonexistent model-controlled role knob.
assert "role=" not in prompt
assert "`role`" not in prompt
assert "Default is 'leaf'" not in prompt
if can_delegate:
assert "automatically" in prompt
assert "depth" in prompt
if depth + 1 >= max_depth:
assert "children MUST be leaves" in prompt
else:
assert "children can themselves be orchestrators" in prompt

View File

@@ -167,12 +167,12 @@ _ORCHESTRATOR_BLOCK = (
"for the final summary, not your workers.\n\n"
)
_LEAF_CHILDREN_NOTE = (
"Your own children MUST be leaves (cannot delegate further) because they would be at the depth floor — you cannot "
"pass role='orchestrator' to your own delegate_task calls."
"Your own children MUST be leaves (cannot delegate further) because they will reach the maximum nesting depth. "
"The system manages this automatically; no parameter override is available."
)
_NESTED_CHILDREN_NOTE = (
"Your own children can themselves be orchestrators or leaves, depending on the `role` you pass to delegate_task. "
"Default is 'leaf'; pass role='orchestrator' explicitly when a child needs to further decompose its work."
"Your own children can themselves be orchestrators or leaves; the system determines this automatically "
"based on the remaining depth budget. No parameter override is needed or available."
)
def _build_child_system_prompt(