Skip to content

Commit 102008d

Browse files
committed
fix(deepagents): repair subagent agent spans
1 parent 4496481 commit 102008d

6 files changed

Lines changed: 458 additions & 12 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_attributes.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@
6262
METADATA_LC_AGENT_NAME = "lc_agent_name"
6363
METADATA_VERSIONS = "versions"
6464
METADATA_DEEPAGENTS_VERSION = "deepagents"
65+
METADATA_SUBAGENT_DESCRIPTION = "loongsuite_deepagents_subagent_description"
6566

6667
SUBAGENT_TYPE = "subagent"
6768
TASK_TOOL_NAME = "task"
@@ -77,6 +78,8 @@
7778

7879
CREATE_DEEP_AGENT_MODULE = "deepagents.graph"
7980
CREATE_DEEP_AGENT_NAME = "create_deep_agent"
81+
BUILD_TASK_TOOL_MODULE = "deepagents.middleware.subagents"
82+
BUILD_TASK_TOOL_NAME = "_build_task_tool"
8083

8184
METRIC_CALLS_COUNT = "genai_calls_count"
8285
METRIC_CALLS_DURATION_SECONDS = "genai_calls_duration_seconds"

instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_entry_patch.py

Lines changed: 142 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import logging
1111
import sys
12-
from collections.abc import AsyncIterator, Iterator
12+
from collections.abc import AsyncIterator, Iterator, Mapping
1313
from contextlib import suppress
1414
from importlib import import_module
1515
from typing import Any, Callable
@@ -22,6 +22,8 @@
2222
from opentelemetry.util.genai.types import Error
2323

2424
from ._attributes import (
25+
BUILD_TASK_TOOL_MODULE,
26+
BUILD_TASK_TOOL_NAME,
2527
CREATE_DEEP_AGENT_MODULE,
2628
CREATE_DEEP_AGENT_NAME,
2729
FRAMEWORK_NAME,
@@ -37,11 +39,15 @@
3739
GRAPH_REGISTRY_ATTR,
3840
GRAPH_VERSION_ATTR,
3941
LANGGRAPH_REACT_AGENT_METADATA_KEY,
42+
METADATA_LS_AGENT_TYPE,
43+
METADATA_SUBAGENT_DESCRIPTION,
4044
SPAN_KIND_ENTRY,
45+
SUBAGENT_TYPE,
4146
)
4247
from ._utils import (
4348
active_span_is_entry_or_agent,
4449
config_from_call,
50+
config_with_langgraph_react_metadata,
4551
create_graph_metadata,
4652
detect_deepagents_version,
4753
entry_attributes,
@@ -65,6 +71,7 @@
6571
_MISSING = object()
6672
_top_level_original: Any = _MISSING
6773
_top_level_patched = False
74+
_is_subagent_task_patched = False
6875

6976

7077
def instrument_entry_patch(handler: ExtendedTelemetryHandler) -> None:
@@ -94,6 +101,7 @@ def instrument_entry_patch(handler: ExtendedTelemetryHandler) -> None:
94101
)
95102
return
96103
_sync_top_level_create_deep_agent()
104+
_instrument_subagent_task_tool()
97105
_is_entry_patched = True
98106

99107

@@ -111,6 +119,7 @@ def uninstrument_entry_patch() -> None:
111119
with suppress(Exception):
112120
module = import_module(CREATE_DEEP_AGENT_MODULE)
113121
unwrap(module, CREATE_DEEP_AGENT_NAME)
122+
_uninstrument_subagent_task_tool()
114123
_restore_top_level_create_deep_agent()
115124
_is_entry_patched = False
116125

@@ -175,6 +184,134 @@ def _create_deep_agent_wrapper(
175184
return graph
176185

177186

187+
def _instrument_subagent_task_tool() -> None:
188+
"""Patch deepagents subagent task construction to mark nested graphs."""
189+
global _is_subagent_task_patched # noqa: PLW0603
190+
if _is_subagent_task_patched:
191+
return
192+
try:
193+
wrap_function_wrapper(
194+
BUILD_TASK_TOOL_MODULE,
195+
BUILD_TASK_TOOL_NAME,
196+
_build_task_tool_wrapper,
197+
)
198+
except ModuleNotFoundError as exc:
199+
if exc.name == "deepagents" or exc.name == BUILD_TASK_TOOL_MODULE:
200+
_logger.debug(
201+
"deepagents subagent middleware is not installed; "
202+
"SubAgent task patch skipped."
203+
)
204+
return
205+
raise
206+
except AttributeError:
207+
_logger.debug(
208+
"%s.%s not found; SubAgent task patch skipped.",
209+
BUILD_TASK_TOOL_MODULE,
210+
BUILD_TASK_TOOL_NAME,
211+
)
212+
return
213+
_is_subagent_task_patched = True
214+
215+
216+
def _uninstrument_subagent_task_tool() -> None:
217+
global _is_subagent_task_patched # noqa: PLW0603
218+
if not _is_subagent_task_patched:
219+
return
220+
with suppress(Exception):
221+
module = import_module(BUILD_TASK_TOOL_MODULE)
222+
unwrap(module, BUILD_TASK_TOOL_NAME)
223+
_is_subagent_task_patched = False
224+
225+
226+
def _build_task_tool_wrapper(
227+
wrapped: Callable[..., Any],
228+
_instance: Any,
229+
args: tuple[Any, ...],
230+
kwargs: dict[str, Any],
231+
) -> Any:
232+
_mark_subagent_specs(_subagent_specs_from_call(args, kwargs))
233+
return wrapped(*args, **kwargs)
234+
235+
236+
def _subagent_specs_from_call(
237+
args: tuple[Any, ...],
238+
kwargs: Mapping[str, Any],
239+
) -> Any:
240+
if args:
241+
return args[0]
242+
return kwargs.get("subagents")
243+
244+
245+
def _mark_subagent_specs(subagents: Any) -> None:
246+
for spec in subagents or ():
247+
try:
248+
name = spec.get("name") if isinstance(spec, dict) else None
249+
description = (
250+
spec.get("description") if isinstance(spec, dict) else None
251+
)
252+
runnable = spec.get("runnable") if isinstance(spec, dict) else None
253+
if not name or runnable is None:
254+
continue
255+
spec["runnable"] = _mark_subagent_runnable(
256+
runnable,
257+
name=str(name),
258+
description=str(description) if description else None,
259+
)
260+
except Exception: # noqa: BLE001
261+
_logger.debug("Failed to mark deepagents SubAgent graph", exc_info=True)
262+
263+
264+
def _mark_subagent_runnable(
265+
runnable: Any,
266+
*,
267+
name: str,
268+
description: str | None,
269+
) -> Any:
270+
metadata = create_graph_metadata(runnable, name=name)
271+
metadata.setdefault(METADATA_LS_AGENT_TYPE, SUBAGENT_TYPE)
272+
metadata.setdefault(LANGGRAPH_REACT_AGENT_METADATA_KEY, True)
273+
if description:
274+
metadata.setdefault(METADATA_SUBAGENT_DESCRIPTION, description)
275+
276+
registry = {name: description} if description else {}
277+
_mark_graph(runnable, metadata, registry)
278+
proxy = _SubagentRunnableProxy(runnable, metadata)
279+
_mark_graph(proxy, metadata, registry)
280+
return proxy
281+
282+
283+
class _SubagentRunnableProxy:
284+
"""Proxy that injects deepagents metadata before nested SubAgent calls."""
285+
286+
__slots__ = ("_metadata", "_runnable")
287+
288+
def __init__(self, runnable: Any, metadata: Mapping[str, Any]) -> None:
289+
self._runnable = runnable
290+
self._metadata = dict(metadata)
291+
292+
def __getattr__(self, name: str) -> Any:
293+
return getattr(self._runnable, name)
294+
295+
def invoke(self, value: Any, config: Any = None, **kwargs: Any) -> Any:
296+
return self._runnable.invoke(
297+
value,
298+
config_with_langgraph_react_metadata(config, self._metadata),
299+
**kwargs,
300+
)
301+
302+
async def ainvoke(
303+
self,
304+
value: Any,
305+
config: Any = None,
306+
**kwargs: Any,
307+
) -> Any:
308+
return await self._runnable.ainvoke(
309+
value,
310+
config_with_langgraph_react_metadata(config, self._metadata),
311+
**kwargs,
312+
)
313+
314+
178315
def _mark_graph(
179316
graph: Any,
180317
metadata: dict[str, Any],
@@ -353,7 +490,7 @@ def _call_sync_with_entry(
353490
invocation, token = _start_entry(
354491
graph, method_name, metadata, registry, args, kwargs
355492
)
356-
args, kwargs = inject_langgraph_react_metadata(args, kwargs)
493+
args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata)
357494
try:
358495
result = original(*args, **kwargs)
359496
except Exception as exc:
@@ -375,7 +512,7 @@ async def _call_async_with_entry(
375512
invocation, token = _start_entry(
376513
graph, method_name, metadata, registry, args, kwargs
377514
)
378-
args, kwargs = inject_langgraph_react_metadata(args, kwargs)
515+
args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata)
379516
try:
380517
result = await original(*args, **kwargs)
381518
except Exception as exc:
@@ -397,7 +534,7 @@ def _call_stream_with_entry(
397534
invocation, token = _start_entry(
398535
graph, method_name, metadata, registry, args, kwargs
399536
)
400-
args, kwargs = inject_langgraph_react_metadata(args, kwargs)
537+
args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata)
401538
last_chunk = None
402539
try:
403540
for chunk in original(*args, **kwargs):
@@ -421,7 +558,7 @@ async def _call_astream_with_entry(
421558
invocation, token = _start_entry(
422559
graph, method_name, metadata, registry, args, kwargs
423560
)
424-
args, kwargs = inject_langgraph_react_metadata(args, kwargs)
561+
args, kwargs = inject_langgraph_react_metadata(args, kwargs, metadata)
425562
last_chunk = None
426563
try:
427564
async for chunk in original(*args, **kwargs):

instrumentation-loongsuite/loongsuite-instrumentation-deepagents/src/opentelemetry/instrumentation/deepagents/internal/_utils.py

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,9 @@
4141
LANGGRAPH_REACT_AGENT_METADATA_KEY,
4242
METADATA_DEEPAGENTS_VERSION,
4343
METADATA_LC_AGENT_NAME,
44+
METADATA_LS_AGENT_TYPE,
4445
METADATA_LS_INTEGRATION,
46+
METADATA_SUBAGENT_DESCRIPTION,
4547
METADATA_VERSIONS,
4648
SPAN_KIND_ENTRY,
4749
)
@@ -206,7 +208,38 @@ def config_from_call(args: tuple[Any, ...], kwargs: Mapping[str, Any]) -> Any:
206208
return kwargs.get("config")
207209

208210

209-
def config_with_langgraph_react_metadata(config: Any) -> Any:
211+
def _merge_deepagents_metadata(
212+
target: dict[str, Any],
213+
source: Mapping[str, Any] | None,
214+
) -> None:
215+
if not source:
216+
return
217+
for key, value in source.items():
218+
if key == METADATA_VERSIONS and isinstance(value, Mapping):
219+
versions = dict(obj_get(target, METADATA_VERSIONS, {}) or {})
220+
for version_key, version_value in value.items():
221+
versions.setdefault(version_key, version_value)
222+
if versions:
223+
target[METADATA_VERSIONS] = versions
224+
continue
225+
if key == METADATA_SUBAGENT_DESCRIPTION and value:
226+
target[key] = value
227+
continue
228+
if key in {
229+
METADATA_LS_INTEGRATION,
230+
METADATA_LS_AGENT_TYPE,
231+
METADATA_LC_AGENT_NAME,
232+
METADATA_SUBAGENT_DESCRIPTION,
233+
}:
234+
target[key] = value
235+
continue
236+
target.setdefault(key, value)
237+
238+
239+
def config_with_langgraph_react_metadata(
240+
config: Any,
241+
metadata: Mapping[str, Any] | None = None,
242+
) -> Any:
210243
if _ensure_config is None:
211244
ensured = config or {}
212245
else:
@@ -219,23 +252,26 @@ def config_with_langgraph_react_metadata(config: Any) -> Any:
219252
return ensured
220253

221254
updated = dict(ensured)
222-
metadata = dict(obj_get(updated, "metadata", {}) or {})
223-
metadata.setdefault(LANGGRAPH_REACT_AGENT_METADATA_KEY, True)
224-
updated["metadata"] = metadata
255+
updated_metadata = dict(obj_get(updated, "metadata", {}) or {})
256+
_merge_deepagents_metadata(updated_metadata, metadata)
257+
updated_metadata.setdefault(LANGGRAPH_REACT_AGENT_METADATA_KEY, True)
258+
updated["metadata"] = updated_metadata
225259
return updated
226260

227261

228262
def inject_langgraph_react_metadata(
229263
args: tuple[Any, ...],
230264
kwargs: Mapping[str, Any],
265+
metadata: Mapping[str, Any] | None = None,
231266
) -> tuple[tuple[Any, ...], dict[str, Any]]:
232267
updated_kwargs = dict(kwargs)
233268
if len(args) > 1:
234-
config = config_with_langgraph_react_metadata(args[1])
269+
config = config_with_langgraph_react_metadata(args[1], metadata)
235270
return (args[0], config) + args[2:], updated_kwargs
236271

237272
updated_kwargs["config"] = config_with_langgraph_react_metadata(
238-
updated_kwargs.get("config")
273+
updated_kwargs.get("config"),
274+
metadata,
239275
)
240276
return args, updated_kwargs
241277

0 commit comments

Comments
 (0)