From 93f71b61a04b468dc4b4da6e9b66db2e329c9203 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Wed, 19 Aug 2026 13:52:05 +0800 Subject: [PATCH] fix(memory): reject tool-call-only consolidation --- nanobot/agent/memory.py | 4 ++++ tests/agent/test_consolidator.py | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/nanobot/agent/memory.py b/nanobot/agent/memory.py index c411ccca7..071106c19 100644 --- a/nanobot/agent/memory.py +++ b/nanobot/agent/memory.py @@ -1060,6 +1060,10 @@ class Consolidator: logger.warning("Consolidation provider returned an error, raw-dumping to history") self.store.raw_archive(fallback_messages, session_key=session_key) return None + if response.has_tool_calls is True: + logger.warning("Consolidation provider returned tool calls, raw-dumping to history") + self.store.raw_archive(fallback_messages, session_key=session_key) + return None summary = response.content or "[no summary]" self.store.append_history( summary, diff --git a/tests/agent/test_consolidator.py b/tests/agent/test_consolidator.py index 80a941068..a4f5ce8cc 100644 --- a/tests/agent/test_consolidator.py +++ b/tests/agent/test_consolidator.py @@ -14,6 +14,7 @@ from nanobot.providers.base import ( GenerationSettings, LLMResponse, ProviderConversationState, + ToolCallRequest, ) from nanobot.runtime_context import ( RUNTIME_CONTEXT_HISTORY_META, @@ -1069,6 +1070,37 @@ class TestCompactIdleSession: "Overview from the temporary turn." ] + @pytest.mark.asyncio + async def test_tool_call_response_uses_raw_fallback( + self, + real_consolidator, + mock_provider, + store, + runtime, + ): + mock_provider.chat_with_retry.return_value = LLMResponse( + content=None, + tool_calls=[ToolCallRequest(id="call-1", name="lookup", arguments={})], + finish_reason="tool_calls", + ) + sessions = real_consolidator.sessions + session = sessions.get_or_create("cli:unexpected-tool") + session.add_message("user", "remember this") + session.add_message("assistant", "important answer") + sessions.save(session) + + result = await real_consolidator.compact_idle_session( + "cli:unexpected-tool", + runtime=runtime, + ) + + assert result is None + entries = store.read_unprocessed_history(since_cursor=0) + assert len(entries) == 1 + assert entries[0]["content"].startswith("[RAW] ") + assert "important answer" in entries[0]["content"] + assert sessions.get_or_create("cli:unexpected-tool").last_consolidated == 2 + @pytest.mark.asyncio async def test_oversized_prefix_falls_back_to_bounded_archive( self,