Skip to content

fix(agent): propagate context compression failures to runner - #10645

Closed
Soulter wants to merge 2 commits into
masterfrom
codex/fix-context-compression-failure
Closed

Soulter wants to merge 2 commits into
masterfrom
codex/fix-context-compression-failure

Conversation

@Soulter

@Soulter Soulter commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

When a summary request fails, AstrBot currently returns the unchanged history and recounts it with a local estimate. A lower estimate can then be reported as successful compression, as in #10551. Compression failure should instead reach the runner, which decides whether to attempt the original request.

Modifications

  • Let summary request exceptions, error responses, and empty summaries fail explicitly. Keep a single summary request attempt.
  • Propagate ContextCompressionError through the context manager with the pre-compression messages and token count. Failed summaries no longer trigger automatic history truncation or a successful-compression log.
  • In the runner, stop with an error only when that count exceeds the active provider's configured max_context_tokens. Otherwise, log the failure and attempt the uncompressed request. The 82% compression threshold remains separate from the 100% context-limit check.
  • Cover provider-reported and estimated counts, the exact limit boundary, streaming and non-streaming delivery, and persona-specific error replies. The existing run_agent() handler catches the overflow exception and invokes on_agent_done.

Related to #10551. This changes failure handling, not token estimation: the issue's last reported total of 1,043,200 is below its configured limit of 1,048,576, so it still permits a request attempt. New content or estimation error can cause the upstream model to reject that attempt. Successful compression retains the existing post-compression trimming behavior.

  • This is NOT a breaking change to public API routes or schemas. Direct internal compressor/context-manager callers must now handle compression failures instead of receiving unchanged history.

Screenshots or Test Results

Validated against master at 776793b82:

python -m pytest tests/agent tests/test_tool_loop_agent_runner.py tests/test_astr_agent_run_util.py tests/unit/test_cron_context_compression.py -q --tb=short
195 passed, 1 warning in 3.03s

ruff format .
519 files left unchanged

ruff check .
All checks passed!

git diff --cached --check
Passed

The warning is the existing audioop deprecation warning. The test run reused the local development environment with the latest master's declared OpenTelemetry dependencies installed in a temporary directory.

Checklist

  • Bug fix; no new feature requiring a separate proposal.
  • Regression coverage and verification results are provided above.
  • No WebUI navigation, page structure, or label changes; documentation and screenshots do not need updates.
  • No new project dependencies.
  • No malicious code introduced.

Summary by Sourcery

Propagate context compression failures to the runner so it can safely decide whether to retry with the original context or stop on provider-limit overflow.

Bug Fixes:

  • Propagate context compression failures instead of silently falling back to unchanged history and potentially undercounted token estimates.
  • Allow the runner to retry the uncompressed request when the preserved context is within the provider limit, while failing clearly when it exceeds that limit.

Enhancements:

  • Separate the compression trigger threshold from the provider's hard context-limit check and preserve pre-compression messages and token usage across failures.

Documentation:

  • Update English and Chinese context-compression documentation to describe uncompressed-request fallback and over-limit errors.

Tests:

  • Add coverage for compression failures, provider-reported and estimated token counts, exact context limits, streaming and non-streaming delivery, and persona-specific error responses.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="astrbot/core/agent/context/manager.py" line_range="114-118" />
<code_context>
         """
         logger.debug("Compress triggered, starting compression...")

-        messages = await self.compressor(messages)
+        try:
+            messages = await self.compressor(messages)
+        except Exception as e:
+            raise ContextCompressionError(
+                f"Context compression failed: {e}",
+                messages=messages,
+                token_count=prev_tokens,
+            ) from e
</code_context>
<issue_to_address>
**Original conversation history is lost**

When a custom compressor mutates its input list in place and then raises, `_run_compression()` stores the same mutated `messages` list in `ContextCompressionError`, so the runner uses altered history instead of the pre-compression context and the caller loses history.

Preserve a copy of the messages before calling the compressor and store that copy in `ContextCompressionError` if compression fails.
</issue_to_address>

### Comment 2
<location path="astrbot/core/agent/runners/tool_loop_agent_runner.py" line_range="842-847" />
<code_context>
+            # Keep the count from before compression; a fresh local estimate may
+            # undercount the unchanged history and conceal a context overflow.
+            if max_tokens > 0 and e.token_count > max_tokens:
+                self._transition_state(AgentState.ERROR)
+                self.stats.end_time = time.time()
+                raise RuntimeError(
+                    f"Context compression failed and the context exceeds the model's "
+                    f"maximum context length ({e.token_count} > {max_tokens} tokens): {e}"
+                ) from e
+            logger.warning(
+                f"{e}. Attempting the uncompressed request "
</code_context>
<issue_to_address>
**Overflow leaves no final response**

When a caller relies on the runner’s final response after `run_agent()` handles a context-overflow exception, the overflow branch raises without assigning `self.final_llm_resp`, unlike the runner’s ordinary provider-error path. Callers that inspect `get_final_llm_resp()` after `run_agent()` handles the exception receive `None` instead of the terminal error response, so they cannot recognize or process the failure through the runner result.

Store the overflow error as the runner’s final response before raising, or route it through the runner’s normal error-response finalization path.
</issue_to_address>

Sourcery assessment

Approval pending. 2 findings to address first.

Blocking findings: astrbot/core/agent/context/manager.py:118, astrbot/core/agent/runners/tool_loop_agent_runner.py:847


Sourcery is free for open source - if you like our reviews please consider sharing them ✨

Comment on lines +114 to +118
messages = await self.compressor(messages)
except Exception as e:
raise ContextCompressionError(
f"Context compression failed: {e}",
messages=messages,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Original conversation history is lost

When a custom compressor mutates its input list in place and then raises, _run_compression() stores the same mutated messages list in ContextCompressionError, so the runner uses altered history instead of the pre-compression context and the caller loses history.

Preserve a copy of the messages before calling the compressor and store that copy in ContextCompressionError if compression fails.

Prompt for AI agents
In `astrbot/core/agent/context/manager.py` at lines 114-118:

**Original conversation history is lost**

When a custom compressor mutates its input list in place and then raises, `_run_compression()` stores the same mutated `messages` list in `ContextCompressionError`, so the runner uses altered history instead of the pre-compression context and the caller loses history.

Preserve a copy of the messages before calling the compressor and store that copy in `ContextCompressionError` if compression fails.

Comment on lines +842 to +847
self._transition_state(AgentState.ERROR)
self.stats.end_time = time.time()
raise RuntimeError(
f"Context compression failed and the context exceeds the model's "
f"maximum context length ({e.token_count} > {max_tokens} tokens): {e}"
) from e

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Medium · Overflow leaves no final response

When a caller relies on the runner’s final response after run_agent() handles a context-overflow exception, the overflow branch raises without assigning self.final_llm_resp, unlike the runner’s ordinary provider-error path. Callers that inspect get_final_llm_resp() after run_agent() handles the exception receive None instead of the terminal error response, so they cannot recognize or process the failure through the runner result.

Store the overflow error as the runner’s final response before raising, or route it through the runner’s normal error-response finalization path.

Prompt for AI agents
In `astrbot/core/agent/runners/tool_loop_agent_runner.py` at lines 842-847:

**Overflow leaves no final response**

When a caller relies on the runner’s final response after `run_agent()` handles a context-overflow exception, the overflow branch raises without assigning `self.final_llm_resp`, unlike the runner’s ordinary provider-error path. Callers that inspect `get_final_llm_resp()` after `run_agent()` handles the exception receive `None` instead of the terminal error response, so they cannot recognize or process the failure through the runner result.

Store the overflow error as the runner’s final response before raising, or route it through the runner’s normal error-response finalization path.

@Soulter Soulter closed this Oct 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant