refactor(backend): consolidate thread_id resolution into shared get_thread_id() utility (#2522)

Extract duplicated thread_id fallback logic from 11 files into a single
  deerflow.utils.runtime.get_thread_id() function with a documented 3-level
  cascade (runtime.context → runtime.config → get_config()).

  The module docstring also clarifies the __pregel_runtime injection pattern used in
  gateway mode.
This commit is contained in:
Willem Jiang
2026-04-26 10:52:37 +08:00
parent 9dc25987e0
commit a55de566b9
14 changed files with 185 additions and 84 deletions
@@ -3,33 +3,16 @@ from typing import Annotated
from langchain.tools import InjectedToolCallId, ToolRuntime, tool
from langchain_core.messages import ToolMessage
from langgraph.config import get_config
from langgraph.types import Command
from langgraph.typing import ContextT
from deerflow.agents.thread_state import ThreadState
from deerflow.config.paths import VIRTUAL_PATH_PREFIX, get_paths
from deerflow.utils.runtime import get_thread_id
OUTPUTS_VIRTUAL_PREFIX = f"{VIRTUAL_PATH_PREFIX}/outputs"
def _get_thread_id(runtime: ToolRuntime[ContextT, ThreadState]) -> str | None:
"""Resolve the current thread id from runtime context or RunnableConfig."""
thread_id = runtime.context.get("thread_id") if runtime.context else None
if thread_id:
return thread_id
runtime_config = getattr(runtime, "config", None) or {}
thread_id = runtime_config.get("configurable", {}).get("thread_id")
if thread_id:
return thread_id
try:
return get_config().get("configurable", {}).get("thread_id")
except RuntimeError:
return None
def _normalize_presented_filepath(
runtime: ToolRuntime[ContextT, ThreadState],
filepath: str,
@@ -51,7 +34,7 @@ def _normalize_presented_filepath(
if runtime.state is None:
raise ValueError("Thread runtime state is not available")
thread_id = _get_thread_id(runtime)
thread_id = get_thread_id(runtime)
if not thread_id:
raise ValueError("Thread ID is not available in runtime context or runtime config")
@@ -14,6 +14,7 @@ from deerflow.agents.thread_state import ThreadState
from deerflow.sandbox.security import LOCAL_BASH_SUBAGENT_DISABLED_MESSAGE, is_host_bash_allowed
from deerflow.subagents import SubagentExecutor, get_available_subagent_names, get_subagent_config
from deerflow.subagents.executor import SubagentStatus, cleanup_background_task, get_background_task_result, request_cancel_background_task
from deerflow.utils.runtime import get_thread_id
logger = logging.getLogger(__name__)
@@ -105,9 +106,7 @@ async def task_tool(
if runtime is not None:
sandbox_state = runtime.state.get("sandbox")
thread_data = runtime.state.get("thread_data")
thread_id = runtime.context.get("thread_id") if runtime.context else None
if thread_id is None:
thread_id = runtime.config.get("configurable", {}).get("thread_id")
thread_id = get_thread_id(runtime)
# Try to get parent model from configurable
metadata = runtime.config.get("metadata", {})
@@ -28,6 +28,7 @@ from deerflow.skills.manager import (
validate_skill_name,
)
from deerflow.skills.security_scanner import scan_skill_content
from deerflow.utils.runtime import get_thread_id
logger = logging.getLogger(__name__)
@@ -42,14 +43,6 @@ def _get_lock(name: str) -> asyncio.Lock:
return lock
def _get_thread_id(runtime: ToolRuntime[ContextT, ThreadState] | None) -> str | None:
if runtime is None:
return None
if runtime.context and runtime.context.get("thread_id"):
return runtime.context.get("thread_id")
return runtime.config.get("configurable", {}).get("thread_id")
def _history_record(*, action: str, file_path: str, prev_content: str | None, new_content: str | None, thread_id: str | None, scanner: dict[str, Any]) -> dict[str, Any]:
return {
"action": action,
@@ -98,7 +91,7 @@ async def _skill_manage_impl(
"""
name = validate_skill_name(name)
lock = _get_lock(name)
thread_id = _get_thread_id(runtime)
thread_id = get_thread_id(runtime)
async with lock:
if action == "create":