From ddd7c826635bb7bbc36ff240b15b87c4af871fc2 Mon Sep 17 00:00:00 2001 From: fcsguyxcw <3056887696@qq.com> Date: Tue, 8 Sep 2026 18:46:37 +0800 Subject: [PATCH 1/2] fix(agents): preserve memory when summarization fails --- camel/agents/chat_agent.py | 57 ++++++++++++++++++++++--------- test/agents/test_chat_agent.py | 61 ++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 16 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index ca172d4d8d..27c3ab837d 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -1046,10 +1046,11 @@ def _get_context_with_summarization( f"exceed limit, full compression." ) summary = self.summarize(include_summaries=True) - self._update_memory_with_summary( - summary.get("summary", ""), include_summaries=True - ) - return self.memory.get_context() + if self._try_update_memory_with_summary( + summary, include_summaries=True + ): + return self.memory.get_context() + return openai_messages, num_tokens threshold = self._calculate_next_summary_threshold() if num_tokens > threshold: @@ -1058,10 +1059,11 @@ def _get_context_with_summarization( f"({threshold}). Triggering summarization." ) summary = self.summarize(include_summaries=False) - self._update_memory_with_summary( - summary.get("summary", ""), include_summaries=False - ) - return self.memory.get_context() + if self._try_update_memory_with_summary( + summary, include_summaries=False + ): + return self.memory.get_context() + return openai_messages, num_tokens return openai_messages, num_tokens @@ -1082,10 +1084,11 @@ async def _get_context_with_summarization_async( f"exceed limit, full compression." ) summary = await self.asummarize(include_summaries=True) - self._update_memory_with_summary( - summary.get("summary", ""), include_summaries=True - ) - return self.memory.get_context() + if self._try_update_memory_with_summary( + summary, include_summaries=True + ): + return self.memory.get_context() + return openai_messages, num_tokens threshold = self._calculate_next_summary_threshold() if num_tokens > threshold: @@ -1094,10 +1097,11 @@ async def _get_context_with_summarization_async( f"({threshold}). Triggering summarization." ) summary = await self.asummarize(include_summaries=False) - self._update_memory_with_summary( - summary.get("summary", ""), include_summaries=False - ) - return self.memory.get_context() + if self._try_update_memory_with_summary( + summary, include_summaries=False + ): + return self.memory.get_context() + return openai_messages, num_tokens return openai_messages, num_tokens @@ -1143,6 +1147,27 @@ def _calculate_next_summary_threshold(self) -> int: return threshold + def _try_update_memory_with_summary( + self, + summary_result: Dict[str, Any], + include_summaries: bool = False, + ) -> bool: + r"""Update memory only after summarization succeeds.""" + summary = summary_result.get("summary", "") + if ( + summary_result.get("status") != "success" + or not isinstance(summary, str) + or not summary.strip() + ): + logger.warning( + "Skipping memory update because summarization failed: %s", + summary_result.get("status") or "unknown error", + ) + return False + + self._update_memory_with_summary(summary, include_summaries) + return True + def _update_memory_with_summary( self, summary: str, include_summaries: bool = False ) -> None: diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index da43a41611..8158b47afb 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -292,6 +292,67 @@ def test_clean_snapshot_in_memory_skips_missing_records(): assert entry.cached is True +def _make_agent_with_summarization_history(): + model = DummyModel(ModelType.GPT_4O_MINI) + model._token_counter = MagicMock() + model._token_counter.count_tokens_from_messages.return_value = 10 + agent = ChatAgent( + system_message="You are a helpful assistant.", + model=model, + token_limit=100_000, + summarize_threshold=50, + ) + agent.update_memory( + BaseMessage.make_user_message("user", "Remember this."), + OpenAIBackendRole.USER, + ) + agent.update_memory( + BaseMessage.make_assistant_message("assistant", "I will."), + OpenAIBackendRole.ASSISTANT, + ) + agent._calculate_next_summary_threshold = MagicMock(return_value=0) + return agent + + +@pytest.mark.parametrize( + "summary_result", + [ + { + "summary": "", + "status": "Failed to generate summary using model: transient", + }, + {"summary": "partial summary", "status": "Error: write failed"}, + {"summary": "", "status": "success"}, + ], +) +def test_failed_automatic_summarization_preserves_memory(summary_result): + agent = _make_agent_with_summarization_history() + context_before = agent.memory.get_context() + agent.summarize = MagicMock(return_value=summary_result) + + context_after = agent._get_context_with_summarization() + + assert context_after == context_before + assert agent.memory.get_context() == context_before + + +@pytest.mark.asyncio +async def test_failed_async_automatic_summarization_preserves_memory(): + agent = _make_agent_with_summarization_history() + context_before = agent.memory.get_context() + agent.asummarize = AsyncMock( + return_value={ + "summary": "", + "status": "Failed to generate summary from model response.", + } + ) + + context_after = await agent._get_context_with_summarization_async() + + assert context_after == context_before + assert agent.memory.get_context() == context_before + + @pytest.mark.model_backend def test_chat_agent_stored_messages(): system_msg = BaseMessage( From e0ddfbc07430f30b38094fc8680ea82db0e2737e Mon Sep 17 00:00:00 2001 From: fcsguyxcw <3056887696@qq.com> Date: Wed, 9 Sep 2026 10:02:48 +0800 Subject: [PATCH 2/2] fix: retain valid summaries when markdown saving fails --- camel/agents/chat_agent.py | 8 ++--- test/agents/test_chat_agent.py | 53 ++++++++++++++++++++++++++++++++-- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/camel/agents/chat_agent.py b/camel/agents/chat_agent.py index 27c3ab837d..c4a1a66dc6 100644 --- a/camel/agents/chat_agent.py +++ b/camel/agents/chat_agent.py @@ -1152,13 +1152,9 @@ def _try_update_memory_with_summary( summary_result: Dict[str, Any], include_summaries: bool = False, ) -> bool: - r"""Update memory only after summarization succeeds.""" + r"""Update memory when summary text exists, even if saving failed.""" summary = summary_result.get("summary", "") - if ( - summary_result.get("status") != "success" - or not isinstance(summary, str) - or not summary.strip() - ): + if not isinstance(summary, str) or not summary.strip(): logger.warning( "Skipping memory update because summarization failed: %s", summary_result.get("status") or "unknown error", diff --git a/test/agents/test_chat_agent.py b/test/agents/test_chat_agent.py index 8158b47afb..ed657375ae 100644 --- a/test/agents/test_chat_agent.py +++ b/test/agents/test_chat_agent.py @@ -292,14 +292,14 @@ def test_clean_snapshot_in_memory_skips_missing_records(): assert entry.cached is True -def _make_agent_with_summarization_history(): +def _make_agent_with_summarization_history(token_limit=100_000): model = DummyModel(ModelType.GPT_4O_MINI) model._token_counter = MagicMock() model._token_counter.count_tokens_from_messages.return_value = 10 agent = ChatAgent( system_message="You are a helpful assistant.", model=model, - token_limit=100_000, + token_limit=token_limit, summarize_threshold=50, ) agent.update_memory( @@ -321,7 +321,6 @@ def _make_agent_with_summarization_history(): "summary": "", "status": "Failed to generate summary using model: transient", }, - {"summary": "partial summary", "status": "Error: write failed"}, {"summary": "", "status": "success"}, ], ) @@ -353,6 +352,54 @@ async def test_failed_async_automatic_summarization_preserves_memory(): assert agent.memory.get_context() == context_before +@pytest.mark.asyncio +@pytest.mark.parametrize("async_mode", [False, True]) +@pytest.mark.parametrize("include_summaries", [False, True]) +async def test_automatic_summarization_uses_summary_when_save_fails( + async_mode, include_summaries +): + agent = _make_agent_with_summarization_history(token_limit=1000) + count_tokens = agent.model_backend.token_counter.count_tokens_from_messages + count_tokens.side_effect = lambda messages: 30 * len(messages) + # Exercise the real threshold so repeated context builds detect retries. + del agent._calculate_next_summary_threshold + for _ in range(10): + agent.update_memory( + BaseMessage.make_user_message("user", "Remember this."), + OpenAIBackendRole.USER, + ) + agent.update_memory( + BaseMessage.make_assistant_message("assistant", "I will."), + OpenAIBackendRole.ASSISTANT, + ) + if include_summaries: + agent._summary_token_count = agent.token_limit + summary = "[CONTEXT_SUMMARY] A perfectly good summary." + result = {"summary": summary, "status": "Error: write failed"} + summarizer = ( + AsyncMock(return_value=result) + if async_mode + else MagicMock(return_value=result) + ) + if async_mode: + agent.asummarize = summarizer + else: + agent.summarize = summarizer + + for _ in range(5): + if async_mode: + messages, _ = await agent._get_context_with_summarization_async() + else: + messages, _ = agent._get_context_with_summarization() + assert len(messages) == 3 + assert messages[1]["content"] == summary + assert "Remember this." in messages[2]["content"] + + summarizer.assert_called_once_with(include_summaries=include_summaries) + if async_mode: + summarizer.assert_awaited_once() + + @pytest.mark.model_backend def test_chat_agent_stored_messages(): system_msg = BaseMessage(