@@ -360,6 +360,58 @@ def _wrap_subgraph_spawn_node(node_map: Any, node_name: Any, node_executor: Any,
360360 return True
361361
362362
363+ def _wrap_subgraph_invoke_methods_in_place (node_name : Any , subgraph : Any , process_agent_id : str | None ) -> bool :
364+ """Wrap a compiled subgraph's own invoke/ainvoke in place for lineage.
365+
366+ AAASM-4472: used when the subgraph is reached via an already-unwrapped
367+ ``PregelNode.bound`` rather than arriving bare in the node map. Unlike
368+ ``_wrap_subgraph_spawn_node`` (which replaces the whole node_map entry for a
369+ bare subgraph), the outer ``PregelNode`` container here must stay intact —
370+ its channels/triggers/mapper/writers are what the Pregel runtime uses to
371+ build the executable task — so this mutates the bound Runnable's own
372+ invoke/ainvoke attributes instead. The original methods are captured before
373+ reassignment so the wrapper calls the real invoke/ainvoke rather than
374+ recursing into itself once installed.
375+ """
376+ node_delegation_reason = f"langgraph_node:{ node_name } "
377+ wrapped_any = False
378+
379+ original_invoke = getattr (subgraph , "invoke" , None )
380+ if callable (original_invoke ):
381+
382+ def sync_subgraph_wrapper (* args : Any , ** kwargs : Any ) -> Any :
383+ spawn_ctx = SpawnContext (
384+ parent_agent_id = process_agent_id or "" ,
385+ depth = _current_spawn_depth (),
386+ spawned_by_tool = None ,
387+ delegation_reason = node_delegation_reason ,
388+ )
389+ with spawn_context_scope (spawn_ctx ):
390+ return original_invoke (* args , ** kwargs )
391+
392+ subgraph .invoke = sync_subgraph_wrapper
393+ wrapped_any = True
394+
395+ original_ainvoke = getattr (subgraph , "ainvoke" , None )
396+ if callable (original_ainvoke ) and not getattr (subgraph , "_agent_assembly_ainvoke_spawned" , False ):
397+
398+ async def async_subgraph_wrapper (* args : Any , ** kwargs : Any ) -> Any :
399+ spawn_ctx = SpawnContext (
400+ parent_agent_id = process_agent_id or "" ,
401+ depth = _current_spawn_depth (),
402+ spawned_by_tool = None ,
403+ delegation_reason = node_delegation_reason ,
404+ )
405+ with spawn_context_scope (spawn_ctx ):
406+ return await original_ainvoke (* args , ** kwargs )
407+
408+ subgraph .ainvoke = async_subgraph_wrapper
409+ subgraph ._agent_assembly_ainvoke_spawned = True
410+ wrapped_any = True
411+
412+ return wrapped_any
413+
414+
363415def _wrap_node_invoke_methods (node_name : Any , node_executor : Any , callback_handler : Any ) -> bool :
364416 """Wrap a node executor's invoke/ainvoke methods. Return True if either was wrapped."""
365417 wrapped_any = False
@@ -384,19 +436,28 @@ def _wrap_node_entry(
384436 if _is_tool_node (node_executor ):
385437 return _wrap_tool_node_subgraphs (node_executor , process_agent_id )
386438
439+ # AAASM-4434/AAASM-4472: current langgraph node-map entries are PregelNode
440+ # wrappers whose own .invoke/.ainvoke are never called by the Pregel runtime
441+ # — it dispatches through PregelNode.bound. Unwrap *before* any other
442+ # detection (including _is_compiled_subgraph) and dispatch against the
443+ # unwrapped object: checking _is_compiled_subgraph() against the outer
444+ # PregelNode itself always fails (only the wrapped object exposes `.nodes`),
445+ # so subgraph detection must run post-unwrap to work whether a subgraph
446+ # arrives bare or already PregelNode-wrapped.
447+ if _is_pregel_node_wrapper (node_executor ):
448+ bound = node_executor .bound
449+ if _is_compiled_subgraph (bound ):
450+ return _wrap_subgraph_invoke_methods_in_place (node_name , bound , process_agent_id )
451+ return _wrap_node_invoke_methods (node_name , bound , callback_handler )
452+
387453 if callable (node_executor ):
388454 return _wrap_callable_node_executor (node_map , node_name , node_executor , callback_handler )
389455
390- # Spawn point: node is itself a compiled subgraph — wrap for lineage.
456+ # Spawn point: node is itself a compiled subgraph, not PregelNode-wrapped —
457+ # wrap for lineage.
391458 if _is_compiled_subgraph (node_executor ):
392459 return _wrap_subgraph_spawn_node (node_map , node_name , node_executor , process_agent_id )
393460
394- # AAASM-4434: current langgraph node-map entries are PregelNode wrappers whose
395- # own .invoke/.ainvoke are never called by the Pregel runtime — it dispatches
396- # through PregelNode.bound. Wrap that inner Runnable instead, or hooks never fire.
397- if _is_pregel_node_wrapper (node_executor ):
398- return _wrap_node_invoke_methods (node_name , node_executor .bound , callback_handler )
399-
400461 return _wrap_node_invoke_methods (node_name , node_executor , callback_handler )
401462
402463
0 commit comments