fix: harden context compaction pipeline — Markdown summaries, read-only action history, and non-executable output rejection - #3493
Merged
Conversation
…o context format_summary_output() was re-serializing parsed JSON back to JSON via json.dumps, causing the compressed summary injected into conversation context to be raw JSON. Models then mimicked JSON syntax in their final answers. Now the function converts JSON dicts to Markdown with section headings (# Compact Result of History / ## Task Overview / ## Completed Work / etc.) so the injected summary reads as natural language. - String values render as paragraphs, list values as bullet items - Unknown schema keys get title-cased headings - Non-dict JSON and parse failures fall back to plain text - All call sites (llm_summary, step_renderer, offline) benefit automatically - Updated test to assert Markdown output
JasonW404
force-pushed
the
fix/compaction_format
branch
from
July 25, 2026 07:35
fef4c76 to
dc97c5f
Compare
The compression pipeline was doing a pointless JSON round-trip: Prompt asks JSON → LLM outputs JSON → format_summary_output() converts to Markdown → HistoryCompressor json.loads() back to dict → renderer converts back to text This caused three problems: 1. HistoryCompressor silently degraded to mechanical truncation because json.loads() failed on Markdown output (regression from prior fix) 2. _render_summary() rendered dict summaries as flat 'key: value' lines without Markdown headings 3. _render_current_action() fallback dumped compact content as raw JSON, causing models to mimic JSON syntax in final answers Changes: - config.py: prompts now request Markdown with '# Compact Result of History' / '## Section' headings instead of JSON - llm_summary.py: prompt builds Markdown section descriptions instead of json.dumps(schema); uses _strip_code_fences() instead of format_summary_output() - history_compression.py: HistorySummaryCandidate.summary is now str; removed json.loads() gate; incremental compression passes previous summary as plain Markdown text - rendering.py: _render_summary() handles legacy dict data via _summary_dict_to_markdown(); _render_current_action() renders compact actions as structured natural language instead of json.dumps() - budget.py: simplified format_summary_output() to code-fence stripping only (JSON conversion removed) - Tests updated to match new string-based summary and natural language action rendering
The old json.loads() gate served double duty as a quality filter — unstructured LLM output was rejected and the compressor fell back to mechanical truncation. After removing the JSON intermediate format, any non-empty text was accepted as a valid summary. Replace with a lightweight check: the summary must contain at least one '##' section heading, ensuring the LLM produced structured Markdown output. Plain text responses (e.g. 'lossy fallback') are rejected and trigger the safe fallback path.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Expand the default summary schema with two new sections to produce more complete compressed context: - unresolved_issues: problems, errors, or questions encountered but not yet resolved - next_steps: concrete planned next actions and their expected outcomes These complement the existing pending_items (blockers/waiting) and key_decisions (resolved findings) by capturing open problems and forward-looking plans that the agent needs to continue effectively after compression. Updated _SUMMARY_FIELD_HEADINGS and test mock to match.
… _strip_code_fences Cover previously uncovered paths to satisfy codecov/patch: - _summary_dict_to_markdown: legacy dict-typed summary rendering - _render_current_action: compact path with tool_calls (list and dict) - _strip_code_fences: code fence stripping utility
- _summary_dict_to_markdown: list values, empty list, None, numeric, all-empty fallback - _render_current_action: non-standard tool_calls (string), list with non-dict elements
Update the system prompt text to mention 'unresolved issues' and 'next steps' alongside the existing fields for consistency with the expanded schema.
…cutable outputs - Replace Step/Called tool assistant format with read-only <completed_action_history> in user role - Preserve string tool_calls.arguments (python_interpreter code no longer degrades to empty) - Add InvalidActionFormatError for non-executable model outputs - Non-executable outputs (Step N/Called tool, Markdown action record, action/tool_calls JSON, fenced action JSON, incomplete <code>/<RUN> protocol) now record as step error and continue loop instead of returning as final answer - Natural language final answers remain compatible - Add tests covering full degradation chain: invalid format -> parser error -> loop continues -> real answer
Jasonxia007
approved these changes
Jul 27, 2026
Dallas98
approved these changes
Jul 27, 2026
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
The context compaction pipeline had multiple issues that caused models to produce degraded or incorrect outputs after context compression:
1. JSON round-trip in compression pipeline
The compression pipeline was doing a pointless JSON round-trip:
This caused:
json.loads(summary_text)always failed on Markdown output, causing history compression to silently degrade to mechanical truncation_render_summary()flat format: Dict-typed summaries rendered askey: valuelines without Markdown headings_render_current_action()JSON injection: Compacted action items fell back tojson.dumps(item.content), injecting raw JSON that models mimicked in final answers2. Action history in assistant role caused mimicry
Compacted action history was rendered as assistant-role messages with
Step N/Called toolformat. Models treated these as response templates and reproduced the same structured format instead of generating real answers.3. Non-executable outputs returned as final answers
When the model produced non-executable structured output (e.g.
Step N / Called tool, Markdown action records, action/tool_calls JSON, fenced action JSON, incomplete<code>or<RUN>protocol), the agent loop treated them as valid final answers and terminated — instead of recognizing them as format errors and continuing the loop.4.
python_interpretercode lossString-form
tool_calls.argumentswere not preserved during compaction.python_interpretertool calls degraded to empty arguments, losing the actual code.5. Missing compression schema sections
The compression schema lacked
unresolved_issuesandnext_stepssections, losing important forward-looking context.Fix
1. Eliminated JSON intermediate format
The LLM now outputs Markdown directly, and all consumers work with plain text.
Before (4-step conversion):
After (1-step):
2. Markdown heading quality gate
Replaced the old
json.loads()gate with a lightweight"##" in summary_textcheck that rejects plain text while accepting structured Markdown.3. Read-only action history in user role
Compacted action history is now injected as a user-role message wrapped in
<completed_action_history>tags. This makes it clearly read-only context that the model should not mimic or continue.4.
InvalidActionFormatErrorand non-executable output rejectionAdded
InvalidActionFormatErrorto the parser. The following non-executable outputs are now caught, recorded as step errors, and the agent loop continues instead of terminating:Step N/Called toolStep 3:\nCalled tool search...## Action\n- tool: search{"action": "search", ...}```json\n{"action": ...}\n```<code>/<RUN><code>print("hello")(no closing tag)Natural language final answers remain fully compatible.
5. String
tool_calls.argumentspreservationString-form
tool_calls.arguments(e.g.python_interpretercode) are now preserved verbatim during compaction instead of being dropped to empty.6. Expanded compression schema
Added two new sections:
unresolved_issueskey_decisions(resolved findings)next_stepspending_items(blockers/waiting)Full schema (7 sections):
Changes
sdk/.../context/config.pyunresolved_issuesandnext_stepsto schemasdk/.../context/llm_summary.py_strip_code_fences()sdk/.../context/history_compression.pysummary: str;"##" in summary_textquality gate; incremental passes Markdown directlysdk/.../context/rendering.py_render_summary()handles legacy dict;_render_current_action()renders natural language;<completed_action_history>user-role formatsdk/.../context/budget.pyformat_summary_output()simplified to code-fence strippingsdk/.../core_agent.pyInvalidActionFormatError; non-executable output detection; step error recording + loop continuationtest/.../test_context_helper_contracts.pytest/.../test_context_item.pytest/.../test_history_context_runtime.pytest/.../test_core_agent.pytest/.../test_guardrail_checkpoints.pyTesting
py_compile: passedgit diff --check: passedNew functional tests verify the full degradation chain:
Step N / Called toolon first attemptInvalidActionFormatError