refactor(agent): carry one blocked-call body through the pruned-arg check

One block decision travelled as block_message + block_error_type +
block_payload, with _blocked_tool_result re-deriving the message from the
payload it was also handed, and _pruned_tool_arguments_block existing only to
package two module constants for its single caller. Replace message+payload
with a single block_body dict: scope/plugin blocks become {"error": msg}
(byte-identical to before), the pruned-arg block is built inline, and
_blocked_tool_result serialises whatever it is given. What the model sees is
unchanged.

Also: the _dispatch_authorized_once docstring now lists the pruned-arg check
between pre-hooks and guardrails where it actually runs, and the refusal text
gains one static sentence telling the model how to legitimately remove a
marker that already landed on disk (match the HERMES-CONTEXT-COMPRESSION
prefix instead of quoting the full marker, which the guard would refuse).
This commit is contained in:
kshitijk4poor
2026-09-24 15:35:25 +05:30
committed by kshitij
parent 159f716313
commit b969fdadfb

View File

@@ -640,34 +640,17 @@ _PRUNED_TOOL_ARGUMENTS_ERROR = "suspected_pruned_tool_arguments"
_PRUNED_TOOL_ARGUMENTS_MESSAGE = (
"Tool was not executed because effect-capable arguments contain a Hermes context-compression artifact. "
"Recover the exact content from its durable source or re-read it, then issue a complete new call; "
"do not retry these arguments."
"do not retry these arguments. To remove a marker that already landed in a file, match it by its "
"HERMES-CONTEXT-COMPRESSION prefix (e.g. a terminal sed on that line) instead of quoting the full marker."
)
def _pruned_tool_arguments_block(function_name: str, function_args: dict[str, Any]) -> dict[str, Any] | None:
paths = _context_pruned_argument_paths(function_name, function_args)
if not paths:
return None
return {
"error": _PRUNED_TOOL_ARGUMENTS_ERROR,
"message": _PRUNED_TOOL_ARGUMENTS_MESSAGE,
"argument_paths": paths,
}
def _blocked_tool_result(
agent,
ref: _ToolCallRef,
*,
block_message: Optional[str],
block_error_type: str,
guardrail_decision,
block_payload: dict[str, Any] | None = None,
) -> str:
"""Synthesize a blocked tool result and emit its terminal post_tool_call."""
if block_message is not None:
result = json.dumps(block_payload or {"error": block_message}, ensure_ascii=False)
error_type, error_message = block_error_type, block_message
def _blocked_tool_result(agent, ref: _ToolCallRef, *, block_body: dict[str, Any] | None, block_error_type: str, guardrail_decision) -> str:
"""Synthesize the result for a call blocked by scope/plugin/pruned-args (``block_body``, the
JSON the model sees) or by guardrail policy (``guardrail_decision``) and emit its terminal post_tool_call."""
if block_body is not None:
result = json.dumps(block_body, ensure_ascii=False)
error_type, error_message = block_error_type, block_body.get("message") or block_body["error"]
else:
result = agent._guardrail_block_result(guardrail_decision)
error_type = "guardrail_block"
@@ -704,7 +687,7 @@ def _dispatch_authorized_once(
begin_execution,
authorization_gate: _ConcurrentToolAuthorizationGate | None,
) -> Any:
"""Hermes policy (scope → plugin pre-hooks → guardrails) then the one real dispatch.
"""Hermes policy (scope → plugin pre-hooks → pruned-arg check → guardrails) then the one real dispatch.
Plugin ``modify`` hooks may rewrite ``ref.args`` (mirrored into ``state.args``).
``begin_execution`` (concurrent start-order gate) is advanced exactly once on every
@@ -717,38 +700,38 @@ def _dispatch_authorized_once(
callback()
block_message, block_error_type = scope_block, "tool_scope_block"
block_payload = None
if block_message is None:
block_error_type = "plugin_block"
resolve = lambda: _pre_tool_block(agent, ref) # noqa: E731
block_message, ref.args = resolve() if authorization_gate is None else authorization_gate.run(resolve)
state.args = ref.args
block_body = None if block_message is None else {"error": block_message}
# Checked once, after plugin modify hooks (which may replace arguments) and
# before guardrails or real dispatch: a copied compression marker in an
# effect-capable argument must never reach the tool.
if block_message is None:
block_payload = _pruned_tool_arguments_block(ref.name, ref.args)
if block_payload is not None:
block_message = block_payload["message"]
if block_body is None:
pruned_paths = _context_pruned_argument_paths(ref.name, ref.args)
if pruned_paths:
block_body = {
"error": _PRUNED_TOOL_ARGUMENTS_ERROR,
"message": _PRUNED_TOOL_ARGUMENTS_MESSAGE,
"argument_paths": pruned_paths,
}
block_error_type = _PRUNED_TOOL_ARGUMENTS_ERROR
guardrail_decision = None
if block_message is None:
if block_body is None:
guardrail_decision = agent._tool_guardrails.before_call(ref.name, ref.args)
if guardrail_decision.allows_execution:
guardrail_decision = None
if block_message is not None or guardrail_decision is not None:
if block_body is not None or guardrail_decision is not None:
_advance_start_order()
state.blocked = True
return _blocked_tool_result(
agent,
ref,
block_message=block_message,
block_error_type=block_error_type,
guardrail_decision=guardrail_decision,
block_payload=block_payload,
agent, ref,
block_body=block_body, block_error_type=block_error_type, guardrail_decision=guardrail_decision,
)
if ref.name == "memory":