feat(agent): 压缩清理占位符信息化——保留工具名/原始大小/首行要点,便于模型判断是否需要重跑

This commit is contained in:
matevip 2026-07-03 18:53:27 +08:00
parent 36d1f1027d
commit 727adcd24b
3 changed files with 81 additions and 4 deletions

View File

@ -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 字符
* <p>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) {

View File

@ -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");
}

View File

@ -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<full test output...>";
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:"));
}
}