mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-23 08:25:57 +00:00
edf345cd72
- Freeze all config models (AppConfig + 15 sub-configs) with frozen=True - Purify from_file() — remove 9 load_*_from_dict() side-effect calls - Replace mtime/reload/push/pop machinery with single ContextVar + init_app_config() - Delete 10 sub-module globals and their getters/setters/loaders - Migrate 50+ consumers from get_*_config() to get_app_config().xxx - Expand DeerFlowContext: app_config + thread_id + agent_name (frozen dataclass) - Wire into Gateway runtime (worker.py) and DeerFlowClient via context= parameter - Remove sandbox_id from runtime.context — flows through ThreadState.sandbox only - Middleware/tools access runtime.context directly via Runtime[DeerFlowContext] generic - resolve_context() retained at server entry points for LangGraph Server fallback
1106 lines
37 KiB
Markdown
1106 lines
37 KiB
Markdown
# Config Refactor Implementation Plan
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||
|
||
**Goal:** Eliminate global mutable state in the configuration system — frozen AppConfig, pure `from_file()`, single ContextVar, `Runtime[DeerFlowContext]` propagation.
|
||
|
||
**Architecture:** All config models become `frozen=True`. `from_file()` becomes a pure function (no side effects). Sub-config module globals are deleted; consumers migrate to `get_app_config().xxx`. Agent execution path uses LangGraph `Runtime[DeerFlowContext]` for typed, per-invocation config access. Gateway path uses a single ContextVar.
|
||
|
||
**Tech Stack:** Pydantic v2 (`frozen=True`, `model_copy`), Python `contextvars.ContextVar`, LangGraph `Runtime`/`ToolRuntime` (>= 1.1.5)
|
||
|
||
**Design Spec:** `docs/plans/2026-04-12-config-refactor-design.md`
|
||
**Issues:** #2151 (implementation), #1811 (RFC)
|
||
|
||
---
|
||
|
||
## File Structure
|
||
|
||
### New files
|
||
|
||
| File | Responsibility |
|
||
|------|---------------|
|
||
| `deerflow/config/context.py` | `DeerFlowContext` frozen dataclass + `init_app_config()` / `get_app_config()` backed by single ContextVar |
|
||
|
||
### Modified files (config layer)
|
||
|
||
| File | Change |
|
||
|------|--------|
|
||
| `deerflow/config/app_config.py` | `frozen=True`, purify `from_file()`, delete mtime/reload/reset/push/pop machinery |
|
||
| `deerflow/config/memory_config.py` | `frozen=True`, delete globals (`_memory_config`, `get_memory_config`, `set_memory_config`, `load_memory_config_from_dict`) |
|
||
| `deerflow/config/title_config.py` | Same pattern |
|
||
| `deerflow/config/summarization_config.py` | Same pattern |
|
||
| `deerflow/config/subagents_config.py` | Same pattern |
|
||
| `deerflow/config/guardrails_config.py` | Same pattern (also delete `reset_guardrails_config`) |
|
||
| `deerflow/config/tool_search_config.py` | Same pattern |
|
||
| `deerflow/config/checkpointer_config.py` | Same pattern |
|
||
| `deerflow/config/stream_bridge_config.py` | Same pattern |
|
||
| `deerflow/config/acp_config.py` | Same pattern |
|
||
| `deerflow/config/extensions_config.py` | `frozen=True`, delete globals (`_extensions_config`, `reload_extensions_config`, `reset_extensions_config`, `set_extensions_config`) |
|
||
| `deerflow/config/__init__.py` | Update exports — remove deleted getters, add `init_app_config`, `DeerFlowContext` |
|
||
|
||
### Modified files (consumers — production code)
|
||
|
||
| File | Change |
|
||
|------|--------|
|
||
| `deerflow/agents/lead_agent/agent.py` | `get_app_config()` calls stay; `get_summarization_config()` → `get_app_config().summarization` |
|
||
| `deerflow/agents/lead_agent/prompt.py` | `get_memory_config()` → `get_app_config().memory`; `get_acp_agents()` → `get_app_config()` based |
|
||
| `deerflow/agents/middlewares/memory_middleware.py` | `get_memory_config()` → read from `Runtime` or `get_app_config().memory` |
|
||
| `deerflow/agents/middlewares/title_middleware.py` | `get_title_config()` → read from `Runtime` or `get_app_config().title` |
|
||
| `deerflow/agents/middlewares/tool_error_handling_middleware.py` | `get_guardrails_config()` → `get_app_config().guardrails` |
|
||
| `deerflow/agents/memory/updater.py` | `get_memory_config()` → `get_app_config().memory` |
|
||
| `deerflow/agents/memory/queue.py` | `get_memory_config()` → `get_app_config().memory` |
|
||
| `deerflow/agents/memory/storage.py` | `get_memory_config()` → `get_app_config().memory` |
|
||
| `deerflow/agents/checkpointer/provider.py` | `get_checkpointer_config()` → `get_app_config().checkpointer` |
|
||
| `deerflow/runtime/store/provider.py` | `get_checkpointer_config()` → `get_app_config().checkpointer` |
|
||
| `deerflow/runtime/stream_bridge/async_provider.py` | `get_stream_bridge_config()` → `get_app_config().stream_bridge` |
|
||
| `deerflow/subagents/registry.py` | `get_subagents_app_config()` → `get_app_config().subagents` |
|
||
| `deerflow/tools/tools.py` | `get_acp_agents()` → `get_app_config()` based |
|
||
| `deerflow/client.py` | Remove `reload_app_config`/`reload_extensions_config` imports and calls; use `init_app_config()` |
|
||
| `app/gateway/routers/mcp.py` | `reload_extensions_config()` → construct new config + `init_app_config()` |
|
||
| `app/gateway/routers/skills.py` | Same |
|
||
| `app/gateway/routers/memory.py` | `get_memory_config()` → `get_app_config().memory` |
|
||
| `app/gateway/app.py` | Call `init_app_config()` at startup |
|
||
|
||
### Modified files (tests)
|
||
|
||
~100 test locations need updating. Pattern: replace `patch("...get_memory_config", ...)` with `patch("...get_app_config", ...)` returning a frozen AppConfig with the desired sub-config.
|
||
|
||
---
|
||
|
||
## Task 1: Freeze all sub-config models
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/memory_config.py`
|
||
- Modify: `deerflow/config/title_config.py`
|
||
- Modify: `deerflow/config/summarization_config.py`
|
||
- Modify: `deerflow/config/subagents_config.py`
|
||
- Modify: `deerflow/config/guardrails_config.py`
|
||
- Modify: `deerflow/config/tool_search_config.py`
|
||
- Modify: `deerflow/config/checkpointer_config.py`
|
||
- Modify: `deerflow/config/stream_bridge_config.py`
|
||
- Modify: `deerflow/config/token_usage_config.py`
|
||
- Modify: `deerflow/config/skills_config.py`
|
||
- Modify: `deerflow/config/skill_evolution_config.py`
|
||
- Modify: `deerflow/config/sandbox_config.py`
|
||
- Modify: `deerflow/config/model_config.py`
|
||
- Modify: `deerflow/config/tool_config.py`
|
||
- Modify: `deerflow/config/agents_config.py`
|
||
- Modify: `deerflow/config/extensions_config.py` (McpServerConfig, McpOAuthConfig, SkillStateConfig, ExtensionsConfig)
|
||
- Test: `tests/test_config_frozen.py`
|
||
|
||
- [ ] **Step 1: Write test that all config models are frozen**
|
||
|
||
```python
|
||
# tests/test_config_frozen.py
|
||
import pytest
|
||
from pydantic import ValidationError
|
||
|
||
from deerflow.config.memory_config import MemoryConfig
|
||
from deerflow.config.title_config import TitleConfig
|
||
from deerflow.config.summarization_config import SummarizationConfig
|
||
from deerflow.config.subagents_config import SubagentsAppConfig
|
||
from deerflow.config.guardrails_config import GuardrailsConfig
|
||
from deerflow.config.tool_search_config import ToolSearchConfig
|
||
from deerflow.config.checkpointer_config import CheckpointerConfig
|
||
from deerflow.config.stream_bridge_config import StreamBridgeConfig
|
||
from deerflow.config.token_usage_config import TokenUsageConfig
|
||
from deerflow.config.skills_config import SkillsConfig
|
||
from deerflow.config.skill_evolution_config import SkillEvolutionConfig
|
||
from deerflow.config.sandbox_config import SandboxConfig
|
||
from deerflow.config.model_config import ModelConfig
|
||
from deerflow.config.tool_config import ToolConfig, ToolGroupConfig
|
||
from deerflow.config.extensions_config import ExtensionsConfig, McpServerConfig
|
||
|
||
|
||
@pytest.mark.parametrize("cls,kwargs", [
|
||
(MemoryConfig, {}),
|
||
(TitleConfig, {}),
|
||
(SummarizationConfig, {}),
|
||
(SubagentsAppConfig, {}),
|
||
(GuardrailsConfig, {}),
|
||
(ToolSearchConfig, {}),
|
||
(TokenUsageConfig, {}),
|
||
(SkillsConfig, {}),
|
||
(SkillEvolutionConfig, {}),
|
||
(McpServerConfig, {}),
|
||
(ExtensionsConfig, {}),
|
||
])
|
||
def test_config_model_is_frozen(cls, kwargs):
|
||
"""All config models must be frozen — mutation raises ValidationError."""
|
||
instance = cls(**kwargs)
|
||
first_field = next(iter(cls.model_fields))
|
||
with pytest.raises(ValidationError):
|
||
setattr(instance, first_field, getattr(instance, first_field))
|
||
```
|
||
|
||
- [ ] **Step 2: Run test to verify it fails**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_frozen.py -v`
|
||
Expected: FAIL — models are not frozen yet
|
||
|
||
- [ ] **Step 3: Add `frozen=True` to every config model**
|
||
|
||
Add `model_config = ConfigDict(frozen=True)` (or update existing `ConfigDict`) in each file listed above. For models that already have `ConfigDict(extra="allow")`, change to `ConfigDict(extra="allow", frozen=True)`.
|
||
|
||
Example for `memory_config.py`:
|
||
```python
|
||
from pydantic import BaseModel, ConfigDict, Field
|
||
|
||
class MemoryConfig(BaseModel):
|
||
model_config = ConfigDict(frozen=True)
|
||
# ... fields unchanged
|
||
```
|
||
|
||
- [ ] **Step 4: Run test to verify it passes**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_frozen.py -v`
|
||
Expected: PASS
|
||
|
||
- [ ] **Step 5: Run full test suite, fix any tests that mutate config objects**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v 2>&1 | head -100`
|
||
|
||
If tests fail because they mutate frozen config objects, fix them using `model_copy(update={...})` or by constructing fresh instances.
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): make all config models frozen"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 2: Freeze AppConfig
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/app_config.py`
|
||
- Test: `tests/test_config_frozen.py` (extend)
|
||
|
||
- [ ] **Step 1: Add AppConfig frozen test**
|
||
|
||
```python
|
||
# Append to tests/test_config_frozen.py
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
def test_app_config_is_frozen():
|
||
config = AppConfig(sandbox={"use": "test"})
|
||
with pytest.raises(ValidationError):
|
||
config.log_level = "debug"
|
||
```
|
||
|
||
- [ ] **Step 2: Run test — should fail**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_frozen.py::test_app_config_is_frozen -v`
|
||
Expected: FAIL
|
||
|
||
- [ ] **Step 3: Set `frozen=True` on AppConfig**
|
||
|
||
In `app_config.py`, change:
|
||
```python
|
||
model_config = ConfigDict(extra="allow", frozen=False)
|
||
```
|
||
to:
|
||
```python
|
||
model_config = ConfigDict(extra="allow", frozen=True)
|
||
```
|
||
|
||
- [ ] **Step 4: Run test — should pass**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_frozen.py::test_app_config_is_frozen -v`
|
||
Expected: PASS
|
||
|
||
- [ ] **Step 5: Run full test suite, fix failures**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v 2>&1 | head -100`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): make AppConfig frozen"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 3: Purify `from_file()`
|
||
|
||
Remove the 8 `load_*_from_dict()` side-effect calls from `AppConfig.from_file()`. Sub-config data already flows through AppConfig fields — the globals are redundant.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/app_config.py`
|
||
- Test: `tests/test_from_file_pure.py`
|
||
|
||
- [ ] **Step 1: Write test that `from_file()` does not mutate sub-module globals**
|
||
|
||
```python
|
||
# tests/test_from_file_pure.py
|
||
from unittest.mock import patch
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
|
||
def test_from_file_does_not_call_load_functions(tmp_path):
|
||
"""from_file() must be pure — no side effects on sub-modules."""
|
||
config_file = tmp_path / "config.yaml"
|
||
config_file.write_text("""
|
||
config_version: 6
|
||
models: []
|
||
sandbox:
|
||
use: "deerflow.sandbox.local:LocalSandboxProvider"
|
||
memory:
|
||
enabled: false
|
||
title:
|
||
enabled: false
|
||
""")
|
||
|
||
load_fns = [
|
||
"deerflow.config.app_config.load_title_config_from_dict",
|
||
"deerflow.config.app_config.load_summarization_config_from_dict",
|
||
"deerflow.config.app_config.load_memory_config_from_dict",
|
||
"deerflow.config.app_config.load_subagents_config_from_dict",
|
||
"deerflow.config.app_config.load_tool_search_config_from_dict",
|
||
"deerflow.config.app_config.load_guardrails_config_from_dict",
|
||
"deerflow.config.app_config.load_checkpointer_config_from_dict",
|
||
"deerflow.config.app_config.load_stream_bridge_config_from_dict",
|
||
"deerflow.config.app_config.load_acp_config_from_dict",
|
||
]
|
||
|
||
patches = [patch(fn) for fn in load_fns]
|
||
mocks = [p.start() for p in patches]
|
||
|
||
result = AppConfig.from_file(str(config_file))
|
||
|
||
for mock, fn_name in zip(mocks, load_fns):
|
||
mock.assert_not_called(), f"{fn_name} should not be called by pure from_file()"
|
||
|
||
for p in patches:
|
||
p.stop()
|
||
|
||
assert result.memory.enabled is False
|
||
assert result.title.enabled is False
|
||
```
|
||
|
||
- [ ] **Step 2: Run test — should fail**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_from_file_pure.py -v`
|
||
Expected: FAIL — `from_file()` still calls `load_*_from_dict()`
|
||
|
||
- [ ] **Step 3: Remove all `load_*_from_dict()` calls from `from_file()`**
|
||
|
||
In `app_config.py`, delete these blocks from `from_file()`:
|
||
|
||
```python
|
||
# DELETE all of these:
|
||
if "title" in config_data:
|
||
load_title_config_from_dict(config_data["title"])
|
||
if "summarization" in config_data:
|
||
load_summarization_config_from_dict(config_data["summarization"])
|
||
if "memory" in config_data:
|
||
load_memory_config_from_dict(config_data["memory"])
|
||
if "subagents" in config_data:
|
||
load_subagents_config_from_dict(config_data["subagents"])
|
||
if "tool_search" in config_data:
|
||
load_tool_search_config_from_dict(config_data["tool_search"])
|
||
if "guardrails" in config_data:
|
||
load_guardrails_config_from_dict(config_data["guardrails"])
|
||
if "checkpointer" in config_data:
|
||
load_checkpointer_config_from_dict(config_data["checkpointer"])
|
||
if "stream_bridge" in config_data:
|
||
load_stream_bridge_config_from_dict(config_data["stream_bridge"])
|
||
load_acp_config_from_dict(config_data.get("acp_agents", {}))
|
||
```
|
||
|
||
Also remove the corresponding imports at the top of the file.
|
||
|
||
- [ ] **Step 4: Run test — should pass**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_from_file_pure.py -v`
|
||
Expected: PASS
|
||
|
||
- [ ] **Step 5: Run full test suite, fix failures**
|
||
|
||
Tests that relied on `from_file()` populating sub-module globals will now fail. Fix them by reading from AppConfig fields instead.
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v 2>&1 | head -100`
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): purify from_file() — remove side-effect load calls"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 4: Replace app_config.py lifecycle with single ContextVar
|
||
|
||
Replace the current mtime/reload/push/pop machinery with a simple ContextVar.
|
||
|
||
**Files:**
|
||
- Create: `deerflow/config/context.py`
|
||
- Modify: `deerflow/config/app_config.py`
|
||
- Modify: `deerflow/config/__init__.py`
|
||
- Test: `tests/test_config_context.py`
|
||
|
||
- [ ] **Step 1: Write tests for new ContextVar-based lifecycle**
|
||
|
||
```python
|
||
# tests/test_config_context.py
|
||
import pytest
|
||
from deerflow.config.context import init_app_config, get_app_config, ConfigNotInitializedError
|
||
from deerflow.config.app_config import AppConfig
|
||
from deerflow.config.sandbox_config import SandboxConfig
|
||
|
||
|
||
def _make_config(**overrides) -> AppConfig:
|
||
defaults = {"sandbox": SandboxConfig(use="test")}
|
||
defaults.update(overrides)
|
||
return AppConfig(**defaults)
|
||
|
||
|
||
def test_get_before_init_raises():
|
||
"""get_app_config() must raise if init_app_config() was not called."""
|
||
# Note: this test must run in a fresh context — use contextvars.copy_context()
|
||
import contextvars
|
||
ctx = contextvars.copy_context()
|
||
with pytest.raises(ConfigNotInitializedError):
|
||
ctx.run(get_app_config)
|
||
|
||
|
||
def test_init_then_get():
|
||
import contextvars
|
||
config = _make_config()
|
||
ctx = contextvars.copy_context()
|
||
ctx.run(init_app_config, config)
|
||
result = ctx.run(get_app_config)
|
||
assert result is config
|
||
|
||
|
||
def test_init_replaces_previous():
|
||
import contextvars
|
||
config_a = _make_config(log_level="info")
|
||
config_b = _make_config(log_level="debug")
|
||
ctx = contextvars.copy_context()
|
||
ctx.run(init_app_config, config_a)
|
||
ctx.run(init_app_config, config_b)
|
||
result = ctx.run(get_app_config)
|
||
assert result.log_level == "debug"
|
||
```
|
||
|
||
- [ ] **Step 2: Run test — should fail**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_context.py -v`
|
||
Expected: FAIL — `context.py` does not exist yet
|
||
|
||
- [ ] **Step 3: Create `deerflow/config/context.py`**
|
||
|
||
```python
|
||
"""Single ContextVar for AppConfig lifecycle."""
|
||
|
||
from contextvars import ContextVar
|
||
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
|
||
class ConfigNotInitializedError(RuntimeError):
|
||
"""Raised when get_app_config() is called before init_app_config()."""
|
||
|
||
def __init__(self):
|
||
super().__init__(
|
||
"AppConfig not initialized. Call init_app_config() at process startup."
|
||
)
|
||
|
||
|
||
_app_config_var: ContextVar[AppConfig] = ContextVar("deerflow_app_config")
|
||
|
||
|
||
def init_app_config(config: AppConfig) -> None:
|
||
"""Set the AppConfig for the current context. Call once at process startup."""
|
||
_app_config_var.set(config)
|
||
|
||
|
||
def get_app_config() -> AppConfig:
|
||
"""Get the current AppConfig. Raises ConfigNotInitializedError if not initialized."""
|
||
try:
|
||
return _app_config_var.get()
|
||
except LookupError:
|
||
raise ConfigNotInitializedError()
|
||
```
|
||
|
||
- [ ] **Step 4: Run test — should pass**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/test_config_context.py -v`
|
||
Expected: PASS
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): add context.py with ContextVar-based lifecycle"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 5: Migrate `get_app_config` imports to new context module
|
||
|
||
Replace the old `get_app_config` (from `app_config.py`) with the new one (from `context.py`) across all consumers. The old module's `get_app_config`, `reload_app_config`, `reset_app_config`, `set_app_config`, `push/pop_current_app_config` are deleted.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/__init__.py` — re-export `get_app_config` and `init_app_config` from `context.py`
|
||
- Modify: `deerflow/config/app_config.py` — delete `get_app_config`, `reload_app_config`, `reset_app_config`, `set_app_config`, `push/pop_current_app_config`, `_load_and_cache_app_config`, mtime globals
|
||
- Modify: `deerflow/client.py` — use `init_app_config` instead of `reload_app_config`
|
||
- Modify: `app/gateway/app.py` — call `init_app_config(AppConfig.from_file())` at startup
|
||
- Modify: All test files that import `get_app_config` from `deerflow.config.app_config` — point to new path
|
||
|
||
- [ ] **Step 1: Update `__init__.py` exports**
|
||
|
||
```python
|
||
# deerflow/config/__init__.py
|
||
from .context import get_app_config, init_app_config, ConfigNotInitializedError
|
||
from .app_config import AppConfig
|
||
from .extensions_config import ExtensionsConfig
|
||
from .memory_config import MemoryConfig
|
||
from .paths import Paths, get_paths
|
||
# ... keep type exports, remove getter function exports
|
||
```
|
||
|
||
- [ ] **Step 2: Delete lifecycle functions from `app_config.py`**
|
||
|
||
Delete everything after the `AppConfig` class definition: `_app_config`, `_app_config_path`, `_app_config_mtime`, `_app_config_is_custom`, `_current_app_config`, `_current_app_config_stack`, `_get_config_mtime`, `_load_and_cache_app_config`, `get_app_config`, `reload_app_config`, `reset_app_config`, `set_app_config`, `peek_current_app_config`, `push_current_app_config`, `pop_current_app_config`.
|
||
|
||
- [ ] **Step 3: Update `client.py`**
|
||
|
||
Replace:
|
||
```python
|
||
from deerflow.config.app_config import get_app_config, reload_app_config
|
||
```
|
||
With:
|
||
```python
|
||
from deerflow.config import get_app_config, init_app_config
|
||
from deerflow.config.app_config import AppConfig
|
||
```
|
||
|
||
In `__init__`, replace:
|
||
```python
|
||
if config_path is not None:
|
||
reload_app_config(config_path)
|
||
self._app_config = get_app_config()
|
||
```
|
||
With:
|
||
```python
|
||
config = AppConfig.from_file(config_path)
|
||
init_app_config(config)
|
||
self._app_config = config
|
||
```
|
||
|
||
- [ ] **Step 4: Update `app/gateway/app.py`**
|
||
|
||
Add at startup:
|
||
```python
|
||
from deerflow.config import init_app_config
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
init_app_config(AppConfig.from_file())
|
||
```
|
||
|
||
- [ ] **Step 5: Run full test suite, fix import paths**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v 2>&1 | head -100`
|
||
|
||
Every test that patches `deerflow.config.app_config.get_app_config` or `deerflow.client.reload_app_config` needs updating. The new patch target is `deerflow.config.context.get_app_config` (or via `deerflow.config.get_app_config` depending on import).
|
||
|
||
- [ ] **Step 6: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): migrate to ContextVar-based get_app_config"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 6: Delete sub-config module globals (memory, title, summarization)
|
||
|
||
Migrate the three most-used sub-config getters. Each follows the same pattern: delete the module-level global + getter/setter/loader, update consumers to use `get_app_config().xxx`.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/memory_config.py` — delete `_memory_config`, `get_memory_config`, `set_memory_config`, `load_memory_config_from_dict`
|
||
- Modify: `deerflow/config/title_config.py` — delete `_title_config`, `get_title_config`, `set_title_config`, `load_title_config_from_dict`
|
||
- Modify: `deerflow/config/summarization_config.py` — delete globals
|
||
- Modify: 6 production files that call `get_memory_config()`
|
||
- Modify: 1 production file that calls `get_title_config()`
|
||
- Modify: 1 production file that calls `get_summarization_config()`
|
||
- Modify: associated test files
|
||
|
||
- [ ] **Step 1: Delete globals from `memory_config.py`**
|
||
|
||
Delete lines 64-83 (everything after the class definition):
|
||
```python
|
||
# DELETE:
|
||
_memory_config: MemoryConfig = MemoryConfig()
|
||
def get_memory_config() -> MemoryConfig: ...
|
||
def set_memory_config(config: MemoryConfig) -> None: ...
|
||
def load_memory_config_from_dict(config_dict: dict) -> None: ...
|
||
```
|
||
|
||
- [ ] **Step 2: Migrate production consumers of `get_memory_config()`**
|
||
|
||
In each file, replace `get_memory_config()` with `get_app_config().memory`:
|
||
|
||
| File | Change |
|
||
|------|--------|
|
||
| `agents/middlewares/memory_middleware.py` | `from deerflow.config import get_app_config` → `get_app_config().memory` |
|
||
| `agents/memory/storage.py` | Same pattern |
|
||
| `agents/memory/updater.py` | Same pattern |
|
||
| `agents/memory/queue.py` | Same pattern |
|
||
| `agents/lead_agent/prompt.py` | Same pattern |
|
||
| `app/gateway/routers/memory.py` | Same pattern |
|
||
|
||
- [ ] **Step 3: Delete globals from `title_config.py`**
|
||
|
||
Delete lines 36-53.
|
||
|
||
- [ ] **Step 4: Migrate `get_title_config()` consumer**
|
||
|
||
`agents/middlewares/title_middleware.py` → `get_app_config().title`
|
||
|
||
- [ ] **Step 5: Delete globals from `summarization_config.py`**
|
||
|
||
- [ ] **Step 6: Migrate `get_summarization_config()` consumer**
|
||
|
||
`agents/lead_agent/agent.py` → `get_app_config().summarization`
|
||
|
||
- [ ] **Step 7: Fix tests**
|
||
|
||
Tests that patch `get_memory_config` / `get_title_config` / `get_summarization_config` must now patch `get_app_config` returning a config with the desired sub-config values.
|
||
|
||
Pattern:
|
||
```python
|
||
# Before
|
||
@patch("deerflow.agents.memory.updater.get_memory_config")
|
||
def test_something(mock_config):
|
||
mock_config.return_value = MemoryConfig(enabled=False)
|
||
|
||
# After
|
||
@patch("deerflow.config.context.get_app_config")
|
||
def test_something(mock_config):
|
||
mock_config.return_value = AppConfig(
|
||
sandbox=SandboxConfig(use="test"),
|
||
memory=MemoryConfig(enabled=False),
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 8: Run full test suite**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v`
|
||
|
||
- [ ] **Step 9: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): delete memory/title/summarization module globals"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 7: Delete remaining sub-config module globals
|
||
|
||
Same pattern as Task 6 for the remaining 7 modules.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/subagents_config.py` — delete globals
|
||
- Modify: `deerflow/config/guardrails_config.py` — delete globals + `reset_guardrails_config`
|
||
- Modify: `deerflow/config/tool_search_config.py` — delete globals
|
||
- Modify: `deerflow/config/checkpointer_config.py` — delete globals
|
||
- Modify: `deerflow/config/stream_bridge_config.py` — delete globals
|
||
- Modify: `deerflow/config/acp_config.py` — delete globals
|
||
- Modify: `deerflow/config/extensions_config.py` — delete globals + `reload_extensions_config` + `reset_extensions_config` + `set_extensions_config`
|
||
- Modify: All consumers of these getters (see consumer map in exploration)
|
||
|
||
- [ ] **Step 1: Delete globals from `subagents_config.py`, migrate `subagents/registry.py`**
|
||
|
||
`get_subagents_app_config()` → `get_app_config().subagents`
|
||
|
||
- [ ] **Step 2: Delete globals from `guardrails_config.py`, migrate `tool_error_handling_middleware.py`**
|
||
|
||
`get_guardrails_config()` → `get_app_config().guardrails`
|
||
|
||
- [ ] **Step 3: Delete globals from `tool_search_config.py`**
|
||
|
||
No production consumers outside config system.
|
||
|
||
- [ ] **Step 4: Delete globals from `checkpointer_config.py`, migrate 2 consumers**
|
||
|
||
`get_checkpointer_config()` → `get_app_config().checkpointer`
|
||
|
||
- [ ] **Step 5: Delete globals from `stream_bridge_config.py`, migrate 1 consumer**
|
||
|
||
`get_stream_bridge_config()` → `get_app_config().stream_bridge`
|
||
|
||
- [ ] **Step 6: Delete globals from `acp_config.py`, migrate 2 consumers**
|
||
|
||
`get_acp_agents()` → derive from `get_app_config()`
|
||
|
||
- [ ] **Step 7: Delete globals from `extensions_config.py`, migrate 4 production consumers**
|
||
|
||
`get_extensions_config()` → `get_app_config().extensions`
|
||
`reload_extensions_config()` → `init_app_config(AppConfig.from_file())`
|
||
|
||
Consumers:
|
||
- `deerflow/sandbox/tools.py`
|
||
- `deerflow/client.py`
|
||
- `app/gateway/routers/mcp.py`
|
||
- `app/gateway/routers/skills.py`
|
||
|
||
- [ ] **Step 8: Fix tests**
|
||
|
||
- [ ] **Step 9: Run full test suite**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest -x -v`
|
||
|
||
- [ ] **Step 10: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): delete all remaining sub-config module globals"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 8: Update `__init__.py` exports — final cleanup
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/__init__.py`
|
||
|
||
- [ ] **Step 1: Update exports to final state**
|
||
|
||
```python
|
||
# deerflow/config/__init__.py
|
||
from .app_config import AppConfig
|
||
from .context import ConfigNotInitializedError, get_app_config, init_app_config
|
||
from .extensions_config import ExtensionsConfig
|
||
from .memory_config import MemoryConfig
|
||
from .paths import Paths, get_paths
|
||
from .skill_evolution_config import SkillEvolutionConfig
|
||
from .skills_config import SkillsConfig
|
||
from .tracing_config import (
|
||
get_enabled_tracing_providers,
|
||
get_explicitly_enabled_tracing_providers,
|
||
get_tracing_config,
|
||
is_tracing_enabled,
|
||
validate_enabled_tracing_providers,
|
||
)
|
||
|
||
__all__ = [
|
||
"AppConfig",
|
||
"ConfigNotInitializedError",
|
||
"ExtensionsConfig",
|
||
"MemoryConfig",
|
||
"Paths",
|
||
"SkillEvolutionConfig",
|
||
"SkillsConfig",
|
||
"get_app_config",
|
||
"get_enabled_tracing_providers",
|
||
"get_explicitly_enabled_tracing_providers",
|
||
"get_paths",
|
||
"get_tracing_config",
|
||
"init_app_config",
|
||
"is_tracing_enabled",
|
||
"validate_enabled_tracing_providers",
|
||
]
|
||
```
|
||
|
||
- [ ] **Step 2: Run full test suite**
|
||
|
||
- [ ] **Step 3: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): clean up __init__.py exports"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 9: Update Gateway config update flow
|
||
|
||
Gateway API currently writes config files then calls `reload_*`. Change to: write file → construct new AppConfig → `init_app_config()` → rebuild agent.
|
||
|
||
**Files:**
|
||
- Modify: `app/gateway/routers/mcp.py`
|
||
- Modify: `app/gateway/routers/skills.py`
|
||
- Modify: `deerflow/client.py` (update_mcp_config, update_skill methods)
|
||
|
||
- [ ] **Step 1: Update `mcp.py` router**
|
||
|
||
Replace `reload_extensions_config()` call with:
|
||
```python
|
||
from deerflow.config import init_app_config
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
init_app_config(AppConfig.from_file())
|
||
```
|
||
|
||
- [ ] **Step 2: Update `skills.py` router**
|
||
|
||
Same pattern.
|
||
|
||
- [ ] **Step 3: Update `client.py` methods**
|
||
|
||
In `update_mcp_config()` and `update_skill()`, replace `reload_extensions_config()` with `init_app_config(AppConfig.from_file())`.
|
||
|
||
- [ ] **Step 4: Run full test suite**
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): Gateway updates construct new config instead of reload"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 10: Create `DeerFlowContext` and wire into agent creation ✅
|
||
|
||
Completed. `DeerFlowContext` with `app_config` field created, wired into `create_agent(context_schema=DeerFlowContext)` and `DeerFlowClient.stream(context=...)`.
|
||
|
||
---
|
||
|
||
## Task 11: Expand DeerFlowContext with `thread_id` and `agent_name`, add `resolve_context()`
|
||
|
||
Expand `DeerFlowContext` from config-only to full per-invocation context. Add `resolve_context()` helper for unified access across all entry points.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/config/deer_flow_context.py`
|
||
- Test: `tests/test_deer_flow_context.py` (extend)
|
||
|
||
- [ ] **Step 1: Write tests for expanded DeerFlowContext**
|
||
|
||
```python
|
||
# Extend tests/test_deer_flow_context.py
|
||
from unittest.mock import patch
|
||
from deerflow.config.deer_flow_context import DeerFlowContext, resolve_context
|
||
|
||
def test_deer_flow_context_fields():
|
||
config = AppConfig(sandbox=SandboxConfig(use="test"))
|
||
ctx = DeerFlowContext(app_config=config, thread_id="t1", agent_name="test-agent")
|
||
assert ctx.thread_id == "t1"
|
||
assert ctx.agent_name == "test-agent"
|
||
assert ctx.app_config is config
|
||
|
||
def test_deer_flow_context_agent_name_default():
|
||
config = AppConfig(sandbox=SandboxConfig(use="test"))
|
||
ctx = DeerFlowContext(app_config=config, thread_id="t1")
|
||
assert ctx.agent_name is None
|
||
|
||
def test_resolve_context_returns_typed_context():
|
||
"""When runtime.context is DeerFlowContext, return it directly."""
|
||
config = AppConfig(sandbox=SandboxConfig(use="test"))
|
||
ctx = DeerFlowContext(app_config=config, thread_id="t1")
|
||
runtime = MagicMock()
|
||
runtime.context = ctx
|
||
assert resolve_context(runtime) is ctx
|
||
|
||
def test_resolve_context_fallback_from_configurable():
|
||
"""When runtime.context is None (LangGraph Server), fallback to configurable."""
|
||
runtime = MagicMock()
|
||
runtime.context = None
|
||
config = AppConfig(sandbox=SandboxConfig(use="test"))
|
||
with patch("deerflow.config.deer_flow_context.get_app_config", return_value=config), \
|
||
patch("deerflow.config.deer_flow_context.get_config", return_value={"configurable": {"thread_id": "t2", "agent_name": "ag"}}):
|
||
ctx = resolve_context(runtime)
|
||
assert ctx.thread_id == "t2"
|
||
assert ctx.agent_name == "ag"
|
||
assert ctx.app_config is config
|
||
```
|
||
|
||
- [ ] **Step 2: Update `deer_flow_context.py`**
|
||
|
||
```python
|
||
"""Per-invocation context for DeerFlow agent execution."""
|
||
from __future__ import annotations
|
||
|
||
from dataclasses import dataclass
|
||
from typing import Any
|
||
|
||
from deerflow.config.app_config import AppConfig
|
||
|
||
|
||
@dataclass(frozen=True)
|
||
class DeerFlowContext:
|
||
"""Typed, immutable, per-invocation context injected via LangGraph Runtime."""
|
||
app_config: AppConfig
|
||
thread_id: str
|
||
agent_name: str | None = None
|
||
|
||
|
||
def resolve_context(runtime: Any) -> DeerFlowContext:
|
||
"""Extract or construct DeerFlowContext from runtime.
|
||
|
||
Gateway/Client paths: runtime.context is already DeerFlowContext → return directly.
|
||
LangGraph Server path: runtime.context is None → fallback to ContextVar + configurable.
|
||
"""
|
||
if isinstance(runtime.context, DeerFlowContext):
|
||
return runtime.context
|
||
from langgraph.config import get_config
|
||
from deerflow.config import get_app_config
|
||
cfg = get_config().get("configurable", {})
|
||
return DeerFlowContext(
|
||
app_config=get_app_config(),
|
||
thread_id=cfg.get("thread_id", ""),
|
||
agent_name=cfg.get("agent_name"),
|
||
)
|
||
```
|
||
|
||
- [ ] **Step 3: Run tests**
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): expand DeerFlowContext with thread_id, agent_name, resolve_context()"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 12: Remove sandbox_id from runtime.context
|
||
|
||
Remove the mutable `sandbox_id` side channel from `runtime.context`. All sandbox_id access goes through `ThreadState.sandbox` (state channel).
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/sandbox/tools.py` — delete 3× `runtime.context["sandbox_id"] = sandbox_id`
|
||
- Modify: `deerflow/sandbox/middleware.py` — delete context fallback in `after_agent`
|
||
- Test: `tests/test_sandbox_*.py` (verify existing tests still pass)
|
||
|
||
- [ ] **Step 1: Delete sandbox_id writes from `sandbox/tools.py`**
|
||
|
||
Remove lines:
|
||
- `tools.py:813`: `runtime.context["sandbox_id"] = sandbox_id`
|
||
- `tools.py:849`: `runtime.context["sandbox_id"] = sandbox_id`
|
||
- `tools.py:872`: `runtime.context["sandbox_id"] = sandbox_id`
|
||
|
||
- [ ] **Step 2: Delete context fallback from `sandbox/middleware.py:after_agent`**
|
||
|
||
Remove lines 76-80:
|
||
```python
|
||
# DELETE:
|
||
if (runtime.context or {}).get("sandbox_id") is not None:
|
||
sandbox_id = runtime.context.get("sandbox_id")
|
||
logger.info(f"Releasing sandbox {sandbox_id} from context")
|
||
get_sandbox_provider().release(sandbox_id)
|
||
return None
|
||
```
|
||
|
||
The state-based path (lines 69-74) already handles all cases.
|
||
|
||
- [ ] **Step 3: Run sandbox tests**
|
||
|
||
Run: `cd backend && PYTHONPATH=. uv run pytest tests/ -k sandbox -v`
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(sandbox): remove sandbox_id from runtime.context, use state channel only"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 13: Wire DeerFlowContext into Gateway runtime and DeerFlowClient
|
||
|
||
Update the two primary entry points to construct and pass full `DeerFlowContext`.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/runtime/runs/worker.py` — replace dict context with DeerFlowContext
|
||
- Modify: `deerflow/client.py` — add thread_id to DeerFlowContext construction
|
||
- Test: existing client/runtime tests
|
||
|
||
- [ ] **Step 1: Update `worker.py`**
|
||
|
||
Replace:
|
||
```python
|
||
runtime = Runtime(context={"thread_id": thread_id}, store=store)
|
||
```
|
||
With:
|
||
```python
|
||
from deerflow.config.deer_flow_context import DeerFlowContext
|
||
from deerflow.config import get_app_config
|
||
|
||
context = DeerFlowContext(app_config=get_app_config(), thread_id=thread_id)
|
||
```
|
||
And pass `context=context` to the `agent.astream()` call instead of injecting `__pregel_runtime` manually.
|
||
|
||
Also remove the dict-style `config["context"].setdefault("thread_id", ...)` line.
|
||
|
||
- [ ] **Step 2: Update `client.py`**
|
||
|
||
Replace:
|
||
```python
|
||
context = DeerFlowContext(app_config=self._app_config)
|
||
```
|
||
With:
|
||
```python
|
||
context = DeerFlowContext(app_config=self._app_config, thread_id=thread_id)
|
||
```
|
||
|
||
Where `thread_id` comes from the `kwargs` or config.
|
||
|
||
- [ ] **Step 3: Run full test suite**
|
||
|
||
- [ ] **Step 4: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): wire DeerFlowContext into Gateway runtime and DeerFlowClient"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 14: Migrate middleware/tools from dict access to `resolve_context()`
|
||
|
||
Replace all `runtime.context.get("thread_id")` / `(runtime.context or {}).get(...)` patterns with `resolve_context(runtime).thread_id`.
|
||
|
||
**Files (middleware):**
|
||
- `deerflow/agents/middlewares/thread_data_middleware.py`
|
||
- `deerflow/agents/middlewares/uploads_middleware.py`
|
||
- `deerflow/agents/middlewares/memory_middleware.py`
|
||
- `deerflow/agents/middlewares/loop_detection_middleware.py`
|
||
- `deerflow/sandbox/middleware.py`
|
||
|
||
**Files (tools):**
|
||
- `deerflow/tools/builtins/present_file_tool.py`
|
||
- `deerflow/tools/builtins/setup_agent_tool.py`
|
||
- `deerflow/tools/builtins/task_tool.py`
|
||
- `deerflow/tools/skill_manage_tool.py`
|
||
- `deerflow/sandbox/tools.py`
|
||
|
||
- [ ] **Step 1: Update all middleware**
|
||
|
||
Pattern:
|
||
```python
|
||
# Before
|
||
thread_id = (runtime.context or {}).get("thread_id")
|
||
if thread_id is None:
|
||
config = get_config()
|
||
thread_id = config.get("configurable", {}).get("thread_id")
|
||
|
||
# After
|
||
from deerflow.config.deer_flow_context import resolve_context
|
||
ctx = resolve_context(runtime)
|
||
thread_id = ctx.thread_id
|
||
```
|
||
|
||
- [ ] **Step 2: Update all tools**
|
||
|
||
Same pattern. For tools using `ToolRuntime`, `resolve_context()` works identically.
|
||
|
||
- [ ] **Step 3: Fix tests**
|
||
|
||
Tests that mock `runtime.context` as a dict need to either:
|
||
- Pass a `DeerFlowContext` instance
|
||
- Or mock `runtime.context = None` with configurable fallback (LangGraph Server path)
|
||
|
||
- [ ] **Step 4: Run full test suite**
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): migrate middleware/tools to resolve_context() typed access"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 15: Migrate middleware to read config from Runtime
|
||
|
||
Convert middleware from global getter to reading `app_config` from `resolve_context()` at execution time.
|
||
|
||
**Files:**
|
||
- Modify: `deerflow/agents/middlewares/memory_middleware.py` — `get_app_config().memory` → `resolve_context(runtime).app_config.memory`
|
||
- Modify: `deerflow/agents/middlewares/title_middleware.py` — same pattern for `.title`
|
||
- Modify: associated tests
|
||
|
||
- [ ] **Step 1: Update MemoryMiddleware**
|
||
|
||
```python
|
||
ctx = resolve_context(runtime)
|
||
memory_config = ctx.app_config.memory
|
||
if not memory_config.enabled:
|
||
return None
|
||
```
|
||
|
||
- [ ] **Step 2: Update TitleMiddleware**
|
||
|
||
```python
|
||
ctx = resolve_context(runtime)
|
||
title_config = ctx.app_config.title
|
||
```
|
||
|
||
- [ ] **Step 3: Fix tests**
|
||
|
||
- [ ] **Step 4: Run full test suite**
|
||
|
||
- [ ] **Step 5: Commit**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): middleware reads config from Runtime[DeerFlowContext]"
|
||
```
|
||
|
||
---
|
||
|
||
## Task 16: Final cleanup and verification
|
||
|
||
- [ ] **Step 1: Grep for remaining dict-style context access**
|
||
|
||
```bash
|
||
cd backend && grep -rn 'runtime\.context\.get\|runtime\.context\[' --include="*.py" packages/ | grep -v __pycache__
|
||
```
|
||
|
||
Expected: No matches in production code.
|
||
|
||
- [ ] **Step 2: Grep for remaining deleted function references**
|
||
|
||
```bash
|
||
cd backend && grep -rn "get_memory_config\|get_title_config\|get_summarization_config\|get_subagents_app_config\|get_guardrails_config\|get_tool_search_config\|get_checkpointer_config\|get_stream_bridge_config\|get_acp_agents\|reload_app_config\|reload_extensions_config\|reset_app_config\|reset_extensions_config\|reset_guardrails_config\|set_app_config\|set_extensions_config\|push_current_app_config\|pop_current_app_config\|load_memory_config_from_dict\|load_title_config_from_dict" --include="*.py" | grep -v __pycache__
|
||
```
|
||
|
||
Expected: No matches (or only in comments/docs).
|
||
|
||
- [ ] **Step 3: Run full test suite**
|
||
|
||
```bash
|
||
cd backend && PYTHONPATH=. uv run pytest -v
|
||
```
|
||
|
||
Expected: All tests pass.
|
||
|
||
- [ ] **Step 4: Run linter**
|
||
|
||
```bash
|
||
cd backend && make lint
|
||
```
|
||
|
||
- [ ] **Step 5: Commit any final fixes**
|
||
|
||
```bash
|
||
git add -A
|
||
git commit -m "refactor(config): final cleanup — remove dead references"
|
||
```
|
||
|
||
- [ ] **Step 6: Update CLAUDE.md**
|
||
|
||
Update the Configuration System section in `backend/CLAUDE.md` to reflect the new architecture:
|
||
- `get_app_config()` backed by ContextVar (no mtime/reload)
|
||
- `init_app_config()` called at process startup
|
||
- Sub-config accessed via `get_app_config().memory`, etc.
|
||
- `DeerFlowContext` with `thread_id`, `agent_name`, `app_config` for agent execution path
|
||
- `resolve_context()` for unified access across Gateway/Client/LangGraph Server paths
|
||
- `sandbox_id` flows through state channel, not context
|
||
- All config models frozen
|
||
|
||
- [ ] **Step 7: Commit docs update**
|
||
|
||
```bash
|
||
git add backend/CLAUDE.md
|
||
git commit -m "docs: update CLAUDE.md for new config architecture"
|
||
```
|