mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-14 16:08:41 +00:00
fix(summarization): keep a Human Input Card reply as the current request (#5416)
Summarization rescues the latest user message by id so the current request survives compaction (#4882). It picked that message with is_real_user_message, which rejects every hide_from_ui HumanMessage, including Human Input Card replies. When a run starts from a card reply, the older visible request was kept and the user's answer was summarized away. Use is_genuine_user_message, which accepts hidden messages that carry a valid human_input_response and still skips other hidden injections. Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
96d6ff3aca
commit
f5cf25a8b6
@ -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
|
||||
|
||||
|
||||
@ -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] = []
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user