mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(sdk): align glob path default with grep (#3666)
> [!IMPORTANT] > **TL;DR:** Omitting `path` in `glob` now searches each backend's own default root (e.g. `FilesystemBackend`'s `cwd`) instead of forcing a literal `"/"`, matching how `grep` already behaves. Explicit `path="/"` is unchanged. > [!WARNING] > **Custom backends:** `BackendProtocol.glob`/`aglob` now declare `path: str | None = None`, and the tool layer passes `None` (not `"/"`) when the caller omits `path`. Built-in backends are updated to treat `None` as their default root. A custom `BackendProtocol` subclass that assumed `path` is always a string must add a guard such as `path = path or "/"` at the top of its `glob`/`aglob` to avoid an `AttributeError` on unscoped calls. --- `glob` and `grep` are paired filesystem search tools. Previously their schemas disagreed on the default search root: `glob.path` defaulted to `"/"` while `grep.path` defaulted to `null`. This makes omitted `glob.path` flow through as `None`, so each backend resolves its own default root instead of being handed a literal `"/"`. **Behavior with an omitted `path`:** - **`FilesystemBackend`** searches its configured `root_dir` / `cwd` — the agent's working directory, *not* the real OS root. Both an omitted `path` and an explicit `path="/"` resolve to `cwd`. - **`StateBackend`**, **`StoreBackend`**, **sandbox**, **context hub**, **Harbor**, and **composite** backends search their respective default root. - Error messages for an omitted `path` now display `<default>` rather than a misleading `'/'`. **Behavior with an explicit `path`:** - `path="/"` remains fully supported and behaves exactly as before, so existing callers are unaffected. - Any other explicit `path` is resolved relative to the backend as it always has been. **Schema change:** - Generated tool schemas now expose `glob.path` as nullable with default `null`, matching `grep.path`. Unscoped `glob()` calls keep the same runtime behavior. This is source-compatible for normal callers. The one compatibility risk is code that introspects the Python signature or generated tool schema and expects `glob.path.default == "/"` or a non-nullable `path` — those will now see `None` / a nullable field.
This commit is contained in:
@@ -402,21 +402,22 @@ class CompositeBackend(BackendProtocol):
|
||||
# Path specified but doesn't match a route - search only default
|
||||
return self._coerce_grep_result(await self.default.agrep(pattern, path, glob))
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Find files matching a glob pattern, routing by path prefix."""
|
||||
results: list[FileInfo] = []
|
||||
|
||||
backend, backend_path, route_prefix = _route_for_path(
|
||||
default=self.default,
|
||||
sorted_routes=self.sorted_routes,
|
||||
path=path,
|
||||
)
|
||||
if route_prefix is not None:
|
||||
glob_result = backend.glob(pattern, backend_path)
|
||||
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
|
||||
if isinstance(glob_result, GlobResult) and glob_result.error:
|
||||
return glob_result
|
||||
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
|
||||
if path is not None:
|
||||
backend, backend_path, route_prefix = _route_for_path(
|
||||
default=self.default,
|
||||
sorted_routes=self.sorted_routes,
|
||||
path=path,
|
||||
)
|
||||
if route_prefix is not None:
|
||||
glob_result = backend.glob(pattern, backend_path)
|
||||
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
|
||||
if isinstance(glob_result, GlobResult) and glob_result.error:
|
||||
return glob_result
|
||||
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
|
||||
|
||||
# Path doesn't match any specific route - search default backend AND all routed backends
|
||||
default_result = self.default.glob(pattern, path)
|
||||
@@ -433,21 +434,22 @@ class CompositeBackend(BackendProtocol):
|
||||
results.sort(key=lambda x: x.get("path", ""))
|
||||
return GlobResult(matches=results)
|
||||
|
||||
async def aglob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
async def aglob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Async version of glob."""
|
||||
results: list[FileInfo] = []
|
||||
|
||||
backend, backend_path, route_prefix = _route_for_path(
|
||||
default=self.default,
|
||||
sorted_routes=self.sorted_routes,
|
||||
path=path,
|
||||
)
|
||||
if route_prefix is not None:
|
||||
glob_result = await backend.aglob(pattern, backend_path)
|
||||
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
|
||||
if isinstance(glob_result, GlobResult) and glob_result.error:
|
||||
return glob_result
|
||||
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
|
||||
if path is not None:
|
||||
backend, backend_path, route_prefix = _route_for_path(
|
||||
default=self.default,
|
||||
sorted_routes=self.sorted_routes,
|
||||
path=path,
|
||||
)
|
||||
if route_prefix is not None:
|
||||
glob_result = await backend.aglob(pattern, backend_path)
|
||||
matches = glob_result.matches if isinstance(glob_result, GlobResult) else glob_result
|
||||
if isinstance(glob_result, GlobResult) and glob_result.error:
|
||||
return glob_result
|
||||
return GlobResult(matches=[_remap_file_info_path(fi, route_prefix) for fi in (matches or [])])
|
||||
|
||||
# Path doesn't match any specific route - search default backend AND all routed backends
|
||||
default_result = await self.default.aglob(pattern, path)
|
||||
|
||||
@@ -264,7 +264,7 @@ class ContextHubBackend(BackendProtocol):
|
||||
|
||||
return GrepResult(matches=matches)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult: # noqa: ARG002
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult: # noqa: ARG002
|
||||
"""Return files matching `pattern` (`path` unused — flat namespace)."""
|
||||
try:
|
||||
cache = self._ensure_cache()
|
||||
|
||||
@@ -773,12 +773,12 @@ class FilesystemBackend(BackendProtocol):
|
||||
|
||||
return results, None
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult: # noqa: C901, PLR0912 # Complex virtual_mode logic
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult: # noqa: C901, PLR0912, PLR0915 # Complex virtual_mode logic
|
||||
"""Find files matching a glob pattern.
|
||||
|
||||
Args:
|
||||
pattern: Glob pattern to match files against (e.g., `'*.py'`, `'**/*.txt'`).
|
||||
path: Base directory to search from. Defaults to root (`/`).
|
||||
path: Base directory to search from. Defaults to `root_dir` / `cwd`.
|
||||
|
||||
Returns:
|
||||
GlobResult with matching files or error.
|
||||
@@ -791,11 +791,12 @@ class FilesystemBackend(BackendProtocol):
|
||||
raise ValueError(msg)
|
||||
|
||||
try:
|
||||
search_path = self.cwd if path == "/" else self._resolve_path(path)
|
||||
search_path = self.cwd if path is None or path == "/" else self._resolve_path(path)
|
||||
if not search_path.exists() or not search_path.is_dir():
|
||||
return GlobResult(matches=[])
|
||||
except (OSError, RuntimeError) as e:
|
||||
return GlobResult(error=f"Error globbing path '{path}': {e}", matches=[])
|
||||
display_path = path if path is not None else "<default>"
|
||||
return GlobResult(error=f"Error globbing path '{display_path}': {e}", matches=[])
|
||||
|
||||
results: list[FileInfo] = []
|
||||
try:
|
||||
@@ -851,7 +852,8 @@ class FilesystemBackend(BackendProtocol):
|
||||
except (OSError, RuntimeError, ValueError) as e:
|
||||
# rglob() raised mid-iteration. Return whatever was accumulated
|
||||
# but flag the partial result so callers don't trust it as complete.
|
||||
msg = f"Glob of '{path}' aborted partway: {e}"
|
||||
display_path = path if path is not None else "<default>"
|
||||
msg = f"Glob of '{display_path}' aborted partway: {e}"
|
||||
logger.warning("%s", msg, exc_info=True)
|
||||
results.sort(key=lambda x: x.get("path", ""))
|
||||
return GlobResult(error=msg, matches=results)
|
||||
|
||||
@@ -465,7 +465,7 @@ class BackendProtocol(abc.ABC): # noqa: B024
|
||||
"""Async version of `grep`."""
|
||||
return await asyncio.to_thread(self.grep, pattern, path, glob)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> "GlobResult":
|
||||
def glob(self, pattern: str, path: str | None = None) -> "GlobResult":
|
||||
"""Find files matching a glob pattern.
|
||||
|
||||
Args:
|
||||
@@ -478,9 +478,9 @@ class BackendProtocol(abc.ABC): # noqa: B024
|
||||
- `?` matches a single character
|
||||
- `[abc]` matches one character from set
|
||||
|
||||
path: Base directory to search from.
|
||||
path: Optional base directory to search from.
|
||||
|
||||
Default: `'/'` (root).
|
||||
If omitted, the backend chooses its default search root.
|
||||
|
||||
The pattern is applied relative to this path.
|
||||
|
||||
@@ -498,11 +498,11 @@ class BackendProtocol(abc.ABC): # noqa: B024
|
||||
message=("`glob_info` is deprecated and will be removed in deepagents==0.7.0; rename to `glob` instead."),
|
||||
package="deepagents",
|
||||
)
|
||||
return GlobResult(matches=self.glob_info(pattern, path))
|
||||
return GlobResult(matches=self.glob_info(pattern, path or "/"))
|
||||
|
||||
raise NotImplementedError
|
||||
|
||||
async def aglob(self, pattern: str, path: str = "/") -> "GlobResult":
|
||||
async def aglob(self, pattern: str, path: str | None = None) -> "GlobResult":
|
||||
"""Async version of `glob`."""
|
||||
return await asyncio.to_thread(self.glob, pattern, path)
|
||||
|
||||
|
||||
@@ -829,11 +829,12 @@ except PermissionError:
|
||||
|
||||
return GrepResult(matches=matches)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Structured glob matching returning `GlobResult`."""
|
||||
search_path = path or "/"
|
||||
# Encode pattern and path as base64 to avoid escaping issues
|
||||
pattern_b64 = base64.b64encode(pattern.encode("utf-8")).decode("ascii")
|
||||
path_b64 = base64.b64encode(path.encode("utf-8")).decode("ascii")
|
||||
path_b64 = base64.b64encode(search_path.encode("utf-8")).decode("ascii")
|
||||
|
||||
cmd = _GLOB_COMMAND_TEMPLATE.format(path_b64=path_b64, pattern_b64=pattern_b64)
|
||||
result = self.execute(cmd)
|
||||
@@ -864,7 +865,7 @@ except PermissionError:
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
return GlobResult(matches=None, error=f"Path '{path}': {error}")
|
||||
return GlobResult(matches=None, error=f"Path '{search_path}': {error}")
|
||||
return GlobResult(matches=file_infos)
|
||||
|
||||
@property
|
||||
|
||||
@@ -300,7 +300,7 @@ class StateBackend(BackendProtocol):
|
||||
files = self._read_files()
|
||||
return grep_matches_from_files(files, pattern, path if path is not None else "/", glob)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Get FileInfo for files matching glob pattern."""
|
||||
files = self._read_files()
|
||||
result = _glob_search_files(files, pattern, path)
|
||||
|
||||
@@ -701,7 +701,7 @@ class StoreBackend(BackendProtocol):
|
||||
continue
|
||||
return grep_matches_from_files(files, pattern, path, glob)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Find files matching a glob pattern in the store."""
|
||||
store = self._get_store()
|
||||
namespace = self._get_namespace()
|
||||
|
||||
@@ -546,14 +546,14 @@ def _filter_files_by_path(files: dict[str, Any], normalized_path: str) -> dict[s
|
||||
def _glob_search_files(
|
||||
files: dict[str, Any],
|
||||
pattern: str,
|
||||
path: str = "/",
|
||||
path: str | None = None,
|
||||
) -> str:
|
||||
r"""Search files dict for paths matching glob pattern.
|
||||
|
||||
Args:
|
||||
files: Dictionary of file paths to FileData.
|
||||
pattern: Glob pattern (e.g., "*.py", "**/*.ts").
|
||||
path: Base path to search from.
|
||||
path: Base path to search from. `None` defaults to root.
|
||||
|
||||
Returns:
|
||||
Newline-separated file paths, sorted by modification time (most recent first).
|
||||
|
||||
@@ -308,7 +308,7 @@ class GlobSchema(BaseModel):
|
||||
"""Input schema for the `glob` tool."""
|
||||
|
||||
pattern: str = Field(description="Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').")
|
||||
path: str = Field(default="/", description="Base directory to search from. Defaults to root '/'.")
|
||||
path: str | None = Field(default=None, description="Base directory to search from. Defaults to the backend's default root.")
|
||||
|
||||
|
||||
class GrepSchema(BaseModel):
|
||||
@@ -385,7 +385,7 @@ Returns a list of absolute file paths that match the pattern.
|
||||
|
||||
Examples:
|
||||
- `**/*.py` - Find all Python files
|
||||
- `*.txt` - Find all text files in root
|
||||
- `*.txt` - Find all text files in the backend's default root
|
||||
- `/subdir/**/*.md` - Find all markdown files under /subdir"""
|
||||
|
||||
GREP_TOOL_DESCRIPTION = """Search for a text pattern across files.
|
||||
@@ -1197,12 +1197,12 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
|
||||
def sync_glob(
|
||||
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
|
||||
runtime: ToolRuntime[None, FilesystemState],
|
||||
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
|
||||
path: Annotated[str | None, "Base directory to search from. Defaults to the backend's default root."] = None,
|
||||
) -> ToolMessage:
|
||||
"""Synchronous wrapper for glob tool."""
|
||||
resolved_backend = self._get_backend(runtime)
|
||||
try:
|
||||
validated_path = validate_path(path)
|
||||
permission_path = validate_path(path if path is not None else "/")
|
||||
except ValueError as e:
|
||||
return ToolMessage(
|
||||
content=f"Error: {e}",
|
||||
@@ -1210,16 +1210,17 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
status="error",
|
||||
)
|
||||
if _check_fs_permission(self._permissions, "read", validated_path) == "deny":
|
||||
if _check_fs_permission(self._permissions, "read", permission_path) == "deny":
|
||||
return ToolMessage(
|
||||
content=f"Error: permission denied for read on {validated_path}",
|
||||
content=f"Error: permission denied for read on {permission_path}",
|
||||
name="glob",
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
status="error",
|
||||
)
|
||||
backend_path = permission_path if path is not None else None
|
||||
ctx = contextvars.copy_context()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
future = executor.submit(lambda: ctx.run(resolved_backend.glob, pattern, path=validated_path))
|
||||
future = executor.submit(lambda: ctx.run(resolved_backend.glob, pattern, path=backend_path))
|
||||
try:
|
||||
glob_result = future.result(timeout=GLOB_TIMEOUT)
|
||||
except concurrent.futures.TimeoutError:
|
||||
@@ -1248,12 +1249,12 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
|
||||
async def async_glob(
|
||||
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
|
||||
runtime: ToolRuntime[None, FilesystemState],
|
||||
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
|
||||
path: Annotated[str | None, "Base directory to search from. Defaults to the backend's default root."] = None,
|
||||
) -> ToolMessage:
|
||||
"""Asynchronous wrapper for glob tool."""
|
||||
resolved_backend = self._get_backend(runtime)
|
||||
try:
|
||||
validated_path = validate_path(path)
|
||||
permission_path = validate_path(path if path is not None else "/")
|
||||
except ValueError as e:
|
||||
return ToolMessage(
|
||||
content=f"Error: {e}",
|
||||
@@ -1261,16 +1262,17 @@ class FilesystemMiddleware(AgentMiddleware[FilesystemState, ContextT, ResponseT]
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
status="error",
|
||||
)
|
||||
if _check_fs_permission(self._permissions, "read", validated_path) == "deny":
|
||||
if _check_fs_permission(self._permissions, "read", permission_path) == "deny":
|
||||
return ToolMessage(
|
||||
content=f"Error: permission denied for read on {validated_path}",
|
||||
content=f"Error: permission denied for read on {permission_path}",
|
||||
name="glob",
|
||||
tool_call_id=runtime.tool_call_id,
|
||||
status="error",
|
||||
)
|
||||
backend_path = permission_path if path is not None else None
|
||||
try:
|
||||
glob_result = await asyncio.wait_for(
|
||||
resolved_backend.aglob(pattern, path=validated_path),
|
||||
resolved_backend.aglob(pattern, path=backend_path),
|
||||
timeout=GLOB_TIMEOUT,
|
||||
)
|
||||
except TimeoutError:
|
||||
|
||||
@@ -109,6 +109,8 @@ def test_composite_backend_filesystem_plus_store(tmp_path: Path):
|
||||
# glob
|
||||
gl = comp.glob("*.md", path="/").matches
|
||||
assert any(i["path"] == "/memories/notes.md" for i in gl)
|
||||
gl_default = comp.glob("*.md").matches
|
||||
assert gl_default == gl
|
||||
|
||||
|
||||
def test_composite_backend_store_to_store():
|
||||
|
||||
@@ -63,6 +63,25 @@ def test_filesystem_backend_normal_mode(tmp_path: Path):
|
||||
assert any(i["path"] == str(f2) for i in g)
|
||||
|
||||
|
||||
def test_filesystem_backend_glob_default_matches_backend_root(tmp_path: Path) -> None:
|
||||
root = tmp_path / "root"
|
||||
in_root = root / "dir" / "inside.py"
|
||||
outside_root = tmp_path / "outside.py"
|
||||
write_file(in_root, "print('inside')")
|
||||
write_file(outside_root, "print('outside')")
|
||||
|
||||
be = FilesystemBackend(root_dir=str(root), virtual_mode=False)
|
||||
|
||||
omitted = be.glob("**/*.py").matches or []
|
||||
explicit_root = be.glob("**/*.py", path="/").matches or []
|
||||
|
||||
omitted_paths = {info["path"] for info in omitted}
|
||||
explicit_root_paths = {info["path"] for info in explicit_root}
|
||||
assert omitted_paths == explicit_root_paths
|
||||
assert str(in_root) in omitted_paths
|
||||
assert str(outside_root) not in omitted_paths
|
||||
|
||||
|
||||
def test_filesystem_backend_virtual_mode(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||
root = tmp_path
|
||||
f1 = root / "a.txt"
|
||||
|
||||
@@ -145,7 +145,7 @@ class TestDeprecatedMethodsRouteToNewNames:
|
||||
|
||||
def test_glob_info_delegates_to_glob(self) -> None:
|
||||
class MyBackend(BackendProtocol):
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
return GlobResult(matches=[{"path": f"{path}/{pattern}"}])
|
||||
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
|
||||
@@ -82,3 +82,15 @@ class TestFilesystemToolSchemas:
|
||||
f"Sync schema: {sync_schema}\n"
|
||||
f"Async schema: {async_schema}"
|
||||
)
|
||||
|
||||
def test_glob_path_defaults_to_nullable_backend_root(self) -> None:
|
||||
"""Verify `glob.path` matches `grep.path` optional default semantics."""
|
||||
middleware = FilesystemMiddleware(backend=StateBackend())
|
||||
glob_tool = next(tool for tool in middleware.tools if tool.name == "glob")
|
||||
|
||||
schema = glob_tool.tool_call_schema.model_json_schema()
|
||||
path_schema = schema["properties"]["path"]
|
||||
|
||||
assert path_schema["default"] is None
|
||||
assert {"type": "null"} in path_schema["anyOf"]
|
||||
assert "backend's default root" in path_schema["description"]
|
||||
|
||||
+11
-4
@@ -146,14 +146,21 @@
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"name": "glob",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "/",
|
||||
"description": "Base directory to search from. Defaults to root '/'.",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Base directory to search from. Defaults to the backend's default root."
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
|
||||
|
||||
+11
-4
@@ -146,14 +146,21 @@
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"name": "glob",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "/",
|
||||
"description": "Base directory to search from. Defaults to root '/'.",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Base directory to search from. Defaults to the backend's default root."
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
|
||||
|
||||
+11
-4
@@ -146,14 +146,21 @@
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"name": "glob",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "/",
|
||||
"description": "Base directory to search from. Defaults to root '/'.",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Base directory to search from. Defaults to the backend's default root."
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
|
||||
|
||||
+11
-4
@@ -146,14 +146,21 @@
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"name": "glob",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "/",
|
||||
"description": "Base directory to search from. Defaults to root '/'.",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Base directory to search from. Defaults to the backend's default root."
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
|
||||
|
||||
+11
-4
@@ -146,14 +146,21 @@
|
||||
},
|
||||
{
|
||||
"function": {
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"description": "Find files matching a glob pattern.\n\nSupports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).\nReturns a list of absolute file paths that match the pattern.\n\nExamples:\n- `**/*.py` - Find all Python files\n- `*.txt` - Find all text files in the backend's default root\n- `/subdir/**/*.md` - Find all markdown files under /subdir",
|
||||
"name": "glob",
|
||||
"parameters": {
|
||||
"properties": {
|
||||
"path": {
|
||||
"default": "/",
|
||||
"description": "Base directory to search from. Defaults to root '/'.",
|
||||
"type": "string"
|
||||
"anyOf": [
|
||||
{
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"type": "null"
|
||||
}
|
||||
],
|
||||
"default": null,
|
||||
"description": "Base directory to search from. Defaults to the backend's default root."
|
||||
},
|
||||
"pattern": {
|
||||
"description": "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md').",
|
||||
|
||||
@@ -194,9 +194,10 @@ class LocalSubprocessSandbox(BaseSandbox):
|
||||
match["path"] = self._to_virtual_path(match["path"])
|
||||
return result
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Run glob against mapped real paths."""
|
||||
result = super().glob(pattern, path=self._to_real_path(path))
|
||||
mapped_path = self._to_real_path(path) if path is not None else None
|
||||
result = super().glob(pattern, path=mapped_path)
|
||||
if result.error is not None:
|
||||
result.error = self._to_virtual_path(result.error)
|
||||
if result.matches is not None:
|
||||
|
||||
@@ -435,13 +435,14 @@ done
|
||||
"""
|
||||
raise NotImplementedError(_SYNC_NOT_SUPPORTED)
|
||||
|
||||
async def aglob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
async def aglob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Find files matching glob pattern using shell commands.
|
||||
|
||||
Please note that this implementation does not currently support all glob
|
||||
patterns.
|
||||
"""
|
||||
safe_path = shlex.quote(path)
|
||||
search_path = path or "/"
|
||||
safe_path = shlex.quote(search_path)
|
||||
safe_pattern = shlex.quote(pattern)
|
||||
|
||||
cmd = f"""
|
||||
@@ -462,7 +463,7 @@ done
|
||||
if result.exit_code != 0:
|
||||
detail = result.output.strip() if result.output else ""
|
||||
return GlobResult(
|
||||
error=f"Path not found or not accessible: {path}"
|
||||
error=f"Path not found or not accessible: {search_path}"
|
||||
+ (f" ({detail})" if detail else "")
|
||||
)
|
||||
|
||||
@@ -488,7 +489,7 @@ done
|
||||
|
||||
return GlobResult(matches=file_infos)
|
||||
|
||||
def glob(self, pattern: str, path: str = "/") -> GlobResult:
|
||||
def glob(self, pattern: str, path: str | None = None) -> GlobResult:
|
||||
"""Find files matching glob pattern using shell commands."""
|
||||
raise NotImplementedError(_SYNC_NOT_SUPPORTED)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user