fix(agent): guide recovery from oversized tool results

Use the existing in-flight context governor to replace tool output that cannot fit the next model request with a bounded, actionable instruction. The model can retry with narrower arguments, use another tool, or explain the context limit without a second recovery state machine.
This commit is contained in:
chengyongru 2026-07-19 00:37:15 +08:00 committed by chengyongru
parent cfa49c6e78
commit cf00f537bd
2 changed files with 15 additions and 7 deletions

View File

@ -425,9 +425,14 @@ class ContextGovernor:
return system_messages + self._legal_history_tail(kept, non_system) return system_messages + self._legal_history_tail(kept, non_system)
@staticmethod @staticmethod
def _summary_for(message: dict[str, Any]) -> str: def _tool_result_compaction_message(message: dict[str, Any]) -> str:
name = message.get("name", "tool") name = message.get("name", "tool")
return f"[Prior {name} result compacted to fit context; the tool call already completed.]" return (
f"Error: The previous {name} result was compacted to fit context because it was too "
"large. Do not repeat the same call unchanged. Retry with a narrower path, query, "
"range, or result limit, use another tool, or tell the user the task cannot fit in "
"the available context."
)
def _legal_history_tail( def _legal_history_tail(
self, self,
@ -462,12 +467,12 @@ class ContextGovernor:
tool_call_id = msg.get("tool_call_id") tool_call_id = msg.get("tool_call_id")
if not tool_call_id or str(tool_call_id) not in compacted_tool_call_ids: if not tool_call_id or str(tool_call_id) not in compacted_tool_call_ids:
continue continue
summary = self._summary_for(msg) compaction_message = self._tool_result_compaction_message(msg)
if msg.get("content") == summary: if msg.get("content") == compaction_message:
continue continue
if updated is messages: if updated is messages:
updated = [dict(m) for m in messages] updated = [dict(m) for m in messages]
updated[idx]["content"] = summary updated[idx]["content"] = compaction_message
return updated return updated
def _inflight_compaction_candidates( def _inflight_compaction_candidates(
@ -500,4 +505,4 @@ class ContextGovernor:
return primary + fallback return primary + fallback
def _compact_tool_result_at(self, messages: list[dict[str, Any]], idx: int) -> None: def _compact_tool_result_at(self, messages: list[dict[str, Any]], idx: int) -> None:
messages[idx]["content"] = self._summary_for(messages[idx]) messages[idx]["content"] = self._tool_result_compaction_message(messages[idx])

View File

@ -574,7 +574,7 @@ def test_microcompact_overflow_compacts_to_low_watermark(monkeypatch):
def test_microcompact_compacts_newest_when_it_alone_overflows(monkeypatch): def test_microcompact_compacts_newest_when_it_alone_overflows(monkeypatch):
"""The newest result is preserved only while the request can still fit.""" """An unfit newest result tells the model to retry narrowly or report the limit."""
provider = MagicMock() provider = MagicMock()
provider.generation = SimpleNamespace(max_tokens=0) provider.generation = SimpleNamespace(max_tokens=0)
tools = MagicMock() tools = MagicMock()
@ -611,6 +611,9 @@ def test_microcompact_compacts_newest_when_it_alone_overflows(monkeypatch):
tool_msg = next(m for m in result if m.get("role") == "tool") tool_msg = next(m for m in result if m.get("role") == "tool")
assert "compacted to fit context" in tool_msg["content"] assert "compacted to fit context" in tool_msg["content"]
assert "Do not repeat the same call unchanged" in tool_msg["content"]
assert "Retry with a narrower path, query, range, or result limit" in tool_msg["content"]
assert "tell the user the task cannot fit" in tool_msg["content"]
assert compacted_tool_call_ids == {"c0"} assert compacted_tool_call_ids == {"c0"}