1515_ORIGINAL_COMPILE = "_agent_assembly_original_compile"
1616_NODE_WRAPPED_FLAG = "_agent_assembly_node_wrapped"
1717_INVOKE_WRAPPED_FLAG = "_agent_assembly_invoke_wrapped"
18+ _GRAPH_INVOKE_NODE_NAME = "graph.invoke"
1819
1920# Thread-local storage for the name of the most-recently-completed node so that
2021# the next _record_node_enter can emit a directed Messages edge between them.
@@ -308,69 +309,86 @@ def sync_subgraph_wrapper(*args: Any, **kwargs: Any) -> Any:
308309 return sync_subgraph_wrapper
309310
310311
312+ def _wrap_callable_node_executor (node_map : Any , node_name : Any , node_executor : Any , callback_handler : Any ) -> bool :
313+ """Wrap a directly-callable node executor in the node map. Return True if replaced."""
314+ wrapped_executor = _make_assembly_node_wrapper (str (node_name ), node_executor , callback_handler )
315+ if wrapped_executor is node_executor :
316+ return False
317+ try :
318+ node_map [node_name ] = wrapped_executor
319+ except Exception :
320+ return False
321+ return True
322+
323+
324+ def _wrap_subgraph_spawn_node (node_map : Any , node_name : Any , node_executor : Any , process_agent_id : str | None ) -> bool :
325+ """Wrap a compiled-subgraph node (spawn point) for lineage. Return True when wrapped."""
326+ node_delegation_reason = f"langgraph_node:{ node_name } "
327+ sync_wrapper = _make_subgraph_spawn_wrapper (
328+ str (node_name ),
329+ node_executor ,
330+ process_agent_id ,
331+ spawned_by_tool = None ,
332+ delegation_reason = node_delegation_reason ,
333+ )
334+ with contextlib .suppress (Exception ):
335+ node_map [node_name ] = sync_wrapper
336+ if hasattr (node_executor , "ainvoke" ) and not getattr (node_executor , "_agent_assembly_ainvoke_spawned" , False ):
337+ async_wrapper = _make_subgraph_spawn_wrapper (
338+ str (node_name ),
339+ node_executor ,
340+ process_agent_id ,
341+ async_ = True ,
342+ spawned_by_tool = None ,
343+ delegation_reason = node_delegation_reason ,
344+ )
345+ node_executor .ainvoke = async_wrapper
346+ node_executor ._agent_assembly_ainvoke_spawned = True
347+ return True
348+
349+
350+ def _wrap_node_invoke_methods (node_name : Any , node_executor : Any , callback_handler : Any ) -> bool :
351+ """Wrap a node executor's invoke/ainvoke methods. Return True if either was wrapped."""
352+ wrapped_any = False
353+ invoke = getattr (node_executor , "invoke" , None )
354+ if callable (invoke ):
355+ node_executor .invoke = _make_assembly_node_wrapper (str (node_name ), invoke , callback_handler )
356+ wrapped_any = True
357+
358+ ainvoke = getattr (node_executor , "ainvoke" , None )
359+ if callable (ainvoke ):
360+ node_executor .ainvoke = _make_assembly_node_wrapper (str (node_name ), ainvoke , callback_handler )
361+ wrapped_any = True
362+ return wrapped_any
363+
364+
365+ def _wrap_node_entry (
366+ node_map : Any , node_name : Any , node_executor : Any , callback_handler : Any , process_agent_id : str | None
367+ ) -> bool :
368+ """Dispatch a single (node_name, node_executor) entry to the right wrapper. Return True if wrapped."""
369+ # ToolNode: intercept any compiled-subgraph tools it holds.
370+ # Must come before the callable() check since ToolNode is also callable.
371+ if _is_tool_node (node_executor ):
372+ return _wrap_tool_node_subgraphs (str (node_name ), node_executor , process_agent_id )
373+
374+ if callable (node_executor ):
375+ return _wrap_callable_node_executor (node_map , node_name , node_executor , callback_handler )
376+
377+ # Spawn point: node is itself a compiled subgraph — wrap for lineage.
378+ if _is_compiled_subgraph (node_executor ):
379+ return _wrap_subgraph_spawn_node (node_map , node_name , node_executor , process_agent_id )
380+
381+ return _wrap_node_invoke_methods (node_name , node_executor , callback_handler )
382+
383+
311384def _wrap_node_map (node_map : Any , callback_handler : Any , process_agent_id : str | None = None ) -> bool :
312385 items_method = getattr (node_map , "items" , None )
313386 if not callable (items_method ):
314387 return False
315388
316389 wrapped_any = False
317390 for node_name , node_executor in list (items_method ()):
318- # ToolNode: intercept any compiled-subgraph tools it holds.
319- # Must come before the callable() check since ToolNode is also callable.
320- if _is_tool_node (node_executor ):
321- if _wrap_tool_node_subgraphs (str (node_name ), node_executor , process_agent_id ):
322- wrapped_any = True
323- continue
324-
325- if callable (node_executor ):
326- wrapped_executor = _make_assembly_node_wrapper (str (node_name ), node_executor , callback_handler )
327- if wrapped_executor is node_executor :
328- continue
329- try :
330- node_map [node_name ] = wrapped_executor
331- except Exception :
332- continue
333- wrapped_any = True
334- continue
335-
336- # Spawn point: node is itself a compiled subgraph — wrap for lineage.
337- if _is_compiled_subgraph (node_executor ):
338- node_delegation_reason = f"langgraph_node:{ node_name } "
339- sync_wrapper = _make_subgraph_spawn_wrapper (
340- str (node_name ),
341- node_executor ,
342- process_agent_id ,
343- spawned_by_tool = None ,
344- delegation_reason = node_delegation_reason ,
345- )
346- with contextlib .suppress (Exception ):
347- node_map [node_name ] = sync_wrapper
348- if hasattr (node_executor , "ainvoke" ) and not getattr (
349- node_executor , "_agent_assembly_ainvoke_spawned" , False
350- ):
351- async_wrapper = _make_subgraph_spawn_wrapper (
352- str (node_name ),
353- node_executor ,
354- process_agent_id ,
355- async_ = True ,
356- spawned_by_tool = None ,
357- delegation_reason = node_delegation_reason ,
358- )
359- node_executor .ainvoke = async_wrapper
360- node_executor ._agent_assembly_ainvoke_spawned = True
361- wrapped_any = True
362- continue
363-
364- invoke = getattr (node_executor , "invoke" , None )
365- if callable (invoke ):
366- wrapped_invoke = _make_assembly_node_wrapper (str (node_name ), invoke , callback_handler )
367- node_executor .invoke = wrapped_invoke
368- wrapped_any = True
369-
370- ainvoke = getattr (node_executor , "ainvoke" , None )
371- if callable (ainvoke ):
372- wrapped_ainvoke = _make_assembly_node_wrapper (str (node_name ), ainvoke , callback_handler )
373- node_executor .ainvoke = wrapped_ainvoke
391+ if _wrap_node_entry (node_map , node_name , node_executor , callback_handler , process_agent_id ):
374392 wrapped_any = True
375393
376394 return wrapped_any
@@ -432,11 +450,11 @@ def _wrap_graph_invoke_fallback(compiled_graph: Any, callback_handler: Any) -> N
432450 async def wrapped_async_invoke (* invoke_args : Any , ** invoke_kwargs : Any ) -> Any :
433451 state = _extract_state (invoke_args , invoke_kwargs )
434452 config = _extract_config (invoke_args , invoke_kwargs )
435- _record_node_enter (callback_handler , node_name = "graph.invoke" , state = state , config = config )
453+ _record_node_enter (callback_handler , node_name = _GRAPH_INVOKE_NODE_NAME , state = state , config = config )
436454 result = await invoke (* invoke_args , ** invoke_kwargs )
437455 _record_node_exit (
438456 callback_handler ,
439- node_name = "graph.invoke" ,
457+ node_name = _GRAPH_INVOKE_NODE_NAME ,
440458 previous_state = state ,
441459 next_state = result ,
442460 config = config ,
@@ -449,11 +467,11 @@ async def wrapped_async_invoke(*invoke_args: Any, **invoke_kwargs: Any) -> Any:
449467 def wrapped_sync_invoke (* invoke_args : Any , ** invoke_kwargs : Any ) -> Any :
450468 state = _extract_state (invoke_args , invoke_kwargs )
451469 config = _extract_config (invoke_args , invoke_kwargs )
452- _record_node_enter (callback_handler , node_name = "graph.invoke" , state = state , config = config )
470+ _record_node_enter (callback_handler , node_name = _GRAPH_INVOKE_NODE_NAME , state = state , config = config )
453471 result = invoke (* invoke_args , ** invoke_kwargs )
454472 _record_node_exit (
455473 callback_handler ,
456- node_name = "graph.invoke" ,
474+ node_name = _GRAPH_INVOKE_NODE_NAME ,
457475 previous_state = state ,
458476 next_state = result ,
459477 config = config ,
0 commit comments