diff --git a/src/fast_agent/history/compaction.py b/src/fast_agent/history/compaction.py index 9ed91c9c9..4c06f5ca7 100644 --- a/src/fast_agent/history/compaction.py +++ b/src/fast_agent/history/compaction.py @@ -23,6 +23,7 @@ from fast_agent.constants import FAST_AGENT_COMPACTION_CHANNEL from fast_agent.core.logging.logger import get_logger from fast_agent.event_progress import ProgressAction +from fast_agent.mcp.helpers.content_helpers import get_text from fast_agent.mcp.prompt import Prompt from fast_agent.types.llm_stop_reason import LlmStopReason @@ -185,6 +186,10 @@ def estimate_tokens(messages: list[PromptMessageExtended]) -> int: for message in messages: chars += len(message.all_text()) for content in message.content: + if get_text(content) is not None: + # Text is already counted via all_text() above; only serialize + # non-text payloads here (matches the "non-text" docstring). + continue try: chars += len(content.model_dump_json()) except Exception: diff --git a/tests/unit/fast_agent/history/test_compaction.py b/tests/unit/fast_agent/history/test_compaction.py index 49de697c8..7514017ea 100644 --- a/tests/unit/fast_agent/history/test_compaction.py +++ b/tests/unit/fast_agent/history/test_compaction.py @@ -16,6 +16,7 @@ from fast_agent.config import CompactionSettings, Settings, get_settings from fast_agent.context import Context from fast_agent.history.compaction import ( + _CHARS_PER_TOKEN, DEFAULT_COMPACTION_PROMPT, FAST_AGENT_COMPACTION_CHANNEL, CompactionSkipped, @@ -359,6 +360,12 @@ def test_counts_non_text_content_and_channels(self): assert estimate_tokens([msg]) > plain + 5_000 + def test_does_not_double_count_text(self): + # Text is counted once via all_text(); the per-content loop must skip it + # (docstring: "Count text plus serialized non-text payloads"). + text = "x" * 1200 + assert estimate_tokens([_user(text)]) == len(text) // _CHARS_PER_TOKEN + class _FakeLLM: def __init__(self, summary: str = "SUMMARY OF WORK") -> None: