fix: fix poison state from bad approval response (#5979)

* fix: detect and fail on malformed approval responses

* fix: guard against None approvals in utils.py

* fix: add extra warning

* fix: stop silent drops in deserialize_approvals

* fix: patch v3 stream error handling to prevent sending end_turn after an error occurs, and ensures stop_reason is always set when an error occurs

* fix: Prevents infinite client hangs by ensuring a terminal event is ALWAYS sent

* fix:  Ensures terminal events are sent even if inner stream generator fails to
  send them
This commit is contained in:
Charles Packer
2025-11-06 20:53:00 -08:00
committed by Caren Thomas
parent 4acda9c80f
commit 363a5c1f92
5 changed files with 189 additions and 40 deletions

View File

@@ -254,14 +254,33 @@ def deserialize_approvals(data: Optional[List[Dict]]) -> List[Union[ApprovalRetu
return []
approvals = []
for item in data:
if "type" in item and item.get("type") == MessageReturnType.approval:
approval_return = ApprovalReturn(**item)
approvals.append(approval_return)
elif "status" in item:
tool_return = ToolReturn(**item)
approvals.append(tool_return)
else:
for idx, item in enumerate(data):
try:
# Check for ApprovalReturn (has type="approval")
if "type" in item and item.get("type") == MessageReturnType.approval:
approval_return = ApprovalReturn(**item)
approvals.append(approval_return)
# Check for ToolReturn (has status field)
elif "status" in item:
# Handle field name variations (tool_return vs func_response)
if "tool_return" in item and "func_response" not in item:
# Client SDK uses "tool_return", internal uses "func_response"
item = {**item, "func_response": item["tool_return"]}
tool_return = ToolReturn(**item)
approvals.append(tool_return)
else:
# Unknown format - log warning with diagnostic info
# Truncate large fields for logging
item_preview = {k: (v[:100] + "..." if isinstance(v, str) and len(v) > 100 else v) for k, v in item.items()}
logger.warning(
f"deserialize_approvals: Skipping unrecognized approval item at index {idx}. "
f"Item preview: {item_preview}. Expected 'type=approval' or 'status' field."
)
continue
except Exception as e:
# Log validation errors but continue processing other items
item_preview = {k: (v[:100] + "..." if isinstance(v, str) and len(v) > 100 else v) for k, v in item.items()}
logger.warning(f"deserialize_approvals: Failed to deserialize approval item at index {idx}: {e}. Item preview: {item_preview}")
continue
return approvals