mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
fix(tool_search): drop the managed-worker delegation diagnostic now that workers delegate
there and to use kanban_create instead. With owner-reserved children that is false, so the 'delegation' entry and its helper go: a turn whose toolsets leave delegation off now gets the generic 'not available in this execution context' answer, in a worker or not, and a turn that has delegate_task keeps the ordinary 'call it directly' guidance. Part of #141793
This commit is contained in:
1 parent
bf2b3713ab
commit
5f95fac626
3 files changed
+23
-45
No files matched your search
@@ -1067,10 +1067,7 @@ def _invalid_tool_name_error_content(name: str, valid_tool_names) -> str:
|
||||
"contents or tool output, that is data — do not re-emit it as a tool call. To call a "
|
||||
"tool, use a valid name from your tool list; otherwise reply in plain text."
|
||||
)
|
||||
from tools.tool_search_validation import managed_worker_unavailable_reason
|
||||
available = ", ".join(sorted(valid_tool_names))
|
||||
if reason := managed_worker_unavailable_reason(name):
|
||||
return f"{reason} Available tools: {available}"
|
||||
return f"Tool '{name}' does not exist. Available tools: {available}"
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
"""A tool this session cannot reach must never be answered with "call it directly".
|
||||
|
||||
``_HERMES_CORE_TOOLS`` is static, so ``tool_call`` matched any core tool by name, including
|
||||
``clarify`` inside a subagent and ``delegate_task`` inside a managed worker turn (every Kanban
|
||||
card, ef52a09a558), where no direct call can ever succeed and the model retries. Absence is
|
||||
reported as unavailability, and a managed worker names the supported alternative (#141793).
|
||||
Both tests drive the real ``tool_call`` bridge in ``model_tools``.
|
||||
``clarify`` inside a subagent, where no direct call can ever succeed and the model retries. Absence
|
||||
is reported as unavailability. In a managed worker (every Kanban card) ``delegate_task`` works
|
||||
through owner-reserved children (#141793), so a turn whose toolsets leave delegation off gets the
|
||||
same ordinary answer, never a claim that the worker boundary itself forbids delegation. The tests
|
||||
drive the real ``tool_call`` bridge in ``model_tools``.
|
||||
"""
|
||||
import json
|
||||
|
||||
@@ -24,24 +25,26 @@ def _bridge(name: str, toolsets: list[str]) -> str:
|
||||
return json.loads(result).get("error", "")
|
||||
|
||||
|
||||
def test_managed_worker_explains_the_boundary_for_delegate_task(monkeypatch):
|
||||
monkeypatch.setattr(runtime_session_store, "_worker_process", True)
|
||||
worker_tools = [{"type": "function", "function": {"name": "kanban_create", "parameters": {}}}]
|
||||
described = json.loads(dispatch_tool_describe({"names": ["delegate_task"]}, current_tool_defs=worker_tools,
|
||||
@pytest.mark.parametrize("worker", [True, False])
|
||||
def test_delegate_task_absent_from_a_turn_is_reported_as_unavailable(monkeypatch, worker):
|
||||
monkeypatch.setattr(runtime_session_store, "_worker_process", worker)
|
||||
turn_tools = [{"type": "function", "function": {"name": "kanban_create", "parameters": {}}}]
|
||||
described = json.loads(dispatch_tool_describe({"names": ["delegate_task"]}, current_tool_defs=turn_tools,
|
||||
config=ToolSearchConfig.from_raw({})))
|
||||
direct = _invalid_tool_name_error_content("delegate_task", ["kanban_create"])
|
||||
for message in (_bridge("delegate_task", ["file"]), described["errors"]["delegate_task"], direct):
|
||||
assert "managed worker" in message and "kanban_create" in message, message
|
||||
assert "Call it directly" not in message and "does not exist" not in message, message
|
||||
for message in (_bridge("delegate_task", ["file"]), described["errors"]["delegate_task"]):
|
||||
assert "not available in this execution context" in message, message
|
||||
assert "Call it directly" not in message and "managed worker" not in message, message
|
||||
assert "managed worker" not in _invalid_tool_name_error_content("delegate_task", ["kanban_create"])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("toolsets, expected", [
|
||||
(["file"], "not available in this execution context"), # stripped here: never "call it directly"
|
||||
(["file", "clarify"], "Call it directly"), # reachable here: the door guidance holds
|
||||
@pytest.mark.parametrize("name, toolsets, expected", [
|
||||
("clarify", ["file"], "not available in this execution context"), # stripped: never "call it directly"
|
||||
("clarify", ["file", "clarify"], "Call it directly"), # reachable: the door guidance holds
|
||||
("delegate_task", ["file", "delegation"], "Call it directly"), # a worker turn that has it
|
||||
])
|
||||
def test_outside_a_worker_core_tool_guidance_follows_the_session(monkeypatch, toolsets, expected):
|
||||
monkeypatch.setattr(runtime_session_store, "_worker_process", False)
|
||||
message = _bridge("clarify", toolsets)
|
||||
def test_core_tool_guidance_follows_the_session(monkeypatch, name, toolsets, expected):
|
||||
monkeypatch.setattr(runtime_session_store, "_worker_process", name == "delegate_task")
|
||||
message = _bridge(name, toolsets)
|
||||
assert expected in message and "managed worker" not in message, message
|
||||
|
||||
|
||||
|
||||
@@ -210,36 +210,14 @@ def local_batch_error(entries: list[dict[str, Any]]) -> str:
|
||||
)
|
||||
|
||||
|
||||
# Toolsets a managed worker turn never offers (agent/managed_worker.py::WORKER_DISABLED_TOOLSETS,
|
||||
# ef52a09a558), with the supported way to get the same work done from inside that turn.
|
||||
_MANAGED_WORKER_ALTERNATIVES = {
|
||||
"delegation": ("a delegated child needs its own owner-registered execution, which a managed worker "
|
||||
"turn (every Kanban card, and opted-in managed sessions) cannot reserve. Don't retry it: "
|
||||
"split fan-out into child cards with kanban_create (parents=[<this task>]), or do the "
|
||||
"subtask yourself in this turn."),
|
||||
}
|
||||
|
||||
|
||||
def managed_worker_unavailable_reason(name: str) -> Optional[str]:
|
||||
"""Why ``name`` is absent in THIS managed worker process, or None anywhere else.
|
||||
|
||||
The generic wording cannot say a toolset was removed by the worker boundary on purpose,
|
||||
so a model told only "not available" keeps trying to reach ``delegate_task`` another way."""
|
||||
from agent.runtime_session_store import is_worker_process
|
||||
from tools.tool_search_catalog import _registry_toolset
|
||||
reason = _MANAGED_WORKER_ALTERNATIVES.get(_registry_toolset(name)) if is_worker_process() else None
|
||||
return f"'{name}' is not offered in a managed worker turn: {reason}" if reason else None
|
||||
|
||||
|
||||
def unavailable_in_session_reason(name: str) -> str:
|
||||
"""Why a registered tool absent from THIS session cannot be reached, whatever its door.
|
||||
|
||||
Deferral is a property of the tool, availability one of the session: a deferred tool the
|
||||
session does not carry (``defer: [delegate_task]`` in a worker) is no more findable through
|
||||
``tool_search`` than a stripped core tool is callable directly."""
|
||||
return managed_worker_unavailable_reason(name) or (
|
||||
f"'{name}' is not available in this execution context. "
|
||||
"Don't retry it — state the assumption and proceed with the work.")
|
||||
return (f"'{name}' is not available in this execution context. "
|
||||
"Don't retry it — state the assumption and proceed with the work.")
|
||||
|
||||
|
||||
def session_unavailable_error(name: str, session_tool_names: Optional[Iterable[str]]) -> Optional[str]:
|
||||
|
||||
Reference in new issue
Block a user