#7733·QwenPaw

[Feature]: Agent-autonomous context management — a smooth handover across context eviction

Author: MCQSJCreated Sep 13, 2026Updated Sep 17, 2026
Labelsenhancement

Summary

Context eviction in a long-running task is triggered by a pure token threshold. The agent — the only party that knows which part of the work is still live — has no say in when it happens and no warning that it is about to. Compaction runs, the agent wakes up in a thinner context, and the handover is not smooth: in the worst case the turn dies outright, a running task loses its place, and the evicted history cannot be reliably recalled.

This issue proposes an agent-autonomous context management model. Judgement stays with the agent: when to compact and what is worth keeping are its calls. The platform supplies only three things — a timing signal, a durable write channel, and post-eviction visibility. The goal is that context eviction becomes an ordinary step in a long task rather than an event that damages it.

Three outcomes this design optimizes for:

  1. A smooth handover — eviction is a normal transition, not an exception.
  2. Nothing in flight is lost — in-progress tasks, unresolved decisions and progress survive the boundary.
  3. History stays recoverable — what was evicted can be reliably found again.

Component(s) Affected

  • Core / Backend (agents/context/scroll)

Current behaviour — where the handover breaks

1. There is no window before eviction. ContextConfig (agentscope/agent/_config.py:51) exposes trigger_ratio (0.8) and reserve_ratio (0.1) only — there is no soft warn_ratio, and the installed 2.2.1 tree contains no pre-compaction hook (grep -rniE "pre_compact|pre-compact|before_compact|compaction_hook" returns nothing). The crossing of the hard threshold is instantaneous and invisible from the agent's side, so nothing can be checkpointed beforehand.

2. The agent has no lever of its own. compact exists as a capability but is registered as a user-facing command (SYSTEM_COMMANDS, agents/command_handler.py:79; _CONVERSATION_COMMANDS, runtime/builtin_commands.py) — it is not exposed as a model-callable tool. The agent holds the context but no lever; the user holds the lever but not the context. Consequently the agent cannot checkpoint-then-compact at a point it chooses, nor attach its own instruction about what matters. (#7679 covers the loop/scheduling side; this is the model-side, mid-task counterpart.)

3. After eviction there is no explicit notice, and recall is lossy. The agent sees the [context compressed] map and the [archived task state] block; _LIVE_TURN_BANNER (agents/context/scroll/eviction_index.py) is a positional separator ("do not answer an archived headline"), not an event notice. The map is also uneven: spans without a milestone collapse into a single (no milestone) line, and the continuation summary can be stale (in this reporter's own session: summary_update_failed). So the two questions the agent actually needs answered — what did I just lose and how do I get it back — are not answerable from the model's side.

4. The residual path is fatal rather than graceful. When compaction cannot fit the in-flight request, the turn ends with an unhandled error instead of degrading. That is the "abnormal interruption / lost task" case in practice, and it also kills background jobs, where the user never sees the reason.

Observed evidence (sanitized — QwenPaw 2.2.1, no session IDs, paths or credentials)
scroll: context still over the compression trigger (116896 > 104857) after
        compaction and tool-result folding
scroll: compact timing outcome=unfit ... active_turn_fold=0.1ms
runtime: unhandled error session=…: CONTEXT_UNFIT: context compaction could
        not fit the active request into the model input window
        (129419 > 126976 tokens)
cron _execute_once: job_id=… status=error error=ContextWindowUnfitError(...)

Frequency within a single session: outcome=unfit 85 vs outcome=done 397.

Why the ladder folds zero in this case (confirmed in source, offered only as a pointer for whoever implements the graceful path): the last rung is _batch_fold_seen_active_results (manager.py:676), whose candidates come from _tool_result_fold_candidates(seen_active_only=True) and only accept results already consumed by a successful request (manager.py:1557-1563); meanwhile the most recent 5 results are unconditionally protected, and that check runs before the seen_active_only branch (manager.py:1528-1536, _PROTECTED_RECENT_TOOL_RESULTS = 5). When the excess weight sits in the newest entries — here, four large tool results (~200 KB of text) in one batch — every rung folds zero, the count stays over the hard limit, and ContextWindowUnfitError is raised (manager.py:699, also manager.py:486). That raise is not rescued: recover_from_context_overflow (manager.py:375) is only called from the _call_model handler and only for a provider 400 (react_agent.py:707-716), while the framework-detected unfit comes out of compress() via _compress_context_impl (react_agent.py:284-286) — a different call path.

To be explicit: this is not a request to remove the protection window. Keeping unconsumed content intact is deliberate. What is asked is that the residual path degrade instead of dying, and that the agent be told what happened.

Proposed design — agent-autonomous context management

Principle: the agent decides, the platform only provides timing, channel and visibility. Explicitly not proposed: platform-side summarization, "importance" heuristics, or automatic extraction of key points. The platform cannot know which thread is still live; the agent can.

1. A window before eviction — timing only, no judgement. Add warn_ratio (default ≈ 0.8 × trigger_ratio). When crossed, inject a one-shot agent-visible notice carrying the remaining budget, e.g. "≈N tokens until compaction. If this task is mid-flight, checkpoint it now." Once per episode — no repeated nagging.

2. A durable write channel the agent owns. Let the agent write what it judges worth keeping — task state, decisions, progress, open questions — into a file that stays resident and visible after eviction, by reusing the existing system_prompt_files mechanism rather than inventing new storage. What to write, how detailed, and when, is entirely the agent's call; the single contract is written = durable = still visible after eviction. This is the primary ask.

3. A lever the agent can pull. Expose compaction to the agent (as a tool, or by reusing the existing /compact path) so it can checkpoint-then-compact at a moment of its own choosing, carrying its own instruction about what matters. Optionally allow an optional callback between the trigger check and eviction in compress(), best-effort: a failure must never abort compaction, matching the existing _offload_dialog semantics.

4. A notice after eviction — and a recall path that actually works. In the first step after eviction, state plainly that eviction happened, which seq range it covered, and how to recall it — as an event, not a positional separator. Alongside it, make recall dependable: spans without a milestone should not silently collapse into a single opaque line, and the summary injection should not present a stale version as current.

5. A graceful ending instead of a fatal one. Before an unfit compaction becomes terminal, persist the interrupted turn (already done today), emit a durable checkpoint, and surface a recoverable outcome — "continue in a new session, here is the checkpoint" — instead of an unhandled runtime error, and make the next turn aware of it so the agent knows why its context got thin. This applies to background jobs as well, which today fail with no visible reason.

Alternatives Considered

  • Stronger "record proactively" rules in AGENTS.md / system prompt — discussed in #1439. It relies on the agent remembering to run them every single turn, which is exactly what a long task erodes; good agent, wrong toolkit.
  • Platform-side automatic summarization — a post-hoc remedy that also oversteps: the platform cannot know what is still live. (The journal part of #1439 is not present in 2.2.1 either, so it is not even a stopgap.)
  • Asynchronous distillation (memory / auto_dream style) — useful for long-term recall, but it happens after the fact and does not give the agent anything to hold on to at the boundary.
  • Relying on the headline map alone — right direction, wrong timing: it happens after eviction, and its coverage is uneven.
  • Relying only on budget accounting during compaction (the direction of #7628) — necessary but orthogonal: fitting inside the window does not tell the agent to checkpoint.

Additional Context

  • Related: #7628 (open — complementary; budget accounting during compaction), #7679 (open — loop-side /compact), #1439 (closed as completed; the hook and notice described there are absent in 2.2.1, grep returns 0 matches), #2047 (closed — user-facing).
  • Code locations: agents/context/scroll/manager.py (compress, recover_from_context_overflow, _tool_result_fold_candidates, _PROTECTED_RECENT_TOOL_RESULTS), agents/context/scroll/eviction_index.py (_LIVE_TURN_BANNER), agents/react_agent.py:707-716, agents/react_agent.py:284-286, agents/command_handler.py:79, runtime/builtin_commands.py, agentscope/agent/_config.py:51.
  • Environment: QwenPaw 2.2.1, Linux x86_64, 128K context window. Logs above are sanitized.

Willing to Contribute

  • I am willing to open a PR for this feature (after discussion).