fix(agent): preserve destroyed tool-call argument bytes in the WARNING log
Review follow-up (W1): the pre-send transcript sanitizer
(agent_runtime_helpers.sanitize_tool_call_arguments) runs on the
PERSISTED messages list before every api_messages build and rewrites any
json.loads-failing argument string to "{}" in the transcript, prepending
a corruption marker to the paired tool result. That in-transcript repair
is deliberate (the stored turn must be replayable next call), but it
destroys the model's original bytes — for a truncated write_file call
those bytes are the user's streamed file content (#80498), and they
previously survived only as an 80-char log preview.
Until a sidecar-preservation design exists, make the bytes recoverable:
both destruction sites (the transcript sanitizer's WARNING and
_repair_tool_call_arguments' unrepairable-path WARNING) now log the full
original argument string bounded at 100KB instead of 80 chars. Corrupted
calls are rare; an oversized WARNING is a fair price for the only copy
of real user content.
This commit is contained in:
@@ -33,6 +33,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from hermes_cli.timeouts import get_provider_request_timeout
|
||||
from agent.message_sanitization import _FULL_ARGS_LOG_BOUND
|
||||
from agent.prompt_builder import format_steer_marker
|
||||
from agent.tool_dispatch_helpers import _trajectory_normalize_msg, make_tool_result_message
|
||||
from agent.trajectory import convert_scratchpad_to_think
|
||||
@@ -385,10 +386,19 @@ def sanitize_tool_call_arguments(
|
||||
# itself becomes an orphan (#58168).
|
||||
tool_call_id = _ra().AIAgent._get_tool_call_id_static(tool_call) or None
|
||||
function_name = function.get("name", "?")
|
||||
preview = arguments[:80]
|
||||
# Log the FULL original argument string (bounded), not an
|
||||
# 80-char preview: this branch is about to overwrite the
|
||||
# only copy of these bytes in the transcript with "{}", and
|
||||
# for a truncated write_file/patch call the destroyed
|
||||
# arguments contain real user content (#80498 — streamed
|
||||
# file content survived only as a log preview). A corrupted
|
||||
# call is rare, so the oversized WARNING is a fair price for
|
||||
# making the data recoverable from agent.log.
|
||||
preview = arguments[:_FULL_ARGS_LOG_BOUND]
|
||||
log.warning(
|
||||
"Corrupted tool_call arguments repaired before request "
|
||||
"(session=%s, message_index=%s, tool_call_id=%s, function=%s, preview=%r)",
|
||||
"(session=%s, message_index=%s, tool_call_id=%s, function=%s, "
|
||||
"original_arguments=%r)",
|
||||
session_id or "-",
|
||||
message_index,
|
||||
tool_call_id or "-",
|
||||
|
||||
@@ -183,6 +183,15 @@ def _escape_invalid_chars_in_json_strings(raw: str) -> str:
|
||||
return "".join(out)
|
||||
|
||||
|
||||
# When a repair is about to destroy the only copy of a tool call's original
|
||||
# argument bytes (rewriting them to "{}"), the WARNING log is the last
|
||||
# surviving copy of content that can hold real user data (#80498). Bound the
|
||||
# logged string at this size instead of a short preview so it stays
|
||||
# recoverable from agent.log without letting a pathological payload flood
|
||||
# the log.
|
||||
_FULL_ARGS_LOG_BOUND = 100_000
|
||||
|
||||
|
||||
def _repair_tool_call_arguments(raw_args: str, tool_name: str = "?") -> str:
|
||||
"""Attempt to repair malformed tool_call argument JSON.
|
||||
|
||||
@@ -271,11 +280,15 @@ def _repair_tool_call_arguments(raw_args: str, tool_name: str = "?") -> str:
|
||||
pass
|
||||
|
||||
# Last resort: replace with empty object so the API request doesn't
|
||||
# crash the entire session.
|
||||
# crash the entire session. Log the FULL original string (bounded) —
|
||||
# for callers that discard the original (e.g. the pre-send transcript
|
||||
# sanitizer), this WARNING is the last surviving copy of bytes that can
|
||||
# contain real user content (#80498: a truncated write_file call's
|
||||
# streamed file content).
|
||||
logger.warning(
|
||||
"Unrepairable tool_call arguments for %s — "
|
||||
"replaced with empty object (was: %s)",
|
||||
tool_name, raw_stripped[:80],
|
||||
tool_name, raw_stripped[:_FULL_ARGS_LOG_BOUND],
|
||||
)
|
||||
return "{}"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user