agent/compaction_display.py
Rama main · réplica de lectura del repositorio original.
"""Client-facing projection helpers for model-only compaction carriers."""
from __future__ import annotations
import re
from typing import Any, Dict, Optional
from agent.context_compressor import (
_LEAN_RECOVERY_HEADING, _LEAN_USER_MESSAGES_HEADING, ContextCompressor, is_compaction_summary_message,
)
from agent.message_content import flatten_message_text
_COMPACTION_INTERNAL_FIELDS = (
"tool_calls",
"finish_reason",
"reasoning",
# Provider replay/metadata fields that ride the wire on every request but are invisible to
# ``msg["content"]``/``msg["tool_calls"]`` accounting. Codex Responses sessions in particular carry
# ``codex_reasoning_items`` blobs of ``encrypted_content`` that can dominate the serialized session (a
# measured 214-turn session held ~115K tokens / 27% of its payload there — #55572).
# ``reasoning_details`` is handled separately (see ``_reasoning_details_text_chars``): its signed/base64
# envelope is excluded from the budget, mirroring the preflight estimator's exclusion in
# ``model_metadata._estimate_message_tokens_without_images`` (#73298).
# An assistant turn may carry only reasoning/thinking content with no visible text (extended-thinking
# turns, thinking-only recovery responses). Such a turn is persisted with its reasoning fields and is
# recallable from the transcript, but dropping it here as "empty" makes it vanish from the
# resumed/reloaded session view while the desktop's reasoning disclosure has nothing to render. Keep it
# when it carries reasoning so the "Thinking…" block still shows. (#44022)
"reasoning_content",
"reasoning_details",
"codex_reasoning_items",
"codex_message_items",
)
def project_compaction_message_for_display(message: dict[str, Any]) -> Optional[dict[str, Any]]:
"""Return authentic transcript content, or ``None`` for a pure handoff.
Model-facing recovery history retains the complete carrier. Display
projections instead remove the handoff, inherited tool state, and internal
reasoning while preserving any real prior-tail content or live user ask
embedded in the carrier.
"""
if not isinstance(message, dict):
return None
if not is_compaction_summary_message(message):
return message.copy()
projected = ContextCompressor._strip_context_summary_handoff_message(message)
if projected is None:
return None
projected = projected.copy()
for key in _COMPACTION_INTERNAL_FIELDS:
projected.pop(key, None)
# A safety-refused ask embedded in a composite carrier keeps its ``content_refused`` tag through
# the unwrap, or a reopened chat would redraw it as an ordinary user bubble with no refused notice
# (#149708 review): the model-facing handoff is gone, the user's live ask survives as a greyed
# refused block. Every other carrier drops its internal ``display_kind`` as before.
from agent.turn_failure_copy import CONTENT_REFUSED_DISPLAY_KIND
if projected.get("display_kind") != CONTENT_REFUSED_DISPLAY_KIND:
projected.pop("display_kind", None)
return projected
# Lean-mode sections written for the model, not for a reader: the compacted turns' user messages
# quoted again, and the footer naming ``session_search`` (``ContextCompressor._augment_summary_lean``).
_MODEL_ONLY_SECTIONS = (_LEAN_USER_MESSAGES_HEADING, _LEAN_RECOVERY_HEADING)
_HISTORICAL_ONLY_LINE = "Historical only; newer protected-tail messages after this summary win."
_COMPACTED_COUNT = re.compile(r"The (\d+) compacted message\(s\) remain fully preserved")
def compaction_summary(message: Any) -> Optional[dict[str, Any]]:
"""What a compaction handoff tells a reader: ``{summary, compacted_messages}``, or ``None`` for any
other message. ``summary`` is the summary as the compressor wrote it, without the model-facing
preamble and end marker, prior-tail content or live ask merged into the carrier, the lean
sections above and the snapshot's ranking line; ``compacted_messages`` is the count the recovery
footer names (``None`` when it has none)."""
if not is_compaction_summary_message(message):
return None
text = ContextCompressor._strip_summary_prefix(flatten_message_text(message.get("content")))
sections = {}
for heading in _MODEL_ONLY_SECTIONS:
start = text.find(heading)
if start >= 0:
end = text.find("\n## ", start + len(heading))
sections[heading] = text[start:end if end >= 0 else len(text)]
text = text[:start] + (text[end + 1:] if end >= 0 else "")
# The footer's count, never one a quoted user message happens to spell out (the quotes are cut first).
counts = _COMPACTED_COUNT.findall(sections.get(_LEAN_RECOVERY_HEADING, text))
text = "\n".join(line for line in text.splitlines() if line.strip() != _HISTORICAL_ONLY_LINE)
return {"summary": re.sub(r"\n{3,}", "\n\n", text).strip(),
"compacted_messages": int(counts[-1]) if counts else None}