99from agenta .sdk .utils .logging import get_module_logger
1010
1111from ...dtos import AgentResult
12+ from ...errors import AgentRunFailed
1213from ...streaming import AgentStream
1314from ...utils .wire import sanitize_runner_error
1415from .messages import TOOL_APPROVAL_REQUEST
@@ -304,11 +305,17 @@ async def _agent_run_to_vercel_parts_impl(
304305 if _conform (file_part ) is not None :
305306 content_parts_emitted += 1
306307 yield file_part
308+ elif etype == "attachment_delivery" :
309+ content_parts_emitted += 1
310+ yield _attachment_delivery_part (data )
307311 elif etype == "usage" :
308312 usage = _usage_metadata (data )
309313 elif etype == "error" :
310314 error_emitted = True
311- yield {"type" : "error" , "errorText" : data .get ("message" , "" )}
315+ for part in _error_parts (
316+ data .get ("message" , "" ), failure_code = "runner_error"
317+ ):
318+ yield part
312319 elif etype == "done" :
313320 # Last non-null stop reason wins; see the routing-layer twin's `done` note.
314321 reason = data .get ("stopReason" )
@@ -324,7 +331,8 @@ async def _agent_run_to_vercel_parts_impl(
324331 # exception is very often just that same failure resurfacing as a raised
325332 # `RuntimeError` (`result_from_wire`) -- yielding it too would duplicate the message
326333 # the user already saw under a second, "Agent run failed: ..."-prefixed frame.
327- yield {"type" : "error" , "errorText" : sanitize_runner_error (exc )}
334+ for part in _error_parts (sanitize_runner_error (exc ), error = exc ):
335+ yield part
328336 error_emitted = True
329337 finally :
330338 # Every exit path — including the raw exception above — must still drain to a
@@ -349,7 +357,10 @@ async def _agent_run_to_vercel_parts_impl(
349357 # "no output" frame on top of it would bury the actionable message (the swallowed-
350358 # provider-error path both streams a live error event AND fails the terminal result,
351359 # so this backstop must not double up on it).
352- yield {"type" : "error" , "errorText" : "The agent produced no output." }
360+ for part in _error_parts (
361+ "The agent produced no output." , failure_code = "no_output"
362+ ):
363+ yield part
353364 finish : Dict [str , Any ] = {"type" : "finish" }
354365 finish_reason = _map_finish_reason (stop_reason )
355366 if finish_reason is not None :
@@ -584,11 +595,17 @@ async def _agent_stream_to_vercel_stream_impl(
584595 if _conform (file_part ) is not None :
585596 content_parts_emitted += 1
586597 yield file_part
598+ elif etype == "attachment_delivery" :
599+ content_parts_emitted += 1
600+ yield _attachment_delivery_part (data )
587601 elif etype == "usage" :
588602 usage = _usage_metadata (data )
589603 elif etype == "error" :
590604 error_emitted = True
591- yield {"type" : "error" , "errorText" : data .get ("message" , "" )}
605+ for part in _error_parts (
606+ data .get ("message" , "" ), failure_code = "runner_error"
607+ ):
608+ yield part
592609 elif etype == "done" :
593610 # Prefer the LAST non-null stop reason. The handler appends a corrective
594611 # terminal `done` after the runner's `done` when the authoritative result
@@ -605,7 +622,8 @@ async def _agent_stream_to_vercel_stream_impl(
605622 # out live this turn, so a swallowed-provider-error recovery (live error event, then
606623 # a failed terminal result raised as this same exception) doesn't duplicate the
607624 # user-facing message under a second, differently-worded frame.
608- yield {"type" : "error" , "errorText" : sanitize_runner_error (exc )}
625+ for part in _error_parts (sanitize_runner_error (exc ), error = exc ):
626+ yield part
609627 error_emitted = True
610628 finally :
611629 # Every exit path — including the raw exception above — must still drain to a
@@ -617,7 +635,10 @@ async def _agent_stream_to_vercel_stream_impl(
617635 # note) -- a swallowed-provider-error turn both streams a live error event and fails
618636 # the terminal result, so this backstop must not double up on it and bury the real
619637 # message under "The agent produced no output."
620- yield {"type" : "error" , "errorText" : "The agent produced no output." }
638+ for part in _error_parts (
639+ "The agent produced no output." , failure_code = "no_output"
640+ ):
641+ yield part
621642 finish : Dict [str , Any ] = {"type" : "finish" }
622643 finish_reason = _map_finish_reason (stop_reason )
623644 if finish_reason is not None :
@@ -850,6 +871,32 @@ def _as_text(value: Any) -> str:
850871 return value if isinstance (value , str ) else str (value )
851872
852873
874+ def _attachment_delivery_part (data : Dict [str , Any ]) -> Dict [str , Any ]:
875+ delivery = {
876+ key : data [key ]
877+ for key in ("attachmentId" , "outcome" , "reasonCode" , "workingPath" )
878+ if data .get (key ) is not None
879+ }
880+ return {"type" : "data-attachment-delivery" , "data" : delivery }
881+
882+
883+ def _error_parts (
884+ error_text : Any ,
885+ * ,
886+ failure_code : Optional [str ] = None ,
887+ error : Optional [BaseException ] = None ,
888+ ) -> Iterator [Dict [str , Any ]]:
889+ resolved_code = failure_code or getattr (error , "failure_code" , None )
890+ if not isinstance (resolved_code , str ) or not resolved_code :
891+ resolved_code = AgentRunFailed .failure_code
892+ resolved_text = _as_text (error_text )
893+ yield {
894+ "type" : "data-agent-error" ,
895+ "data" : {"code" : resolved_code , "errorText" : resolved_text },
896+ }
897+ yield {"type" : "error" , "errorText" : resolved_text }
898+
899+
853900def _safe_result (run : AgentStream ) -> Optional [AgentResult ]:
854901 try :
855902 return run .result ()
0 commit comments