mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-06-18 13:46:02 +00:00
feat(subagents): attribute subagent spans to parent thread's Langfuse session (#3611)
The subagent execution path did not call inject_langfuse_metadata(...) and built its model with attach_tracing=True, so subagent LLM/tool spans landed in Langfuse as isolated top-level traces carrying fresh session ids and the default user. They were findable in the unfiltered trace list but did not group under the parent thread's session card, and Langfuse cost attribution for subagent traffic did not line up with the parent conversation — even though DeerFlow's internal token accounting (SubagentTokenCollector) was already correct. Extend the lead-agent tracing wiring to the subagent path so a single subagent run produces one trace that shares the parent thread's session_id and user_id, with a subagent:<name> trace name: - subagents/executor.py: append build_tracing_callbacks() output to run_config["callbacks"] (preserving SubagentTokenCollector) and call inject_langfuse_metadata(...) with thread_id, user_id, and the normalized subagent:<name> trace name. Build the model with attach_tracing=False so model-level tracing does not double-count with the graph-root callbacks — the same pairing the lead agent uses. - tools/builtins/task_tool.py: resolve user_id via resolve_runtime_user_id(runtime) at the parent tool layer (before the background thread starts) and thread it through SubagentExecutor.__init__, because the _current_user contextvar is not guaranteed to survive the _execution_pool boundary. Trace topology is unchanged: subagent traces remain separate top-level traces in the same session, not nested as child spans under the lead trace (Plan B follow-up). Tests: tests/test_subagent_executor.py::TestSubagentTracingWiring covers the callback append, the session/user/trace-name injection, the disabled-langfuse no-op, the DEFAULT_USER_ID fallback, the empty-name trace-name fallback, and the env-tag emission. Existing test_create_agent_threads_explicit_app_config_to_model_and_middlewares now also asserts attach_tracing=False. Docs: CLAUDE.md Tracing System section documents subagents/executor.py as a third injection point alongside worker.py and client.py.
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
import asyncio
|
||||
import atexit
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
import uuid
|
||||
from collections.abc import Callable, Coroutine
|
||||
@@ -27,6 +28,7 @@ from deerflow.skills.tool_policy import filter_tools_by_skill_allowed_tools
|
||||
from deerflow.skills.types import Skill
|
||||
from deerflow.subagents.config import SubagentConfig, resolve_subagent_model_name
|
||||
from deerflow.subagents.token_collector import SubagentTokenCollector
|
||||
from deerflow.tracing import build_tracing_callbacks, inject_langfuse_metadata
|
||||
|
||||
if TYPE_CHECKING:
|
||||
# Imported lazily at runtime inside _build_initial_state: importing
|
||||
@@ -286,6 +288,7 @@ class SubagentExecutor:
|
||||
thread_data: ThreadDataState | None = None,
|
||||
thread_id: str | None = None,
|
||||
trace_id: str | None = None,
|
||||
user_id: str | None = None,
|
||||
):
|
||||
"""Initialize the executor.
|
||||
|
||||
@@ -300,6 +303,8 @@ class SubagentExecutor:
|
||||
thread_data: Thread data from parent agent.
|
||||
thread_id: Thread ID for sandbox operations.
|
||||
trace_id: Trace ID from parent for distributed tracing.
|
||||
user_id: User ID captured from the parent tool's runtime context.
|
||||
When None, the tracing layer falls back to DEFAULT_USER_ID.
|
||||
"""
|
||||
self.config = config
|
||||
self.app_config = app_config
|
||||
@@ -316,6 +321,7 @@ class SubagentExecutor:
|
||||
self.thread_id = thread_id
|
||||
# Generate trace_id if not provided (for top-level calls)
|
||||
self.trace_id = trace_id or str(uuid.uuid4())[:8]
|
||||
self.user_id = user_id
|
||||
|
||||
self._base_tools = _filter_tools(
|
||||
tools,
|
||||
@@ -336,7 +342,7 @@ class SubagentExecutor:
|
||||
app_config = self.app_config or get_app_config()
|
||||
if self.model_name is None:
|
||||
self.model_name = resolve_subagent_model_name(self.config, self.parent_model, app_config=app_config)
|
||||
model = create_chat_model(name=self.model_name, thinking_enabled=False, app_config=app_config)
|
||||
model = create_chat_model(name=self.model_name, thinking_enabled=False, app_config=app_config, attach_tracing=False)
|
||||
|
||||
from deerflow.agents.middlewares.tool_error_handling_middleware import build_subagent_runtime_middlewares
|
||||
|
||||
@@ -522,6 +528,36 @@ class SubagentExecutor:
|
||||
"callbacks": [collector],
|
||||
"tags": [collector_caller],
|
||||
}
|
||||
|
||||
# Inject tracing callbacks at the graph level so a single subagent run
|
||||
# produces one trace with all node / LLM / tool calls as child spans.
|
||||
# This mirrors the lead agent pattern: graph-level tracing paired with
|
||||
# attach_tracing=False on the model avoids double-counted traces.
|
||||
tracing_callbacks = build_tracing_callbacks()
|
||||
if tracing_callbacks:
|
||||
existing_callbacks = list(run_config.get("callbacks") or [])
|
||||
run_config["callbacks"] = [*existing_callbacks, *tracing_callbacks]
|
||||
|
||||
# Normalize subagent name for tracing so it matches the lead-agent
|
||||
# naming shape (lowercase, hyphens only). Inline because there is no
|
||||
# shared helper — runtime/runs/naming.py only handles lead-agent runs.
|
||||
if self.config.name:
|
||||
normalized_name = self.config.name.strip().lower().replace("_", "-")
|
||||
assistant_id = f"subagent:{normalized_name}"
|
||||
else:
|
||||
assistant_id = "subagent"
|
||||
|
||||
# Inject Langfuse trace-attribute metadata so the subagent trace
|
||||
# links to the parent thread and carries the correct session/user IDs.
|
||||
inject_langfuse_metadata(
|
||||
run_config,
|
||||
thread_id=self.thread_id,
|
||||
user_id=self.user_id,
|
||||
assistant_id=assistant_id,
|
||||
model_name=self.model_name,
|
||||
environment=os.environ.get("DEER_FLOW_ENV") or os.environ.get("ENVIRONMENT"),
|
||||
)
|
||||
|
||||
context: dict[str, Any] = {}
|
||||
if self.thread_id:
|
||||
run_config["configurable"] = {"thread_id": self.thread_id}
|
||||
|
||||
Reference in New Issue
Block a user