mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-15 11:58:34 +08:00
feat(agent): 压缩清理占位符信息化——保留工具名/原始大小/首行要点,便于模型判断是否需要重跑
This commit is contained in:
parent
36d1f1027d
commit
727adcd24b
@ -1027,6 +1027,33 @@ public class ConversationWindowManager {
|
|||||||
+ "' can be called again if its result is needed.]";
|
+ "' 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 字符)。
|
* Phase 1 - Soft trim:对工具结果做 head+tail 裁剪(保留首尾各 200 字符)。
|
||||||
* <p>Spill-marker responses are left untouched so their on-disk pointer
|
* <p>Spill-marker responses are left untouched so their on-disk pointer
|
||||||
@ -1081,7 +1108,8 @@ public class ConversationWindowManager {
|
|||||||
replaced.add(r);
|
replaced.add(r);
|
||||||
continue;
|
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;
|
changed = true;
|
||||||
}
|
}
|
||||||
if (changed) {
|
if (changed) {
|
||||||
|
|||||||
@ -71,9 +71,12 @@ class ConversationWindowManagerSpillMarkerPreservationTest {
|
|||||||
ToolResponseMessage trm0 = (ToolResponseMessage) messages.get(0);
|
ToolResponseMessage trm0 = (ToolResponseMessage) messages.get(0);
|
||||||
ToolResponseMessage trm1 = (ToolResponseMessage) messages.get(1);
|
ToolResponseMessage trm1 = (ToolResponseMessage) messages.get(1);
|
||||||
assertEquals(SPILL_BODY, trm0.getResponses().getFirst().responseData(),
|
assertEquals(SPILL_BODY, trm0.getResponses().getFirst().responseData(),
|
||||||
"Phase 2 hard clear must not replace a spill-marker body with [tool result removed]");
|
"Phase 2 hard clear must not replace a spill-marker body with the cleared placeholder");
|
||||||
assertEquals("[tool result removed]", trm1.getResponses().getFirst().responseData(),
|
String clearedBody = trm1.getResponses().getFirst().responseData();
|
||||||
|
assertTrue(clearedBody.contains("cleared to save context"),
|
||||||
"non-spill bodies should still be replaced by Phase 2");
|
"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,
|
assertEquals(1, cleared,
|
||||||
"clear counter should reflect only the non-spill body that was actually replaced");
|
"clear counter should reflect only the non-spill body that was actually replaced");
|
||||||
}
|
}
|
||||||
@ -120,7 +123,7 @@ class ConversationWindowManagerSpillMarkerPreservationTest {
|
|||||||
ToolResponseMessage trm = (ToolResponseMessage) messages.getFirst();
|
ToolResponseMessage trm = (ToolResponseMessage) messages.getFirst();
|
||||||
assertEquals(SPILL_BODY, trm.getResponses().get(0).responseData(),
|
assertEquals(SPILL_BODY, trm.getResponses().get(0).responseData(),
|
||||||
"the spill response in a mixed message must survive Phase 2");
|
"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");
|
"the non-spill response in a mixed message must still be cleared");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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:"));
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user