fix(agent): protect bootstrap prefix from pruning
This commit is contained in:
committed by
Peter Steinberger
parent
bf00b733c9
commit
5ddf9b2c65
@@ -837,6 +837,7 @@ This is intended to reduce token usage for chatty agents that accumulate large t
|
|||||||
High level:
|
High level:
|
||||||
- Never touches user/assistant messages.
|
- Never touches user/assistant messages.
|
||||||
- Protects the last `keepLastAssistants` assistant messages (no tool results after that point are pruned).
|
- Protects the last `keepLastAssistants` assistant messages (no tool results after that point are pruned).
|
||||||
|
- Protects the bootstrap prefix (nothing before the first user message is pruned).
|
||||||
- Modes:
|
- Modes:
|
||||||
- `adaptive`: soft-trims oversized tool results (keep head/tail) when the estimated context ratio crosses `softTrimRatio`.
|
- `adaptive`: soft-trims oversized tool results (keep head/tail) when the estimated context ratio crosses `softTrimRatio`.
|
||||||
Then hard-clears the oldest eligible tool results when the estimated context ratio crosses `hardClearRatio` **and**
|
Then hard-clears the oldest eligible tool results when the estimated context ratio crosses `hardClearRatio` **and**
|
||||||
|
|||||||
@@ -141,6 +141,43 @@ describe("context-pruning", () => {
|
|||||||
expect(toolText(findToolResult(next, "t4"))).toContain("w".repeat(20_000));
|
expect(toolText(findToolResult(next, "t4"))).toContain("w".repeat(20_000));
|
||||||
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
|
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("never prunes tool results before the first user message", () => {
|
||||||
|
const settings = computeEffectiveSettings({
|
||||||
|
mode: "aggressive",
|
||||||
|
keepLastAssistants: 0,
|
||||||
|
hardClear: { placeholder: "[cleared]" },
|
||||||
|
});
|
||||||
|
if (!settings) throw new Error("expected settings");
|
||||||
|
|
||||||
|
const messages: AgentMessage[] = [
|
||||||
|
makeAssistant("bootstrap tool calls"),
|
||||||
|
makeToolResult({
|
||||||
|
toolCallId: "t0",
|
||||||
|
toolName: "read",
|
||||||
|
text: "x".repeat(20_000),
|
||||||
|
}),
|
||||||
|
makeAssistant("greeting"),
|
||||||
|
makeUser("u1"),
|
||||||
|
makeToolResult({
|
||||||
|
toolCallId: "t1",
|
||||||
|
toolName: "bash",
|
||||||
|
text: "y".repeat(20_000),
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
|
const next = pruneContextMessages({
|
||||||
|
messages,
|
||||||
|
settings,
|
||||||
|
ctx: { model: { contextWindow: 1000 } } as unknown as ExtensionContext,
|
||||||
|
isToolPrunable: () => true,
|
||||||
|
contextWindowTokensOverride: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toolText(findToolResult(next, "t0"))).toBe("x".repeat(20_000));
|
||||||
|
expect(toolText(findToolResult(next, "t1"))).toBe("[cleared]");
|
||||||
|
});
|
||||||
|
|
||||||
it("mode aggressive clears eligible tool results before cutoff", () => {
|
it("mode aggressive clears eligible tool results before cutoff", () => {
|
||||||
const messages: AgentMessage[] = [
|
const messages: AgentMessage[] = [
|
||||||
makeUser("u1"),
|
makeUser("u1"),
|
||||||
|
|||||||
@@ -153,6 +153,13 @@ function findAssistantCutoffIndex(
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function findFirstUserIndex(messages: AgentMessage[]): number | null {
|
||||||
|
for (let i = 0; i < messages.length; i++) {
|
||||||
|
if (messages[i]?.role === "user") return i;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
function softTrimToolResultMessage(params: {
|
function softTrimToolResultMessage(params: {
|
||||||
msg: ToolResultMessage;
|
msg: ToolResultMessage;
|
||||||
settings: EffectiveContextPruningSettings;
|
settings: EffectiveContextPruningSettings;
|
||||||
@@ -207,13 +214,20 @@ export function pruneContextMessages(params: {
|
|||||||
);
|
);
|
||||||
if (cutoffIndex === null) return messages;
|
if (cutoffIndex === null) return messages;
|
||||||
|
|
||||||
|
// Bootstrap safety: never prune anything before the first user message. This protects initial
|
||||||
|
// "identity" reads (SOUL.md, USER.md, etc.) which typically happen before the first inbound user
|
||||||
|
// message exists in the session transcript.
|
||||||
|
const firstUserIndex = findFirstUserIndex(messages);
|
||||||
|
const pruneStartIndex =
|
||||||
|
firstUserIndex === null ? messages.length : firstUserIndex;
|
||||||
|
|
||||||
const isToolPrunable =
|
const isToolPrunable =
|
||||||
params.isToolPrunable ?? makeToolPrunablePredicate(settings.tools);
|
params.isToolPrunable ?? makeToolPrunablePredicate(settings.tools);
|
||||||
|
|
||||||
if (settings.mode === "aggressive") {
|
if (settings.mode === "aggressive") {
|
||||||
let next: AgentMessage[] | null = null;
|
let next: AgentMessage[] | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < cutoffIndex; i++) {
|
for (let i = pruneStartIndex; i < cutoffIndex; i++) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
if (!msg || msg.role !== "toolResult") continue;
|
if (!msg || msg.role !== "toolResult") continue;
|
||||||
if (!isToolPrunable(msg.toolName)) continue;
|
if (!isToolPrunable(msg.toolName)) continue;
|
||||||
@@ -248,7 +262,7 @@ export function pruneContextMessages(params: {
|
|||||||
const prunableToolIndexes: number[] = [];
|
const prunableToolIndexes: number[] = [];
|
||||||
let next: AgentMessage[] | null = null;
|
let next: AgentMessage[] | null = null;
|
||||||
|
|
||||||
for (let i = 0; i < cutoffIndex; i++) {
|
for (let i = pruneStartIndex; i < cutoffIndex; i++) {
|
||||||
const msg = messages[i];
|
const msg = messages[i];
|
||||||
if (!msg || msg.role !== "toolResult") continue;
|
if (!msg || msg.role !== "toolResult") continue;
|
||||||
if (!isToolPrunable(msg.toolName)) continue;
|
if (!isToolPrunable(msg.toolName)) continue;
|
||||||
|
|||||||
Reference in New Issue
Block a user