55import traceback
66import typing as T
77import uuid
8+ from collections .abc import Sequence
9+ from collections .abc import Set as AbstractSet
810
911import mcp
1012
@@ -66,22 +68,23 @@ def _is_supported_image_ref(cls, image_ref: str) -> bool:
6668 return ext in cls ._ALLOWED_IMAGE_EXTENSIONS
6769
6870 @classmethod
69- async def _prepare_handoff_image_urls (
70- cls ,
71- run_context : ContextWrapper [AstrAgentContext ],
72- tool_args : dict [str , T .Any ],
73- ) -> list [str ]:
74- image_urls = tool_args .get ("image_urls" )
71+ def _coerce_image_urls (cls , image_urls : T .Any ) -> list [T .Any ]:
7572 if image_urls is None :
76- candidates : list [T .Any ] = []
77- elif isinstance (image_urls , str ):
78- candidates = [image_urls ]
79- else :
80- try :
81- candidates = list (image_urls )
82- except (TypeError , ValueError ):
83- candidates = [image_urls ]
73+ return []
74+ if isinstance (image_urls , str ):
75+ return [image_urls ]
76+ if isinstance (image_urls , (Sequence , AbstractSet )) and not isinstance (
77+ image_urls , (str , bytes , bytearray )
78+ ):
79+ return list (image_urls )
80+ logger .warning (
81+ "Unsupported image_urls type in handoff tool args: %s" ,
82+ type (image_urls ).__name__ ,
83+ )
84+ return []
8485
86+ @classmethod
87+ def _filter_supported_image_urls (cls , candidates : list [T .Any ]) -> list [str ]:
8588 normalized = normalize_and_dedupe_strings (candidates )
8689 sanitized = [item for item in normalized if cls ._is_supported_image_ref (item )]
8790 dropped_count = len (normalized ) - len (sanitized )
@@ -90,8 +93,13 @@ async def _prepare_handoff_image_urls(
9093 "Dropped %d invalid image_urls entries in handoff tool args." ,
9194 dropped_count ,
9295 )
96+ return sanitized
9397
94- # Merge current event image attachments so sub-agent behavior matches main-agent flow.
98+ @classmethod
99+ async def _iter_event_image_paths (
100+ cls , run_context : ContextWrapper [AstrAgentContext ]
101+ ) -> list [str ]:
102+ paths : list [str ] = []
95103 event = getattr (run_context .context , "event" , None )
96104 message_obj = getattr (event , "message_obj" , None )
97105 message = getattr (message_obj , "message" , None )
@@ -101,22 +109,27 @@ async def _prepare_handoff_image_urls(
101109 continue
102110 try :
103111 path = await component .convert_to_file_path ()
104- if (
105- path
106- and cls ._is_supported_image_ref (path )
107- and path not in sanitized
108- ):
109- sanitized .append (path )
112+ if path and cls ._is_supported_image_ref (path ):
113+ paths .append (path )
110114 except Exception as e :
111115 logger .error (
112116 "Failed to convert handoff image component at index %d: %s" ,
113117 idx ,
114118 e ,
115119 exc_info = True ,
116120 )
121+ return paths
117122
118- tool_args ["image_urls" ] = sanitized
119- return sanitized
123+ @classmethod
124+ async def _prepare_handoff_image_urls (
125+ cls ,
126+ run_context : ContextWrapper [AstrAgentContext ],
127+ image_urls : T .Any ,
128+ ) -> list [str ]:
129+ candidates = cls ._coerce_image_urls (image_urls )
130+ event_paths = await cls ._iter_event_image_paths (run_context )
131+ candidates .extend (event_paths )
132+ return cls ._filter_supported_image_urls (candidates )
120133
121134 @classmethod
122135 async def execute (cls , tool , run_context , ** tool_args ):
@@ -138,7 +151,7 @@ async def execute(cls, tool, run_context, **tool_args):
138151 ):
139152 yield r
140153 return
141- async for r in cls ._execute_handoff (tool , run_context , ** tool_args ):
154+ async for r in cls ._execute_handoff (tool , run_context , tool_args ):
142155 yield r
143156 return
144157
@@ -241,10 +254,14 @@ async def _execute_handoff(
241254 cls ,
242255 tool : HandoffTool ,
243256 run_context : ContextWrapper [AstrAgentContext ],
244- ** tool_args ,
257+ tool_args : dict [ str , T . Any ] ,
245258 ):
246259 input_ = tool_args .get ("input" )
247- image_urls = await cls ._prepare_handoff_image_urls (run_context , tool_args )
260+ image_urls = await cls ._prepare_handoff_image_urls (
261+ run_context ,
262+ tool_args .get ("image_urls" ),
263+ )
264+ tool_args ["image_urls" ] = image_urls
248265
249266 # Build handoff toolset from registered tools plus runtime computer tools.
250267 toolset = cls ._build_handoff_toolset (run_context , tool .agent .tools )
@@ -345,10 +362,7 @@ async def _do_handoff_background(
345362 result_text = ""
346363 prepared_tool_args = dict (tool_args )
347364 try :
348- await cls ._prepare_handoff_image_urls (run_context , prepared_tool_args )
349- async for r in cls ._execute_handoff (
350- tool , run_context , ** prepared_tool_args
351- ):
365+ async for r in cls ._execute_handoff (tool , run_context , prepared_tool_args ):
352366 if isinstance (r , mcp .types .CallToolResult ):
353367 for content in r .content :
354368 if isinstance (content , mcp .types .TextContent ):
0 commit comments