3535import pickle
3636import sys
3737from dataclasses import dataclass
38- from typing import Callable , Dict , FrozenSet , Optional
38+ from typing import Any , Callable , Dict , FrozenSet , Optional
3939
4040# Mirrors conductor.client.worker.worker._MAX_UNWRAP_DEPTH.
4141_MAX_UNWRAP_DEPTH = 32
@@ -67,6 +67,79 @@ def _walk_qualname(module_obj, qualname: str):
6767# .coroutine; our Guardrail / ToolDef → .func.
6868_CONTAINER_ATTRS = ("func" , "coroutine" )
6969
70+ def _extract_from_closure (func : Callable ) -> Optional [Callable ]:
71+ """Extract the original user function from a closure's cell variables.
72+
73+ - Shared by :func:`_find_embedded_function` below.
74+ - In turn shared by :mod:`conductor.ai.agents.frameworks.serializer`'s
75+ discovery and :class:`FunctionRef`'s parent-verification /
76+ child-reconstruction — one implementation, can't drift apart.
77+ """
78+ closure = getattr (func , "__closure__" , None )
79+ if not closure :
80+ return None
81+
82+ for cell in closure :
83+ try :
84+ val = cell .cell_contents
85+ except ValueError :
86+ continue
87+ if inspect .isfunction (val ):
88+ # Skip internal wrappers that take (ctx, input) or (context, ...)
89+ try :
90+ sig = inspect .signature (val )
91+ param_names = list (sig .parameters .keys ())
92+ # Internal wrappers typically start with ctx/context as first param
93+ if param_names and param_names [0 ] in ("ctx" , "context" ):
94+ continue
95+ return val
96+ except (ValueError , TypeError ):
97+ continue
98+ return None
99+
100+
101+ # How many attribute-nesting levels _find_embedded_function will descend.
102+ # openai-agents' FunctionTool -> on_invoke_tool (invoker instance) ->
103+ # _invoke_tool_impl (closure fn) is 2 levels deep; matches serializer.py's
104+ # own default so parent discovery and child reconstruction stay in lockstep.
105+ _DEEP_EXTRACT_MAX_DEPTH = 2
106+
107+
108+ def _find_embedded_function (obj : Any , max_depth : int = _DEEP_EXTRACT_MAX_DEPTH ) -> Optional [Callable ]:
109+ """Walk an object's attributes to find an embedded plain function.
110+
111+ - Generic, framework-agnostic: no hardcoded attribute names.
112+ - For containers that expose the original function only behind further
113+ nested attributes and/or closures, not a single named attribute — e.g.
114+ openai-agents' ``FunctionTool.on_invoke_tool`` is itself a callable
115+ *instance*, not a function; the real closure lives one level deeper,
116+ on that instance's own ``_invoke_tool_impl`` attribute.
117+ - Deliberately avoids hardcoding that private attribute name: a
118+ shape-based walk degrades to "not found" (the pre-existing, actionable
119+ ``SpawnSafetyError``) instead of breaking outright if openai-agents
120+ restructures its internals.
121+ """
122+ if max_depth <= 0 :
123+ return None
124+
125+ for attr_name in vars (obj ) if hasattr (obj , "__dict__" ) else []:
126+ val = getattr (obj , attr_name , None )
127+ if val is None :
128+ continue
129+
130+ if inspect .isfunction (val ):
131+ func = _extract_from_closure (val )
132+ if func is not None :
133+ return func
134+ return val
135+
136+ if hasattr (val , "__dict__" ) and not isinstance (val , type ):
137+ result = _find_embedded_function (val , max_depth - 1 )
138+ if result is not None :
139+ return result
140+
141+ return None
142+
70143
71144def _wrapped_depth_to (obj , fn ) -> Optional [int ]:
72145 """Number of ``__wrapped__`` hops from *obj* down to *fn*, or ``None``."""
@@ -97,12 +170,25 @@ class FunctionRef:
97170 object rather than a wraps-wrapper — e.g. langchain's ``@tool`` rebinds
98171 it to a ``StructuredTool`` holding the original in ``.func`` (sync) or
99172 ``.coroutine`` (async). The hop is taken before the ``__wrapped__`` walk.
173+
174+ ``deep_extract`` handles containers that don't expose the original
175+ function via any single named attribute:
176+
177+ - e.g. openai-agents' ``FunctionTool``, whose ``on_invoke_tool`` is
178+ itself a callable instance, not a function — the real function is
179+ nested further behind that instance's own attributes and a closure.
180+ - Uses the generic :func:`_find_embedded_function` walk instead of a
181+ fixed ``attr_hop``, so it isn't tied to a specific (private,
182+ renameable) attribute path.
183+ - Tried last, after ``attr_hop``/``_CONTAINER_ATTRS``, before the
184+ ``__wrapped__`` walk.
100185 """
101186
102187 module : str
103188 qualname : str
104189 unwrap_depth : int = 0
105190 attr_hop : str = ""
191+ deep_extract : bool = False
106192
107193 @classmethod
108194 def of (cls , fn : Callable ) -> "FunctionRef" :
@@ -158,10 +244,22 @@ def of(cls, fn: Callable) -> "FunctionRef":
158244 depth = _wrapped_depth_to (inner , fn )
159245 if depth is not None :
160246 return cls (module , qualname , depth , attr )
247+ # Still no match — container may hold the original behind further
248+ # nested attributes and/or a closure, not a single named attribute.
249+ # e.g. openai-agents' FunctionTool.on_invoke_tool: callable instance,
250+ # not a function; real closure is one level deeper.
251+ # Generic walk, no hardcoded attribute names (_find_embedded_function).
252+ # No __wrapped__ fallback needed here (unlike _CONTAINER_ATTRS): fn
253+ # always arrives as _find_embedded_function's own prior return value
254+ # (how serializer.py derives WorkerInfo.func) — identical walk always
255+ # matches by identity.
256+ if _find_embedded_function (obj ) is fn :
257+ return cls (module , qualname , 0 , "" , deep_extract = True )
161258 raise SpawnSafetyError (
162259 f"'{ module } .{ qualname } ' does not resolve back to { fn !r} (rebound "
163- f"without a __wrapped__ chain or a func/coroutine container "
164- f"attribute). { _REMEDIES } "
260+ f"without a __wrapped__ chain, a func/coroutine container "
261+ f"attribute, or a discoverable nested/closure-held function). "
262+ f"{ _REMEDIES } "
165263 )
166264
167265 def resolve (self ) -> Callable :
@@ -173,6 +271,13 @@ def resolve(self) -> Callable:
173271 obj = _walk_qualname (module_obj , self .qualname )
174272 if self .attr_hop :
175273 obj = getattr (obj , self .attr_hop )
274+ if self .deep_extract :
275+ obj = _find_embedded_function (obj )
276+ if obj is None :
277+ raise SpawnSafetyError (
278+ f"'{ self .module } .{ self .qualname } ' no longer yields a "
279+ f"discoverable nested function (definition changed?)."
280+ )
176281 for _ in range (self .unwrap_depth ):
177282 obj = obj .__wrapped__
178283 _RESOLVE_CACHE [self ] = obj
0 commit comments