From 993fb0ff9db46f9b2636b4c1d42a8e3fb5b3460b Mon Sep 17 00:00:00 2001 From: Willem Jiang Date: Mon, 6 Apr 2026 08:24:51 +0800 Subject: [PATCH 1/3] fix: escape shell variables in production langgraph command (#1877) (#1880) Escape shell variables to prevent Docker Compose from attempting substitution at parse time. Rename allow_blocking_flag to allow_blocking for consistency with dev version. Fixes the 'allow_blocking_flag not set' warning and enables --allow-blocking flag to work correctly. --- docker/docker-compose.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml index 8c1432da7..53707fdcc 100644 --- a/docker/docker-compose.yaml +++ b/docker/docker-compose.yaml @@ -125,7 +125,7 @@ services: UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:0.7.20} UV_INDEX_URL: ${UV_INDEX_URL:-https://pypi.org/simple} container_name: deer-flow-langgraph - command: sh -c 'cd /app/backend && allow_blocking_flag="" && if [ "${LANGGRAPH_ALLOW_BLOCKING:-0}" = "1" ]; then allow_blocking_flag="--allow-blocking"; fi && uv run langgraph dev --no-browser ${allow_blocking_flag} --no-reload --host 0.0.0.0 --port 2024 --n-jobs-per-worker ${LANGGRAPH_JOBS_PER_WORKER:-10}' + command: sh -c 'cd /app/backend && allow_blocking="" && if [ "\${LANGGRAPH_ALLOW_BLOCKING:-0}" = "1" ]; then allow_blocking="--allow-blocking"; fi && uv run langgraph dev --no-browser \${allow_blocking} --no-reload --host 0.0.0.0 --port 2024 --n-jobs-per-worker \${LANGGRAPH_JOBS_PER_WORKER:-10}' volumes: - ${DEER_FLOW_CONFIG_PATH}:/app/backend/config.yaml:ro - ${DEER_FLOW_EXTENSIONS_CONFIG_PATH}:/app/backend/extensions_config.json:ro From ed90a2ee9d9a972824cece2ffe3a6af658425486 Mon Sep 17 00:00:00 2001 From: amonduuuul <92299558+yuemeng200@users.noreply.github.com> Date: Mon, 6 Apr 2026 08:34:25 +0800 Subject: [PATCH 2/3] fix(docker): recover invalid .venv to prevent startup restart loops (#1871) * fix(docker): recover invalid .venv before service startup * Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Willem Jiang Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- docker/docker-compose-dev.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose-dev.yaml b/docker/docker-compose-dev.yaml index 53dcb80b2..02552e49f 100644 --- a/docker/docker-compose-dev.yaml +++ b/docker/docker-compose-dev.yaml @@ -123,7 +123,7 @@ services: UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:0.7.20} UV_INDEX_URL: ${UV_INDEX_URL:-https://pypi.org/simple} container_name: deer-flow-gateway - command: sh -c "cd backend && uv sync && PYTHONPATH=. uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 --reload --reload-include='*.yaml .env' > /app/logs/gateway.log 2>&1" + command: sh -c "{ cd backend && (uv sync || (echo '[startup] uv sync failed; recreating .venv and retrying once' && uv venv --allow-existing .venv && uv sync)) && PYTHONPATH=. uv run uvicorn app.gateway.app:app --host 0.0.0.0 --port 8001 --reload --reload-include='*.yaml .env'; } > /app/logs/gateway.log 2>&1" volumes: - ../backend/:/app/backend/ # Preserve the .venv built during Docker image build — mounting the full backend/ @@ -180,7 +180,7 @@ services: UV_IMAGE: ${UV_IMAGE:-ghcr.io/astral-sh/uv:0.7.20} UV_INDEX_URL: ${UV_INDEX_URL:-https://pypi.org/simple} container_name: deer-flow-langgraph - command: sh -c "cd backend && uv sync && allow_blocking='' && if [ \"\${LANGGRAPH_ALLOW_BLOCKING:-0}\" = '1' ]; then allow_blocking='--allow-blocking'; fi && uv run langgraph dev --no-browser \${allow_blocking} --host 0.0.0.0 --port 2024 --n-jobs-per-worker \${LANGGRAPH_JOBS_PER_WORKER:-10} > /app/logs/langgraph.log 2>&1" + command: sh -c "cd backend && { (uv sync || (echo '[startup] uv sync failed; recreating .venv and retrying once' && uv venv --allow-existing .venv && uv sync)) && allow_blocking='' && if [ \"\${LANGGRAPH_ALLOW_BLOCKING:-0}\" = '1' ]; then allow_blocking='--allow-blocking'; fi && uv run langgraph dev --no-browser \${allow_blocking} --host 0.0.0.0 --port 2024 --n-jobs-per-worker \${LANGGRAPH_JOBS_PER_WORKER:-10}; } > /app/logs/langgraph.log 2>&1" volumes: - ../backend/:/app/backend/ # Preserve the .venv built during Docker image build — mounting the full backend/ From 29575c32f9a3aa0c98bf1c7107ec1c3ef97884b3 Mon Sep 17 00:00:00 2001 From: suyua9 <1521777066@qq.com> Date: Mon, 6 Apr 2026 10:09:39 +0800 Subject: [PATCH 3/3] fix: expose custom events from DeerFlowClient.stream() (#1827) * fix: expose custom client stream events Signed-off-by: suyua9 <1521777066@qq.com> * fix(client): normalize streamed custom mode values * test(client): satisfy backend ruff import ordering --------- Signed-off-by: suyua9 <1521777066@qq.com> Co-authored-by: Willem Jiang --- backend/packages/harness/deerflow/client.py | 18 ++++++- backend/tests/test_client.py | 55 +++++++++++++++++++++ 2 files changed, 72 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/client.py b/backend/packages/harness/deerflow/client.py index 27ebe537e..37d528c98 100644 --- a/backend/packages/harness/deerflow/client.py +++ b/backend/packages/harness/deerflow/client.py @@ -345,6 +345,7 @@ class DeerFlowClient: Yields: StreamEvent with one of: - type="values" data={"title": str|None, "messages": [...], "artifacts": [...]} + - type="custom" data={...} - type="messages-tuple" data={"type": "ai", "content": str, "id": str} - type="messages-tuple" data={"type": "ai", "content": str, "id": str, "usage_metadata": {...}} - type="messages-tuple" data={"type": "ai", "content": "", "id": str, "tool_calls": [...]} @@ -365,7 +366,22 @@ class DeerFlowClient: seen_ids: set[str] = set() cumulative_usage: dict[str, int] = {"input_tokens": 0, "output_tokens": 0, "total_tokens": 0} - for chunk in self._agent.stream(state, config=config, context=context, stream_mode="values"): + for item in self._agent.stream( + state, + config=config, + context=context, + stream_mode=["values", "custom"], + ): + if isinstance(item, tuple) and len(item) == 2: + mode, chunk = item + mode = str(mode) + else: + mode, chunk = "values", item + + if mode == "custom": + yield StreamEvent(type="custom", data=chunk) + continue + messages = chunk.get("messages", []) for msg in messages: diff --git a/backend/tests/test_client.py b/backend/tests/test_client.py index e78c0ea26..a88bb43c6 100644 --- a/backend/tests/test_client.py +++ b/backend/tests/test_client.py @@ -5,6 +5,7 @@ import concurrent.futures import json import tempfile import zipfile +from enum import Enum from pathlib import Path from unittest.mock import MagicMock, patch @@ -205,6 +206,33 @@ class TestStream: msg_events = _ai_events(events) assert msg_events[0].data["content"] == "Hello!" + def test_custom_events_are_forwarded(self, client): + """stream() forwards custom stream events alongside normal values output.""" + ai = AIMessage(content="Hello!", id="ai-1") + agent = MagicMock() + agent.stream.return_value = iter( + [ + ("custom", {"type": "task_started", "task_id": "task-1"}), + ("values", {"messages": [HumanMessage(content="hi", id="h-1"), ai]}), + ] + ) + + with ( + patch.object(client, "_ensure_agent"), + patch.object(client, "_agent", agent), + ): + events = list(client.stream("hi", thread_id="t-custom")) + + agent.stream.assert_called_once() + call_kwargs = agent.stream.call_args.kwargs + assert call_kwargs["stream_mode"] == ["values", "custom"] + + assert events[0].type == "custom" + assert events[0].data == {"type": "task_started", "task_id": "task-1"} + assert any(event.type == "messages-tuple" and event.data["content"] == "Hello!" for event in events) + assert any(event.type == "values" for event in events) + assert events[-1].type == "end" + def test_context_propagation(self, client): """stream() passes agent_name to the context.""" agent = _make_agent_mock([{"messages": [AIMessage(content="ok", id="ai-1")]}]) @@ -222,6 +250,33 @@ class TestStream: assert call_kwargs["context"]["thread_id"] == "t1" assert call_kwargs["context"]["agent_name"] == "test-agent-1" + def test_custom_mode_is_normalized_to_string(self, client): + """stream() forwards custom events even when the mode is not a plain string.""" + + class StreamMode(Enum): + CUSTOM = "custom" + + def __str__(self): + return self.value + + agent = _make_agent_mock( + [ + (StreamMode.CUSTOM, {"type": "task_started", "task_id": "task-1"}), + {"messages": [AIMessage(content="Hello!", id="ai-1")]}, + ] + ) + + with ( + patch.object(client, "_ensure_agent"), + patch.object(client, "_agent", agent), + ): + events = list(client.stream("hi", thread_id="t-custom-enum")) + + assert events[0].type == "custom" + assert events[0].data == {"type": "task_started", "task_id": "task-1"} + assert any(event.type == "messages-tuple" and event.data["content"] == "Hello!" for event in events) + assert events[-1].type == "end" + def test_tool_call_and_result(self, client): """stream() emits messages-tuple events for tool calls and results.""" ai = AIMessage(content="", id="ai-1", tool_calls=[{"name": "bash", "args": {"cmd": "ls"}, "id": "tc-1"}])