deer-flow/backend/tests/test_tool_call_json_depth.py
一片一片 a0ce43d46a
fix(agents): tolerate deeply nested JSON in tool-call normalization (#7742)
Loop detection, clarification and dangling-tool-call repair parse model-provided
tool arguments and options. A JSON string, Python structure or native option
label nested past the interpreter recursion limit raised RecursionError and
aborted the run; these paths now treat it like any other unusable payload
(opaque args, an empty replay object, or a skipped option), and the dict-option
flatten and _stable_tool_key use guarded serialization.

Rebased onto current main; the stale-base docs commit is superseded by main and
the AGENTS note is trimmed to stay inside the guidance chain budget.
2026-11-01 23:32:00 +08:00

81 lines
2.7 KiB
Python

"""Tool-call argument normalization must tolerate deeply nested JSON.
A model (or a replayed history) can hand the middlewares a JSON string or a
Python structure nested beyond the interpreter's recursion limit. These
payloads are unusable for loop detection or replay repair, but they must not
abort the graph with ``RecursionError``; the parsers fall back to the same
"opaque" path they use for any unparsable input.
"""
from deerflow.agents.middlewares.clarification_middleware import ClarificationMiddleware
from deerflow.agents.middlewares.dangling_tool_call_middleware import (
_normalize_tool_arguments,
_parse_json_object,
)
from deerflow.agents.middlewares.loop_detection_middleware import (
_hash_tool_calls,
_normalize_tool_call_args,
)
# Above the JSON C parser's recursion limit on CPython 3.12 for the platforms this
# repository builds on (Linux/macOS 64-bit allow roughly 8,000 levels).
DEEP_DEPTH = 20_000
DEEP_ARRAY = "[" * DEEP_DEPTH + "]" * DEEP_DEPTH
def _deep_dict(depth: int = DEEP_DEPTH) -> dict:
root: dict = {}
node = root
for _ in range(depth):
child: dict = {}
node["x"] = child
node = child
return root
def _deep_list(depth: int = DEEP_DEPTH) -> list:
root: list = []
node = root
for _ in range(depth):
child: list = []
node.append(child)
node = child
return root
def test_loop_detection_keeps_a_deep_json_string_as_opaque_args():
assert _normalize_tool_call_args(DEEP_ARRAY) == ({}, DEEP_ARRAY)
def test_loop_detection_keeps_deep_structures_as_opaque_args():
assert _normalize_tool_call_args(_deep_list()) == ({}, None)
def test_loop_detection_hash_keeps_deep_dict_args_serializable():
assert _hash_tool_calls([{"name": "bash", "args": _deep_dict()}]) is not None
def test_clarification_options_keep_a_deep_json_string_as_one_option():
assert ClarificationMiddleware._normalize_options(None, DEEP_ARRAY) == [DEEP_ARRAY]
def test_clarification_native_deep_options_are_skipped():
assert ClarificationMiddleware._normalize_options(None, [_deep_list()]) == []
def test_clarification_dict_options_flatten_without_recursion():
assert ClarificationMiddleware._flatten_dict_option_values(_deep_dict()) == []
def test_loop_detection_read_file_key_tolerates_a_deep_path():
assert _hash_tool_calls([{"name": "read_file", "args": {"path": _deep_list()}}]) is not None
def test_dangling_parser_rejects_a_deep_json_string():
assert _parse_json_object(DEEP_ARRAY) is None
def test_dangling_arguments_repair_falls_back_to_empty_object():
assert _normalize_tool_arguments(DEEP_ARRAY) == "{}"
assert _normalize_tool_arguments(_deep_dict()) == "{}"