mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-09-20 11:36:17 +00:00
* fix(sandbox): report truncated remote glob and grep results
BoxLite, Tenki, E2B, and OpenSandbox run find/grep in the sandbox, cap
the raw output with `| head`, and then filter those lines in Python:
ignored directories such as node_modules are dropped and grep's glob
scope is applied. They reported truncated only when max_results matches
survived the filter. When the capped lines were mostly filtered out, a
search with real matches past the cap came back short or empty with
truncated=False, and glob_tool/grep_tool rendered it as "No files
matched" / "No matches found". With the default max_results=200 and
1,200 files under node_modules, glob("**/*.py") reported no matches for
a workspace that has src/app.py.
remote_search_command now lets one line past its limit through, and
parse_remote_search_output(..., limit=) returns RemoteSearchOutput(text,
truncated): the first `limit` lines and whether the extra line arrived.
Exactly `limit` lines stays a complete result. Each provider passes the
cap it already computed to both calls and returns that truncated from
glob and grep when fewer than max_results results survive filtering.
The glob and grep tools now describe an empty truncated result as
incomplete instead of reporting no matches, which also covers AIO grep's
forwarded truncated flag. Sandbox.glob/grep document truncated as "the
matches may be incomplete".
* docs(changelog): reference #5427 in the remote search truncation entry
---------
Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
97 lines
4.4 KiB
Python
97 lines
4.4 KiB
Python
"""Remote ``grep`` / ``glob`` command wrapper and stdout contract.
|
|
|
|
Remote providers search with ``grep ... | head`` or ``find ... | head`` under
|
|
``sh -lc``. POSIX ``sh`` has no ``pipefail`` and the search's stderr is
|
|
discarded, so the pipeline status is ``head``'s: a missing search root, a
|
|
missing ``grep``/``find`` binary (127) or an unreadable tree all printed
|
|
nothing and exited 0, exactly like a genuine "no matches" (#5376).
|
|
|
|
:func:`remote_search_command` checks the root first and records the search
|
|
command's own status after the bounded output, the same technique as
|
|
:mod:`deerflow.sandbox.remote_list_dir`. The script always exits 0 so SDKs that
|
|
raise on a non-zero exit still return the marker; the marker alone decides.
|
|
|
|
Callers filter the bounded lines in Python (ignored directories, glob scope),
|
|
so returning fewer than ``max_results`` does not prove the search was complete.
|
|
The command lets one line past ``limit`` through, and the parser reports
|
|
whether it arrived; exactly ``limit`` lines is a complete result.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import shlex
|
|
from typing import Literal, NamedTuple
|
|
|
|
SearchTool = Literal["grep", "find"]
|
|
|
|
_STATUS_PREFIX = "__DF_SEARCH_STATUS__:"
|
|
_MISSING_ROOT = "missing"
|
|
# head closing the pipe after the limit kills the search with SIGPIPE: a
|
|
# successful truncation, not an error.
|
|
_SIGPIPE = 141
|
|
# Statuses of a complete search. grep: 0 = matches, 1 = no match. Everything
|
|
# else fails, even after printed results.
|
|
_OK_STATUSES: dict[str, tuple[int, ...]] = {"grep": (0, 1, _SIGPIPE), "find": (0, _SIGPIPE)}
|
|
# grep 2 / find 1 usually mean an unreadable file or subdirectory. Printed
|
|
# results are then incomplete and callers have no partial-result channel, so
|
|
# the error tells the agent to narrow the search instead.
|
|
_READ_ERROR_STATUS: dict[str, int] = {"grep": 2, "find": 1}
|
|
|
|
|
|
class RemoteSearchOutput(NamedTuple):
|
|
"""Search output lines, and whether the search produced more than ``limit``."""
|
|
|
|
text: str
|
|
truncated: bool
|
|
|
|
|
|
def remote_search_command(search: str, root: str, *, limit: int) -> str:
|
|
"""Wrap a ``grep``/``find`` command so its outcome survives ``| head``.
|
|
|
|
``search`` must write only results to stdout; callers keep ``2>/dev/null``.
|
|
Pass the same ``limit`` to :func:`parse_remote_search_output`.
|
|
"""
|
|
quoted = shlex.quote(root)
|
|
# One extra line is the truncation signal; the parser drops it.
|
|
n = int(limit) + 1
|
|
return (
|
|
f"set +e; if [ ! -e {quoted} ]; then printf '%s\\n' {_STATUS_PREFIX}{_MISSING_ROOT}; exit 0; fi; "
|
|
f'_st=/tmp/df_search_$$; {{ {search}; echo $? > "$_st"; }} | head -n {n}; '
|
|
f'st=$(cat "$_st" 2>/dev/null); rm -f "$_st"; '
|
|
f"printf '\\n%s\\n' {_STATUS_PREFIX}\"$st\"; exit 0"
|
|
)
|
|
|
|
|
|
def parse_remote_search_output(stdout: str | None, root: str, *, tool: SearchTool, limit: int) -> RemoteSearchOutput:
|
|
"""Return at most ``limit`` output lines without the status marker.
|
|
|
|
``truncated`` is true when the search printed more than ``limit`` lines, so
|
|
results filtered from ``text`` may be incomplete.
|
|
|
|
Raises:
|
|
FileNotFoundError: The search root does not exist.
|
|
OSError: The search did not complete (missing binary, unreadable
|
|
root or subtree, invalid invocation) or its status was lost.
|
|
"""
|
|
# Split on "\n" only: splitlines() would also split on characters that are
|
|
# legal in Linux filenames. Callers keep their own per-line handling.
|
|
lines = (stdout or "").split("\n")
|
|
if lines and lines[-1] == "":
|
|
lines.pop()
|
|
if not lines or not lines[-1].startswith(_STATUS_PREFIX):
|
|
raise OSError(f"Failed to {tool} under {root}: search status marker missing")
|
|
raw = lines.pop()[len(_STATUS_PREFIX) :]
|
|
if raw == _MISSING_ROOT:
|
|
raise FileNotFoundError(root)
|
|
if lines and lines[-1] == "":
|
|
lines.pop()
|
|
try:
|
|
status = int(raw)
|
|
except ValueError:
|
|
raise OSError(f"Failed to {tool} under {root}: search status unavailable") from None
|
|
if status in _OK_STATUSES[tool]:
|
|
return RemoteSearchOutput("\n".join(lines[:limit]), len(lines) > limit)
|
|
if status == _READ_ERROR_STATUS[tool]:
|
|
raise OSError(f"Failed to {tool} under {root}: {tool} exited with code {status}, usually because some files or directories could not be read; results would be incomplete, so search a narrower path")
|
|
raise OSError(f"Failed to {tool} under {root}: command exited with code {status}")
|