mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-19 02:56:17 +00:00
fix(sandbox): report an exactly-full AIO glob result as complete (#5449)
* fix(sandbox): report an exactly-full AIO glob result as complete AioSandbox.glob's include_dirs branch returned as soon as it had collected max_results matches, without looking at the rest of the listing. A listing that held exactly that many matches and nothing more was therefore reported as truncated, and the glob tool told the model the result was incomplete — prompting a re-search or distrust of a complete answer. The same line returned one match for max_results=0, one past the caller's cap. Look one match past the cap before deciding, which is what the include_dirs=False branch in the same function already does and what #5427 moved parse_remote_search_output to for BoxLite, Tenki, E2B and OpenSandbox. * review: filtered-tail cases, the glob contract docstring, and the cap wording Addresses the three items from the review on #5449. - Two regression cases over a tail of ignored / out-of-root / pattern-miss entries: an exactly-full result stays complete when only filtered entries follow, and a third eligible match after that tail still reports truncation. Both fail against the previous return-on-the-max-th-match behaviour. - 'Sandbox.glob' promised the conservative flag ('``max_results`` was reached') that this change deliberately stops producing on the AIO branch. The contract now reads as 'may be incomplete' and records that providers differ in how precisely they can decide it. - The changelog no longer lumps 'parse_remote_search_output' in with the filtered-match cap: its raw-output cap is a separate limit with its own one-line-past accounting, and the other providers' filtered-match cap is unchanged. Also corrects the docstring on the existing test, which still described the removed early return in the present tense. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
This commit is contained in:
parent
0745fb268f
commit
6bab87aca4
10
CHANGELOG.md
10
CHANGELOG.md
@ -582,6 +582,16 @@ This section accumulates work toward the **2.1.0** milestone
|
||||
|
||||
### Fixed
|
||||
|
||||
- **sandbox:** Stop AIO's `glob` from reporting an exactly-full result as
|
||||
truncated. Its `include_dirs` branch returned as soon as it had collected
|
||||
`max_results` matches, so a listing that held exactly that many — and no more
|
||||
— came back flagged as cut off, and the tool told the model the result was
|
||||
incomplete. That branch already holds the whole listing, so it now looks one
|
||||
match past the cap before deciding, matching the sibling `include_dirs=False`
|
||||
branch, which has always decided from the full list. This concerns the
|
||||
filtered-match cap only: the raw-output cap `parse_remote_search_output` owns
|
||||
is a separate limit with its own one-line-past accounting, and the other
|
||||
providers' filtered-match cap is unchanged.
|
||||
- **middleware:** Stop a guard that removes tool calls from breaking every later
|
||||
turn of a Claude or OpenAI Responses thread. Token-budget and loop-detection
|
||||
hard stops, subagent-limit truncation, and safety suppression cleared
|
||||
|
||||
@ -397,6 +397,12 @@
|
||||
|
||||
### 修复
|
||||
|
||||
- **沙箱:** AIO 的 `glob` 不再把"恰好填满"的结果报告为截断。其 `include_dirs` 分支在收集到
|
||||
`max_results` 个匹配时就立即返回,因此一个只有这么多匹配、后面再无匹配的目录列表也会被标记为
|
||||
被截断,工具据此告诉模型结果不完整。该分支本就持有整份目录列表,现在改为多看一个匹配再判断,
|
||||
与同一函数的 `include_dirs=False` 分支一致(后者一直是按完整列表判断的)。这里涉及的只是
|
||||
**过滤后匹配数**上限;`parse_remote_search_output` 管的是**原始输出行数**上限,是另一条限制、
|
||||
有自己的"多放一行"记账方式,其他 provider 的过滤后匹配数上限未作改动。
|
||||
- **中间件:** 移除工具调用的守卫不再导致 Claude 或 OpenAI Responses 线程之后的每一轮都失败。
|
||||
token 预算与循环检测的硬停止、subagent 数量限制的截断以及安全终止抑制只清空了 `tool_calls`,
|
||||
却把 provider 自身的工具调用块留在消息 content 中。Anthropic 与 Responses API 会重新发送这些块,
|
||||
|
||||
@ -630,8 +630,15 @@ class AioSandbox(Sandbox):
|
||||
rel_path = entry.path[len(root_path) :].lstrip("/")
|
||||
if path_matches(pattern, rel_path):
|
||||
matches.append(entry.path)
|
||||
if len(matches) >= max_results:
|
||||
return matches, True
|
||||
# Look one match past the cap before deciding. Returning on
|
||||
# the max-th match cannot tell a listing that held exactly
|
||||
# ``max_results`` from one that held more, so an exhausted
|
||||
# listing was reported as truncated; it also returned a match
|
||||
# for ``max_results=0``. The ``include_dirs=False`` branch
|
||||
# below and the shared ``parse_remote_search_output`` path
|
||||
# decide the same way.
|
||||
if len(matches) > max_results:
|
||||
return matches[:max_results], True
|
||||
return matches, False
|
||||
|
||||
def grep(
|
||||
|
||||
@ -203,8 +203,14 @@ class Sandbox(ABC):
|
||||
"""Find paths that match a glob pattern under a root directory.
|
||||
|
||||
Returns the matches and ``truncated``, which is true whenever the
|
||||
matches may be incomplete: ``max_results`` was reached, or the search
|
||||
stopped at an output cap before filtering.
|
||||
matches may be incomplete: the search stopped at an output cap before
|
||||
filtering, or an eligible match beyond ``max_results`` was dropped.
|
||||
|
||||
Providers differ in how precisely they can decide the second case. One
|
||||
that holds the whole listing can tell an exactly-full result from a
|
||||
cut-off one and reports the former as complete; one reading a capped
|
||||
stream cannot, and reports it as truncated. Treat the flag as "may be
|
||||
incomplete", never as a count.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@ -284,6 +284,115 @@ def test_aio_sandbox_glob_include_dirs_filters_nested_ignored(monkeypatch) -> No
|
||||
assert truncated is False
|
||||
|
||||
|
||||
def test_aio_sandbox_glob_include_dirs_exactly_full_is_not_truncated(monkeypatch) -> None:
|
||||
"""A listing whose matches exactly fill max_results is complete.
|
||||
|
||||
The branch used to return as soon as it had collected ``max_results``
|
||||
matches, without looking at the remaining entries, so a listing that held
|
||||
exactly that many was reported as cut off even though every entry was
|
||||
seen. It now scans one eligible match past the cap, the same rule the
|
||||
sibling ``include_dirs=False`` branch has always applied to the full list.
|
||||
"""
|
||||
with patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient"):
|
||||
sandbox = AioSandbox(id="test-sandbox", base_url="http://localhost:8080")
|
||||
monkeypatch.setattr(
|
||||
sandbox._client.file,
|
||||
"list_path",
|
||||
lambda **kwargs: SimpleNamespace(
|
||||
data=SimpleNamespace(
|
||||
files=[
|
||||
SimpleNamespace(name="a", path="/mnt/workspace/a"),
|
||||
SimpleNamespace(name="b", path="/mnt/workspace/b"),
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
matches, truncated = sandbox.glob("/mnt/workspace", "**", include_dirs=True, max_results=2)
|
||||
|
||||
assert matches == ["/mnt/workspace/a", "/mnt/workspace/b"]
|
||||
assert truncated is False
|
||||
|
||||
|
||||
def test_aio_sandbox_glob_include_dirs_reports_a_dropped_match_as_truncated(monkeypatch) -> None:
|
||||
"""The counterpart: a match past the cap still reports truncated."""
|
||||
with patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient"):
|
||||
sandbox = AioSandbox(id="test-sandbox", base_url="http://localhost:8080")
|
||||
monkeypatch.setattr(
|
||||
sandbox._client.file,
|
||||
"list_path",
|
||||
lambda **kwargs: SimpleNamespace(
|
||||
data=SimpleNamespace(
|
||||
files=[
|
||||
SimpleNamespace(name="a", path="/mnt/workspace/a"),
|
||||
SimpleNamespace(name="b", path="/mnt/workspace/b"),
|
||||
SimpleNamespace(name="c", path="/mnt/workspace/c"),
|
||||
]
|
||||
)
|
||||
),
|
||||
)
|
||||
|
||||
matches, truncated = sandbox.glob("/mnt/workspace", "**", include_dirs=True, max_results=2)
|
||||
|
||||
assert matches == ["/mnt/workspace/a", "/mnt/workspace/b"]
|
||||
assert truncated is True
|
||||
|
||||
|
||||
def _filtered_tail_entries(*, second_eligible: bool):
|
||||
"""Two eligible matches, then entries only the ignore rules, the pattern or
|
||||
the root scope reject — a tail that must not count toward the cap."""
|
||||
entries = [
|
||||
SimpleNamespace(name="a.py", path="/mnt/workspace/a.py"),
|
||||
SimpleNamespace(name="b.py", path="/mnt/workspace/b.py"),
|
||||
# ignored directory
|
||||
SimpleNamespace(name="lib.py", path="/mnt/workspace/node_modules/lib.py"),
|
||||
# outside the search root
|
||||
SimpleNamespace(name="c.py", path="/mnt/elsewhere/c.py"),
|
||||
# inside the root but not matched by the pattern
|
||||
SimpleNamespace(name="notes.txt", path="/mnt/workspace/notes.txt"),
|
||||
]
|
||||
if second_eligible:
|
||||
entries.append(SimpleNamespace(name="c.py", path="/mnt/workspace/c.py"))
|
||||
return entries
|
||||
|
||||
|
||||
def _patched_sandbox(monkeypatch, entries):
|
||||
with patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient"):
|
||||
sandbox = AioSandbox(id="test-sandbox", base_url="http://localhost:8080")
|
||||
monkeypatch.setattr(
|
||||
sandbox._client.file,
|
||||
"list_path",
|
||||
lambda **kwargs: SimpleNamespace(data=SimpleNamespace(files=entries)),
|
||||
)
|
||||
return sandbox
|
||||
|
||||
|
||||
def test_aio_sandbox_glob_include_dirs_looks_past_a_filtered_tail(monkeypatch) -> None:
|
||||
"""Trailing entries the filters reject do not make a full result truncated.
|
||||
|
||||
The cap counts *eligible* matches, so the branch has to keep scanning
|
||||
rather than assume the max-th match was the last one: here two matches fill
|
||||
``max_results`` and everything after them is ignored, out of root, or a
|
||||
pattern miss, which leaves the result complete.
|
||||
"""
|
||||
sandbox = _patched_sandbox(monkeypatch, _filtered_tail_entries(second_eligible=False))
|
||||
|
||||
matches, truncated = sandbox.glob("/mnt/workspace", "**/*.py", include_dirs=True, max_results=2)
|
||||
|
||||
assert matches == ["/mnt/workspace/a.py", "/mnt/workspace/b.py"]
|
||||
assert truncated is False
|
||||
|
||||
|
||||
def test_aio_sandbox_glob_include_dirs_reports_a_match_after_a_filtered_tail(monkeypatch) -> None:
|
||||
"""An eligible match beyond that filtered tail still reports truncation."""
|
||||
sandbox = _patched_sandbox(monkeypatch, _filtered_tail_entries(second_eligible=True))
|
||||
|
||||
matches, truncated = sandbox.glob("/mnt/workspace", "**/*.py", include_dirs=True, max_results=2)
|
||||
|
||||
assert matches == ["/mnt/workspace/a.py", "/mnt/workspace/b.py"]
|
||||
assert truncated is True
|
||||
|
||||
|
||||
def test_aio_sandbox_grep_invalid_regex_raises() -> None:
|
||||
with patch("deerflow.community.aio_sandbox.aio_sandbox.AioSandboxClient"):
|
||||
sandbox = AioSandbox(id="test-sandbox", base_url="http://localhost:8080")
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user