feat: save oversized tool results to file instead of destructive truncation (#5210)

Previously, tool results exceeding 100K characters were silently chopped
with only a '[Truncated]' notice — the rest of the content was lost
permanently. The model had no way to access the truncated portion.

Now, oversized results are written to HERMES_HOME/cache/tool_responses/
and the model receives:
  - A 1,500-char head preview for immediate context
  - The file path so it can use read_file/search_files on the full output

This preserves the context window protection (inline content stays small)
while making the full data recoverable. Falls back to the old destructive
truncation if the file write fails.

Inspired by Block/goose's large response handler pattern.
This commit is contained in:
Teknium
2026-04-05 10:29:57 -07:00
committed by GitHub
parent d932980c1a
commit 51ed7dc2f3
3 changed files with 240 additions and 27 deletions

View File

@@ -1002,16 +1002,19 @@ class TestExecuteToolCalls:
assert messages[0]["role"] == "tool"
assert messages[0]["tool_call_id"] == "c1"
def test_result_truncation_over_100k(self, agent):
def test_result_truncation_over_100k(self, agent, tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
tc = _mock_tool_call(name="web_search", arguments="{}", call_id="c1")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc])
messages = []
big_result = "x" * 150_000
with patch("run_agent.handle_function_call", return_value=big_result):
agent._execute_tool_calls(mock_msg, messages, "task-1")
# Content should be truncated
# Content should be replaced with preview + file path
assert len(messages[0]["content"]) < 150_000
assert "Truncated" in messages[0]["content"]
assert "Large tool response" in messages[0]["content"]
assert "Full output saved to:" in messages[0]["content"]
class TestConcurrentToolExecution:
@@ -1230,8 +1233,10 @@ class TestConcurrentToolExecution:
assert "cancelled" in messages[0]["content"].lower() or "skipped" in messages[0]["content"].lower()
assert "cancelled" in messages[1]["content"].lower() or "skipped" in messages[1]["content"].lower()
def test_concurrent_truncates_large_results(self, agent):
"""Concurrent path should truncate results over 100k chars."""
def test_concurrent_truncates_large_results(self, agent, tmp_path, monkeypatch):
"""Concurrent path should save oversized results to file."""
monkeypatch.setenv("HERMES_HOME", str(tmp_path / ".hermes"))
(tmp_path / ".hermes").mkdir()
tc1 = _mock_tool_call(name="web_search", arguments='{}', call_id="c1")
tc2 = _mock_tool_call(name="web_search", arguments='{}', call_id="c2")
mock_msg = _mock_assistant_msg(content="", tool_calls=[tc1, tc2])
@@ -1244,7 +1249,8 @@ class TestConcurrentToolExecution:
assert len(messages) == 2
for m in messages:
assert len(m["content"]) < 150_000
assert "Truncated" in m["content"]
assert "Large tool response" in m["content"]
assert "Full output saved to:" in m["content"]
def test_invoke_tool_dispatches_to_handle_function_call(self, agent):
"""_invoke_tool should route regular tools through handle_function_call."""