diff --git a/backend/packages/harness/deerflow/runtime/journal.py b/backend/packages/harness/deerflow/runtime/journal.py index ffbd7479f..919c1a98a 100644 --- a/backend/packages/harness/deerflow/runtime/journal.py +++ b/backend/packages/harness/deerflow/runtime/journal.py @@ -225,7 +225,7 @@ class RunJournal(BaseCallbackHandler): if 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": + 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( diff --git a/backend/tests/test_run_journal.py b/backend/tests/test_run_journal.py index a68895d27..d789d3b83 100644 --- a/backend/tests/test_run_journal.py +++ b/backend/tests/test_run_journal.py @@ -854,6 +854,76 @@ class TestChatModelStartHumanMessage: assert j._first_human_msg == "Real question" + @pytest.mark.anyio + async def test_skips_hidden_human_messages(self, journal_setup): + """HumanMessages hidden from the UI are internal context, not user input.""" + from langchain_core.messages import HumanMessage + + j, store = journal_setup + messages_batch = [ + [ + HumanMessage(content="What is the weather today?"), + HumanMessage( + content="Your todo list from earlier...", + name="todo_reminder", + additional_kwargs={"hide_from_ui": True}, + ), + ], + ] + j.on_chat_model_start({}, messages_batch, run_id=uuid4(), tags=["lead_agent"]) + await j.flush() + + assert j._first_human_msg == "What is the weather today?" + 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"] == "What is the weather today?" + + @pytest.mark.anyio + async def test_only_hidden_human_messages_are_not_captured(self, journal_setup): + """A prompt containing only internal HumanMessages has no user input.""" + from langchain_core.messages import HumanMessage + + j, store = journal_setup + hidden_message = HumanMessage( + content="Internal context", + additional_kwargs={"hide_from_ui": True}, + ) + j.on_chat_model_start({}, [[hidden_message]], run_id=uuid4(), tags=["lead_agent"]) + 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_visible_human_message_after_hidden_only_prompt_is_captured(self, journal_setup): + """Skipping an internal-only prompt does not block later user input.""" + from langchain_core.messages import HumanMessage + + j, store = journal_setup + hidden_message = HumanMessage( + content="Internal context", + additional_kwargs={"hide_from_ui": True}, + ) + j.on_chat_model_start({}, [[hidden_message]], run_id=uuid4(), tags=["lead_agent"]) + j.on_chat_model_start( + {}, + [[HumanMessage(content="Real question")]], + run_id=uuid4(), + tags=["lead_agent"], + ) + await j.flush() + + assert j._first_human_msg == "Real question" + 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 question" + @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."""