ag-ui-protocol / ag-ui-protocol/ag-ui
bug: make_json_safe causes RecursionError on objects with circular references
- Vorherrschende Sprache
- Python
- Sterne
- 15.9k
- Forks
- 1.4k
- Ø Merge
- 1 T. 17 Std.
- Gemergte PRs (30 T.)
- 163
Beschreibung
### Description
The `make_json_safe` function in `ag_ui_langgraph/utils.py` causes a `RecursionError: maximum recursion depth exceeded` when serializing objects that contain circular references.
This happens during streaming when RAW events are serialized, and any object in the state/config has a circular reference (e.g., `asyncio.Event` objects which internally reference the event loop, which in turn references tasks and callbacks).
### Steps to Reproduce
1. Create a LangGraph agent with `ag_ui_langgraph`
2. Include any object with circular references in the state or config (e.g., `asyncio.Event`, or any object whose `__dict__` eventually references itself)
3. Stream the agent response
### Error
```python
File "ag_ui_langgraph/utils.py", line 334, in make_json_safe
return {key: make_json_safe(sub_value) for key, sub_value in value.items()}
^^^^^^^^^^^^^^^^^^^^^^^^^
File "ag_ui_langgraph/utils.py", line 358, in make_json_safe
**make_json_safe(value.__dict__),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Previous line repeated many times]
File "ag_ui_langgraph/utils.py", line 351, in make_json_safe
if is_json_primitive(value):
^^^^^^^^^^^^^^^^^^^^^^^^
RecursionError: maximum recursion depth exceeded
```
### Root Cause
The `make_json_safe` function recursively processes objects but doesn't track already-visited objects. When an object has circular references in its `__dict__`, the function recurses infinitely.
https://github.com/ag-ui-protocol/ag-ui/blob/main/integrations/langgraph/python/ag_ui_langgraph/utils.py#L291-L361
### Proposed Fix
Add cycle detection using a `seen` set that tracks object IDs:
```python
def make_json_safe(value: Any, _seen: Optional[Set[int]] = None) -> Any:
"""Convert a value to a JSON-safe format with cycle detection."""
if _seen is None:
_seen = set()
# Check for cycles
obj_id = id(value)
if obj_id in _seen:
return f""
# Primitives don't need cycle tracking
if value is None or isinstance(value, (bool, int, float, str)):
return value
# Track this object to detect cycles
_seen = _seen | {obj_id}
# Dict
if isinstance(value, dict):
return {key: make_json_safe(sub_value, _seen) for key, sub_value in value.items()}
# List / tuple
if isinstance(value, (list, tuple)):
return [make_json_safe(sub_value, _seen) for sub_value in value]
# ... rest of the function, passing _seen to recursive calls
```
### Workaround
Currently we're monkey-patching `make_json_safe` in our server startup to add cycle detection:
```python
import ag_ui_langgraph.utils as ag_ui_utils
def _make_json_safe_with_cycle_detection(value: Any, seen: Optional[Set[int]] = None) -> Any:
"""JSON-safe serialization with cycle detection to prevent RecursionError."""
if seen is None:
seen = set()
# Check for cycles using object id
obj_id = id(value)
if obj_id in seen:
return f""
# Primitives don't need cycle tracking
if value is None or isinstance(value, (bool, int, float, str)):
return value
# Add to seen set for complex objects
seen = seen | {obj_id} # Create new set to avoid mutation issues
# Pydantic models
if isinstance(value, BaseModel):
try:
return _make_json_safe_with_cycle_detection(
value.model_dump(by_alias=True, exclude_none=True), seen
)
except Exception:
return repr(value)
# Dict
if isinstance(value, dict):
return {
key: _make_json_safe_with_cycle_detection(sub_value, seen)
for key, sub_value in value.items()
}
# List / tuple
if isinstance(value, (list, tuple)):
return [_make_json_safe_with_cycle_detection(sub_value, seen) for sub_value in value]
# Enum
if isinstance(value, Enum):
enum_value = value.value
if enum_value is None or isinstance(enum_value, (bool, int, float, str)):
return enum_value
return {
"__type__": type(value).__name__,
"name": value.name,
"value": _make_json_safe_with_cycle_detection(enum_value, seen),
}
# LangChain-style objects with to_dict
if hasattr(value, "to_dict"):
try:
return _make_json_safe_with_cycle_detection(value.to_dict(), seen)
except Exception:
pass
# Arbitrary object with __dict__
if hasattr(value, "__dict__"):
try:
return {
"__type__": type(value).__name__,
**_make_json_safe_with_cycle_detection(value.__dict__, seen),
}
except Exception:
return f"<{type(value).__name__}>"
return repr(value)
ag_ui_utils.make_json_safe = _make_json_safe_with_cycle_detection
```
### Environment
- `ag-ui-langgraph`: 0.0.21
- Python: 3.12
- LangGraph: 1.0.x
### Impact
This is a critical issue that causes the entire streaming response to fail when any circular reference exists in the serialized data. It's particularly problematic with async Python objects that internally reference the event loop.
Beitragsleitfaden
Bewertung
Dieses Issue wurde noch nicht bewertet.