mcp-use 1.3.11__py3-none-any.whl → 1.3.12__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of mcp-use might be problematic. Click here for more details.

@@ -8,15 +8,12 @@ import re
8
8
  from typing import Any, NoReturn
9
9
 
10
10
  from jsonschema_pydantic import jsonschema_to_pydantic
11
- from langchain_core.tools import BaseTool, ToolException
11
+ from langchain_core.tools import BaseTool
12
12
  from mcp.types import (
13
13
  CallToolResult,
14
- EmbeddedResource,
15
- ImageContent,
16
14
  Prompt,
17
15
  ReadResourceRequestParams,
18
16
  Resource,
19
- TextContent,
20
17
  )
21
18
  from pydantic import BaseModel, Field, create_model
22
19
 
@@ -60,47 +57,6 @@ class LangChainAdapter(BaseAdapter):
60
57
  schema[key] = self.fix_schema(value) # Apply recursively
61
58
  return schema
62
59
 
63
- def _parse_mcp_tool_result(self, tool_result: CallToolResult) -> str:
64
- """Parse the content of a CallToolResult into a string.
65
-
66
- Args:
67
- tool_result: The result object from calling an MCP tool.
68
-
69
- Returns:
70
- A string representation of the tool result content.
71
-
72
- Raises:
73
- ToolException: If the tool execution failed, returned no content,
74
- or contained unexpected content types.
75
- """
76
- if tool_result.isError:
77
- raise ToolException(f"Tool execution failed: {tool_result.content}")
78
-
79
- decoded_result = ""
80
- for item in tool_result.content or []:
81
- match item.type:
82
- case "text":
83
- item: TextContent
84
- decoded_result += item.text
85
- case "image":
86
- item: ImageContent
87
- decoded_result += item.data # Assuming data is string-like or base64
88
- case "resource":
89
- resource: EmbeddedResource = item.resource
90
- if hasattr(resource, "text"):
91
- decoded_result += resource.text
92
- elif hasattr(resource, "blob"):
93
- # Assuming blob needs decoding or specific handling; adjust as needed
94
- decoded_result += (
95
- resource.blob.decode() if isinstance(resource.blob, bytes) else str(resource.blob)
96
- )
97
- else:
98
- raise ToolException(f"Unexpected resource type: {resource.type}")
99
- case _:
100
- raise ToolException(f"Unexpected content type: {item.type}")
101
-
102
- return decoded_result
103
-
104
60
  def _convert_tool(self, mcp_tool: dict[str, Any], connector: BaseConnector) -> BaseTool:
105
61
  """Convert an MCP tool to LangChain's tool format.
106
62
 
@@ -140,7 +96,7 @@ class LangChainAdapter(BaseAdapter):
140
96
  """
141
97
  raise NotImplementedError("MCP tools only support async operations")
142
98
 
143
- async def _arun(self, **kwargs: Any) -> Any:
99
+ async def _arun(self, **kwargs: Any) -> str | dict:
144
100
  """Asynchronously execute the tool with given arguments.
145
101
 
146
102
  Args:
@@ -157,8 +113,7 @@ class LangChainAdapter(BaseAdapter):
157
113
  try:
158
114
  tool_result: CallToolResult = await self.tool_connector.call_tool(self.name, kwargs)
159
115
  try:
160
- # Use the helper function to parse the result
161
- return adapter_self._parse_mcp_tool_result(tool_result)
116
+ return str(tool_result.content)
162
117
  except Exception as e:
163
118
  # Log the exception for debugging
164
119
  logger.error(f"Error parsing tool result: {e}")
@@ -213,6 +213,42 @@ class MCPAgent:
213
213
  self._initialized = True
214
214
  logger.info("✨ Agent initialization complete")
215
215
 
216
+ def _normalize_output(self, value: object) -> str:
217
+ """Normalize model outputs into a plain text string."""
218
+ try:
219
+ if isinstance(value, str):
220
+ return value
221
+
222
+ # LangChain messages may have .content which is str or list-like
223
+ content = getattr(value, "content", None)
224
+ if content is not None:
225
+ return self._normalize_output(content)
226
+
227
+ if isinstance(value, list):
228
+ parts: list[str] = []
229
+ for item in value:
230
+ if isinstance(item, dict):
231
+ if "text" in item and isinstance(item["text"], str):
232
+ parts.append(item["text"])
233
+ elif "content" in item:
234
+ parts.append(self._normalize_output(item["content"]))
235
+ else:
236
+ # Fallback to str for unknown shapes
237
+ parts.append(str(item))
238
+ else:
239
+ # recurse on .content or str
240
+ part_content = getattr(item, "text", None)
241
+ if isinstance(part_content, str):
242
+ parts.append(part_content)
243
+ else:
244
+ parts.append(self._normalize_output(getattr(item, "content", item)))
245
+ return "".join(parts)
246
+
247
+ return str(value)
248
+
249
+ except Exception:
250
+ return str(value)
251
+
216
252
  async def _create_system_message_from_tools(self, tools: list[BaseTool]) -> None:
217
253
  """Create the system message based on provided tools using the builder."""
218
254
  # Use the override if provided, otherwise use the imported default
@@ -232,9 +268,12 @@ class MCPAgent:
232
268
  )
233
269
 
234
270
  # Update conversation history if memory is enabled
271
+ # Note: The system message should not be included in the conversation history,
272
+ # as it will be automatically added using the create_tool_calling_agent function with the prompt parameter
235
273
  if self.memory_enabled:
236
- history_without_system = [msg for msg in self._conversation_history if not isinstance(msg, SystemMessage)]
237
- self._conversation_history = [self._system_message] + history_without_system
274
+ self._conversation_history = [
275
+ msg for msg in self._conversation_history if not isinstance(msg, SystemMessage)
276
+ ]
238
277
 
239
278
  def _create_agent(self) -> AgentExecutor:
240
279
  """Create the LangChain agent with the configured system message.
@@ -248,14 +287,25 @@ class MCPAgent:
248
287
  if self._system_message:
249
288
  system_content = self._system_message.content
250
289
 
251
- prompt = ChatPromptTemplate.from_messages(
252
- [
253
- ("system", system_content),
254
- MessagesPlaceholder(variable_name="chat_history"),
255
- ("human", "{input}"),
256
- MessagesPlaceholder(variable_name="agent_scratchpad"),
257
- ]
258
- )
290
+ if self.memory_enabled:
291
+ # Query already in chat_history — don't re-inject it
292
+ prompt = ChatPromptTemplate.from_messages(
293
+ [
294
+ ("system", system_content),
295
+ MessagesPlaceholder(variable_name="chat_history"),
296
+ ("human", "{input}"),
297
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
298
+ ]
299
+ )
300
+ else:
301
+ # No memory — inject input directly
302
+ prompt = ChatPromptTemplate.from_messages(
303
+ [
304
+ ("system", system_content),
305
+ ("human", "{input}"),
306
+ MessagesPlaceholder(variable_name="agent_scratchpad"),
307
+ ]
308
+ )
259
309
 
260
310
  tool_names = [tool.name for tool in self._tools]
261
311
  logger.info(f"🧠 Agent ready with tools: {', '.join(tool_names)}")
@@ -286,10 +336,6 @@ class MCPAgent:
286
336
  """Clear the conversation history."""
287
337
  self._conversation_history = []
288
338
 
289
- # Re-add the system message if it exists
290
- if self._system_message and self.memory_enabled:
291
- self._conversation_history = [self._system_message]
292
-
293
339
  def add_to_history(self, message: BaseMessage) -> None:
294
340
  """Add a message to the conversation history.
295
341
 
@@ -315,15 +361,6 @@ class MCPAgent:
315
361
  """
316
362
  self._system_message = SystemMessage(content=message)
317
363
 
318
- # Update conversation history if memory is enabled
319
- if self.memory_enabled:
320
- # Remove old system message if it exists
321
- history_without_system = [msg for msg in self._conversation_history if not isinstance(msg, SystemMessage)]
322
- self._conversation_history = history_without_system
323
-
324
- # Add new system message
325
- self._conversation_history.insert(0, self._system_message)
326
-
327
364
  # Recreate the agent with the new system message if initialized
328
365
  if self._initialized and self._tools:
329
366
  self._agent_executor = self._create_agent()
@@ -467,10 +504,6 @@ class MCPAgent:
467
504
  display_query = query[:50].replace("\n", " ") + "..." if len(query) > 50 else query.replace("\n", " ")
468
505
  logger.info(f"💬 Received query: '{display_query}'")
469
506
 
470
- # Add the user query to conversation history if memory is enabled
471
- if self.memory_enabled:
472
- self.add_to_history(HumanMessage(content=query))
473
-
474
507
  # Use the provided history or the internal history
475
508
  history_to_use = external_history if external_history is not None else self._conversation_history
476
509
 
@@ -583,7 +616,8 @@ class MCPAgent:
583
616
  if isinstance(next_step_output, AgentFinish):
584
617
  logger.info(f"✅ Agent finished at step {step_num + 1}")
585
618
  agent_finished_successfully = True
586
- result = next_step_output.return_values.get("output", "No output generated")
619
+ output_value = next_step_output.return_values.get("output", "No output generated")
620
+ result = self._normalize_output(output_value)
587
621
  # End the chain if we have a run manager
588
622
  if run_manager:
589
623
  await run_manager.on_chain_end({"output": result})
@@ -666,6 +700,7 @@ class MCPAgent:
666
700
  logger.info(f"🏆 Tool returned directly at step {step_num + 1}")
667
701
  agent_finished_successfully = True
668
702
  result = tool_return.return_values.get("output", "No output generated")
703
+ result = self._normalize_output(result)
669
704
  break
670
705
 
671
706
  except OutputParserException as e:
@@ -719,8 +754,11 @@ class MCPAgent:
719
754
  logger.error(f"❌ Final structured output attempt failed: {e}")
720
755
  raise RuntimeError(f"Failed to generate structured output after {steps} steps: {str(e)}") from e
721
756
 
757
+ if self.memory_enabled:
758
+ self.add_to_history(HumanMessage(content=query))
759
+
722
760
  if self.memory_enabled and not output_schema:
723
- self.add_to_history(AIMessage(content=result))
761
+ self.add_to_history(AIMessage(content=self._normalize_output(result)))
724
762
 
725
763
  logger.info(f"🎉 Agent execution complete in {time.time() - start_time} seconds")
726
764
  if not success:
@@ -873,7 +911,7 @@ class MCPAgent:
873
911
  steps_taken=steps_taken,
874
912
  tools_used_count=len(self.tools_used_names),
875
913
  tools_used_names=self.tools_used_names,
876
- response=str(result),
914
+ response=str(self._normalize_output(result)),
877
915
  execution_time_ms=int((time.time() - start_time) * 1000),
878
916
  error_type=error,
879
917
  conversation_history_length=len(self._conversation_history),
@@ -976,9 +1014,6 @@ class MCPAgent:
976
1014
  effective_max_steps = max_steps or self.max_steps
977
1015
  self._agent_executor.max_iterations = effective_max_steps
978
1016
 
979
- if self.memory_enabled:
980
- self.add_to_history(HumanMessage(content=query))
981
-
982
1017
  history_to_use = external_history if external_history is not None else self._conversation_history
983
1018
  inputs = {"input": query, "chat_history": history_to_use}
984
1019
 
@@ -991,6 +1026,10 @@ class MCPAgent:
991
1026
  if not isinstance(message, ToolAgentAction):
992
1027
  self.add_to_history(message)
993
1028
  yield event
1029
+
1030
+ if self.memory_enabled:
1031
+ self.add_to_history(HumanMessage(content=query))
1032
+
994
1033
  # 5. House-keeping -------------------------------------------------------
995
1034
  # Restrict agent cleanup in _generate_response_chunks_async to only occur
996
1035
  # when the agent was initialized in this generator and is not client-managed
mcp_use/agents/remote.py CHANGED
@@ -4,6 +4,7 @@ Remote agent implementation for executing agents via API.
4
4
 
5
5
  import json
6
6
  import os
7
+ from collections.abc import AsyncGenerator
7
8
  from typing import Any, TypeVar
8
9
  from uuid import UUID
9
10
 
@@ -17,7 +18,7 @@ T = TypeVar("T", bound=BaseModel)
17
18
 
18
19
  # API endpoint constants
19
20
  API_CHATS_ENDPOINT = "/api/v1/chats/get-or-create"
20
- API_CHAT_EXECUTE_ENDPOINT = "/api/v1/chats/{chat_id}/execute"
21
+ API_CHAT_STREAM_ENDPOINT = "/api/v1/chats/{chat_id}/stream"
21
22
  API_CHAT_DELETE_ENDPOINT = "/api/v1/chats/{chat_id}"
22
23
 
23
24
  UUID_ERROR_MESSAGE = """A UUID is a 36 character string of the format xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \n
@@ -129,12 +130,25 @@ class RemoteAgent:
129
130
 
130
131
  # Parse into the Pydantic model
131
132
  try:
133
+ logger.info(f"🔍 Attempting to validate result_data against {output_schema.__name__}")
134
+ logger.info(f"🔍 Result data type: {type(result_data)}")
135
+ logger.info(f"🔍 Result data: {result_data}")
132
136
  return output_schema.model_validate(result_data)
133
137
  except Exception as e:
134
- logger.warning(f"Failed to parse structured output: {e}")
138
+ logger.warning(f"Failed to parse structured output: {e}")
139
+ logger.warning(f"🔍 Validation error details: {type(e).__name__}: {str(e)}")
140
+ logger.warning(f"🔍 Result data that failed validation: {result_data}")
141
+
135
142
  # Fallback: try to parse it as raw content if the model has a content field
136
143
  if hasattr(output_schema, "model_fields") and "content" in output_schema.model_fields:
137
- return output_schema.model_validate({"content": str(result_data)})
144
+ logger.info("🔄 Attempting fallback with content field")
145
+ try:
146
+ fallback_result = output_schema.model_validate({"content": str(result_data)})
147
+ logger.info("✅ Fallback parsing succeeded")
148
+ return fallback_result
149
+ except Exception as fallback_e:
150
+ logger.error(f"❌ Fallback parsing also failed: {fallback_e}")
151
+ raise
138
152
  raise
139
153
 
140
154
  async def _upsert_chat_session(self) -> str:
@@ -153,7 +167,7 @@ class RemoteAgent:
153
167
  headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
154
168
  chat_url = f"{self.base_url}{API_CHATS_ENDPOINT}"
155
169
 
156
- logger.info(f"📝 Upserting chat session for agent {self.agent_id}")
170
+ logger.info(f"📝 [{self.chat_id}] Upserting chat session for agent {self.agent_id}")
157
171
 
158
172
  try:
159
173
  chat_response = await self._client.post(chat_url, json=chat_payload, headers=headers)
@@ -162,9 +176,9 @@ class RemoteAgent:
162
176
  chat_data = chat_response.json()
163
177
  chat_id = chat_data["id"]
164
178
  if chat_response.status_code == 201:
165
- logger.info(f"✅ New chat session created: {chat_id}")
179
+ logger.info(f"✅ [{self.chat_id}] New chat session created")
166
180
  else:
167
- logger.info(f"✅ Resumed chat session: {chat_id}")
181
+ logger.info(f"✅ [{self.chat_id}] Resumed chat session")
168
182
 
169
183
  return chat_id
170
184
 
@@ -182,144 +196,156 @@ class RemoteAgent:
182
196
  except Exception as e:
183
197
  raise RuntimeError(f"Failed to create chat session: {str(e)}") from e
184
198
 
185
- async def run(
199
+ async def stream(
186
200
  self,
187
201
  query: str,
188
202
  max_steps: int | None = None,
189
203
  external_history: list[BaseMessage] | None = None,
190
204
  output_schema: type[T] | None = None,
191
- ) -> str | T:
192
- """Run a query on the remote agent.
193
-
194
- Args:
195
- query: The query to execute
196
- max_steps: Maximum number of steps (default: 10)
197
- external_history: External history (not supported yet for remote execution)
198
- output_schema: Optional Pydantic model for structured output
199
-
200
- Returns:
201
- The result from the remote agent execution (string or structured output)
202
- """
205
+ ) -> AsyncGenerator[str, None]:
206
+ """Stream the execution of a query on the remote agent using HTTP streaming."""
203
207
  if external_history is not None:
204
208
  logger.warning("External history is not yet supported for remote execution")
205
209
 
206
- try:
207
- logger.info(f"🌐 Executing query on remote agent {self.agent_id}")
208
-
209
- # Step 1: Ensure chat session exists on the backend by upserting.
210
- # This happens once per agent instance.
211
- if not self._session_established:
212
- logger.info(f"🔧 Establishing chat session for agent {self.agent_id}")
213
- self.chat_id = await self._upsert_chat_session()
214
- self._session_established = True
215
-
216
- chat_id = self.chat_id
217
-
218
- # Step 2: Execute the agent within the chat context
219
- execution_payload = {"query": query, "max_steps": max_steps or 10}
220
-
221
- # Add structured output schema if provided
222
- if output_schema is not None:
223
- execution_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
224
- logger.info(f"🔧 Using structured output with schema: {output_schema.__name__}")
225
-
226
- headers = {"Content-Type": "application/json", "x-api-key": self.api_key}
227
- execution_url = f"{self.base_url}{API_CHAT_EXECUTE_ENDPOINT.format(chat_id=chat_id)}"
228
- logger.info(f"🚀 Executing agent in chat {chat_id}")
229
-
230
- response = await self._client.post(execution_url, json=execution_payload, headers=headers)
231
- response.raise_for_status()
232
-
233
- result = response.json()
234
- logger.info(f"🔧 Response: {result}")
235
- logger.info("✅ Remote execution completed successfully")
236
-
237
- # Check for error responses (even with 200 status)
238
- if isinstance(result, dict):
239
- # Check for actual error conditions (not just presence of error field)
240
- if result.get("status") == "error" or (result.get("error") is not None):
241
- error_msg = result.get("error", str(result))
242
- logger.error(f"❌ Remote agent execution failed: {error_msg}")
243
- raise RuntimeError(f"Remote agent execution failed: {error_msg}")
244
-
245
- # Check if the response indicates agent initialization failure
246
- if "failed to initialize" in str(result):
247
- logger.error(f"❌ Agent initialization failed: {result}")
248
- raise RuntimeError(
249
- f"Agent initialization failed on remote server. "
250
- f"This usually indicates:\n"
251
- f"• Invalid agent configuration (LLM model, system prompt)\n"
252
- f"• Missing or invalid MCP server configurations\n"
253
- f"• Network connectivity issues with MCP servers\n"
254
- f"• Missing environment variables or credentials\n"
255
- f"Raw error: {result}"
256
- )
257
-
258
- # Handle structured output
259
- if output_schema is not None:
260
- return self._parse_structured_response(result, output_schema)
210
+ if not self._session_established:
211
+ logger.info(f"🔧 [{self.chat_id}] Establishing chat session for agent {self.agent_id}")
212
+ self.chat_id = await self._upsert_chat_session()
213
+ self._session_established = True
261
214
 
262
- # Regular string output
263
- if isinstance(result, dict) and "result" in result:
264
- return result["result"]
265
- elif isinstance(result, str):
266
- return result
267
- else:
268
- return str(result)
215
+ chat_id = self.chat_id
216
+ stream_url = f"{self.base_url}{API_CHAT_STREAM_ENDPOINT.format(chat_id=chat_id)}"
217
+
218
+ # Prepare the request payload
219
+ request_payload = {"messages": [{"role": "user", "content": query}], "max_steps": max_steps or 30}
220
+ if output_schema is not None:
221
+ request_payload["output_schema"] = self._pydantic_to_json_schema(output_schema)
222
+
223
+ headers = {"Content-Type": "application/json", "x-api-key": self.api_key, "Accept": "text/event-stream"}
224
+
225
+ try:
226
+ logger.info(f"🌐 [{self.chat_id}] Connecting to HTTP stream for agent {self.agent_id}")
227
+
228
+ async with self._client.stream("POST", stream_url, headers=headers, json=request_payload) as response:
229
+ logger.info(f"✅ [{self.chat_id}] HTTP stream connection established")
230
+
231
+ if response.status_code != 200:
232
+ error_text = await response.aread()
233
+ raise RuntimeError(f"Failed to stream from remote agent: {error_text.decode()}")
234
+
235
+ # Read the streaming response line by line
236
+ try:
237
+ async for line in response.aiter_lines():
238
+ if line:
239
+ yield line
240
+ except UnicodeDecodeError as e:
241
+ logger.error(f"❌ [{self.chat_id}] UTF-8 decoding error at position {e.start}: {e.reason}")
242
+ logger.error(f"❌ [{self.chat_id}] Error occurred while reading stream for agent {self.agent_id}")
243
+ # Try to read raw bytes and decode with error handling
244
+ logger.info(f"🔄 [{self.chat_id}] Attempting to read raw bytes with error handling...")
245
+ logger.info(f"✅ [{self.chat_id}] Agent execution stream completed")
269
246
 
270
247
  except httpx.HTTPStatusError as e:
271
248
  status_code = e.response.status_code
272
249
  response_text = e.response.text
273
250
 
274
- # Provide specific error messages based on status code
275
- if status_code == 401:
276
- logger.error(f"❌ Authentication failed: {response_text}")
277
- raise RuntimeError(
278
- "Authentication failed: Invalid or missing API key. "
279
- "Please check your API key and ensure the MCP_USE_API_KEY environment variable is set correctly."
280
- ) from e
281
- elif status_code == 403:
282
- logger.error(f"❌ Access forbidden: {response_text}")
283
- raise RuntimeError(
284
- f"Access denied: You don't have permission to execute agent '{self.agent_id}'. "
285
- "Check if the agent exists and you have the necessary permissions."
286
- ) from e
287
- elif status_code == 404:
288
- logger.error(f"❌ Agent not found: {response_text}")
289
- raise RuntimeError(
290
- f"Agent not found: Agent '{self.agent_id}' does not exist or you don't have access to it. "
291
- "Please verify the agent ID and ensure it exists in your account."
292
- ) from e
293
- elif status_code == 422:
294
- logger.error(f"❌ Validation error: {response_text}")
295
- raise RuntimeError(
296
- f"Request validation failed: {response_text}. "
297
- "Please check your query parameters and output schema format."
298
- ) from e
299
- elif status_code == 500:
300
- logger.error(f"❌ Server error: {response_text}")
301
- raise RuntimeError(
302
- "Internal server error occurred during agent execution. "
303
- "Please try again later or contact support if the issue persists."
304
- ) from e
251
+ if status_code == 404:
252
+ raise RuntimeError(f"Chat or agent not found: {response_text}") from e
305
253
  else:
306
- logger.error(f" Remote execution failed with status {status_code}: {response_text}")
307
- raise RuntimeError(f"Remote agent execution failed: {status_code} - {response_text}") from e
308
- except httpx.TimeoutException as e:
309
- logger.error(f" Remote execution timed out: {e}")
310
- raise RuntimeError(
311
- "Remote agent execution timed out. The server may be overloaded or the query is taking too long to "
312
- "process. Try again or use a simpler query."
313
- ) from e
314
- except httpx.ConnectError as e:
315
- logger.error(f"❌ Remote execution connection error: {e}")
316
- raise RuntimeError(
317
- f"Remote agent connection failed: Cannot connect to {self.base_url}. "
318
- f"Check if the server is running and the URL is correct."
319
- ) from e
254
+ raise RuntimeError(f"Failed to stream from remote agent: {status_code} - {response_text}") from e
255
+ except Exception as e:
256
+ logger.error(f"❌ [{self.chat_id}] An error occurred during HTTP streaming: {e}")
257
+ raise RuntimeError(f"Failed to stream from remote agent: {str(e)}") from e
258
+
259
+ async def run(
260
+ self,
261
+ query: str,
262
+ max_steps: int | None = None,
263
+ external_history: list[BaseMessage] | None = None,
264
+ output_schema: type[T] | None = None,
265
+ ) -> str | T:
266
+ """
267
+ Executes the agent and returns the final result.
268
+ This method uses HTTP streaming to avoid timeouts for long-running tasks.
269
+ It consumes the entire stream and returns only the final result.
270
+ """
271
+ final_result = None
272
+ steps_taken = 0
273
+ finished = False
274
+
275
+ try:
276
+ # Consume the ENTIRE stream to ensure proper execution
277
+ async for event in self.stream(query, max_steps, external_history, output_schema):
278
+ logger.debug(f"[{self.chat_id}] Processing stream event: {event}...")
279
+
280
+ # Parse AI SDK format events to extract final result
281
+ # The events follow the AI SDK streaming protocol
282
+ if event.startswith("0:"): # Text event
283
+ try:
284
+ text_data = json.loads(event[2:]) # Remove "0:" prefix
285
+ if final_result is None:
286
+ final_result = ""
287
+ final_result += text_data
288
+ result_preview = final_result[:200] if len(final_result) > 200 else final_result
289
+ logger.debug(f"Accumulated text result: {result_preview}...")
290
+ except json.JSONDecodeError:
291
+ logger.warning(f"Failed to parse text event: {event[:100]}")
292
+ continue
293
+
294
+ elif event.startswith("9:"): # Tool call event
295
+ steps_taken += 1
296
+ logger.debug(f"Tool call executed, total steps: {steps_taken}")
297
+
298
+ elif event.startswith("d:"): # Finish event
299
+ logger.debug("Received finish event, marking as finished")
300
+ finished = True
301
+ # Continue consuming to ensure stream cleanup
302
+
303
+ elif event.startswith("3:"): # Error event
304
+ try:
305
+ error_data = json.loads(event[2:])
306
+ error_msg = error_data if isinstance(error_data, str) else json.dumps(error_data)
307
+ raise RuntimeError(f"Agent execution failed: {error_msg}")
308
+ except json.JSONDecodeError as e:
309
+ raise RuntimeError("Agent execution failed with unknown error") from e
310
+
311
+ # Log completion of stream consumption
312
+ logger.info(f"Stream consumption complete. Finished: {finished}, Steps taken: {steps_taken}")
313
+
314
+ if final_result is None:
315
+ logger.warning(f"No final result captured from stream (structured output: {output_schema is not None})")
316
+ final_result = "" # Return empty string instead of error message
317
+
318
+ # For structured output, try to parse the result
319
+ if output_schema:
320
+ logger.info(f"🔍 Attempting structured output parsing for schema: {output_schema.__name__}")
321
+ logger.info(f"🔍 Raw final result type: {type(final_result)}")
322
+ logger.info(f"🔍 Raw final result length: {len(str(final_result)) if final_result else 0}")
323
+ logger.info(f"🔍 Raw final result preview: {str(final_result)[:500] if final_result else 'None'}...")
324
+
325
+ if isinstance(final_result, str) and final_result:
326
+ try:
327
+ # Try to parse as JSON first
328
+ parsed_result = json.loads(final_result)
329
+ logger.info("✅ Successfully parsed structured result as JSON")
330
+ return self._parse_structured_response(parsed_result, output_schema)
331
+ except json.JSONDecodeError as e:
332
+ logger.warning(f"❌ Could not parse result as JSON: {e}")
333
+ logger.warning(f"🔍 Raw string content: {final_result[:1000]}...")
334
+ # Try to parse directly
335
+ return self._parse_structured_response({"content": final_result}, output_schema)
336
+ else:
337
+ logger.warning(f"❌ Final result is empty or not string: {final_result}")
338
+ # Try to parse the result directly
339
+ return self._parse_structured_response(final_result, output_schema)
340
+
341
+ # Regular string output
342
+ return final_result if isinstance(final_result, str) else str(final_result)
343
+
344
+ except RuntimeError:
345
+ raise
320
346
  except Exception as e:
321
- logger.error(f" Remote execution error: {e}")
322
- raise RuntimeError(f"Remote agent execution failed: {str(e)}") from e
347
+ logger.error(f"Error executing agent: {e}")
348
+ raise RuntimeError(f"Failed to execute agent: {str(e)}") from e
323
349
 
324
350
  async def close(self) -> None:
325
351
  """Close the HTTP client."""