mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-07 19:20:34 +00:00
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.
81 lines
2.7 KiB
Python
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()) == "{}"
|