Skip to content
8 changes: 7 additions & 1 deletion packages/agent/src/harness/compaction/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ export interface CompactionSettings {
reserveTokens: number;
/** Approximate recent-context tokens to keep after compaction. */
keepRecentTokens: number;
/** Explicit active context ceiling override. */
maxContextTokens?: number;
Comment thread
codeg-dev marked this conversation as resolved.
}

/** Default compaction settings used by the harness. */
Expand Down Expand Up @@ -246,7 +248,11 @@ export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEst
/** Return whether context usage exceeds the configured compaction threshold. */
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
if (!settings.enabled) return false;
return contextTokens > contextWindow - settings.reserveTokens;
const activeCeiling =
settings.maxContextTokens && settings.maxContextTokens > 0
? Math.min(contextWindow - settings.reserveTokens, settings.maxContextTokens)
: contextWindow - settings.reserveTokens;
return contextTokens > activeCeiling;
}

const ESTIMATED_IMAGE_CHARS = 4800;
Expand Down
1 change: 0 additions & 1 deletion packages/ai/src/api/google-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -499,7 +499,6 @@ export function mapStopReason(reason: FinishReason): StopReason {
case FinishReason.LANGUAGE:
case FinishReason.MALFORMED_FUNCTION_CALL:
case FinishReason.UNEXPECTED_TOOL_CALL:
case FinishReason.TOO_MANY_TOOL_CALLS:
case FinishReason.NO_IMAGE:
return "error";
default: {
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/src/core/agent-session.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5453,7 +5453,7 @@ export class AgentSession {
usageMessage?.role === "assistant" && this._isAssistantFromBeforeLatestCompaction(usageMessage)
? estimateMessagesTokens(providerMessages)
: estimate.tokens;
return contextTokens > model.contextWindow - settings.reserveTokens;
return shouldCompact(contextTokens, model.contextWindow, settings);
};

if (!settings.enabled || !isOversized()) return;
Expand Down
8 changes: 7 additions & 1 deletion packages/coding-agent/src/core/compaction/compaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ export interface CompactionSettings {
enabled: boolean;
reserveTokens: number;
keepRecentTokens: number;
maxContextTokens?: number;
speculativeEnabled?: boolean;
speculativeFraction?: number;
speculativeCooldownMs?: number;
Expand Down Expand Up @@ -339,7 +340,12 @@ export function estimateContextTokens(messages: AgentMessage[]): ContextUsageEst
*/
export function shouldCompact(contextTokens: number, contextWindow: number, settings: CompactionSettings): boolean {
if (!settings.enabled) return false;
return contextTokens > contextWindow - settings.reserveTokens;
const isLarge = contextWindow >= 500_000;
const defaultCeiling = isLarge ? 384_000 : Math.max(0, contextWindow - settings.reserveTokens);
const configuredCeiling =
settings.maxContextTokens && settings.maxContextTokens > 0 ? settings.maxContextTokens : defaultCeiling;
const maxActive = Math.min(Math.max(0, contextWindow - settings.reserveTokens), configuredCeiling);
Comment thread
codeg-dev marked this conversation as resolved.
return contextTokens > maxActive;
}

// ============================================================================
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,19 +144,59 @@ export const BUILTIN_CONTEXT_REDUCTION_OPTIONS: ReduceContextOptions = {

export const BUILTIN_CONTEXT_REDUCTION_GATE_RATIO = 0.5;

export interface ContextPressure {
activeTokens: number | null;
requestBodyBytes: number | null;
nonReclaimableTokens?: number | null;
generation: number;
}

export interface ContextReductionLatch {
isLatched: () => boolean;
engage: () => void;
release: () => void;
getGeneration: () => number;
bumpGeneration: () => void;
}

export function createContextReductionLatch(): ContextReductionLatch {
let latched = false;
let generation = 0;
return {
isLatched: () => latched,
engage: () => {
latched = true;
},
release: () => {
latched = false;
},
getGeneration: () => generation,
bumpGeneration: () => {
generation += 1;
latched = false;
},
};
}

export interface ShouldApplyContextReductionInput {
usageTokens: number | null;
contextWindow: number;
gateRatio?: number;
isProviderNativeCompactionPath?: boolean;
latch?: ContextReductionLatch;
}

export function shouldApplyContextReduction(input: ShouldApplyContextReductionInput): boolean {
const gate = input.gateRatio ?? BUILTIN_CONTEXT_REDUCTION_GATE_RATIO;
if (input.isProviderNativeCompactionPath === true) return false;
if (input.latch?.isLatched()) return true;
if (input.usageTokens === null) return false;
if (input.contextWindow <= 0) return false;
return input.usageTokens >= input.contextWindow * gate;
const gate = input.gateRatio ?? BUILTIN_CONTEXT_REDUCTION_GATE_RATIO;
const shouldEngage = input.usageTokens >= input.contextWindow * gate;
if (shouldEngage && input.latch) {
input.latch.engage();
}
return shouldEngage;
}

function approxTextTokens(text: string): number {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import * as checkpointState from "./checkpoint-state.ts";
import * as breaker from "./circuit-breaker.ts";
import {
BUILTIN_CONTEXT_REDUCTION_OPTIONS,
createContextReductionLatch,
reduceContextMessages,
shouldApplyContextReduction,
} from "./context-reduction.ts";
Expand Down Expand Up @@ -193,6 +194,7 @@ export default function compactionExtension(
const lanePolicy = createCompactionLanePolicy();
const restorationDirectiveState = checkpointState.createRestorationDirectiveState();
const emergencyPruneLatch = createEmergencyPruneLatch();
const contextReductionLatch = createContextReductionLatch();
const degradationState = createDegradationMonitorState();
const restorationState = state.restoration ?? restoration.createRestorationTrackerState();
state = { ...state, restoration: restorationState };
Expand Down Expand Up @@ -717,6 +719,7 @@ export default function compactionExtension(
});

pi.on("model_select", (event, ctx) => {
contextReductionLatch.bumpGeneration();
if (lanePolicy.disablesSenpiCompaction(ctx)) {
invalidateSpeculativeCompaction(ctx);
return;
Expand Down Expand Up @@ -746,10 +749,15 @@ export default function compactionExtension(
}
});

pi.on("session_tree", () => {
contextReductionLatch.bumpGeneration();
});

pi.on("session_compact", async (event: SessionCompactEvent, ctx) => {
const compactEvent = event;
invalidateSpeculativeCompaction(ctx);
if (compactEvent.accepted) {
contextReductionLatch.bumpGeneration();
persistAcceptedMetadata(compactEvent.requestId);
const branchEntries = ctx.sessionManager.getBranch();
const firstKeptIndex = branchEntries.findIndex(
Expand Down Expand Up @@ -874,6 +882,7 @@ export default function compactionExtension(
contextWindow,
isProviderNativeCompactionPath:
isOpenAiRemoteCompactionModel(ctx.model) || lanePolicy.disablesSenpiCompaction(ctx),
latch: contextReductionLatch,
Comment thread
codeg-dev marked this conversation as resolved.
Comment thread
codeg-dev marked this conversation as resolved.
})
? reduceContextMessages(event.messages, BUILTIN_CONTEXT_REDUCTION_OPTIONS).messages
: event.messages;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,62 @@ const YIELD_ADJUSTMENT_RATIO = 0.05;
const MIN_EFFECTIVE_KEEP_RECENT_TOKENS = 1024;

export const SPECULATIVE_FRACTION = 0.75;
export const DEFAULT_1M_CEILING = 384_000;
export const DEFAULT_1M_KEEP_RECENT = 35_000;
export const DEFAULT_STANDARD_KEEP_RECENT = 20_000;
export const DEFAULT_WARMUP_FRACTION = 0.75;
export const DEFAULT_TARGET_ACTIVE_FRACTION = 0.6;
export const DEFAULT_RESERVE_TOKENS = 16_384;
export const LARGE_WINDOW_THRESHOLD = 500_000;

export interface ContextBudgetPolicy {
physicalContextWindow: number;
maxActiveContextTokens: number;
keepRecentTokens: number;
warmupFraction: number;
targetActiveFraction: number;
reserveTokens: number;
emergencyHardLimitTokens: number;
}

export interface CompactionYield {
savedTokens: number;
tokensBefore: number;
}

export function isLargeContextModel(contextWindow: number): boolean {
return contextWindow >= LARGE_WINDOW_THRESHOLD;
}

export function resolveContextBudgetPolicy(
contextWindow: number,
settings?: Partial<CompactionSettings>,
): ContextBudgetPolicy {
const isLarge = isLargeContextModel(contextWindow);
const reserveTokens = settings?.reserveTokens ?? DEFAULT_RESERVE_TOKENS;
const defaultCeiling = isLarge ? DEFAULT_1M_CEILING : Math.max(0, contextWindow - reserveTokens);
const configuredCeiling =
settings?.maxContextTokens && settings.maxContextTokens > 0 ? settings.maxContextTokens : defaultCeiling;
Comment thread
codeg-dev marked this conversation as resolved.

const maxActiveContextTokens = Math.min(Math.max(0, contextWindow - reserveTokens), configuredCeiling);

const defaultKeepRecent = isLarge ? DEFAULT_1M_KEEP_RECENT : DEFAULT_STANDARD_KEEP_RECENT;
const keepRecentTokens = settings?.keepRecentTokens ?? defaultKeepRecent;
const warmupFraction = settings?.speculativeFraction ?? DEFAULT_WARMUP_FRACTION;
const targetActiveFraction = DEFAULT_TARGET_ACTIVE_FRACTION;
const emergencyHardLimitTokens = Math.max(0, contextWindow - Math.floor(reserveTokens / 2));

return {
physicalContextWindow: contextWindow,
maxActiveContextTokens,
keepRecentTokens,
warmupFraction,
targetActiveFraction,
reserveTokens,
emergencyHardLimitTokens,
};
}

function clampThresholdRatio(ratio: number): number {
return Math.min(MAX_ADAPTIVE_THRESHOLD_RATIO, Math.max(MIN_ADAPTIVE_THRESHOLD_RATIO, ratio));
}
Expand Down Expand Up @@ -84,14 +134,34 @@ export function computeEffectiveThreshold(contextWindow: number, lastYield?: Com
return clampThresholdRatio(ratio);
}

export function computeEffectiveBlockingThresholdTokens(
contextWindow: number,
settings?: Partial<CompactionSettings>,
lastYield?: CompactionYield | number,
): number {
const ratio = computeEffectiveThreshold(contextWindow, lastYield);
const ratioTokens = Math.floor(contextWindow * ratio);
if (isLargeContextModel(contextWindow) || (settings?.maxContextTokens && settings.maxContextTokens > 0)) {
const policy = resolveContextBudgetPolicy(contextWindow, settings);
return Math.min(ratioTokens, policy.maxActiveContextTokens);
Comment thread
codeg-dev marked this conversation as resolved.
}
return ratioTokens;
}

export function computeEffectiveKeepRecentTokens(
setting: number,
setting: number | undefined,
contextWindow: number,
thresholdRatio: number,
margin = 0.05,
): number {
const isLarge = isLargeContextModel(contextWindow);
const defaultForModel = isLarge ? DEFAULT_1M_KEEP_RECENT : DEFAULT_STANDARD_KEEP_RECENT;
const effectiveSetting =
typeof setting === "number" && setting > 0 && !(isLarge && setting === DEFAULT_STANDARD_KEEP_RECENT)
? setting
: defaultForModel;
Comment thread
codeg-dev marked this conversation as resolved.
Outdated
const capped = Math.floor(contextWindow * (1 - thresholdRatio - margin));
return Math.min(setting, Math.max(MIN_EFFECTIVE_KEEP_RECENT_TOKENS, capped));
return Math.min(effectiveSetting, Math.max(MIN_EFFECTIVE_KEEP_RECENT_TOKENS, capped));
}

export function shouldStartSpeculativeCompaction(
Expand All @@ -104,8 +174,10 @@ export function shouldStartSpeculativeCompaction(
return false;
}

const fraction = settings.speculativeFraction ?? SPECULATIVE_FRACTION;
return usage.tokens >= contextWindow * computeEffectiveThreshold(contextWindow, lastYield) * fraction;
const policy = resolveContextBudgetPolicy(contextWindow, settings);
const blockingThreshold = computeEffectiveBlockingThresholdTokens(contextWindow, settings, lastYield);
const warmupThreshold = Math.floor(blockingThreshold * policy.warmupFraction);
return usage.tokens >= warmupThreshold;
}

export function isAtHardLimit(
Expand All @@ -127,5 +199,6 @@ export function shouldTriggerCompaction(
return false;
}

return usage.tokens >= contextWindow * computeEffectiveThreshold(contextWindow, lastYield);
const blockingThreshold = computeEffectiveBlockingThresholdTokens(contextWindow, settings, lastYield);
return usage.tokens >= blockingThreshold;
}
5 changes: 4 additions & 1 deletion packages/coding-agent/src/core/settings-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const DEFAULT_PROVIDER_STREAM_RETRY_TIMEOUT_MS = 30_000;
export interface CompactionSettings {
enabled?: boolean; // default: true
reserveTokens?: number; // default: 16384
keepRecentTokens?: number; // default: 20000
keepRecentTokens?: number; // default: 20000 (standard), 35000 (1M)
maxContextTokens?: number; // default: undefined (uses 384k for 1M models)
Comment thread
codeg-dev marked this conversation as resolved.
Outdated
speculativeEnabled?: boolean; // default: true
speculativeFraction?: number; // default: 0.75
speculativeCooldownMs?: number; // default: 30000
Expand Down Expand Up @@ -1270,6 +1271,7 @@ export class SettingsManager {
enabled: boolean;
reserveTokens: number;
keepRecentTokens: number;
maxContextTokens?: number;
speculativeEnabled: boolean;
speculativeFraction: number;
speculativeCooldownMs: number;
Expand All @@ -1284,6 +1286,7 @@ export class SettingsManager {
enabled: this.getCompactionEnabled(),
reserveTokens: this.getCompactionReserveTokens(),
keepRecentTokens: this.getCompactionKeepRecentTokens(),
maxContextTokens: this.settings.compaction?.maxContextTokens,
speculativeEnabled: this.settings.compaction?.speculativeEnabled ?? true,
speculativeFraction: this.settings.compaction?.speculativeFraction ?? 0.75,
speculativeCooldownMs: this.settings.compaction?.speculativeCooldownMs ?? 30000,
Expand Down
Loading