mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-28 18:19:28 +00:00
feat(tool_search): probe-validate blind tool_call args against the deferred schema
Port from nearai/ironclaw#5149 (the describe-first live-hardening fix in their progressive tool disclosure work): when a model invokes a deferred tool through the tool_call bridge without the schema-required arguments, return the tool's parameter schema instead of dispatching blind. Pre-fix, a blind call produced an opaque downstream failure ("[TOOL_ERROR] Tool execution failed: KeyError: 'document_id'") that teaches the model nothing about what the tool expects — IronClaw observed cheap models looping ~30 identical invalid calls until the iteration budget died. Post-fix, the model repairs the call in one round-trip. - tools/tool_search.py: new validate_deferred_call_args() — key-absence check of schema 'required' fields only; no type checking (coerce_tool_args already repairs types downstream); fails open on any validator error so it can never block a legitimate dispatch. - model_tools.py: probe after the scope gate in the bridge dispatch. - agent/tool_executor.py: probe in both unwrap sites (concurrent + sequential) before the underlying tool replaces the bridge; sequential path flattens the payload to match its {"error": str} wrapping. - tests: TestDeferredCallSchemaProbe — blind call returns schema (not KeyError), valid/optional calls dispatch, unvalidatable tools fail open, out-of-scope rejection unchanged.
This commit is contained in:
parent
9b97dea1e6
commit
8fbe2e388f
4 changed files with 199 additions and 4 deletions
|
|
@ -431,8 +431,15 @@ def execute_tool_calls_concurrent(agent, assistant_message, messages: list, effe
|
|||
_underlying, _underlying_args, _err = _ts.resolve_underlying_call(function_args)
|
||||
if not _err and _underlying:
|
||||
if _underlying in _tool_search_scoped_names(agent):
|
||||
function_name = _underlying
|
||||
function_args = _underlying_args
|
||||
# Probe-validate before unwrapping (ironclaw#5149):
|
||||
# missing required args return the parameter schema
|
||||
# instead of dispatching into an opaque failure.
|
||||
_probe_err = _ts.validate_deferred_call_args(_underlying, _underlying_args)
|
||||
if _probe_err is not None:
|
||||
_ts_scope_block = _probe_err
|
||||
else:
|
||||
function_name = _underlying
|
||||
function_args = _underlying_args
|
||||
else:
|
||||
_ts_scope_block = json.dumps({
|
||||
"error": (
|
||||
|
|
@ -1113,8 +1120,25 @@ def execute_tool_calls_sequential(agent, assistant_message, messages: list, effe
|
|||
_underlying, _underlying_args, _err = _ts.resolve_underlying_call(function_args)
|
||||
if not _err and _underlying:
|
||||
if _underlying in _tool_search_scoped_names(agent):
|
||||
function_name = _underlying
|
||||
function_args = _underlying_args
|
||||
# Probe-validate before unwrapping (ironclaw#5149):
|
||||
# missing required args return the parameter schema
|
||||
# instead of dispatching into an opaque failure.
|
||||
_probe_err = _ts.validate_deferred_call_args(_underlying, _underlying_args)
|
||||
if _probe_err is not None:
|
||||
# This path wraps _block_msg in {"error": ...} —
|
||||
# flatten the probe payload to one plain string.
|
||||
try:
|
||||
_probe = json.loads(_probe_err)
|
||||
_ts_scope_block = (
|
||||
f"{_probe.get('error', '')} Parameters schema: "
|
||||
f"{json.dumps(_probe.get('parameters', {}), ensure_ascii=False)}. "
|
||||
f"{_probe.get('hint', '')}"
|
||||
).strip()
|
||||
except Exception:
|
||||
_ts_scope_block = _probe_err
|
||||
else:
|
||||
function_name = _underlying
|
||||
function_args = _underlying_args
|
||||
else:
|
||||
_ts_scope_block = (
|
||||
f"'{_underlying}' is not available in this session. "
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue