fix(agent): keep smart routing from compressing session history - #7799
fix(agent): keep smart routing from compressing session history#7799Falicitas wants to merge 1 commit into
Conversation
b7a10f6 to
10f4d1a
Compare
|
Ready for review. Scope is intentionally minimal (fix A + A' on CLI only). Happy to iterate toward fix B (threshold override via the |
Smart model routing rebuilds AIAgent for temporary per-turn model
switches. The rebuild creates a new ContextCompressor bound to the
cheap model's context_length, so preflight compression compares
against the cheap threshold and compresses session history that was
sized for the primary model — even for trivial messages like "hi".
The fix establishes this contract: smart routing either uses the
cheap model when it can safely handle the current request, or falls
back to the primary model. It never triggers destructive session
compression.
Two coordinated changes in run_conversation / smart_model_routing:
1. skip_preflight_compression param on run_conversation()
Set True from the CLI chat, single-query, and /btw paths when the
turn carries a smart-routing label. Prevents preflight from firing
at 50% of the cheap model's (possibly smaller) context.
2. Refuse-to-route on history overflow (smart_model_routing)
resolve_turn_route() accepts current_request_tokens and
max_history_ratio kwargs. When the estimated request exceeds
cheap_ctx * max_history_ratio, smart routing returns the primary
route instead of the cheap route. CLI computes the estimate via
estimate_request_tokens_rough and passes the active compressor's
threshold_percent as the ratio — tuning compression.threshold in
config.yaml tunes smart routing's refuse threshold at the same time.
The default max_history_ratio of 0.50 mirrors
ContextCompressor.threshold_percent. It leaves the upper half of the
cheap context free for tool outputs and responses within the turn,
so typical read_file / web_extract / search_files results can land
on the cheap model without pushing the next API call past its real
context limit.
Behavior summary (default primary=Sonnet 1M, ratio=0.50):
- history < 50% of cheap context: route to cheap, preflight skipped
- history 50-100% of cheap context: refuse, stay on primary
- history > cheap context: refuse, stay on primary
- short message fails the simple-turn filter: stay on primary (existing)
The default cheap model (gemini-2.5-flash, 1M) matches Sonnet 1M
exactly, so the refusal path rarely triggers for default configs.
Users who set cheap to a smaller model (gpt-4o-mini, haiku, etc.) are
now protected from silent session-history loss.
Also extracts an internal _primary_route() helper in
smart_model_routing.py to DRY up three identical primary-route dict
constructions that previously lived inline in resolve_turn_route().
Known limitations (out of scope, left for follow-up):
- Mid-turn tool output explosions >50% of cheap context still hit
in-loop compression on API error. Would require per-call override
architecture (reusing _try_activate_fallback pattern).
- /model command (switch_model) has the same compressor-rebind
pattern but its semantics are "permanent switch" so compression
on downsize may be intentional.
- Session resume with a different current model is a separate
initialization-time variant.
Gateway and batch_runner use the same smart-routing code path but
are not modified here. CLI-scoped for this PR.
Tests:
- test_413_compression.py::TestPreflightCompression adds
test_skip_preflight_compression_flag and
test_skip_preflight_false_still_compresses
- test_smart_model_routing.py adds 5 unit tests covering refuse /
allow / backward-compat / custom ratio / unknown cheap context
Closes NousResearch#7798
10f4d1a to
455cd8b
Compare
|
Status update — the base code this PR modifies ( Three open PRs are reintroducing smart routing in different shapes:
Three ways forward, happy with any:
Let me know the preferred path. |
|
Thanks for this careful analysis and well-structured PR, @Falicitas! Closing as the underlying feature has been removed. This automated hermes-sweeper review found that the
The bug you identified (#7798) was real and the fix design was sound. If smart-routing (or a successor per-turn routing mechanism) is ever reintroduced, the |
Closes #7798
Problem
Smart model routing rebuilds
AIAgenton every route-signature change (cli.py:7081-7083), which creates a newContextCompressorbound to the cheap model'scontext_length(run_agent.py:1314-1326). A trivial turn like"hi"routed to a small cheap model then triggers preflight compression against the cheap threshold and silently rewrites session history that was sized for the primary model.Contract this PR establishes
Two coordinated changes
1.
skip_preflight_compressionkwarg onrun_conversation()(run_agent.py).CLI sets it
Trueat thechat/ single-query //btwcall sites when the turn carries a smart-routing label. Preflight is bypassed on those turns.2. Refuse-to-route on history overflow (
smart_model_routing.resolve_turn_route).New kwargs
current_request_tokensandmax_history_ratio. CLI computes the request size viaestimate_request_tokens_roughand passes the active compressor'sthreshold_percent(default 0.50) as the ratio. Whenest > cheap_ctx × ratio, smart routing returns the primary route.The default
max_history_ratio = 0.50mirrorsContextCompressor.threshold_percentso tuningcompression.thresholdinconfig.yamltunes both simultaneously — both settings express "how much pressure on the active model we tolerate before evasive action". Leaving the upper half of the cheap context free also gives room for typicalread_file/web_extract/search_filesoutputs within the turn. A decoupledsmart_routing.max_history_ratioconfig field remains a non-breaking future extension sinceresolve_turn_routealready accepts it as a kwarg.Behavior
Example:
primary = claude-sonnet-4.6 (1M),ratio = 0.50.gemini-2.5-flash(1M)gpt-4o-mini(128K)gpt-4o-mini(128K)gpt-4o-mini(128K)claude-haiku-4.5(200K)If the cheap model genuinely can't accept a permitted request, existing in-loop compression on API error (
413/400 context_length_exceeded) still catches it — no safety regression.Covered CLI paths:
chat()main REPL, single-query (hermes -- "..."),/btw./backgroundis already safe (no history passed to the fresh bg agent).How to test
hermes chatsession"hi")gpt-4o-mini,self.conversation_historygets replaced with a summary, session lineage splits in SQLiteclaude-sonnet-4.6, history intactAutomated tests:
tests/run_agent/test_413_compression.py::TestPreflightCompression:test_skip_preflight_compression_flag,test_skip_preflight_false_still_compressestests/agent/test_smart_model_routing.py: 5 new unit tests covering refuse/allow/backward-compat/custom ratio/unknown cheap contextaf9caec4(routing + compression + CLI suites). The 6 pre-existing failures intest_auxiliary_client.py/test_credential_pool.py/test_memory_user_id.pyare unrelated and also fail onmain.Platforms: macOS (Darwin 24.6.0), Python 3.11.15.
Scope and known limitations
In scope: CLI call sites (
chat/ single-query //btw). Gateway andbatch_runneruse the same smart-routing code path but are left for follow-up.Known paths this PR does not cover:
_try_activate_fallback/_restore_primary_runtime)./modelcommand (switch_modelatrun_agent.py:1474) has the same compressor-rebind pattern, but comments at1484-1486explicitly document it as a "permanent switch", so compression on downsize is likely intentional. Happy to discuss in a separate issue.Related
fd3e855d— related/modelswitch fix (different code path)871313ae— clearsconversation_historyafter mid-loop compression (partial mitigation only)