From eddfa0dd6bb43e52980c44168f5cdfb025bd5b8d Mon Sep 17 00:00:00 2001 From: LostInTwilight <1652154451@qq.com> Date: Wed, 2 Sep 2026 12:52:47 +0800 Subject: [PATCH] fix(tool_hints): respect max_length for plain (non-path/non-command) tool values Plain tool values (grep patterns, web_search/x_search queries, find_files globs) were never truncated by format_tool_hints(), so long arguments overflowed tool_hint_max_length and were pushed to chat/UI verbatim. Add a hard-truncation fallback for the plain branch, mirroring the existing truncation used by abbreviate_path / _abbreviate_command. This completes the same class of fix started in 99209a80 for is_path tools. Adds 4 regression tests to tests/agent/test_tool_hint.py. --- nanobot/utils/tool_hints.py | 4 ++++ tests/agent/test_tool_hint.py | 37 +++++++++++++++++++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/nanobot/utils/tool_hints.py b/nanobot/utils/tool_hints.py index 15542e6a6..cb5dd6854 100644 --- a/nanobot/utils/tool_hints.py +++ b/nanobot/utils/tool_hints.py @@ -104,6 +104,10 @@ def _fmt_known(tc: ToolCallRequest, fmt: ToolFormat, max_length: int = 40) -> st val = abbreviate_path(val, max_len=max_length) elif fmt[3]: # is_command val = _abbreviate_command(val, max_len=max_length) + elif len(val) > max_length: + # Plain values (grep patterns, search queries, ...) have no path or + # command structure to fold, so fall back to a hard truncation. + val = val[:max_length - 1] + "\u2026" return fmt[1].format(val) diff --git a/tests/agent/test_tool_hint.py b/tests/agent/test_tool_hint.py index d927a9a18..5181bf72c 100644 --- a/tests/agent/test_tool_hint.py +++ b/tests/agent/test_tool_hint.py @@ -303,6 +303,43 @@ class TestToolHintMaxLength: long = _hint([_tc("list_dir", {"path": long_path})], max_length=120) assert len(long) > len(short) + def test_plain_value_tools_respect_max_length(self): + """Plain-value tools must truncate like the is_path/is_command branches. + + grep, web_search, x_search and find_files carry no path or command + structure to fold, so their raw value used to reach the progress hint + untruncated: a 400-char search query produced a 400-char hint. + """ + for name, key in ( + ("grep", "pattern"), + ("web_search", "query"), + ("x_search", "query"), + ("find_files", "query"), + ): + short = _hint([_tc(name, {key: "x" * 400})], max_length=40) + long = _hint([_tc(name, {key: "x" * 400})], max_length=120) + assert len(long) > len(short), name + # Longest template is 'search X "{}"' — 12 chars of overhead. + assert len(short) <= 40 + 12, name + assert "\u2026" in short, name + + def test_plain_value_short_value_untouched(self): + """Values inside the budget must not gain an ellipsis.""" + result = _hint([_tc("grep", {"pattern": "TODO|FIXME"})], max_length=40) + assert result == 'grep "TODO|FIXME"' + + def test_plain_value_exactly_at_max_length_untouched(self): + """A value exactly at max_length already fits.""" + query = "a" * 40 + result = _hint([_tc("web_search", {"query": query})], max_length=40) + assert result == f'search "{query}"' + assert "\u2026" not in result + + def test_plain_value_one_over_max_length_truncates(self): + """One character over the budget truncates instead of overflowing.""" + result = _hint([_tc("grep", {"pattern": "a" * 41})], max_length=40) + assert result == 'grep "' + "a" * 39 + "\u2026" + '"' + class TestToolHintMalformedCalls: """Malformed tool calls must not crash hint formatting (see HKUDS/nanobot)."""