From 1fa91fa39d3da362abab7490abc130c1ddfc297c Mon Sep 17 00:00:00 2001 From: Daoyuan Li <94409450+DaoyuanLi2816@users.noreply.github.com> Date: Fri, 10 Jul 2026 07:13:50 -0700 Subject: [PATCH] Fix AttributeError in ThreadDataMiddleware when runtime.context is None (#3989) `before_agent` guards `context = runtime.context or {}` at the top, but the `run_id` stamp on a trailing HumanMessage still read the raw `runtime.context`, so a None context (thread_id resolved from `config.configurable`) plus a HumanMessage last message raised `AttributeError: 'NoneType' object has no attribute 'get'`. Use the guarded local `context` instead. Adds a regression test. --- .../middlewares/thread_data_middleware.py | 2 +- backend/tests/test_thread_data_middleware.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/agents/middlewares/thread_data_middleware.py b/backend/packages/harness/deerflow/agents/middlewares/thread_data_middleware.py index 8d93de4ff..ad7a64b3c 100644 --- a/backend/packages/harness/deerflow/agents/middlewares/thread_data_middleware.py +++ b/backend/packages/harness/deerflow/agents/middlewares/thread_data_middleware.py @@ -107,7 +107,7 @@ class ThreadDataMiddleware(AgentMiddleware[ThreadDataMiddlewareState]): content=last_message.content, id=last_message.id, name=last_message.name or "user-input", - additional_kwargs={**last_message.additional_kwargs, "run_id": runtime.context.get("run_id"), "timestamp": datetime.now(UTC).isoformat()}, + additional_kwargs={**last_message.additional_kwargs, "run_id": context.get("run_id"), "timestamp": datetime.now(UTC).isoformat()}, ) return { diff --git a/backend/tests/test_thread_data_middleware.py b/backend/tests/test_thread_data_middleware.py index ef3e440f7..1a2a7e4ec 100644 --- a/backend/tests/test_thread_data_middleware.py +++ b/backend/tests/test_thread_data_middleware.py @@ -47,6 +47,23 @@ class TestThreadDataMiddleware: assert _as_posix(result["thread_data"]["uploads_path"]).endswith("threads/thread-from-config/user-data/uploads") assert runtime.context == {} + def test_before_agent_handles_none_context_with_trailing_human_message(self, tmp_path, monkeypatch): + # Regression: run_id was read via the unguarded `runtime.context`, so a None context plus a + # trailing HumanMessage raised AttributeError (thread_id still resolves from config.configurable). + from langchain_core.messages import HumanMessage + + middleware = ThreadDataMiddleware(base_dir=str(tmp_path), lazy_init=True) + runtime = Runtime(context=None) + monkeypatch.setattr( + "deerflow.agents.middlewares.thread_data_middleware.get_config", + lambda: {"configurable": {"thread_id": "thread-from-config"}}, + ) + + result = middleware.before_agent(state={"messages": [HumanMessage(content="hello", id="m1")]}, runtime=runtime) + + assert result is not None + assert runtime.context is None + def test_before_agent_raises_clear_error_when_thread_id_missing_everywhere(self, tmp_path, monkeypatch): middleware = ThreadDataMiddleware(base_dir=str(tmp_path), lazy_init=True) monkeypatch.setattr(