Spaces:
Sleeping
Sleeping
| """Tests for `_compact_old_tool_messages`. | |
| The compaction function runs on every model_node turn and is the only thing | |
| keeping older tool outputs from overflowing the context window. The original | |
| implementation head-truncated to 300 chars, which can amputate the exact line | |
| containing the answer. This file tests the summarize-preferred variant: when | |
| an LLM-backed summarizer is supplied, old long tool outputs are replaced by | |
| the summarizer's result; the head-truncation path remains as a fallback for | |
| cases where the summarizer is unavailable or fails. | |
| """ | |
| from __future__ import annotations | |
| from langchain_core.messages import AIMessage, HumanMessage, ToolMessage | |
| from lilith_agent.app import ( | |
| _COMPACT_KEEP_RECENT, | |
| _COMPACT_MAX_CHARS, | |
| _COMPACT_SUMMARY_PREFIX, | |
| _compact_old_tool_messages, | |
| ) | |
| def _long_tool_msg(name: str, content: str) -> ToolMessage: | |
| return ToolMessage(tool_call_id=f"tc-{name}", name=name, content=content) | |
| def test_short_tool_messages_pass_through_unchanged(): | |
| msgs = [ | |
| HumanMessage("q"), | |
| _long_tool_msg("web_search", "short result"), | |
| AIMessage("ok"), | |
| ] | |
| out = _compact_old_tool_messages(msgs) | |
| assert out[1].content == "short result" | |
| def test_recent_tool_messages_kept_verbatim_even_when_long(): | |
| long = "X" * (_COMPACT_MAX_CHARS * 10) | |
| msgs = [_long_tool_msg("web_search", long) for _ in range(_COMPACT_KEEP_RECENT)] | |
| out = _compact_old_tool_messages(msgs) | |
| for m in out: | |
| assert m.content == long | |
| def test_old_long_message_is_summarized_when_summarizer_provided(): | |
| long = "X" * (_COMPACT_MAX_CHARS * 5) | |
| msgs = [ | |
| _long_tool_msg("web_search", long), # old | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| def fake_summarizer(tool_name: str, content: str) -> str: | |
| return "SUMMARY_OF_FACTS_42" | |
| out = _compact_old_tool_messages(msgs, summarize_fn=fake_summarizer) | |
| assert "SUMMARY_OF_FACTS_42" in out[0].content | |
| assert out[0].content.startswith(_COMPACT_SUMMARY_PREFIX) | |
| # The old raw payload is gone — summarization replaced it. | |
| assert "X" * 1000 not in out[0].content | |
| def test_summarizer_receives_tool_name_and_full_content(): | |
| long = "alpha " * 500 | |
| msgs = [ | |
| _long_tool_msg("arxiv_search", long), | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| recorded: dict = {} | |
| def fake_summarizer(tool_name: str, content: str) -> str: | |
| recorded["name"] = tool_name | |
| recorded["len"] = len(content) | |
| return "ok" | |
| _compact_old_tool_messages(msgs, summarize_fn=fake_summarizer) | |
| assert recorded["name"] == "arxiv_search" | |
| assert recorded["len"] == len(long) | |
| def test_summarizer_failure_falls_back_to_head_truncation(): | |
| long = "Y" * (_COMPACT_MAX_CHARS * 5) | |
| msgs = [ | |
| _long_tool_msg("web_search", long), | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| def broken_summarizer(tool_name: str, content: str) -> str: | |
| raise RuntimeError("llm offline") | |
| out = _compact_old_tool_messages(msgs, summarize_fn=broken_summarizer) | |
| content = out[0].content | |
| # Fallback marker from the original truncation path | |
| assert "COMPACTED" in content | |
| # First `max_chars` preserved verbatim | |
| assert content.startswith("Y" * _COMPACT_MAX_CHARS) | |
| def test_summarizer_returning_empty_falls_back_to_truncation(): | |
| long = "Z" * (_COMPACT_MAX_CHARS * 5) | |
| msgs = [ | |
| _long_tool_msg("web_search", long), | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| def empty_summarizer(tool_name: str, content: str) -> str: | |
| return "" | |
| out = _compact_old_tool_messages(msgs, summarize_fn=empty_summarizer) | |
| assert "COMPACTED" in out[0].content | |
| assert out[0].content.startswith("Z" * _COMPACT_MAX_CHARS) | |
| def test_no_summarizer_uses_truncation_fallback(): | |
| """Backwards-compat: the original (no summarize_fn) path must still truncate.""" | |
| long = "W" * (_COMPACT_MAX_CHARS * 5) | |
| msgs = [ | |
| _long_tool_msg("web_search", long), | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| out = _compact_old_tool_messages(msgs) | |
| assert "COMPACTED" in out[0].content | |
| assert out[0].content.startswith("W" * _COMPACT_MAX_CHARS) | |
| def test_already_summarized_message_is_not_resummarized(): | |
| """If a prior pass already produced a `[COMPACTED SUMMARY] …` content, | |
| a second pass must skip it — otherwise we waste a cheap-model call every | |
| turn on the same already-shrunk payload. | |
| """ | |
| prior_summary = _COMPACT_SUMMARY_PREFIX + "already shrunk facts" | |
| msgs = [ | |
| _long_tool_msg("web_search", prior_summary), | |
| *[_long_tool_msg("web_search", "short") for _ in range(_COMPACT_KEEP_RECENT)], | |
| ] | |
| calls = {"n": 0} | |
| def fake_summarizer(tool_name: str, content: str) -> str: | |
| calls["n"] += 1 | |
| return "should not run" | |
| out = _compact_old_tool_messages(msgs, summarize_fn=fake_summarizer) | |
| assert calls["n"] == 0 | |
| assert out[0].content == prior_summary | |