diff --git a/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py index 8ecff1ec6..e59aa87ac 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/summarization_middleware.py @@ -18,12 +18,12 @@ from langgraph.graph.message import REMOVE_ALL_MESSAGES from langgraph.runtime import Runtime from deerflow.agents.middlewares.dynamic_context_middleware import is_dynamic_context_reminder +from deerflow.agents.middlewares.message_utils import is_genuine_user_message from deerflow.config.app_config import get_app_config from deerflow.config.summarization_config import DEFAULT_KEEP from deerflow.config.task_continuity_config import TaskContinuityConfig from deerflow.extensions.notify import notify_context_compacted from deerflow.models import create_chat_model -from deerflow.utils.messages import is_real_user_message logger = logging.getLogger(__name__) _SUMMARY_TRIGGER_MESSAGE_NAME = "summary" @@ -578,10 +578,11 @@ class DeerFlowSummarizationMiddleware(SummarizationMiddleware): # rescue no longer covers it (see _preserve_dynamic_context_reminders), so # lock its id here and rescue by exact id. This keeps the current request # without "moving cutoff" — which would also retain early AI/Tool turns and - # never compress a first-turn long analysis. + # never compress a first-turn long analysis. A Human Input Card reply is + # hidden from the UI but is still the user's current request. latest_user_id: str | None = None for msg in reversed(messages): - if is_real_user_message(msg): + if is_genuine_user_message(msg): latest_user_id = msg.id break diff --git a/backend/tests/test_summarization_middleware.py b/backend/tests/test_summarization_middleware.py index f35f505a9..9e0efc1b1 100644 --- a/backend/tests/test_summarization_middleware.py +++ b/backend/tests/test_summarization_middleware.py @@ -1461,6 +1461,51 @@ def test_current_request_survives_and_stale_peer_compresses() -> None: assert len(ev.messages_to_summarize) > 0 +def test_human_input_card_reply_survives_as_current_request() -> None: + """A Human Input Card reply is the current request, so it survives compaction. + + The frontend sends the answer as a hidden HumanMessage carrying + ``human_input_response``. It must be rescued like a visible request instead + of the older request that triggered the clarification. + """ + captured: list[SummarizationEvent] = [] + middleware = _middleware(before_summarization=[captured.append], keep=("messages", 6)) + + request = HumanMessage(content="Research topic X and write a report", id="request") + reply = HumanMessage( + content="Only Europe, and only 2025 data", + id="card-reply", + additional_kwargs={ + "hide_from_ui": True, + "human_input_response": { + "version": 1, + "kind": "human_input_response", + "source": "ask_clarification", + "request_id": "clarify-1", + "response_kind": "text", + "value": "Only Europe, and only 2025 data", + }, + }, + ) + messages = [ + request, + AIMessage(content="", id="ai-clarify", tool_calls=[{"id": "clarify-1", "name": "ask_clarification", "args": {"question": "Which region?"}}]), + ToolMessage(content="Which region?", tool_call_id="clarify-1", id="tool-clarify"), + reply, + ] + for k in range(1, 6): + messages.append(AIMessage(content=f"ai{k}", id=f"ai{k}", tool_calls=[{"id": f"tc{k}", "name": "web_search", "args": {}}])) + messages.append(ToolMessage(content=f"r{k}", tool_call_id=f"tc{k}", id=f"tool{k}")) + + middleware.before_model({"messages": messages}, _runtime()) + + assert len(captured) == 1 + ev = captured[0] + assert reply.id in [m.id for m in ev.preserved_messages] + assert reply.id not in [m.id for m in ev.messages_to_summarize] + assert len(ev.messages_to_summarize) > 0 + + def test_first_turn_long_analysis_preserves_current_request() -> None: """First-turn long analysis: the current request (X__user peer) stays preserved even outside the keep window, and early AI/Tool turns still compress.""" captured: list[SummarizationEvent] = []