From 3deda4a899cb57740e7517288f421c2108e27142 Mon Sep 17 00:00:00 2001 From: easonysliu Date: Fri, 13 Mar 2026 21:39:03 +0800 Subject: [PATCH 1/2] fix: handle all content items in multi-content tool results When a tool returns a CallToolResult with multiple content items, only content[0] was being processed. This iterates over all items so text, images, and embedded resources are all properly forwarded to the LLM. Co-Authored-By: Claude (claude-opus-4-6) --- .../agent/runners/tool_loop_agent_runner.py | 92 ++++++++++--------- 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 743b280070..8050ad6b0f 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -758,52 +758,29 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: if isinstance(resp, CallToolResult): res = resp _final_resp = resp - if isinstance(res.content[0], TextContent): + if not res.content: _append_tool_call_result( func_tool_id, - res.content[0].text, + "", ) - elif isinstance(res.content[0], ImageContent): - # Cache the image instead of sending directly - cached_img = tool_image_cache.save_image( - base64_data=res.content[0].data, - tool_call_id=func_tool_id, - tool_name=func_tool_name, - index=0, - mime_type=res.content[0].mimeType or "image/png", - ) - _append_tool_call_result( - func_tool_id, - ( - f"Image returned and cached at path='{cached_img.file_path}'. " - f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." - ), - ) - # Yield image info for LLM visibility (will be handled in step()) - yield _HandleFunctionToolsResult.from_cached_image( - cached_img - ) - elif isinstance(res.content[0], EmbeddedResource): - resource = res.content[0].resource - if isinstance(resource, TextResourceContents): + continue + image_index = 0 + for item in res.content: + if isinstance(item, TextContent): _append_tool_call_result( func_tool_id, - resource.text, + item.text, ) - elif ( - isinstance(resource, BlobResourceContents) - and resource.mimeType - and resource.mimeType.startswith("image/") - ): + elif isinstance(item, ImageContent): # Cache the image instead of sending directly cached_img = tool_image_cache.save_image( - base64_data=resource.blob, + base64_data=item.data, tool_call_id=func_tool_id, tool_name=func_tool_name, - index=0, - mime_type=resource.mimeType, + index=image_index, + mime_type=item.mimeType or "image/png", ) + image_index += 1 _append_tool_call_result( func_tool_id, ( @@ -812,15 +789,48 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: f"with type='image' and path='{cached_img.file_path}'." ), ) - # Yield image info for LLM visibility + # Yield image info for LLM visibility (will be handled in step()) yield _HandleFunctionToolsResult.from_cached_image( cached_img ) - else: - _append_tool_call_result( - func_tool_id, - "The tool has returned a data type that is not supported.", - ) + elif isinstance(item, EmbeddedResource): + resource = item.resource + if isinstance(resource, TextResourceContents): + _append_tool_call_result( + func_tool_id, + resource.text, + ) + elif ( + isinstance(resource, BlobResourceContents) + and resource.mimeType + and resource.mimeType.startswith("image/") + ): + # Cache the image instead of sending directly + cached_img = tool_image_cache.save_image( + base64_data=resource.blob, + tool_call_id=func_tool_id, + tool_name=func_tool_name, + index=image_index, + mime_type=resource.mimeType, + ) + image_index += 1 + _append_tool_call_result( + func_tool_id, + ( + f"Image returned and cached at path='{cached_img.file_path}'. " + f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " + f"with type='image' and path='{cached_img.file_path}'." + ), + ) + # Yield image info for LLM visibility + yield _HandleFunctionToolsResult.from_cached_image( + cached_img + ) + else: + _append_tool_call_result( + func_tool_id, + "The tool has returned a data type that is not supported.", + ) elif resp is None: # Tool 直接请求发送消息给用户 From c4f3a519a93f4b77ce34731ca409d336c3af82ec Mon Sep 17 00:00:00 2001 From: easonysliu Date: Fri, 13 Mar 2026 22:03:07 +0800 Subject: [PATCH 2/2] address review: handle edge cases in multi-content processing - Use "The tool returned no content." placeholder instead of empty string - Include resource type and mimeType in unsupported EmbeddedResource messages Co-Authored-By: Claude (claude-opus-4-6) --- .../agent/runners/tool_loop_agent_runner.py | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/astrbot/core/agent/runners/tool_loop_agent_runner.py b/astrbot/core/agent/runners/tool_loop_agent_runner.py index 8050ad6b0f..d55617b17d 100644 --- a/astrbot/core/agent/runners/tool_loop_agent_runner.py +++ b/astrbot/core/agent/runners/tool_loop_agent_runner.py @@ -647,6 +647,33 @@ async def step_until_done( async for resp in self.step(): yield resp + @staticmethod + def _cache_and_report_image( + base64_data: str, + func_tool_id: str, + func_tool_name: str, + image_index: int, + mime_type: str, + ) -> tuple[T.Any, str]: + """Cache an image and return (cached_img, description_text). + + This is shared by both ImageContent and EmbeddedResource (blob image) + branches so the caching/reporting logic is not duplicated. + """ + cached_img = tool_image_cache.save_image( + base64_data=base64_data, + tool_call_id=func_tool_id, + tool_name=func_tool_name, + index=image_index, + mime_type=mime_type, + ) + description = ( + f"Image returned and cached at path='{cached_img.file_path}'. " + f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " + f"with type='image' and path='{cached_img.file_path}'." + ) + return cached_img, description + async def _handle_function_tools( self, req: ProviderRequest, @@ -761,7 +788,7 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: if not res.content: _append_tool_call_result( func_tool_id, - "", + "The tool returned no content.", ) continue image_index = 0 @@ -772,24 +799,15 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: item.text, ) elif isinstance(item, ImageContent): - # Cache the image instead of sending directly - cached_img = tool_image_cache.save_image( + cached_img, description = self._cache_and_report_image( base64_data=item.data, - tool_call_id=func_tool_id, - tool_name=func_tool_name, - index=image_index, + func_tool_id=func_tool_id, + func_tool_name=func_tool_name, + image_index=image_index, mime_type=item.mimeType or "image/png", ) image_index += 1 - _append_tool_call_result( - func_tool_id, - ( - f"Image returned and cached at path='{cached_img.file_path}'. " - f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." - ), - ) - # Yield image info for LLM visibility (will be handled in step()) + _append_tool_call_result(func_tool_id, description) yield _HandleFunctionToolsResult.from_cached_image( cached_img ) @@ -805,32 +823,36 @@ def _append_tool_call_result(tool_call_id: str, content: str) -> None: and resource.mimeType and resource.mimeType.startswith("image/") ): - # Cache the image instead of sending directly - cached_img = tool_image_cache.save_image( + cached_img, description = self._cache_and_report_image( base64_data=resource.blob, - tool_call_id=func_tool_id, - tool_name=func_tool_name, - index=image_index, + func_tool_id=func_tool_id, + func_tool_name=func_tool_name, + image_index=image_index, mime_type=resource.mimeType, ) image_index += 1 - _append_tool_call_result( - func_tool_id, - ( - f"Image returned and cached at path='{cached_img.file_path}'. " - f"Review the image below. Use send_message_to_user to send it to the user if satisfied, " - f"with type='image' and path='{cached_img.file_path}'." - ), - ) - # Yield image info for LLM visibility + _append_tool_call_result(func_tool_id, description) yield _HandleFunctionToolsResult.from_cached_image( cached_img ) else: + resource_type = type(resource).__name__ + resource_mime = getattr(resource, "mimeType", None) or "unknown" _append_tool_call_result( func_tool_id, - "The tool has returned a data type that is not supported.", + f"Unsupported EmbeddedResource: type={resource_type}, mimeType={resource_mime}.", ) + else: + content_type = type(item).__name__ + logger.warning( + "Unsupported content type %s in tool result for %s", + content_type, + func_tool_name, + ) + _append_tool_call_result( + func_tool_id, + f"Unsupported content type: {content_type}.", + ) elif resp is None: # Tool 直接请求发送消息给用户