diff --git a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java index 7015be02..aac41650 100644 --- a/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java +++ b/mateclaw-server/src/main/java/vip/mate/agent/context/ConversationWindowManager.java @@ -1027,6 +1027,33 @@ public class ConversationWindowManager { + "' can be called again if its result is needed.]"; } + /** + * One-line informative summary for a cleared tool result: tool name, + * original size, and the first line as a gist. Far more useful to the + * model than a bare "removed" marker — it can decide whether re-running + * the tool is worth it without guessing what the output was. + */ + static String buildInformativeCleared(String toolName, String body) { + String safeName = (toolName == null || toolName.isBlank()) ? "tool" : toolName; + int length = body == null ? 0 : body.length(); + String gist = ""; + if (body != null) { + for (String line : body.split("\n", 8)) { + String candidate = line.strip(); + if (!candidate.isEmpty()) { + gist = candidate.length() > 80 ? candidate.substring(0, 80) + "…" : candidate; + break; + } + } + } + StringBuilder sb = new StringBuilder("[").append(safeName) + .append(" → ").append(length).append(" chars, cleared to save context"); + if (!gist.isEmpty()) { + sb.append("; began: \"").append(gist).append('"'); + } + return sb.append("; call the tool again if the result is still needed]").toString(); + } + /** * Phase 1 - Soft trim:对工具结果做 head+tail 裁剪(保留首尾各 200 字符)。 *

Spill-marker responses are left untouched so their on-disk pointer @@ -1081,7 +1108,8 @@ public class ConversationWindowManager { replaced.add(r); continue; } - replaced.add(new ToolResponseMessage.ToolResponse(r.id(), r.name(), "[tool result removed]")); + replaced.add(new ToolResponseMessage.ToolResponse(r.id(), r.name(), + buildInformativeCleared(r.name(), r.responseData()))); changed = true; } if (changed) { diff --git a/mateclaw-server/src/test/java/vip/mate/agent/context/ConversationWindowManagerSpillMarkerPreservationTest.java b/mateclaw-server/src/test/java/vip/mate/agent/context/ConversationWindowManagerSpillMarkerPreservationTest.java index b6112f82..b434e3ed 100644 --- a/mateclaw-server/src/test/java/vip/mate/agent/context/ConversationWindowManagerSpillMarkerPreservationTest.java +++ b/mateclaw-server/src/test/java/vip/mate/agent/context/ConversationWindowManagerSpillMarkerPreservationTest.java @@ -71,9 +71,12 @@ class ConversationWindowManagerSpillMarkerPreservationTest { ToolResponseMessage trm0 = (ToolResponseMessage) messages.get(0); ToolResponseMessage trm1 = (ToolResponseMessage) messages.get(1); assertEquals(SPILL_BODY, trm0.getResponses().getFirst().responseData(), - "Phase 2 hard clear must not replace a spill-marker body with [tool result removed]"); - assertEquals("[tool result removed]", trm1.getResponses().getFirst().responseData(), + "Phase 2 hard clear must not replace a spill-marker body with the cleared placeholder"); + String clearedBody = trm1.getResponses().getFirst().responseData(); + assertTrue(clearedBody.contains("cleared to save context"), "non-spill bodies should still be replaced by Phase 2"); + assertTrue(clearedBody.contains("search") && clearedBody.contains("2000 chars"), + "the cleared placeholder should carry the tool name and original size"); assertEquals(1, cleared, "clear counter should reflect only the non-spill body that was actually replaced"); } @@ -120,7 +123,7 @@ class ConversationWindowManagerSpillMarkerPreservationTest { ToolResponseMessage trm = (ToolResponseMessage) messages.getFirst(); assertEquals(SPILL_BODY, trm.getResponses().get(0).responseData(), "the spill response in a mixed message must survive Phase 2"); - assertEquals("[tool result removed]", trm.getResponses().get(1).responseData(), + assertTrue(trm.getResponses().get(1).responseData().contains("cleared to save context"), "the non-spill response in a mixed message must still be cleared"); } diff --git a/mateclaw-server/src/test/java/vip/mate/agent/context/InformativeClearedPlaceholderTest.java b/mateclaw-server/src/test/java/vip/mate/agent/context/InformativeClearedPlaceholderTest.java new file mode 100644 index 00000000..89c3f04a --- /dev/null +++ b/mateclaw-server/src/test/java/vip/mate/agent/context/InformativeClearedPlaceholderTest.java @@ -0,0 +1,46 @@ +package vip.mate.agent.context; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the informative cleared-tool-output placeholder — name, size and + * first-line gist survive the clearing so the model can judge whether a + * re-run is worthwhile. + */ +class InformativeClearedPlaceholderTest { + + @Test + @DisplayName("placeholder carries tool name, original size and first-line gist") + void carriesNameSizeAndGist() { + String body = "total 47 tests, 0 failures\n"; + String placeholder = ConversationWindowManager.buildInformativeCleared("run_tests", body); + assertTrue(placeholder.contains("run_tests")); + assertTrue(placeholder.contains(body.length() + " chars")); + assertTrue(placeholder.contains("total 47 tests, 0 failures")); + assertTrue(placeholder.contains("call the tool again")); + } + + @Test + @DisplayName("long first line is capped at 80 chars") + void longGistCapped() { + String body = "x".repeat(500); + String placeholder = ConversationWindowManager.buildInformativeCleared("read_file", body); + assertTrue(placeholder.contains("x".repeat(80) + "…")); + assertFalse(placeholder.contains("x".repeat(81))); + } + + @Test + @DisplayName("null / blank bodies degrade gracefully") + void nullAndBlankBodies() { + String placeholder = ConversationWindowManager.buildInformativeCleared(null, null); + assertTrue(placeholder.contains("tool")); + assertTrue(placeholder.contains("0 chars")); + assertFalse(placeholder.contains("began:")); + + String blank = ConversationWindowManager.buildInformativeCleared("shell", "\n\n \n"); + assertFalse(blank.contains("began:")); + } +}