mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-20 15:11:09 +00:00
refactor(config): eliminate global mutable state, wire DeerFlowContext into runtime
- 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
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
# Design: Eliminate Global Mutable State in Configuration System
|
||||
|
||||
> Implements [#1811](https://github.com/bytedance/deer-flow/issues/1811) · Tracked in [#2151](https://github.com/bytedance/deer-flow/issues/2151)
|
||||
|
||||
## Problem
|
||||
|
||||
`deerflow/config/` has three structural issues:
|
||||
|
||||
1. **Dual source of truth** — each sub-config exists both as an `AppConfig` field and a module-level global (e.g. `_memory_config`). Consumers don't know which to trust.
|
||||
2. **Side-effect coupling** — `AppConfig.from_file()` silently mutates 8 sub-module globals via `load_*_from_dict()` calls.
|
||||
3. **Incomplete isolation** — `ContextVar` only scopes `AppConfig`, not the 8 sub-config globals.
|
||||
|
||||
## Design Principle
|
||||
|
||||
**Config is a value object, not live shared state.** Constructed once, immutable, no reload. New config = new object + rebuild agent.
|
||||
|
||||
## Solution
|
||||
|
||||
### 1. Frozen AppConfig (full tree)
|
||||
|
||||
All config models set `frozen=True`. No mutation after construction.
|
||||
|
||||
```python
|
||||
class MemoryConfig(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
|
||||
class AppConfig(BaseModel):
|
||||
model_config = ConfigDict(frozen=True)
|
||||
memory: MemoryConfig
|
||||
title: TitleConfig
|
||||
...
|
||||
```
|
||||
|
||||
Changes use copy-on-write: `config.model_copy(update={...})`.
|
||||
|
||||
### 2. Pure `from_file()`
|
||||
|
||||
`AppConfig.from_file()` becomes a pure function — returns a frozen object, no side effects. All `load_*_from_dict()` calls removed.
|
||||
|
||||
### 3. Delete sub-module globals
|
||||
|
||||
Every sub-config module's global state is deleted:
|
||||
|
||||
| Delete | Files |
|
||||
|--------|-------|
|
||||
| `_memory_config`, `get_memory_config()`, `set_memory_config()`, `load_memory_config_from_dict()` | `memory_config.py` |
|
||||
| `_title_config`, `get_title_config()`, `set_title_config()`, `load_title_config_from_dict()` | `title_config.py` |
|
||||
| Same pattern | `summarization_config.py`, `subagents_config.py`, `guardrails_config.py`, `tool_search_config.py`, `checkpointer_config.py`, `stream_bridge_config.py`, `acp_config.py` |
|
||||
| `_extensions_config`, `reload_extensions_config()`, `reset_extensions_config()`, `set_extensions_config()` | `extensions_config.py` |
|
||||
| `reload_app_config()`, `reset_app_config()`, `set_app_config()`, mtime detection, `push/pop_current_app_config()` | `app_config.py` |
|
||||
|
||||
Consumers migrate from `get_memory_config()` → `get_app_config().memory`.
|
||||
|
||||
### 4. Propagation
|
||||
|
||||
#### Agent path: `Runtime[DeerFlowContext]`
|
||||
|
||||
LangGraph's official DI mechanism. Context is injected per-invocation, type-safe.
|
||||
|
||||
```python
|
||||
@dataclass(frozen=True)
|
||||
class DeerFlowContext:
|
||||
app_config: AppConfig
|
||||
thread_id: str
|
||||
agent_name: str | None = None
|
||||
```
|
||||
|
||||
**Fields:**
|
||||
|
||||
| Field | Type | Source | Mutability |
|
||||
|-------|------|--------|-----------|
|
||||
| `app_config` | `AppConfig` | ContextVar (`get_app_config()`) | Immutable per-run |
|
||||
| `thread_id` | `str` | Caller-provided | Immutable per-run |
|
||||
| `agent_name` | `str \| None` | Caller-provided (bootstrap only) | Immutable per-run |
|
||||
|
||||
**Not in context:** `sandbox_id` is mutable runtime state (lazy-acquired mid-execution). It flows through `ThreadState.sandbox` (state channel), not context. The 3 existing `runtime.context["sandbox_id"] = ...` writes in `sandbox/tools.py` are removed; `SandboxMiddleware.after_agent` reads from `state["sandbox"]` only.
|
||||
|
||||
**Construction per entry point (Gateway is primary):**
|
||||
|
||||
```python
|
||||
# Gateway runtime (worker.py) — primary path
|
||||
context = DeerFlowContext(app_config=get_app_config(), thread_id=thread_id)
|
||||
agent.astream(input, config=config, context=context)
|
||||
|
||||
# DeerFlowClient (client.py)
|
||||
context = DeerFlowContext(app_config=self._app_config, thread_id=thread_id)
|
||||
agent.stream(input, config=config, context=context)
|
||||
|
||||
# LangGraph Server — legacy path, context=None, fallback via resolve_context()
|
||||
```
|
||||
|
||||
**Access in middleware/tools:**
|
||||
|
||||
```python
|
||||
from deerflow.config.deer_flow_context import DeerFlowContext, resolve_context
|
||||
|
||||
# Middleware
|
||||
def before_model(self, state, runtime: Runtime[DeerFlowContext]):
|
||||
ctx = resolve_context(runtime)
|
||||
ctx.app_config.title # typed
|
||||
ctx.thread_id # typed
|
||||
|
||||
# Tool
|
||||
@tool
|
||||
def my_tool(runtime: ToolRuntime[DeerFlowContext]) -> str:
|
||||
ctx = resolve_context(runtime)
|
||||
ctx.app_config.memory # typed
|
||||
```
|
||||
|
||||
`resolve_context()` returns `runtime.context` directly when it's already a `DeerFlowContext` (Gateway/Client paths). For legacy LangGraph Server path (context is None), it falls back to constructing from ContextVar + `configurable`.
|
||||
|
||||
Why `Runtime` over `RunnableConfig.configurable`:
|
||||
- `Runtime` is LangGraph's official DI, not a private dict hack
|
||||
- Generic type parameter (`Runtime[DeerFlowContext]`) gives type safety
|
||||
- `RunnableConfig` is for framework internals (tags, callbacks), not user dependencies
|
||||
|
||||
#### Non-agent path: ContextVar
|
||||
|
||||
Gateway API routers use `get_app_config()` backed by a single ContextVar. This is appropriate — Gateway doesn't run through the LangGraph execution graph.
|
||||
|
||||
### 5. No reload
|
||||
|
||||
Config lifecycle is simple:
|
||||
|
||||
```
|
||||
Process start → from_file() → set ContextVar → run
|
||||
↓
|
||||
Gateway API changed file?
|
||||
↓
|
||||
from_file() → new frozen config
|
||||
→ set ContextVar → rebuild agent
|
||||
```
|
||||
|
||||
- Edit `config.yaml` → restart process
|
||||
- Gateway updates MCP/Skills → construct new config + rebuild agent
|
||||
- No mtime detection, no `reload_*()`, no auto-refresh
|
||||
|
||||
### 6. Structure vs runtime config
|
||||
|
||||
| Type | Example | Reload behavior |
|
||||
|------|---------|----------------|
|
||||
| Structural (agent composition) | model, tools, middleware chain | Requires agent rebuild |
|
||||
| Runtime (execution behavior) | `memory.enabled`, `title.max_words` | Next invocation picks up new config automatically via `Runtime` |
|
||||
|
||||
Middleware reads config from `Runtime` at execution time (not `__init__` capture), so runtime config changes take effect without agent rebuild.
|
||||
|
||||
## What doesn't change
|
||||
|
||||
- `config.yaml` schema
|
||||
- `extensions_config.json` loading
|
||||
- External API behavior (Gateway, DeerFlowClient)
|
||||
|
||||
## Migration scope
|
||||
|
||||
- 50+ call sites: `get_*_config()` → `get_app_config().xxx`
|
||||
- Middleware: `__init__` capture → `Runtime[DeerFlowContext]` read
|
||||
- Tools: global getters → `ToolRuntime[DeerFlowContext]`
|
||||
- Tests: `reset_*_config()` → construct frozen config directly
|
||||
- Gateway update flow: reload → construct new config + rebuild agent
|
||||
- Dependency: upgrade langgraph >= 1.1.5 for `Runtime` support
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user