feat(agents): compact agent history when it outgrows the context window - #3455
Open
cf3901646 wants to merge 1 commit into
Open
feat(agents): compact agent history when it outgrows the context window#3455cf3901646 wants to merge 1 commit into
cf3901646 wants to merge 1 commit into
Conversation
McpClient replays the whole conversation on every turn, so a long session eventually exceeds the model's context window and every request from then on fails. Add dimos/agents/compaction.py, which drops the oldest messages and replaces them with a summary once the history passes a configurable fraction of the window: - resolve_context_window() maps a model name to its window by longest prefix, stripping any provider: prefix, with a conservative default - estimate_tokens() is a cheap offline estimator that charges images heavily so image-carrying histories compact early - compact_history() is pure and model-free, so it is testable offline; summarisation is injected and falls back to an offline digest - tool results are never separated from the call that produced them, since an orphaned ToolMessage is rejected by the OpenAI API - a leading system message is never dropped Wired into McpClient behind McpClientConfig, on by default. Compaction failures are logged and the turn proceeds uncompacted, so a bug here can never fail a turn that would otherwise have worked. 40 unit tests, including a 300-turn tool-heavy session that asserts the history never exceeds the window and that the newest turn survives. Refs dimensionalOS#1899 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
cf3901646
requested review from
Dreamsorcerer,
leshy,
mustafab0,
paul-nechifor and
spomichter
as code owners
August 13, 2026 03:42
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.
Refs #1899
Problem
McpClient._process_messageappends every message toself._historyand replays the whole list on each turn:Nothing bounds that list. Tool-heavy sessions grow it fastest, because each command adds a human turn, an
AIMessagecarrying the tool call, and aToolMessagecarrying the full result. Once the history passes the model's context window the request fails, and because the history is never trimmed, every subsequent turn fails too. The session is dead until someone restarts it.Approach
New module
dimos/agents/compaction.py. Once the history passes a configurable fraction of the context window, the oldest messages are dropped and replaced by a single summary message; recent turns are kept verbatim.compact_history()is pure — it takes a list of messages and returns a new one, and takes no model. Summarisation is injected via asummarizercallable, which is what makes the whole thing testable offline.Three details that drove the design:
Tool-call integrity. Cutting the history at an arbitrary index can leave a
ToolMessagewhose originatingAIMessagewas dropped. The OpenAI API rejects an orphaned tool result, so naive truncation trades a context-overflow error for a 400._split_indexadvances the cut past any leadingToolMessage, so a dropped tool call always takes its results with it. Two tests cover this, including parallel tool calls.Compaction must not re-trigger every turn.
keep_ratiohas to be meaningfully belowtrigger_ratioor every turn pays for a summarisation. This is validated at the API boundary rather than left as a footgun, andtest_repeated_compaction_is_stableasserts a compacted history is not immediately re-compacted.Compaction must never fail a turn. A summarizer failure falls back to the offline digest, and a failure inside compaction itself is logged and the turn proceeds uncompacted. A bug here should degrade the session, not kill it.
Also: a leading
SystemMessageis treated as instruction rather than conversation and is never dropped, andestimate_tokenscharges images a flat 1500 tokens so image-carrying histories compact early rather than late.Context windows
resolve_context_window()maps a model name to its window by longest-prefix match, stripping anyprovider:prefix soollama:llama3.1:8bresolves viallama3.1. Longest-prefix matters:gpt-4.1must not resolve through thegpt-4entry and get an 8k window. Unknown models fall back to a conservative 128k — over-compacting costs some fidelity, under-compacting fails the request.Configuration
On
McpClientConfig, enabled by default:compaction_enabledTruecontext_windowNone(resolved frommodel)compaction_trigger_ratio0.8compaction_keep_ratio0.35compaction_summarize_with_modelTrueChecks I ran
dimos/agents/test_compaction.py— 40 tests, all passing.ruff checkandruff format --checkclean on all four changed files. One pre-existingBLE001remains inmcp_client.pyat thedispatch_continuationhandler; it is untouched by this PR and present onmain.Honest limitations:
uv run pytestsuite in my environment, so the existingMcpClienttests are unverified against this change. The integration is small and additive — one config block, one guarded method, one call site beforestate_graph.stream— but it does deserve a CI run.The issue asks for this to be "tested to see how well it works in practice with real commands".
test_a_long_tool_heavy_session_stays_within_budgetis my stand-in: it drives 300 command turns through the real compaction path and asserts the history never exceeds the window and the newest turn survives intact. That is a simulation of the shape of the load, not a substitute for running it against a live model, which I could not do here.Notes
compaction_summarize_with_modeldefaults toTrue, which spends an extra model call each time compaction fires. If you would rather compaction be free by default, flipping it toFalseuses the offline digest and needs no other change.AI_POLICY.md: this was written with AI assistance. Happy to walk through any part of it, and equally happy to adjust the design if you would rather compaction live somewhere other thanMcpClient.