mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-22 01:35:28 -04:00
fix(code): exclude managed bin dir from agent picker (#4190)
Fix the `/agents` picker listing the internal `bin` directory (managed ripgrep install) as a selectable agent. --- The `/agents` picker lists every real, non-dot subdirectory under `~/.deepagents/`, filtering only symlinks and dot-prefixed names. The managed ripgrep install dir (`~/.deepagents/bin`, `managed_tools.BIN_DIR`) therefore showed up as a phantom `bin` agent. This adds a reserved-name filter to `_is_agent_dir_entry`, sourced from `BIN_DIR.name` to keep a single source of truth. Made by [Open SWE](https://openswe.vercel.app/agents/3cd7dda7-db49-2afd-b636-d678ecc65dc0) --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
@@ -438,17 +439,32 @@ def load_async_subagents(config_path: Path | None = None) -> list[AsyncSubAgent]
|
||||
return agents
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=1)
|
||||
def _reserved_agent_dir_names() -> frozenset[str]:
|
||||
"""Return non-agent directory names reserved by the app under `~/.deepagents/`.
|
||||
|
||||
`bin/` holds the managed `rg` binary (`managed_tools.BIN_DIR`) and must
|
||||
never appear in the agent picker. The name is derived from `BIN_DIR` so it
|
||||
stays a single source of truth rather than being hardcoded here. The result
|
||||
is cached since the reserved set is constant for the process.
|
||||
"""
|
||||
from deepagents_code.managed_tools import BIN_DIR
|
||||
|
||||
return frozenset({BIN_DIR.name})
|
||||
|
||||
|
||||
def _is_agent_dir_entry(entry: Path) -> bool:
|
||||
"""Return whether a `~/.deepagents/` entry should be listed as an agent.
|
||||
|
||||
Filters out symlinks (so dangling links don't masquerade as agents)
|
||||
and dot-prefixed names — `.state/` (app internal state) plus any
|
||||
other hidden directory the user may have placed there.
|
||||
Filters out symlinks (so dangling links don't masquerade as agents),
|
||||
dot-prefixed names — `.state/` (app internal state) plus any other
|
||||
hidden directory the user may have placed there — and reserved names
|
||||
the app owns (e.g. `bin/`, the managed-binary install dir).
|
||||
|
||||
`OSError` from `is_dir`/`is_symlink` propagates so callers can log
|
||||
with the failing entry's name as context.
|
||||
"""
|
||||
if entry.name.startswith("."):
|
||||
if entry.name.startswith(".") or entry.name in _reserved_agent_dir_names():
|
||||
return False
|
||||
return entry.is_dir() and not entry.is_symlink()
|
||||
|
||||
@@ -458,8 +474,9 @@ def get_available_agent_names() -> list[str]:
|
||||
|
||||
Scans the user's `.deepagents` directory and returns each real
|
||||
subdirectory found there. Symlinks excluded so a dangling link does not
|
||||
masquerade as an agent. Dot-prefixed entries (e.g., `.state/`) are
|
||||
skipped so internal app state never appears as an agent.
|
||||
masquerade as an agent. Dot-prefixed entries (e.g., `.state/`) and
|
||||
reserved app-owned directories (e.g., `bin/`, the managed-binary install
|
||||
dir) are skipped so internal state never appears as an agent.
|
||||
|
||||
Filesystem errors (missing parent, permission denied, broken entries) are
|
||||
logged and surfaced as an empty list rather than raised — the caller shows
|
||||
|
||||
@@ -30,6 +30,7 @@ from deepagents_code.agent import (
|
||||
_format_task_description,
|
||||
_format_web_search_description,
|
||||
_format_write_file_description,
|
||||
_reserved_agent_dir_names,
|
||||
_sanitize_agent_message_name,
|
||||
_should_interrupt_tool_call,
|
||||
build_model_identity_section,
|
||||
@@ -40,6 +41,7 @@ from deepagents_code.agent import (
|
||||
load_async_subagents,
|
||||
)
|
||||
from deepagents_code.config import Settings, get_glyphs
|
||||
from deepagents_code.managed_tools import BIN_DIR
|
||||
from deepagents_code.project_utils import ProjectContext
|
||||
|
||||
|
||||
@@ -2933,6 +2935,25 @@ class TestGetAvailableAgentNames:
|
||||
with patch("deepagents_code.agent.settings", _mock_agents_dir(agents_dir)):
|
||||
assert get_available_agent_names() == ["agent"]
|
||||
|
||||
def test_ignores_reserved_bin_dir(self, tmp_path: Path) -> None:
|
||||
"""The managed-binary install dir is excluded from the agent list.
|
||||
|
||||
The reserved dir name is derived from `BIN_DIR.name` rather than the
|
||||
literal `bin` so a future rename of `BIN_DIR` keeps this test exercising
|
||||
the actual reserved name instead of a stale string.
|
||||
"""
|
||||
agents_dir = tmp_path / "agents"
|
||||
agents_dir.mkdir()
|
||||
(agents_dir / "agent").mkdir()
|
||||
(agents_dir / BIN_DIR.name).mkdir()
|
||||
|
||||
with patch("deepagents_code.agent.settings", _mock_agents_dir(agents_dir)):
|
||||
assert get_available_agent_names() == ["agent"]
|
||||
|
||||
def test_reserved_agent_dir_names_includes_bin_dir(self) -> None:
|
||||
"""The reserved-name set is sourced from `BIN_DIR.name` (single source)."""
|
||||
assert _reserved_agent_dir_names() == frozenset({BIN_DIR.name})
|
||||
|
||||
def test_permission_error_returns_empty(self, tmp_path: Path) -> None:
|
||||
"""PermissionError on iterdir → logged + empty list, not raised."""
|
||||
agents_dir = tmp_path / "agents"
|
||||
|
||||
Reference in New Issue
Block a user