diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py b/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py index 8db556bbd2..f15c27565e 100644 --- a/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py +++ b/autogpt_platform/backend/backend/api/features/chat/sdk/response_adapter.py @@ -34,7 +34,10 @@ from backend.api.features.chat.response_model import ( StreamToolInputStart, StreamToolOutputAvailable, ) -from backend.api.features.chat.sdk.tool_adapter import MCP_TOOL_PREFIX +from backend.api.features.chat.sdk.tool_adapter import ( + MCP_TOOL_PREFIX, + pop_pending_tool_output, +) logger = logging.getLogger(__name__) @@ -114,7 +117,15 @@ class SDKResponseAdapter: if isinstance(block, ToolResultBlock) and block.tool_use_id: tool_info = self.current_tool_calls.get(block.tool_use_id, {}) tool_name = tool_info.get("name", "unknown") - output = _extract_tool_output(block.content) + + # Prefer the stashed full output over the SDK's + # (potentially truncated) ToolResultBlock content. + # The SDK truncates large results, writing them to disk, + # which breaks frontend widget parsing. + output = pop_pending_tool_output(tool_name) or ( + _extract_tool_output(block.content) + ) + responses.append( StreamToolOutputAvailable( toolCallId=block.tool_use_id, diff --git a/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py b/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py index 5c536eac1e..c34350bb94 100644 --- a/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py +++ b/autogpt_platform/backend/backend/api/features/chat/sdk/tool_adapter.py @@ -33,6 +33,13 @@ _current_tool_call_id: ContextVar[str | None] = ContextVar( "current_tool_call_id", default=None ) +# Stash for MCP tool outputs before the SDK potentially truncates them. +# Keyed by tool_name → full output string. Consumed (popped) by the +# response adapter when it builds StreamToolOutputAvailable. +_pending_tool_outputs: ContextVar[dict[str, str]] = ContextVar( + "pending_tool_outputs", default=None # type: ignore[arg-type] +) + def set_execution_context( user_id: str | None, @@ -47,6 +54,7 @@ def set_execution_context( _current_user_id.set(user_id) _current_session.set(session) _current_tool_call_id.set(tool_call_id) + _pending_tool_outputs.set({}) def get_execution_context() -> tuple[str | None, ChatSession | None, str | None]: @@ -58,6 +66,22 @@ def get_execution_context() -> tuple[str | None, ChatSession | None, str | None] ) +def pop_pending_tool_output(tool_name: str) -> str | None: + """Pop and return the stashed full output for *tool_name*. + + The SDK CLI may truncate large tool results (writing them to disk and + replacing the content with a file reference). This stash keeps the + original MCP output so the response adapter can forward it to the + frontend for proper widget rendering. + + Returns ``None`` if nothing was stashed for *tool_name*. + """ + pending = _pending_tool_outputs.get(None) + if pending is None: + return None + return pending.pop(tool_name, None) + + def create_tool_handler(base_tool: BaseTool): """Create an async handler function for a BaseTool. @@ -103,6 +127,12 @@ def create_tool_handler(base_tool: BaseTool): else json.dumps(result.output) ) + # Stash the full output before the SDK potentially truncates it. + # The response adapter will pop this for frontend widget rendering. + pending = _pending_tool_outputs.get(None) + if pending is not None: + pending[base_tool.name] = text + return { "content": [{"type": "text", "text": text}], "isError": not result.success,