fix(delegation): align child prompts with depth-derived roles
This commit is contained in:
79
tests/tools/test_delegate_depth_prompt.py
Normal file
79
tests/tools/test_delegate_depth_prompt.py
Normal 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
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user