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."""