mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 09:41:11 +00:00
fix(agents): prefer overflow compaction for fresh reads
This commit is contained in:
@@ -17,17 +17,21 @@ function makeUser(text: string): AgentMessage {
|
||||
});
|
||||
}
|
||||
|
||||
function makeToolResult(id: string, text: string): AgentMessage {
|
||||
function makeToolResult(id: string, text: string, toolName = "grep"): AgentMessage {
|
||||
return castAgentMessage({
|
||||
role: "toolResult",
|
||||
toolCallId: id,
|
||||
toolName: "read",
|
||||
toolName,
|
||||
content: [{ type: "text", text }],
|
||||
isError: false,
|
||||
timestamp: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
function makeReadToolResult(id: string, text: string): AgentMessage {
|
||||
return makeToolResult(id, text, "read");
|
||||
}
|
||||
|
||||
function makeLegacyToolResult(id: string, text: string): AgentMessage {
|
||||
return castAgentMessage({
|
||||
role: "tool",
|
||||
@@ -311,6 +315,51 @@ describe("installToolResultContextGuard", () => {
|
||||
expect(newResult.details).toBeUndefined();
|
||||
});
|
||||
|
||||
it("throws overflow instead of compacting the latest read result during aggregate compaction", async () => {
|
||||
const agent = makeGuardableAgent();
|
||||
|
||||
installToolResultContextGuard({
|
||||
agent,
|
||||
contextWindowTokens: 1_000,
|
||||
});
|
||||
|
||||
const contextForNextCall = [
|
||||
makeUser("u".repeat(2_600)),
|
||||
makeToolResult("call_old", "x".repeat(300)),
|
||||
makeReadToolResult("call_new", "y".repeat(500)),
|
||||
];
|
||||
|
||||
await expect(
|
||||
agent.transformContext?.(contextForNextCall, new AbortController().signal),
|
||||
).rejects.toThrow(PREEMPTIVE_CONTEXT_OVERFLOW_MESSAGE);
|
||||
|
||||
expect(getToolResultText(contextForNextCall[1])).toBe("x".repeat(300));
|
||||
expect(getToolResultText(contextForNextCall[2])).toBe("y".repeat(500));
|
||||
});
|
||||
|
||||
it("keeps the latest read result when older outputs absorb the aggregate overflow", async () => {
|
||||
const agent = makeGuardableAgent();
|
||||
|
||||
installToolResultContextGuard({
|
||||
agent,
|
||||
contextWindowTokens: 1_000,
|
||||
});
|
||||
|
||||
const contextForNextCall = [
|
||||
makeUser("u".repeat(1_400)),
|
||||
makeToolResult("call_old_1", "a".repeat(350)),
|
||||
makeToolResult("call_old_2", "b".repeat(350)),
|
||||
makeReadToolResult("call_new", "c".repeat(500)),
|
||||
];
|
||||
|
||||
const transformed = (await agent.transformContext?.(
|
||||
contextForNextCall,
|
||||
new AbortController().signal,
|
||||
)) as AgentMessage[];
|
||||
|
||||
expect(getToolResultText(transformed[3])).toBe("c".repeat(500));
|
||||
});
|
||||
|
||||
it("throws preemptive context overflow when context exceeds 90% after tool-result compaction", async () => {
|
||||
const agent = makeGuardableAgent();
|
||||
|
||||
|
||||
@@ -49,6 +49,21 @@ type GuardableAgentRecord = {
|
||||
transformContext?: GuardableTransformContext;
|
||||
};
|
||||
|
||||
function getToolResultName(msg: AgentMessage): string | undefined {
|
||||
const toolName = (msg as { toolName?: unknown }).toolName;
|
||||
if (typeof toolName === "string" && toolName.trim().length > 0) {
|
||||
return toolName;
|
||||
}
|
||||
const legacyToolName = (msg as { tool_name?: unknown }).tool_name;
|
||||
return typeof legacyToolName === "string" && legacyToolName.trim().length > 0
|
||||
? legacyToolName
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function isReadToolResultMessage(msg: AgentMessage): boolean {
|
||||
return isToolResultMessage(msg) && getToolResultName(msg) === "read";
|
||||
}
|
||||
|
||||
function truncateTextToBudget(text: string, maxChars: number): string {
|
||||
if (text.length <= maxChars) {
|
||||
return text;
|
||||
@@ -278,6 +293,65 @@ function resolveToolResultCompactionOrder(messages: AgentMessage[]): number[] {
|
||||
return [...olderIndexes, newestIndex];
|
||||
}
|
||||
|
||||
function getNewestToolResultIndex(messages: AgentMessage[]): number | undefined {
|
||||
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
||||
if (isToolResultMessage(messages[i])) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldPreferOverflowForLatestRead(params: {
|
||||
messages: AgentMessage[];
|
||||
contextBudgetChars: number;
|
||||
maxSingleToolResultChars: number;
|
||||
}): boolean {
|
||||
const newestToolResultIndex = getNewestToolResultIndex(params.messages);
|
||||
if (newestToolResultIndex === undefined) {
|
||||
return false;
|
||||
}
|
||||
const newestToolResult = params.messages[newestToolResultIndex];
|
||||
if (!isReadToolResultMessage(newestToolResult)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const initialCache = createMessageCharEstimateCache();
|
||||
if (
|
||||
estimateMessageCharsCached(newestToolResult, initialCache) > params.maxSingleToolResultChars
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const simulatedMessages = cloneMessagesForGuard(params.messages);
|
||||
const estimateCache = createMessageCharEstimateCache();
|
||||
for (const message of simulatedMessages) {
|
||||
if (!isToolResultMessage(message)) {
|
||||
continue;
|
||||
}
|
||||
const truncated = truncateToolResultToChars(
|
||||
message,
|
||||
params.maxSingleToolResultChars,
|
||||
estimateCache,
|
||||
);
|
||||
applyMessageMutationInPlace(message, truncated, estimateCache);
|
||||
}
|
||||
|
||||
const currentChars = estimateContextChars(simulatedMessages, estimateCache);
|
||||
if (currentChars <= params.contextBudgetChars) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newestToolResultAfterPerToolLimit = simulatedMessages[newestToolResultIndex];
|
||||
const newestToolResultTextBefore = getToolResultText(newestToolResultAfterPerToolLimit);
|
||||
compactExistingToolResultsInPlace({
|
||||
messages: simulatedMessages,
|
||||
charsNeeded: currentChars - params.contextBudgetChars,
|
||||
cache: estimateCache,
|
||||
});
|
||||
return getToolResultText(simulatedMessages[newestToolResultIndex]) !== newestToolResultTextBefore;
|
||||
}
|
||||
|
||||
function cloneMessagesForGuard(messages: AgentMessage[]): AgentMessage[] {
|
||||
return messages.map(
|
||||
(msg) => ({ ...(msg as unknown as Record<string, unknown>) }) as unknown as AgentMessage,
|
||||
@@ -388,6 +462,15 @@ export function installToolResultContextGuard(params: {
|
||||
: messages;
|
||||
|
||||
const sourceMessages = Array.isArray(transformed) ? transformed : messages;
|
||||
if (
|
||||
shouldPreferOverflowForLatestRead({
|
||||
messages: sourceMessages,
|
||||
contextBudgetChars,
|
||||
maxSingleToolResultChars,
|
||||
})
|
||||
) {
|
||||
throw new Error(PREEMPTIVE_CONTEXT_OVERFLOW_MESSAGE);
|
||||
}
|
||||
const contextMessages = contextNeedsToolResultCompaction({
|
||||
messages: sourceMessages,
|
||||
contextBudgetChars,
|
||||
|
||||
Reference in New Issue
Block a user