mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
fix(tool-result): exempt retrieval tools from spill to prevent read-back recursion
This commit is contained in:
parent
40bbde1278
commit
c2c1cb5271
@ -2,6 +2,9 @@ package vip.mate.agent.graph.executor;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* Configuration for the tool-result three-layer budget (RFC-008 Phase 3).
|
||||
*
|
||||
@ -51,6 +54,18 @@ public class ToolResultProperties {
|
||||
*/
|
||||
private String storageBaseDir = "";
|
||||
|
||||
/**
|
||||
* Tools whose results must NEVER be spilled. These are the tools the agent
|
||||
* uses to <i>retrieve</i> spilled content — spilling their output would
|
||||
* cause infinite recursion (read spill path → produces another spill →
|
||||
* agent reads new spill → …) and starve {@code MAX_TOOL_CALLS_PER_STEP}.
|
||||
*
|
||||
* <p>Defaults to file-read tools that already cap their own output internally.
|
||||
* Configurable so deployments can add more retrieval-style tools (e.g.,
|
||||
* MCP-provided readers) without code changes.</p>
|
||||
*/
|
||||
private List<String> excludedTools = List.of("read_file", "read_workspace_memory_file");
|
||||
|
||||
public boolean isEnabled() { return enabled; }
|
||||
public void setEnabled(boolean enabled) { this.enabled = enabled; }
|
||||
|
||||
@ -73,4 +88,14 @@ public class ToolResultProperties {
|
||||
public void setStorageBaseDir(String storageBaseDir) {
|
||||
this.storageBaseDir = storageBaseDir == null ? "" : storageBaseDir;
|
||||
}
|
||||
|
||||
public List<String> getExcludedTools() { return excludedTools; }
|
||||
public void setExcludedTools(List<String> excludedTools) {
|
||||
this.excludedTools = excludedTools == null ? List.of() : excludedTools;
|
||||
}
|
||||
|
||||
/** O(1) membership test for the exclusion list, used on every tool result. */
|
||||
public Set<String> excludedToolsSet() {
|
||||
return Set.copyOf(excludedTools);
|
||||
}
|
||||
}
|
||||
|
||||
@ -52,9 +52,29 @@ public class ToolResultStorage {
|
||||
public static final String SPILL_MARKER_PREFIX = "[mate-tool-result-spill]";
|
||||
|
||||
private final ToolResultProperties props;
|
||||
/** Cached at construction; refreshed lazily if the underlying list mutates (rare). */
|
||||
private volatile java.util.Set<String> excludedToolsSnapshot;
|
||||
|
||||
public ToolResultStorage(ToolResultProperties props) {
|
||||
this.props = props;
|
||||
this.excludedToolsSnapshot = props.excludedToolsSet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true when {@code toolName} is in the configured exclusion list.
|
||||
* Excluded tools (typically retrieval tools like {@code read_file}) are
|
||||
* never spilled — spilling their output would create a recursion where
|
||||
* the agent reads a spill path and produces yet another spill.
|
||||
*/
|
||||
private boolean isExcluded(String toolName) {
|
||||
if (toolName == null) return false;
|
||||
java.util.Set<String> snap = excludedToolsSnapshot;
|
||||
java.util.Set<String> live = props.excludedToolsSet();
|
||||
if (live != snap && !live.equals(snap)) {
|
||||
this.excludedToolsSnapshot = live;
|
||||
snap = live;
|
||||
}
|
||||
return snap.contains(toolName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -73,6 +93,10 @@ public class ToolResultStorage {
|
||||
if (!props.isEnabled() || result == null) {
|
||||
return result;
|
||||
}
|
||||
if (isExcluded(toolName)) {
|
||||
// Retrieval-style tool — never spill, would cause read-back recursion.
|
||||
return result;
|
||||
}
|
||||
if (result.length() <= props.getPerResultThresholdChars()) {
|
||||
return result;
|
||||
}
|
||||
@ -115,20 +139,25 @@ public class ToolResultStorage {
|
||||
List<ToolResponseMessage.ToolResponse> mutable = new ArrayList<>(responses);
|
||||
|
||||
while (aggregate > budget) {
|
||||
// Find the largest response that has not yet been spilled.
|
||||
// Find the largest response that has not yet been spilled and is
|
||||
// not produced by an excluded (retrieval-style) tool.
|
||||
int targetIdx = -1;
|
||||
int targetLen = -1;
|
||||
for (int i = 0; i < mutable.size(); i++) {
|
||||
String body = mutable.get(i).responseData();
|
||||
ToolResponseMessage.ToolResponse r = mutable.get(i);
|
||||
String body = r.responseData();
|
||||
if (body == null || body.startsWith(SPILL_MARKER_PREFIX)) continue;
|
||||
if (isExcluded(r.name())) continue; // retrieval tools must not be spilled
|
||||
if (body.length() > targetLen) {
|
||||
targetLen = body.length();
|
||||
targetIdx = i;
|
||||
}
|
||||
}
|
||||
if (targetIdx < 0) {
|
||||
// Nothing left to spill; aggregate is already as small as we can make it.
|
||||
log.warn("[ToolResultStorage] aggregate still {} chars after spilling everything eligible",
|
||||
// Nothing left to spill; remaining oversize is from excluded tools or
|
||||
// already-spilled responses. Accept the over-budget state — better than
|
||||
// breaking the agent's retrieval path.
|
||||
log.warn("[ToolResultStorage] aggregate still {} chars after spilling everything eligible (excluded tools may push past budget)",
|
||||
aggregate);
|
||||
break;
|
||||
}
|
||||
|
||||
@ -162,6 +162,14 @@ mate:
|
||||
per-turn-budget-chars: 16000
|
||||
preview-head-chars: 800
|
||||
storage-base-dir: ""
|
||||
# Retrieval-style tools that must NEVER be spilled. Spilling read_file's
|
||||
# output causes a recursion: the agent reads the spill path, that read also
|
||||
# exceeds the threshold, gets spilled to a new path, agent reads that one,
|
||||
# ad infinitum until MAX_TOOL_CALLS_PER_STEP is hit. Add MCP-provided
|
||||
# readers here if they have the same role.
|
||||
excluded-tools:
|
||||
- read_file
|
||||
- read_workspace_memory_file
|
||||
conversation:
|
||||
window:
|
||||
# 测试时临时调低:2000 token ≈ 2000 中文字,3 轮对话即可触发压缩
|
||||
|
||||
Loading…
Reference in New Issue
Block a user