feat(deps): upgrade deepagents to 0.7.0 with todos restore and delete gating- #395
- Introduced TodoListMiddleware to the middleware stack for better task management. - Updated HITL interrupt configuration to include 'delete' operations requiring approval. - Implemented error handling for delete operations in read-only and memory backends. - Enhanced approval prompt formatting to display file paths for delete actions. - Added tests to ensure delete operations are correctly blocked or prompted for approval. - Updated dependencies to use deepagents 0.7.0 and langchain 1.5.3 for improved functionality.
This commit is contained in:
@@ -22,7 +22,11 @@ import os
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from langchain.agents.middleware import AgentMiddleware, HumanInTheLoopMiddleware
|
||||
from langchain.agents.middleware import (
|
||||
AgentMiddleware,
|
||||
HumanInTheLoopMiddleware,
|
||||
TodoListMiddleware,
|
||||
)
|
||||
|
||||
from . import paths as _paths_mod
|
||||
from .config import (
|
||||
@@ -52,6 +56,15 @@ SUBAGENTS_CONFIG = Path(__file__).parent / "subagents"
|
||||
SKILLS_DIR = str(Path(__file__).parent / "skills")
|
||||
DEFAULT_SKILL_SOURCES = ("/skills/",)
|
||||
|
||||
# Tools requiring human approval on attended agents (deepagents 0.7.0 ships a
|
||||
# recursive `delete` FS tool that would otherwise bypass the execute blocklist).
|
||||
HITL_INTERRUPT_ON: dict[str, bool] = {
|
||||
"execute": True,
|
||||
"run_in_background": True,
|
||||
"schedule_task": True,
|
||||
"delete": True,
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# Lazy state — initialized on first use, not at import time
|
||||
# =============================================================================
|
||||
@@ -359,6 +372,7 @@ def _inject_subagent_middleware(
|
||||
create_context_editing_middleware(chat_model),
|
||||
create_runtime_context_middleware(),
|
||||
ToolErrorHandlerMiddleware(),
|
||||
TodoListMiddleware(),
|
||||
ContextOverflowMapperMiddleware(),
|
||||
]
|
||||
if memory_controls.memory_enabled:
|
||||
@@ -790,6 +804,9 @@ def _get_default_middleware(
|
||||
ModelFallbackMiddleware(events=events),
|
||||
ContextOverflowMapperMiddleware(),
|
||||
ToolErrorHandlerMiddleware(),
|
||||
# deepagents 0.7.0 dropped TodoListMiddleware from its defaults;
|
||||
# EXPERIMENT_WORKFLOW planning and the todo UI pipeline require it.
|
||||
TodoListMiddleware(),
|
||||
*create_tool_selector_middleware(
|
||||
model=tool_selector_model,
|
||||
events=events,
|
||||
@@ -876,15 +893,7 @@ def _get_default_agent():
|
||||
# breaks parallel execute calls (multi-pending-interrupt LangGraph
|
||||
# error). See PR #202.
|
||||
if not cfg.auto_approve:
|
||||
mw.append(
|
||||
HumanInTheLoopMiddleware(
|
||||
interrupt_on={
|
||||
"execute": True,
|
||||
"run_in_background": True,
|
||||
"schedule_task": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
mw.append(HumanInTheLoopMiddleware(interrupt_on=dict(HITL_INTERRUPT_ON)))
|
||||
|
||||
if os.environ.get("EVOSCIENTIST_DEPLOY_MODE", "").lower() == "stripped":
|
||||
kwargs = _build_base_kwargs(
|
||||
@@ -1042,15 +1051,7 @@ def create_cli_agent(
|
||||
# would propagate it to every subagent, breaking parallel execute calls
|
||||
# (multi-pending-interrupt LangGraph error).
|
||||
if not cfg.auto_approve:
|
||||
mw.append(
|
||||
HumanInTheLoopMiddleware(
|
||||
interrupt_on={
|
||||
"execute": True,
|
||||
"run_in_background": True,
|
||||
"schedule_task": True,
|
||||
}
|
||||
)
|
||||
)
|
||||
mw.append(HumanInTheLoopMiddleware(interrupt_on=dict(HITL_INTERRUPT_ON)))
|
||||
|
||||
# Re-load MCP tools from current config (picks up /mcp add changes)
|
||||
kwargs = load_mcp_and_build_kwargs(
|
||||
|
||||
@@ -15,6 +15,7 @@ from pathlib import Path
|
||||
from deepagents.backends import FilesystemBackend, LocalShellBackend
|
||||
from deepagents.backends.protocol import (
|
||||
BackendProtocol,
|
||||
DeleteResult,
|
||||
EditResult,
|
||||
ExecuteResponse,
|
||||
FileDownloadResponse,
|
||||
@@ -904,6 +905,11 @@ class ReadOnlyFilesystemBackend(FilesystemBackend):
|
||||
for file_path, _ in files
|
||||
]
|
||||
|
||||
def delete(self, file_path: str) -> DeleteResult:
|
||||
return DeleteResult(
|
||||
error="This directory is read-only. Delete operations are not permitted here."
|
||||
)
|
||||
|
||||
|
||||
class MemoryFilesystemBackend(FilesystemBackend):
|
||||
"""Filesystem backend for memory files with structured-write enforcement.
|
||||
@@ -920,6 +926,10 @@ class MemoryFilesystemBackend(FilesystemBackend):
|
||||
"Raw edits under /memories are limited to existing "
|
||||
"/memories/profile/... files. Use memory tools for observations."
|
||||
)
|
||||
_RAW_DELETE_ERROR = (
|
||||
"Deletes under /memories are blocked. Manage memory files through "
|
||||
"memory tools instead."
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _is_profile_path(file_path: str) -> bool:
|
||||
@@ -946,6 +956,9 @@ class MemoryFilesystemBackend(FilesystemBackend):
|
||||
for file_path, _ in files
|
||||
]
|
||||
|
||||
def delete(self, file_path: str) -> DeleteResult:
|
||||
return DeleteResult(error=self._RAW_DELETE_ERROR)
|
||||
|
||||
|
||||
def build_memory_agent_backend(
|
||||
*,
|
||||
@@ -1552,6 +1565,12 @@ class AutoskillProposalSandboxBackend(CustomSandboxBackend):
|
||||
for file_path, _ in files
|
||||
]
|
||||
|
||||
def delete(self, file_path: str) -> DeleteResult:
|
||||
return DeleteResult(
|
||||
error="Deletes are blocked for AutoSkills. Manage proposal files "
|
||||
"under /autoskill-proposals/ instead."
|
||||
)
|
||||
|
||||
def execute(self, command: str, *, timeout: int | None = None) -> ExecuteResponse:
|
||||
return super().execute(
|
||||
self._rewrite_autoskill_mount(command),
|
||||
|
||||
@@ -102,7 +102,10 @@ def format_approval_prompt(
|
||||
name = req.get("name", "")
|
||||
args = req.get("args", {})
|
||||
if isinstance(args, dict):
|
||||
command = args.get("command", args.get("path", ""))
|
||||
# deepagents 0.7.0's `delete` tool uses `file_path`, not
|
||||
# `command`/`path` — without this fallback the prompt shows
|
||||
# only "delete" with no target.
|
||||
command = args.get("command", args.get("path", args.get("file_path", "")))
|
||||
else:
|
||||
command = ""
|
||||
if command:
|
||||
@@ -217,7 +220,11 @@ def config_auto_approve(action_requests: list[dict]) -> bool:
|
||||
return True
|
||||
|
||||
try:
|
||||
from ..config.settings import HITL_SHELL_TOOLS, load_config
|
||||
from ..config.settings import (
|
||||
HITL_ALWAYS_PROMPT_TOOLS,
|
||||
HITL_SHELL_TOOLS,
|
||||
load_config,
|
||||
)
|
||||
|
||||
cfg = load_config()
|
||||
except Exception:
|
||||
@@ -234,6 +241,8 @@ def config_auto_approve(action_requests: list[dict]) -> bool:
|
||||
|
||||
for req in action_requests:
|
||||
name = req.get("name", "")
|
||||
if name in HITL_ALWAYS_PROMPT_TOOLS:
|
||||
return False
|
||||
if name not in HITL_SHELL_TOOLS:
|
||||
continue
|
||||
args = req.get("args", {})
|
||||
|
||||
@@ -101,6 +101,15 @@ class ApprovalWidget(Widget):
|
||||
self._selected = 0
|
||||
self._option_widgets: list[Static] = []
|
||||
|
||||
@staticmethod
|
||||
def _extract_command(args: dict) -> str:
|
||||
"""Pull the display-worthy target out of a tool's args dict.
|
||||
|
||||
Checks `command`/`path` first, then deepagents 0.7.0's `delete`
|
||||
tool key `file_path` — without it, `delete` shows no target.
|
||||
"""
|
||||
return args.get("command", args.get("path", args.get("file_path", "")))
|
||||
|
||||
def compose(self) -> ComposeResult:
|
||||
self._option_widgets = []
|
||||
count = len(self._action_requests)
|
||||
@@ -115,10 +124,7 @@ class ApprovalWidget(Widget):
|
||||
for req in self._action_requests:
|
||||
name = req.get("name", "")
|
||||
args = req.get("args", {})
|
||||
if isinstance(args, dict):
|
||||
command = args.get("command", args.get("path", ""))
|
||||
else:
|
||||
command = ""
|
||||
command = self._extract_command(args) if isinstance(args, dict) else ""
|
||||
if command:
|
||||
cmd_str = str(command)
|
||||
if len(cmd_str) > _COMMAND_TRUNCATE_LENGTH:
|
||||
|
||||
@@ -21,10 +21,14 @@ from dotenv import dotenv_values, find_dotenv
|
||||
|
||||
# Tools that run shell commands and need manual HITL approval (subject to
|
||||
# shell_allow_list). Single source of truth for every interrupt consumer
|
||||
# (stream/display.py, channels/consumer.py) — keep aligned with the agent's
|
||||
# (stream/display.py, channels/interaction.py) — keep aligned with the agent's
|
||||
# `interrupt_on` set in EvoScientist.py.
|
||||
HITL_SHELL_TOOLS = ("execute", "run_in_background")
|
||||
|
||||
# Armed non-shell destructive tools must always prompt — no allow-list carve-outs
|
||||
# (their args carry paths, not commands). Keep aligned with HITL_INTERRUPT_ON.
|
||||
HITL_ALWAYS_PROMPT_TOOLS = ("delete",)
|
||||
|
||||
|
||||
class MemoryObservationTarget(StrEnum):
|
||||
"""Runtime locations that can receive `record_observation`."""
|
||||
|
||||
@@ -18,6 +18,7 @@ from ... import paths as _paths
|
||||
MEMORY_AGENT_RECURSION_LIMIT = 100
|
||||
MEMORY_MAINTENANCE_EXCLUDED_TOOLS = frozenset(
|
||||
{
|
||||
"delete",
|
||||
"edit_file",
|
||||
"execute",
|
||||
"task",
|
||||
|
||||
@@ -21,7 +21,7 @@ from ._factory import (
|
||||
resolve_memory_agent_paths,
|
||||
)
|
||||
|
||||
_AUTOSKILLS_EXCLUDED_TOOLS = frozenset({"task", "write_todos"})
|
||||
_AUTOSKILLS_EXCLUDED_TOOLS = frozenset({"delete", "task", "write_todos"})
|
||||
|
||||
|
||||
def _autoskills_system_prompt() -> str:
|
||||
|
||||
@@ -34,7 +34,7 @@ from ._factory import (
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_MEMORY_WORKER_EXCLUDED_TOOLS = frozenset(
|
||||
{"execute", "task", "write_file", "write_todos"}
|
||||
{"delete", "execute", "task", "write_file", "write_todos"}
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1185,7 +1185,11 @@ def _resolve_hitl_approval(
|
||||
return [{"type": "approve"} for _ in action_requests]
|
||||
|
||||
# Config-level auto-approve
|
||||
from ..config.settings import HITL_SHELL_TOOLS, load_config
|
||||
from ..config.settings import (
|
||||
HITL_ALWAYS_PROMPT_TOOLS,
|
||||
HITL_SHELL_TOOLS,
|
||||
load_config,
|
||||
)
|
||||
|
||||
cfg = load_config()
|
||||
if cfg.auto_approve:
|
||||
@@ -1203,6 +1207,10 @@ def _resolve_hitl_approval(
|
||||
name = req.get("name", "")
|
||||
args = req.get("args", {})
|
||||
|
||||
if name in HITL_ALWAYS_PROMPT_TOOLS:
|
||||
needs_prompt = True
|
||||
break
|
||||
|
||||
if name not in HITL_SHELL_TOOLS:
|
||||
continue # Only shell-running tools need manual approval
|
||||
|
||||
|
||||
+3
-3
@@ -16,13 +16,13 @@ classifiers = [
|
||||
"Programming Language :: Python :: 3",
|
||||
]
|
||||
dependencies = [
|
||||
"deepagents[quickjs]~=0.6.12",
|
||||
"deepagents[quickjs]~=0.7.0",
|
||||
"langchain>=1.3",
|
||||
"langchain-anthropic>=1.4",
|
||||
"langchain-anthropic>=1.5",
|
||||
"langchain-openai>=1.2",
|
||||
"langchain-deepseek>=1.1",
|
||||
"langchain-nvidia-ai-endpoints>=1.2",
|
||||
"langchain-google-genai>=4.2",
|
||||
"langchain-google-genai>=4.3",
|
||||
"langchain-ollama>=1.1",
|
||||
"langchain-openrouter>=0.2.5",
|
||||
# 0.11.0+ SSE regressions: teardown noise + mid-turn ReadTimeout
|
||||
|
||||
@@ -11,6 +11,7 @@ import pytest
|
||||
|
||||
from EvoScientist import backends, paths
|
||||
from EvoScientist.backends import (
|
||||
AutoskillProposalSandboxBackend,
|
||||
CustomSandboxBackend,
|
||||
MemoryFilesystemBackend,
|
||||
MergedSkillsBackend,
|
||||
@@ -959,6 +960,52 @@ class TestMemoryFilesystemBackend:
|
||||
assert not (workspace / "created.txt").exists()
|
||||
|
||||
|
||||
# === delete blocking (deepagents 0.7.0 recursive delete tool) ===
|
||||
|
||||
|
||||
class TestDeleteBlocked:
|
||||
"""deepagents 0.7.0 adds a recursive delete tool; guarded backends must refuse it."""
|
||||
|
||||
def test_readonly_backend_blocks_delete(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x")
|
||||
backend = ReadOnlyFilesystemBackend(root_dir=str(tmp_path), virtual_mode=True)
|
||||
result = backend.delete("/f.txt")
|
||||
assert result.error is not None
|
||||
assert (tmp_path / "f.txt").exists()
|
||||
|
||||
async def test_readonly_backend_blocks_adelete(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x")
|
||||
backend = ReadOnlyFilesystemBackend(root_dir=str(tmp_path), virtual_mode=True)
|
||||
result = await backend.adelete("/f.txt")
|
||||
assert result.error is not None
|
||||
assert (tmp_path / "f.txt").exists()
|
||||
|
||||
def test_memory_backend_blocks_delete_everywhere(self, tmp_path):
|
||||
profile = tmp_path / "profile"
|
||||
profile.mkdir()
|
||||
(profile / "USER_PROFILE.md").write_text("x")
|
||||
backend = MemoryFilesystemBackend(root_dir=str(tmp_path), virtual_mode=True)
|
||||
result = backend.delete("/profile/USER_PROFILE.md")
|
||||
assert result.error is not None
|
||||
assert (profile / "USER_PROFILE.md").exists()
|
||||
|
||||
def test_autoskill_backend_blocks_delete(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x")
|
||||
backend = AutoskillProposalSandboxBackend(
|
||||
root_dir=str(tmp_path), virtual_mode=True
|
||||
)
|
||||
result = backend.delete("/f.txt")
|
||||
assert result.error is not None
|
||||
assert (tmp_path / "f.txt").exists()
|
||||
|
||||
def test_sandbox_backend_delete_enabled_by_default(self, tmp_path):
|
||||
(tmp_path / "f.txt").write_text("x")
|
||||
backend = CustomSandboxBackend(root_dir=str(tmp_path), virtual_mode=True)
|
||||
result = backend.delete("/f.txt")
|
||||
assert result.error is None
|
||||
assert not (tmp_path / "f.txt").exists()
|
||||
|
||||
|
||||
# === CustomSandboxBackend._resolve_path ===
|
||||
|
||||
|
||||
@@ -1918,3 +1965,46 @@ class TestPlatformQuote:
|
||||
backends._platform_quote(r"C:\path\%TEMP%\file.py")
|
||||
== r"C:\path\%TEMP%\file.py"
|
||||
)
|
||||
|
||||
|
||||
def test_memory_maintenance_excludes_delete_tool():
|
||||
from EvoScientist.memory.agents._factory import MEMORY_MAINTENANCE_EXCLUDED_TOOLS
|
||||
|
||||
assert "delete" in MEMORY_MAINTENANCE_EXCLUDED_TOOLS
|
||||
|
||||
|
||||
def test_autoskill_composite_route_blocks_or_excludes_delete():
|
||||
"""Codex finding: /autoskill-proposals/ routes to a plain FilesystemBackend whose
|
||||
delete works; the agent-level tool exclusion is the guard that must cover it."""
|
||||
from EvoScientist.memory.agents.autoskills import _AUTOSKILLS_EXCLUDED_TOOLS
|
||||
|
||||
assert "delete" in _AUTOSKILLS_EXCLUDED_TOOLS
|
||||
|
||||
|
||||
def test_autoskill_proposals_route_delete_is_not_backend_blocked(tmp_path):
|
||||
"""Documents WHY the tool exclusion above is the enforcement layer: the raw
|
||||
composite backend's /autoskill-proposals/ route has no backend-level delete
|
||||
guard (unlike /memories/ and the default proposal-root sandbox), so a bare
|
||||
`delete("/autoskill-proposals/...")` call still succeeds at the backend level."""
|
||||
from EvoScientist.backends import build_autoskill_agent_backend
|
||||
|
||||
memory_dir = tmp_path / "memories"
|
||||
proposals_dir = tmp_path / "proposals"
|
||||
memory_dir.mkdir()
|
||||
proposals_dir.mkdir()
|
||||
(proposals_dir / "some-skill").mkdir()
|
||||
(proposals_dir / "some-skill" / "SKILL.md").write_text("x", encoding="utf-8")
|
||||
|
||||
backend = build_autoskill_agent_backend(
|
||||
memory_dir=memory_dir, proposals_dir=proposals_dir
|
||||
)
|
||||
result = backend.delete("/autoskill-proposals/some-skill")
|
||||
|
||||
assert result.error is None
|
||||
assert not (proposals_dir / "some-skill").exists()
|
||||
|
||||
|
||||
def test_memory_worker_excludes_delete_tool():
|
||||
from EvoScientist.memory.agents.memory_worker import _MEMORY_WORKER_EXCLUDED_TOOLS
|
||||
|
||||
assert "delete" in _MEMORY_WORKER_EXCLUDED_TOOLS
|
||||
|
||||
@@ -329,6 +329,37 @@ class TestResolveHitlApproval:
|
||||
finally:
|
||||
disp._session_auto_approve = original
|
||||
|
||||
def test_delete_only_request_prompts_not_auto_approved(self):
|
||||
"""delete is armed in HITL_INTERRUPT_ON but has no `command` arg, so the
|
||||
shell_allow_list carve-out must never silently clear it (C1)."""
|
||||
import EvoScientist.stream.display as disp
|
||||
from EvoScientist.stream.display import _resolve_hitl_approval
|
||||
|
||||
original = disp._session_auto_approve
|
||||
try:
|
||||
disp._session_auto_approve = False
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.auto_approve = False
|
||||
mock_cfg.shell_allow_list = ""
|
||||
with patch(
|
||||
"EvoScientist.config.settings.load_config", return_value=mock_cfg
|
||||
):
|
||||
with patch(
|
||||
"EvoScientist.stream.display._prompt_hitl_approval"
|
||||
) as mock_prompt:
|
||||
mock_prompt.return_value = [{"type": "approve"}]
|
||||
result = _resolve_hitl_approval(
|
||||
{
|
||||
"action_requests": [
|
||||
{"name": "delete", "args": {"file_path": "/f.txt"}}
|
||||
],
|
||||
}
|
||||
)
|
||||
assert result == [{"type": "approve"}]
|
||||
mock_prompt.assert_called_once() # must be prompted, not auto-approved
|
||||
finally:
|
||||
disp._session_auto_approve = original
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Config fields
|
||||
@@ -556,6 +587,22 @@ class TestConsumerHitlHelpers:
|
||||
)
|
||||
assert result is True
|
||||
|
||||
def test_should_auto_approve_delete_not_cleared(self):
|
||||
"""delete has no `command` arg, so shell_allow_list must never clear it
|
||||
the way it clears execute (C1)."""
|
||||
from EvoScientist.channels.interaction import config_auto_approve
|
||||
|
||||
mock_cfg = MagicMock()
|
||||
mock_cfg.auto_approve = False
|
||||
mock_cfg.shell_allow_list = "ls,python"
|
||||
with patch("EvoScientist.config.settings.load_config", return_value=mock_cfg):
|
||||
result = config_auto_approve(
|
||||
[
|
||||
{"name": "delete", "args": {"file_path": "/f.txt"}},
|
||||
]
|
||||
)
|
||||
assert result is False
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Channel reply-interception mechanism (channel.py PendingReplyRegistry)
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""HITL interrupt policy: which tools require approval on attended agents."""
|
||||
|
||||
|
||||
def test_hitl_interrupt_on_arms_expected_tools():
|
||||
from EvoScientist.EvoScientist import HITL_INTERRUPT_ON
|
||||
|
||||
assert HITL_INTERRUPT_ON == {
|
||||
"execute": True,
|
||||
"run_in_background": True,
|
||||
"schedule_task": True,
|
||||
"delete": True,
|
||||
}
|
||||
@@ -252,6 +252,15 @@ class TestApprovalPromptFormat:
|
||||
got = I.format_approval_prompt([{"name": "ask_user", "args": {}}])
|
||||
assert "ask_user" in got
|
||||
|
||||
def test_delete_shows_file_path(self):
|
||||
# deepagents 0.7.0's `delete` tool uses `file_path`, not `command`/
|
||||
# `path` — the prompt must still show the target, not just the name.
|
||||
got = I.format_approval_prompt(
|
||||
[{"name": "delete", "args": {"file_path": "/results/run-3"}}]
|
||||
)
|
||||
assert "delete" in got
|
||||
assert "/results/run-3" in got
|
||||
|
||||
def test_metadata_no_buttons(self):
|
||||
assert I.approval_prompt_metadata({"k": "v"}, with_buttons=False) == {"k": "v"}
|
||||
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
"""deepagents 0.7.0 merges caller middleware into its default stack by `.name`:
|
||||
a name match silently REPLACES the built-in. None of EvoScientist's middleware
|
||||
may collide unintentionally. TodoListMiddleware is deliberately absent from the
|
||||
forbidden set: we pass it on purpose and replacing a profile-added instance
|
||||
(e.g. the Codex harness profile's) with our identical one is desired dedup.
|
||||
"""
|
||||
|
||||
DEEPAGENTS_BASE_STACK_NAMES = {
|
||||
"SkillsMiddleware",
|
||||
"FilesystemMiddleware",
|
||||
"SubAgentMiddleware",
|
||||
"SummarizationMiddleware",
|
||||
"PatchToolCallsMiddleware",
|
||||
"AsyncSubAgentMiddleware",
|
||||
"AnthropicPromptCachingMiddleware",
|
||||
}
|
||||
|
||||
|
||||
def test_no_name_collision_with_deepagents_base_stack():
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
|
||||
ours = {m.name for m in _get_default_middleware()}
|
||||
assert not ours & DEEPAGENTS_BASE_STACK_NAMES
|
||||
|
||||
ours_async = {m.name for m in _get_default_middleware(for_async_subagent=True)}
|
||||
assert not ours_async & DEEPAGENTS_BASE_STACK_NAMES
|
||||
@@ -683,12 +683,5 @@ class TestDelegationPromptSummarize:
|
||||
"""The upstream TASK_TOOL_DESCRIPTION already instructs the LLM to summarize."""
|
||||
from deepagents.middleware.subagents import TASK_TOOL_DESCRIPTION
|
||||
|
||||
assert "not visible to the user" in TASK_TOOL_DESCRIPTION
|
||||
assert "summary of the result" in TASK_TOOL_DESCRIPTION
|
||||
|
||||
def test_framework_task_system_prompt_contains_reconcile_step(self):
|
||||
"""The upstream TASK_SYSTEM_PROMPT includes a reconcile/synthesize step."""
|
||||
from deepagents.middleware.subagents import TASK_SYSTEM_PROMPT
|
||||
|
||||
assert "Reconcile" in TASK_SYSTEM_PROMPT
|
||||
assert "synthesize" in TASK_SYSTEM_PROMPT.lower()
|
||||
assert "not shown to the user" in TASK_TOOL_DESCRIPTION
|
||||
assert "relay a summary yourself" in TASK_TOOL_DESCRIPTION
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
"""deepagents 0.7.0 made TodoListMiddleware opt-in; EvoScientist opts back in everywhere."""
|
||||
|
||||
|
||||
def _names(middleware_list):
|
||||
return {m.name for m in middleware_list}
|
||||
|
||||
|
||||
def test_default_middleware_includes_todos():
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
|
||||
assert "TodoListMiddleware" in _names(_get_default_middleware())
|
||||
|
||||
|
||||
def test_async_subagent_middleware_includes_todos():
|
||||
from EvoScientist.EvoScientist import _get_default_middleware
|
||||
|
||||
assert "TodoListMiddleware" in _names(
|
||||
_get_default_middleware(for_async_subagent=True)
|
||||
)
|
||||
|
||||
|
||||
def test_injected_subagent_middleware_includes_todos(tmp_path):
|
||||
from EvoScientist.EvoScientist import _inject_subagent_middleware
|
||||
|
||||
subs = [{"name": "research-agent"}]
|
||||
_inject_subagent_middleware(subs, workspace_dir=str(tmp_path))
|
||||
assert "TodoListMiddleware" in _names(subs[0]["middleware"])
|
||||
@@ -285,6 +285,38 @@ class TestToolCallWidget(unittest.TestCase):
|
||||
assert w._should_collapse() is True
|
||||
|
||||
|
||||
@unittest.skipUnless(_has_textual, "textual not installed")
|
||||
class TestApprovalWidgetCommandExtraction(unittest.TestCase):
|
||||
"""ApprovalWidget's compact-line command extraction.
|
||||
|
||||
``compose()`` itself needs a mounted App (it opens a Container context
|
||||
manager), so — per the widget's existing style of unit-testing its
|
||||
private helpers directly (see TestToolCallWidget) — these exercise the
|
||||
extraction helper `compose()` calls rather than the full render.
|
||||
"""
|
||||
|
||||
def test_extract_command_prefers_command(self):
|
||||
from EvoScientist.cli.widgets.approval_widget import ApprovalWidget
|
||||
|
||||
assert ApprovalWidget._extract_command({"command": "ls", "path": "/x"}) == "ls"
|
||||
|
||||
def test_extract_command_falls_back_to_path(self):
|
||||
from EvoScientist.cli.widgets.approval_widget import ApprovalWidget
|
||||
|
||||
assert ApprovalWidget._extract_command({"path": "/out.txt"}) == "/out.txt"
|
||||
|
||||
def test_extract_command_falls_back_to_file_path(self):
|
||||
# deepagents 0.7.0's `delete` tool uses `file_path`, not `command`/
|
||||
# `path` — without this fallback the approval prompt shows only the
|
||||
# tool name and hides the deletion target.
|
||||
from EvoScientist.cli.widgets.approval_widget import ApprovalWidget
|
||||
|
||||
assert (
|
||||
ApprovalWidget._extract_command({"file_path": "/results/run-3"})
|
||||
== "/results/run-3"
|
||||
)
|
||||
|
||||
|
||||
@unittest.skipUnless(_has_textual, "textual not installed")
|
||||
class TestSubAgentWidget(unittest.TestCase):
|
||||
"""SubAgentWidget construction and name display."""
|
||||
|
||||
@@ -186,7 +186,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anthropic"
|
||||
version = "0.117.0"
|
||||
version = "0.120.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -198,9 +198,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/41/0d/8f71d535edb0d438f023bd825fb65f67c14fa88a2bd6b75f292a58a63de4/anthropic-0.117.0.tar.gz", hash = "sha256:98107f2b76439641e0ae2a1754087534b8f178dbab99d6eb1bc4b7bc8c744496", size = 989933, upload-time = "2026-07-16T19:36:13.07Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d7/10/4ca013cb166f226bd89e0aeb0fcaff94f45ddf716d4925ce89475d3c587b/anthropic-0.120.2.tar.gz", hash = "sha256:9722efc10c27a30a69f5338ddacdb35bc6a64297a4e4ba729bf83af873d5fb3a", size = 1008421, upload-time = "2026-07-28T17:38:26.986Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/4c/917d21d6619a4475cdafc6d13a69fdb3b901ddac57e76caca5a25c117b6d/anthropic-0.117.0-py3-none-any.whl", hash = "sha256:451a0a6905f11dff7663d13e4ee5dbf909eb8942b1d049803c7b937a13ac47ec", size = 998327, upload-time = "2026-07-16T19:36:11.225Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/af/0f5db57b9397a0f3b7fc204cbef143401a7cadaf982330f97f1ce3d39f34/anthropic-0.120.2-py3-none-any.whl", hash = "sha256:0f0bc2b381dc0eb41c8d886b815d79c2041cd2374f83aed36f574b6dc9c579c1", size = 1022851, upload-time = "2026-07-28T17:38:25.466Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -859,7 +859,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "deepagents"
|
||||
version = "0.6.12"
|
||||
version = "0.7.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "langchain" },
|
||||
@@ -869,9 +869,9 @@ dependencies = [
|
||||
{ name = "langsmith" },
|
||||
{ name = "wcmatch" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e0/db/a6acdc72a9e90c3f07ed10de35c951734a02d4facb693bb59684ad368801/deepagents-0.6.12.tar.gz", hash = "sha256:1f281c0bc5a63132f62e2ee345c1dc593b23188da6e23016401f6879fbe54b5f", size = 211364, upload-time = "2026-06-25T17:26:52.775Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/6b/f61d30d45e25e8dc720b4928da029162329e8f22173bbbe190083e19a75e/deepagents-0.7.0.tar.gz", hash = "sha256:36c49bced5de9ef0deb1e9c05a449adf931706e25ba5f99449d7436eccbc694b", size = 263583, upload-time = "2026-07-29T16:27:29.241Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/98/49/af7219b3c13520fee047bb807cfaefba17f8e4584c551d946773589a4f08/deepagents-0.6.12-py3-none-any.whl", hash = "sha256:28b8fa0119ca0a689e3e18e288c4634e4046062acfc87a1cb34289d3af3a1c88", size = 236120, upload-time = "2026-06-25T17:26:51.736Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/0a/ee3a0518a90bae0ee3f48e23061fe185804d0d3476cb75f21ab834f4698c/deepagents-0.7.0-py3-none-any.whl", hash = "sha256:c0ceb3c12f44638cdb81ea95652d6d6fa8ec76a2372ae8016aa2dd9a493762b1", size = 289723, upload-time = "2026-07-29T16:27:28.178Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -1060,16 +1060,16 @@ requires-dist = [
|
||||
{ name = "certifi", marker = "extra == 'wechat'", specifier = ">=2024.0" },
|
||||
{ name = "cryptography", marker = "extra == 'all-channels'", specifier = ">=41.0" },
|
||||
{ name = "cryptography", marker = "extra == 'qq'", specifier = ">=41.0" },
|
||||
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.6.12" },
|
||||
{ name = "deepagents", extras = ["quickjs"], specifier = "~=0.7.0" },
|
||||
{ name = "discord-py", marker = "extra == 'all-channels'", specifier = ">=2.3" },
|
||||
{ name = "discord-py", marker = "extra == 'discord'", specifier = ">=2.3" },
|
||||
{ name = "faster-whisper", marker = "extra == 'stt'", specifier = ">=1.0" },
|
||||
{ name = "filelock", specifier = ">=3.16" },
|
||||
{ name = "httpx", specifier = ">=0.28" },
|
||||
{ name = "langchain", specifier = ">=1.3" },
|
||||
{ name = "langchain-anthropic", specifier = ">=1.4" },
|
||||
{ name = "langchain-anthropic", specifier = ">=1.5" },
|
||||
{ name = "langchain-deepseek", specifier = ">=1.1" },
|
||||
{ name = "langchain-google-genai", specifier = ">=4.2" },
|
||||
{ name = "langchain-google-genai", specifier = ">=4.3" },
|
||||
{ name = "langchain-mcp-adapters", specifier = ">=0.2" },
|
||||
{ name = "langchain-nvidia-ai-endpoints", specifier = ">=1.2" },
|
||||
{ name = "langchain-ollama", specifier = ">=1.1" },
|
||||
@@ -2038,21 +2038,21 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-anthropic"
|
||||
version = "1.4.8"
|
||||
version = "1.5.3"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anthropic" },
|
||||
{ name = "langchain-core" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/98/22/40ab129b08329ca295b391aa1d48267692b42594757084c6918e22b655ac/langchain_anthropic-1.4.8.tar.gz", hash = "sha256:c76891b2044d56105ff13c106ed12650637b53bd598a4bdf15b4796eefa2a4ec", size = 708524, upload-time = "2026-06-26T21:28:46.916Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/70/5b/25ffc9cc66a8260db9e2da2836ee33b260dfbb874d1ea2f99d70023f2b1d/langchain_anthropic-1.5.3.tar.gz", hash = "sha256:48a7e4fc3856c42f9dcb4396ba112afcdd8dee21b70a402bc43283c970de3941", size = 713962, upload-time = "2026-07-28T17:02:55.903Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/14/746235c4da89d9bc6a608c5f489f628e03feb8f697195c146e452c8f23c8/langchain_anthropic-1.4.8-py3-none-any.whl", hash = "sha256:778e9301b6fd517824f76ec1776975ce8add97a1f6a36c50ae3c2f4b03a66f7f", size = 52366, upload-time = "2026-06-26T21:28:45.535Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/19/584ab8517718f48bb411d051be713ebd8b58351741f8a4ca23d6a305cb1b/langchain_anthropic-1.5.3-py3-none-any.whl", hash = "sha256:b1b72b7c9e4bf5c044660629ebbcb79cef18c140fdc80174750eb86de13ec8bc", size = 54034, upload-time = "2026-07-28T17:02:54.594Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "langchain-core"
|
||||
version = "1.4.9"
|
||||
version = "1.5.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "jsonpatch" },
|
||||
@@ -2065,9 +2065,9 @@ dependencies = [
|
||||
{ name = "typing-extensions" },
|
||||
{ name = "uuid-utils" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2a/b9/e937d0a90b26540bff07e7a7c64349f3b29c2dcc36257cd1cd3fdce17f2a/langchain_core-1.4.9.tar.gz", hash = "sha256:f8078901145bed0466755277500a5a22822a7b628808c4c0a28d4fc88895fcf2", size = 967294, upload-time = "2026-07-08T20:06:54.191Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6e/58/3ad53096eee1e07728e8219ded30ae308b0f2b8b7b26b8ebb6371917c0f5/langchain_core-1.5.2.tar.gz", hash = "sha256:2d13ab35b42eec63d4669a483776b8cdd778ee764107149369fb369d84c08c41", size = 972322, upload-time = "2026-07-28T16:38:37.977Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/84/70/ade2fada52772798ef815b6352b59e71b116aa0c32c3aef5be3dc2cbed12/langchain_core-1.4.9-py3-none-any.whl", hash = "sha256:28e3909e2a10cc81504952d795ac0a9e014c0018121ef89d48dd396fa09ec624", size = 558293, upload-time = "2026-07-08T20:06:52.382Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/e4/024e402a65f5ee2eb6ae9667b5ffc5f08776cb4e712f5a11ee1997d56083/langchain_core-1.5.2-py3-none-any.whl", hash = "sha256:a687dd7c3b22c6c1294e1c1eeb61fb6f3a308e6015a1d75b576b3836ad5b5aed", size = 561643, upload-time = "2026-07-28T16:38:36.38Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2085,7 +2085,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langchain-google-genai"
|
||||
version = "4.2.7"
|
||||
version = "4.3.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "filetype" },
|
||||
@@ -2093,9 +2093,9 @@ dependencies = [
|
||||
{ name = "langchain-core" },
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/0c/bc60dabc362ca7c6ffe8c4bcc2f724c7e566b43eb230cee51419f88f784c/langchain_google_genai-4.2.7.tar.gz", hash = "sha256:03b1463ffe4d42435f43c7870467f2215f684bb46400d2543435d10157c80ac7", size = 281605, upload-time = "2026-07-06T13:51:58.724Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/79/2f/e03b63ad3a61fd1aa479bbc0f3df5d27abb8f9159d111cba96629df844ef/langchain_google_genai-4.3.2.tar.gz", hash = "sha256:6471769a4463fedb10d2d19a9b56c31de1cde505edf7fffd8cdbf98af8c1d7da", size = 286018, upload-time = "2026-07-27T16:27:39.214Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/f9/d73d1e712591723aaddb7a7b1e94978cd2320c29acfe0d26b6169a2f26f0/langchain_google_genai-4.2.7-py3-none-any.whl", hash = "sha256:0d9c388d0e6c629718fca6abb19c6fdca728a9a7873d0324c1ec821288b5b571", size = 70702, upload-time = "2026-07-06T13:51:57.499Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/01/cb/4a2eb187b108a240d57cf8dcf67e818ca75365f769444bf5716e2823cd98/langchain_google_genai-4.3.2-py3-none-any.whl", hash = "sha256:f3b1c09b264612fd1735a9590987bfa0cccca0bc0111691543decb5a03b8667d", size = 72770, upload-time = "2026-07-27T16:27:38.052Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2355,7 +2355,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "langsmith"
|
||||
version = "0.10.6"
|
||||
version = "0.10.12"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -2373,9 +2373,9 @@ dependencies = [
|
||||
{ name = "xxhash" },
|
||||
{ name = "zstandard" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/93/7e/05db6baaed1383386afd34e5d8fbf0b6cff1e87ed6b5c98d444511af6490/langsmith-0.10.6.tar.gz", hash = "sha256:6ec5b26bb57ee41363c07eb2884f24d69910b8d6bc26cae1f9e367af15259f1b", size = 4729044, upload-time = "2026-07-17T17:14:02.897Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/c3/7203f18c6ca0ff4c65087fe8694511fadc8a0037c37010e43d25b9ecacd8/langsmith-0.10.12.tar.gz", hash = "sha256:e27cace529a5c54546ecdc2d3fa7af6d8a84a5b9333e99b497f47cafffa16ef5", size = 4749918, upload-time = "2026-07-29T14:22:52.335Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/d5/2d8d84f5330aa773bae18644aaf7e3288b2f082e31640080784a7d908a24/langsmith-0.10.6-py3-none-any.whl", hash = "sha256:094285b755224f49fd396c3672b0f747294c7b8d9e8278a81d76b487b5cfdb56", size = 661257, upload-time = "2026-07-17T17:14:01.118Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1e/f8/de3b66a9b9212ed41541814d8e062909c009837e650a4c2e43425ee1b43c/langsmith-0.10.12-py3-none-any.whl", hash = "sha256:d280fff7a03ecf612c3fdb172bf716467cfb9c0b9d90661b3523c2d8ee1b9a6e", size = 681040, upload-time = "2026-07-29T14:22:50.372Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
|
||||
Reference in New Issue
Block a user