Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 12 additions & 6 deletions src/background/agent/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ const MAX_EMPTY_LLM_ATTEMPTS = 2;
// ---------------------------------------------------------------------------

const DEFAULT_CONTEXT_WINDOW = 262000;
const RESERVE_TOKENS = 16000; // Reserved for the response
const RESERVE_TOKENS = 16000; // Minimum reserved for the response
const KEEP_RECENT_TOKENS = 40000; // Recent tokens retained during compaction
const TOOL_RESULT_RECENT_ROUNDS = 2;
const IMAGE_MESSAGE_RECENT_LIMIT = 8;
Expand Down Expand Up @@ -1479,7 +1479,7 @@ export class Agent {
messageTokens,
inputTokens: tokens,
contextWindow: this._contextWindow(),
reserveTokens: RESERVE_TOKENS,
reserveTokens: this._reserveTokens(),
maxKeepTokens: KEEP_RECENT_TOKENS,
force,
retainLatestRound: trigger === "manual" || trigger === "slash_command",
Expand All @@ -1489,7 +1489,7 @@ export class Agent {
this._traceContextEstimate("compact_check", contextEstimate, {
compactThreshold: plan.threshold,
contextWindow: this._contextWindow(),
reserveTokens: RESERVE_TOKENS,
reserveTokens: this._reserveTokens(),
toolSchemaCount: Array.isArray(toolSchemas) ? toolSchemas.length : 0,
storedMessageCount: this.messages.length,
sendMessageCount: contextEstimate.messageCount,
Expand Down Expand Up @@ -1584,7 +1584,7 @@ export class Agent {
this._traceContextEstimate("compact_after", updatedEstimate, {
compactThreshold: this._compactThreshold(),
contextWindow: this._contextWindow(),
reserveTokens: RESERVE_TOKENS,
reserveTokens: this._reserveTokens(),
toolSchemaCount: Array.isArray(toolSchemas) ? toolSchemas.length : 0,
storedMessageCount: this.messages.length,
boundaryId: this._currentContextBoundaryId(),
Expand Down Expand Up @@ -2190,7 +2190,7 @@ export class Agent {
snapshot = {
contextUsageId: this._makeSpanId("ctx", ++this._contextUsageSeq),
contextWindow,
reserveTokens: RESERVE_TOKENS,
reserveTokens: this._reserveTokens(),
threshold: this._compactThreshold(),
inputTokensEstimate: Math.ceil(estimate),
messageCount: Math.max(0, Number(extra.messageCount) || 0),
Expand Down Expand Up @@ -2257,7 +2257,13 @@ export class Agent {
}

_compactThreshold(runProfile = this._runProfile) {
return compactThreshold(this._contextWindow(runProfile), RESERVE_TOKENS);
return compactThreshold(this._contextWindow(runProfile), this._reserveTokens(runProfile));
}

/** Room left for the response: at least the max_tokens each request asks for. */
_reserveTokens(runProfile = this._runProfile) {
const maxOutputTokens = Number(runProfile?.model?.maxOutputTokens) || 0;
return Math.max(RESERVE_TOKENS, maxOutputTokens);
}

_makeSpanId(kind: string, id: unknown) {
Expand Down
29 changes: 29 additions & 0 deletions tests/unit/test-compaction-reserve.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import assert from "node:assert/strict";

globalThis.chrome ||= {
storage: { local: { get: async () => ({}), set: async () => {} }, onChanged: { addListener() {} } },
runtime: { id: "test", getManifest: () => ({ version: "0" }) },
};

const { Agent } = await import("../../src/background/agent/agent.ts");

function agentWith(model) {
return Object.assign(Object.create(Agent.prototype), { _runProfile: { model }, _runtimeConfig: {} });
}

// Compaction must leave room for the max_tokens the request will ask for, or a strict
// OpenAI-compatible server rejects input + max_tokens > context window.
for (const model of [
{ contextWindow: 131072, maxOutputTokens: 32768 }, // custom model: default output limit
{ contextWindow: 200000, maxOutputTokens: 64000 },
{ contextWindow: 128000, maxOutputTokens: 8192 },
]) {
const threshold = agentWith(model)._compactThreshold();
assert.ok(threshold + model.maxOutputTokens <= model.contextWindow,
`compaction threshold ${threshold} + max output ${model.maxOutputTokens} exceeds window ${model.contextWindow}`);
}

// Small output limits keep the existing 16,000-token floor.
assert.equal(agentWith({ contextWindow: 128000, maxOutputTokens: 8192 })._compactThreshold(), 128000 - 16000);

console.log("compaction reserve tests passed");