diff --git a/backend/packages/harness/deerflow/runtime/journal.py b/backend/packages/harness/deerflow/runtime/journal.py index 769bc42e5..513550177 100644 --- a/backend/packages/harness/deerflow/runtime/journal.py +++ b/backend/packages/harness/deerflow/runtime/journal.py @@ -197,12 +197,12 @@ class RunJournal(BaseCallbackHandler): [len(batch) for batch in messages], ) - # Capture the first human message sent to any LLM in this run. - if not self._first_human_msg and messages: + # Capture the first user message sent to the lead agent in this run. + caller = self._identify_caller(tags) + if caller == "lead_agent" and not self._first_human_msg and messages: for batch in reversed(messages): for m in reversed(batch): if isinstance(m, HumanMessage) and m.name != "summary" and m.additional_kwargs.get("hide_from_ui") is not True: - caller = self._identify_caller(tags) self.set_first_human_message(m.text) self._put( event_type="llm.human.input", diff --git a/backend/tests/test_run_journal.py b/backend/tests/test_run_journal.py index d789d3b83..5be093718 100644 --- a/backend/tests/test_run_journal.py +++ b/backend/tests/test_run_journal.py @@ -924,6 +924,57 @@ class TestChatModelStartHumanMessage: assert len(human_events) == 1 assert human_events[0]["content"]["content"] == "Real question" + @pytest.mark.anyio + async def test_summarization_prompt_does_not_capture_first_human_message(self, journal_setup): + """Internal summarization prompts must not replace the run's real user input.""" + from langchain_core.messages import HumanMessage + + j, store = journal_setup + summarization_prompt = HumanMessage( + content="\nContext Extraction Assistant\n\n\n\nExtract context...", + ) + j.on_chat_model_start( + {}, + [[summarization_prompt]], + run_id=uuid4(), + tags=["middleware:summarize"], + ) + j.on_chat_model_start( + {}, + [[HumanMessage(content="Real user follow-up")]], + run_id=uuid4(), + tags=["lead_agent"], + ) + await j.flush() + + assert j._first_human_msg == "Real user follow-up" + assert j.get_completion_data()["message_count"] == 1 + events = await store.list_events("t1", "r1") + human_events = [e for e in events if e["event_type"] == "llm.human.input"] + assert len(human_events) == 1 + assert human_events[0]["content"]["content"] == "Real user follow-up" + assert human_events[0]["metadata"]["caller"] == "lead_agent" + + @pytest.mark.anyio + @pytest.mark.parametrize("tags", [["middleware:summarize"], ["subagent:research"]]) + async def test_non_lead_human_prompts_are_not_captured_as_user_input(self, journal_setup, tags): + """Only lead-agent LLM starts create UI-facing human input events.""" + from langchain_core.messages import HumanMessage + + j, store = journal_setup + j.on_chat_model_start( + {}, + [[HumanMessage(content="Internal prompt")]], + run_id=uuid4(), + tags=tags, + ) + await j.flush() + + assert j._first_human_msg is None + assert j.get_completion_data()["message_count"] == 0 + events = await store.list_events("t1", "r1") + assert not any(e["event_type"] == "llm.human.input" for e in events) + @pytest.mark.anyio async def test_only_first_human_message_captured(self, journal_setup): """Subsequent on_chat_model_start calls do not overwrite the first message."""