From a181c3398b55072acb6bcf0d9084da91efd35786 Mon Sep 17 00:00:00 2001 From: Mason Zhou <2427314200@qq.com> Date: Sun, 16 Aug 2026 23:54:06 +0800 Subject: [PATCH] fix: support Studio file-based app loading (#4838) * fix: support Studio file-based app loading * docs: clarify Studio loader invariant --- README.md | 2 ++ backend/README.md | 2 ++ backend/app/gateway/AGENTS.md | 16 ++++++++++------ backend/app/gateway/langgraph_studio.py | 6 +++--- .../tests/test_langgraph_studio_lifespan.py | 19 +++++++++++++++++++ 5 files changed, 36 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index e6b4cba36..6a4543e24 100644 --- a/README.md +++ b/README.md @@ -400,6 +400,8 @@ cannot restore server privileges or be discarded by the runtime's startup cleanup. Keep the backend dependencies synchronized with `uv sync`; this compatibility path requires the declared LangGraph runtime versions and logs a warning if the persisted-store contract no longer matches its expectations. +The documented command uses LangGraph's file-based custom-app loader, which is +also covered directly by DeerFlow's regression tests. For workflows that invoke `backend/langgraph.json` through LangGraph Studio or a direct LangGraph Server, DeerFlow consumes the authenticated identity diff --git a/backend/README.md b/backend/README.md index c824f66ee..07c260c1d 100644 --- a/backend/README.md +++ b/backend/README.md @@ -298,6 +298,8 @@ reactivate server-only privileges or be discarded by runtime startup cleanup. Run `uv sync` after dependency changes; this compatibility path requires the declared LangGraph runtime versions and warns when the persisted-store contract does not match its expectations. +The same file-based custom-app loading path used by this command is covered by +the backend regression suite. --- diff --git a/backend/app/gateway/AGENTS.md b/backend/app/gateway/AGENTS.md index a3f0eb65b..356104264 100644 --- a/backend/app/gateway/AGENTS.md +++ b/backend/app/gateway/AGENTS.md @@ -23,12 +23,16 @@ genuine system assistant IDs from the CLI-provided graph registry, removes their persisted active/version rows so graph registration recreates them, and demotes every other legacy `created_by=system` marker in both active assistants and version history. This must happen before runtime 0.30.0 loads and purges -system-marked rows; a user application lifespan is too late. An empty graph -registry or absent persistence file is a no-op, while persistence parse/write -errors fail startup closed. The harness requires in-memory runtime 0.30.0 or -newer, and a persisted store containing no expected registered assistant row -emits a drift warning so changes to LangGraph's internal persistence contract -are observable. Because current create/update writes and all legacy +system-marked rows; a user application lifespan is too late. LangGraph executes +this file-backed custom app without first registering its module in +`sys.modules`; keep its annotations eager so dataclass processing remains +compatible with that loader, and preserve the direct file-loader regression +test. An empty graph registry or absent persistence file is a no-op, while +persistence parse/write errors fail startup closed. The harness requires +in-memory runtime 0.30.0 or newer, and a persisted store containing no +expected registered assistant row emits a drift warning so changes to +LangGraph's internal persistence contract are observable. Because current +create/update writes and all legacy versions are sanitized, ordinary owner-scoped assistant version selection remains enabled. Ordinary authenticated users retain owner-scoped assistant reads/searches. diff --git a/backend/app/gateway/langgraph_studio.py b/backend/app/gateway/langgraph_studio.py index 21329a67f..c2c505f5b 100644 --- a/backend/app/gateway/langgraph_studio.py +++ b/backend/app/gateway/langgraph_studio.py @@ -3,11 +3,11 @@ ``langgraph dev`` imports this custom application before entering the locked in-memory runtime lifespan. That ordering is intentional: runtime 0.30.0 loads and purges persisted ``created_by=system`` assistants before graph registration -and before a user application lifespan can run. +and before a user application lifespan can run. Keep annotations eager in this +module: LangGraph's file loader executes it without first registering the module +in ``sys.modules``, which breaks dataclasses with postponed annotations. """ -from __future__ import annotations - import json import logging import os diff --git a/backend/tests/test_langgraph_studio_lifespan.py b/backend/tests/test_langgraph_studio_lifespan.py index 8f37f83a1..5d92721f8 100644 --- a/backend/tests/test_langgraph_studio_lifespan.py +++ b/backend/tests/test_langgraph_studio_lifespan.py @@ -2,8 +2,10 @@ from __future__ import annotations +import importlib.util import json import logging +import sys from pathlib import Path from uuid import NAMESPACE_DNS, uuid4, uuid5 @@ -198,3 +200,20 @@ def test_langgraph_config_loads_the_pre_runtime_studio_app(): config = json.loads((Path(__file__).resolve().parents[1] / "langgraph.json").read_text(encoding="utf-8")) assert config["http"]["app"].endswith("app/gateway/langgraph_studio.py:langgraph_app") + + +def test_studio_app_supports_langgraph_file_loader(): + """The CLI executes the custom app without first registering its module.""" + module_path = Path(__file__).resolve().parents[1] / "app" / "gateway" / "langgraph_studio.py" + module_name = f"langgraph_studio_file_loader_{uuid4().hex}" + spec = importlib.util.spec_from_file_location(module_name, module_path) + + assert spec is not None + assert spec.loader is not None + assert module_name not in sys.modules + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + assert module_name not in sys.modules + assert module.langgraph_app is not None