Repository navigation
feat(coding-agent): trigger auto-compaction at a configurable context ratio - #326
Merged
Merged
Conversation
Add an optional beforeModelRequest hook to AgentLoopConfig, AgentOptions, and Agent. The loop calls it once before every provider request: phase "initial" for the first request of a prompt or continue(), "follow-up" for each later request. It runs after injected steering, so the context it receives is exactly what will be sent, and it may replace the context, model, or thinking level. Tool-call markup resampling does not call it again. On follow-up requests that have not received steering yet, the loop polls steering once more after the hook (unless aborted), so input queued during long request preparation such as compaction reaches the same request while one-at-a-time delivery stays one message per turn. prepareNextTurn keeps its existing timing and semantics.
… ratio Automatic compaction previously fired at contextWindow - reserveTokens (16384 by default), which triggers at ~74% of a 64K window but ~98% of a 1M window. It was checked at four places with three token measurements, missed the first request of a run and continue() outside CLM mode, and compacted right after a final answer the user might never follow up. Trigger policy (v2, default): - Threshold is floor(contextWindow * triggerRatio), default 0.9 for every model. compaction.triggerRatio (0.5-0.95) and compaction.modelTriggerRatios["provider/id"] configure it; tokenLimit can only trigger earlier. An explicit reserveTokens smaller than the window still caps it at contextWindow - reserveTokens. Invalid values fall back to defaults and are reported as settings diagnostics. - One checkpoint before every model request through the new agent-core beforeModelRequest hook (CLM maintenance first, native fallback). The pre-run check still measures the incoming prompt; the hook re-measures the first request when it no longer matches what was checked (steering queued meanwhile, agent_start listeners, model object replaced). - A final answer no longer compacts on its own; only overflow and truncated-length recovery run after a response. - Usage reported before the latest compaction is ignored; without trustworthy usage the estimate includes the system prompt and tools. - Three consecutive automatic failures pause automatic compaction (and stop measuring) for the session. Cancellations and errors raised after a compaction was saved do not count. A successful compaction, accepted CLM edit, model switch, or context-window change resumes it. - Unexpected errors while measuring or preparing maintenance fail open: they are recorded as step-compaction-failure entries, counted, and the request is sent unchanged. compaction.triggerPolicy = "legacy" restores the previous threshold, checkpoints, and measurement. The interactive UI restores the working indicator when a compaction ends inside an active agent run.
liuedcson
requested review from
MelodyVAR,
Uking-xxx and
ZouR-Ma
as code owners
October 29, 2026 09:56
Bring in the summary-budget change from main. Summary requests no longer derive an output cap from reserveTokens, so reserveTokens now only participates in trigger decisions: the legacy policy threshold, and an explicit cap on the v2 threshold. Resolve the settings and compaction integrity documentation on that basis and drop comments that still described reserveTokens as a summary output budget.
…mall windows Providers clamp each request's max_tokens to the remaining context minus a 4096-token safety margin. With the default 0.9 trigger ratio, a small window leaves less than that margin before compaction fires: on a 32K window about 3.3K tokens remain, so output is clamped to almost nothing and every turn near the threshold falls into length recovery. Cap the v2 threshold at contextWindow - CONTEXT_SAFETY_TOKENS - min(model.maxTokens, 8192), never below half the window. With the default ratio this gives 20,480 tokens (62.5%) for 32K, 53,248 (81.25%) for 64K and leaves 128K and larger windows at 90%. A lower configured ratio, tokenLimit, or explicit reserveTokens can still trigger earlier. Export CONTEXT_SAFETY_TOKENS from @step-harness/providers so the trigger and the request clamp share one value.
Summary serialization omits provider thinking blocks since the summary input change; update both examples to match and note that stored messages keep them. Also remove a stray blank line in the coding-agent changelog.
Replace comparison sections and references to other agent products with descriptions of StepCode's own behavior, design rationale, and follow-ups. Compatibility notes for importing external configuration, provider names, and license attributions are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Automatic compaction timing was hard to predict and sometimes wrong:
contextWindow - reserveTokens(16384 by default): ~74% of a 64K window, ~92% of 200K, ~98.4% of 1M. Large-window models compacted with almost no room for estimation error.prompt(),_runAgentPrompt,prepareNextTurn,_handlePostAgentRun) with three different token measurements. Outside CLM mode, the first request of a run and the first request afteragent.continue()were never checked before sending.Changes
agent-core — new optional
beforeModelRequestloop hook (AgentLoopConfig,AgentOptions,Agent). Called once before every provider request (phase: "initial" | "follow-up"), after injected steering, so it sees exactly what will be sent; it may replace context/model/thinking. Tool-call resampling does not call it again. On follow-up requests without injected steering, the loop polls steering once more after the hook (not when aborted), so input queued during compaction reaches the same request while one-at-a-time delivery stays one message per turn.prepareNextTurnis unchanged.coding-agent — trigger policy v2 (default):
trigger-policy.ts):triggerRatiois 0.9. Providers clampmax_tokenstocontextWindow - input - CONTEXT_SAFETY_TOKENS(4096), so on a small window 10% headroom is smaller than that margin and output would be clamped to almost nothing before compaction fires. The second term keeps the safety margin plus up to 8192 output tokens, never below half the window.CONTEXT_SAFETY_TOKENSis now exported from@step-harness/providersso the trigger and the request clamp share one value.compaction.triggerRatio(0.5–0.95),compaction.modelTriggerRatios["provider/id"],compaction.tokenLimit(only earlier),compaction.triggerPolicy("v2"|"legacy"). Invalid values fall back and are surfaced as settings diagnostics.AgentSessioninstallsbeforeModelRequestand runs CLM maintenance first, native compaction as fallback. The pre-run check in_runAgentPromptstill measures the incoming prompt (so CLM can protect it); the hook re-measures the first request if it differs from what was checked (steering queued meanwhile,agent_startlisteners, model object replaced).prompt()/_handlePostAgentRunkeep only overflow and truncated-length recovery; no threshold compaction after a final answer./compact), accepted CLM edit, model switch, or context-window change resumes it.step-compaction-failure, counted, request sent unchanged. Legacy and cancellation still propagate.cli — restore the working indicator when compaction ends inside an active run (compaction now happens after
turn_start).docs — besides the trigger documentation, this branch updates the summary-serialization examples after #325 (thinking is omitted from summary input) and rewrites several design and lifecycle documents to describe StepCode's own behavior and follow-ups instead of comparisons with other agent products.
Commits:
feat(agent-core): the hook, reviewable alone.feat(coding-agent): trigger policy v2.main(fix(compaction): align summary inputs and output budgets with upstream #325): summary requests no longer derive an output cap fromreserveTokens; documentation conflicts resolved on that basis.fix(coding-agent): response-room ceiling for small windows.docs(compaction)anddocs: documentation follow-ups described above.Behavior and compatibility
Changed default trigger (with an output limit of 8K or more):
Escape paths.
compaction.triggerPolicy = "legacy"restores the previous threshold, checkpoints (incl. compaction after a final answer), and measurement. An explicitcompaction.reserveTokenssmaller than the window still caps the v2 threshold atcontextWindow - reserveTokens, so users who tuned it keep that bound. A lowertriggerRatioortokenLimitcan always trigger earlier; a higher ratio cannot exceed the small-window ceiling.Summary output. Since fix(compaction): align summary inputs and output budgets with upstream #325, summary requests use the provider's normal output sizing;
reserveTokensonly participates in trigger decisions.Timing / events. Mid-run threshold compaction now emits
compaction_start/compaction_endafterturn_start(previously before). Event shapes andreasonvalues are unchanged. Steering typed during a follow-up compaction is delivered to the same request.Public interface. New optional
beforeModelRequeston agent-core; newCONTEXT_SAFETY_TOKENSexport from providers; existing consumers unaffected.AgentSessionchains any previously installed hook and merges partial updates with the compacted context.Persisted data. No session format change. Unexpected maintenance faults add
step-compaction-failurecustom entries (existing type).Validation
pnpm run check— pass after the merge and small-window fix.git diff --checkandcheck:public-boundary— pass.packages/coding-agent(after merge): trigger policy, session trigger, end-to-end simulation, auto-CLM, live context, and the fix(compaction): align summary inputs and output budgets with upstream #325 compaction regressions — 206 passed, 2 skipped.packages/providers:openai-completions-thinking-token-budget,faux-provider— 34 passed.packages/agent-core545 passed / 1 skipped;apps/cli749 passed;packages/coding-agent5098 passed / 58 skipped, 6 failures intest/completion-check-cli.test.tsthat also fail on a cleanmainin that environment (the test overridesHOME, which breaks a$HOME-relative localgitwrapper). CI is the authoritative full run.agent-core/test/agent-loop.test.ts: hook per request with full outgoing context, continuation phase, steering queued during the hook reaches the same request with one-at-a-time preserved.coding-agent/test/compaction-trigger-policy.test.ts: threshold table for 8K–1M windows, small-output models, the response-room invariant at the threshold, per-model / tokenLimit / legacy reserve, validation + diagnostics.coding-agent/test/suite/agent-session-compaction-trigger.test.ts(12): mid-loop compaction, no compaction after a final answer, custom ratio, breaker pause/resume, post-commit errors not counted, chained hook partial update, steering during pre-run compaction,agent_startsteering, same-id model refresh, fail-open measurement faults (native + CLM).coding-agent/test/suite/compaction-trigger-simulation.test.ts(9 end-to-end scenarios with a scripted faux model): long tool loop, multi-prompt chat, huge paste, steering during compaction, summarizer outage → pause →/compactresume, switch to a smaller-window model, per-model ratio vs tokenLimit, legacy, disabled.apps/cli/test/session-events-working-tracker.test.ts: working indicator restored only while streaming.Limitations
triggerRatiois the knob if some model shows more truncation.reserveTokensdefaults (16384 in settings vs 24576 inDEFAULT_COMPACTION_SETTINGS); background/idle pre-compaction.compaction_start(e.g. summarizer auth resolution) keep their existing silent behavior.