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