From d65f8876a4ab36c18c6042f7027bcea9379686ca Mon Sep 17 00:00:00 2001 From: Pushkar Kolhe Date: Mon, 5 Oct 2026 02:19:42 -0700 Subject: [PATCH] fix(hook): trace subagents launched by subagents emit_subagent_observations walked an agent's turns with no subagent transcript map, so an Agent call inside a subagent produced a Tool span but never the inner agent's own span, generations or tools. Pass the map through so nested agents nest under their launching agent. --- hooks/langfuse_hook.py | 9 +++++++-- .../test_emit_turn_observations.py | 20 ++++++++++++++----- 2 files changed, 22 insertions(+), 7 deletions(-) diff --git a/hooks/langfuse_hook.py b/hooks/langfuse_hook.py index e1dbe6d..cdd8f64 100644 --- a/hooks/langfuse_hook.py +++ b/hooks/langfuse_hook.py @@ -2434,6 +2434,7 @@ def emit_single_tool_observation( parent_otel_span, subagent, tool_use_timestamp, + subagent_transcripts_by_tool_use_id=subagent_transcripts_by_tool_use_id, ) workflow_end_timestamp = None @@ -2708,6 +2709,7 @@ def emit_pending_subagent(pending_subagent: Dict[str, Any]) -> None: parent_otel_span, pending_subagent["subagent"], pending_subagent.get("display_start_timestamp") or pending_subagent.get("start_timestamp"), + subagent_transcripts_by_tool_use_id=subagent_transcripts_by_tool_use_id, ) latest_end_timestamp = _get_latest_timestamp(latest_end_timestamp, subagent_end_timestamp) @@ -2896,7 +2898,9 @@ def emit_subagent_observations(langfuse: Langfuse, parent_otel_span: Any, span_name: Optional[str] = None, extra_metadata: Optional[Dict[str, Any]] = None, generation_name: str = "Subagent LLM Call", - empty_output_fallback: Optional[str] = None) -> Optional[datetime]: + empty_output_fallback: Optional[str] = None, + subagent_transcripts_by_tool_use_id: Optional[Dict[str, Dict[str, Any]]] = None, + ) -> Optional[datetime]: path = subagent.get("path") if not isinstance(path, Path): return start_timestamp @@ -2955,7 +2959,8 @@ def emit_subagent_observations(langfuse: Langfuse, parent_otel_span: Any, turn, previous_start_timestamp, generation_name=generation_name, - subagent_transcripts_by_tool_use_id=None, + # Agents launched by this agent are listed in the same map. + subagent_transcripts_by_tool_use_id=subagent_transcripts_by_tool_use_id, history_prefix=list(subagent_history), ) subagent_history.extend(build_turn_history_messages(turn)) diff --git a/tests/integration/test_emit_turn_observations.py b/tests/integration/test_emit_turn_observations.py index 4a8e23b..219d1ca 100644 --- a/tests/integration/test_emit_turn_observations.py +++ b/tests/integration/test_emit_turn_observations.py @@ -34,7 +34,7 @@ def test_emit_turn_observations_creates_generation_tool_and_subagent_observation assert "Async agent launched successfully." in agent_tool.output -def test_nested_subagents_document_current_non_recursive_emission_behavior( +def test_nested_subagents_emit_inner_agent_under_outer_agent( hook_module, fixture_transcript_path, read_fixture_jsonl, @@ -54,10 +54,20 @@ def test_nested_subagents_document_current_non_recursive_emission_behavior( subagent_transcripts_by_tool_use_id=subagents, ) - names = [observation.name for observation in fake_langfuse.observations] - assert "Subagent: Outer agent" in names - assert "Tool: Agent" in names - assert "Subagent: Inner agent" not in names + by_name = {observation.name: observation for observation in fake_langfuse.observations} + outer = by_name["Subagent: Outer agent"] + inner = by_name["Subagent: Inner agent"] + + ancestors = [] + span = inner._otel_span.parent + while span is not None: + ancestors.append(span) + span = span.parent + assert outer._otel_span in ancestors + assert any( + observation._otel_span.parent is inner._otel_span and observation.as_type == "generation" + for observation in fake_langfuse.observations + ) # The fixture's human wait: the AskUserQuestion tool_use is written at