From 65a186187d25903e45e16fdaa5f4103b212f5bda Mon Sep 17 00:00:00 2001 From: Jason Cat Date: Sat, 10 Oct 2026 21:27:35 +0800 Subject: [PATCH] fix(workspace-changes): keep the package importable as a first import (#6602) (#6608) `workspace_changes/api.py` imports `deerflow.runtime.user_context` for the AUTO sentinel (#6282). Importing that submodule runs the runtime package init, which imports `.runs` -> `worker` -> `deerflow.workspace_changes` back for `capture_workspace_snapshot` and friends, so importing `.api` as the package's first statement failed with ImportError while those names were still unbound. Resolve the re-export lazily via a module-level `__getattr__`, the pattern `deerflow/subagents/__init__.py` already uses. --- .../deerflow/workspace_changes/__init__.py | 15 +++++++++++- backend/tests/test_gateway_imports.py | 24 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/backend/packages/harness/deerflow/workspace_changes/__init__.py b/backend/packages/harness/deerflow/workspace_changes/__init__.py index 4d14413677..ccc5db5f6f 100644 --- a/backend/packages/harness/deerflow/workspace_changes/__init__.py +++ b/backend/packages/harness/deerflow/workspace_changes/__init__.py @@ -1,4 +1,3 @@ -from .api import get_workspace_changes_response from .diff import compare_snapshots, get_changed_output_paths, get_changed_paths from .recorder import capture_workspace_snapshot, record_workspace_changes from .scanner import scan_workspace_roots @@ -32,3 +31,17 @@ __all__ = [ "record_workspace_changes", "scan_workspace_roots", ] + + +def __getattr__(name: str): + # `.api` reads `deerflow.runtime.user_context` for the AUTO sentinel, and + # importing that package runs `deerflow.runtime`'s init, which imports + # `.runs` -> `worker` -> this package. Resolving it lazily keeps this + # package importable as a process's first import instead of failing with + # ImportError while its own names are still unbound. + if name == "get_workspace_changes_response": + from .api import get_workspace_changes_response + + globals()[name] = get_workspace_changes_response + return get_workspace_changes_response + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/backend/tests/test_gateway_imports.py b/backend/tests/test_gateway_imports.py index 0e08ea3189..ac8635c5d0 100644 --- a/backend/tests/test_gateway_imports.py +++ b/backend/tests/test_gateway_imports.py @@ -72,3 +72,27 @@ def test_subagent_package_public_executor_exports_are_lazy_importable() -> None: ) assert result.returncode == 0, result.stderr assert "SubagentExecutor SubagentResult" in result.stdout + + +def test_workspace_changes_package_imports_standalone() -> None: + """The workspace-changes package must import as the process's first import. + + ``workspace_changes.api`` reads ``deerflow.runtime.user_context`` for the + ``AUTO`` sentinel, which runs ``deerflow.runtime``'s package init and imports + ``.runs`` -> ``worker`` -> ``deerflow.workspace_changes`` again. Importing + ``.api`` before this package bound its own names made that re-entrant import + fail with ImportError whenever ``deerflow.runtime`` had not been imported + yet. + """ + result = subprocess.run( + [ + sys.executable, + "-c", + "import deerflow.workspace_changes as w; print(w.scan_workspace_roots.__name__, w.get_workspace_changes_response.__name__)", + ], + capture_output=True, + text=True, + env=_gateway_import_env(), + ) + assert result.returncode == 0, result.stderr + assert "scan_workspace_roots get_workspace_changes_response" in result.stdout