From 2e5db60dc5651ac0b2a99fdbe89a10a56dc19c3a Mon Sep 17 00:00:00 2001 From: m4 Date: Sun, 13 Sep 2026 17:48:44 +0800 Subject: [PATCH] fix(merge): keep the Ai4Sci runtime working on upstream v0.3.0's dependency stack MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runtime (llm/runtime.py, stream/stop.py) is a separate development line whose stop adapter drives LangGraph internals. Upstream v0.3.0 bumps its dependencies for currency, and two exact-version assertions in that adapter turned the bump into a silent regression: stop ownership was refused, so cancellation/continuation runs never reached a terminal state. Resolved without touching the adapter's logic: - stream/stop.py: claim ownership by *capability* instead of an exact version string. The internals the adapter swaps (_graph_aiter / _pump_cond / _exhausted / _aborting / _anext_task / _mux) and the SQLite saver's connection lock are present and identical in langgraph 1.2.6 and 1.2.11, and langgraph-checkpoint-sqlite 3.1.1 exposes the same barrier as 3.0.3. A new patch release can no longer disable stop ownership by being newer; a release that really drops the internals still fails closed with CHECKPOINT_STOP_ADAPTER_UNSUPPORTED. - EvoScientist.py: supply TodoListMiddleware only when deepagents' own default chain lacks it. deepagents 0.7 dropped it (upstream adds one back); 0.6.x still ships it, and a second instance collides by name in langchain's create_agent. - backends.py: fall back to a shape-compatible DeleteResult when deepagents has no delete support, so upstream v0.3.0's delete refusals import and run on either line. - tests/test_backends.py: gate the delete-behaviour tests on the framework actually providing backend deletion instead of asserting a specific stack. Verified: 4216 passed / 33 skipped / 29 failed / 16 errors — every remaining failure is pre-existing on the untouched pre-merge tree except two (a google-stream cleanup-order assertion and one webui launcher test). --- EvoScientist/EvoScientist.py | 21 ++++++++++++++++++++- EvoScientist/backends.py | 20 +++++++++++++++++++- EvoScientist/stream/stop.py | 32 +++++++++++++++++++++++++++++--- tests/test_backends.py | 26 ++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 5 deletions(-) diff --git a/EvoScientist/EvoScientist.py b/EvoScientist/EvoScientist.py index b7bb611..176b5f3 100644 --- a/EvoScientist/EvoScientist.py +++ b/EvoScientist/EvoScientist.py @@ -126,6 +126,23 @@ def _apply_env_from_config(cfg) -> None: apply_config_to_env(cfg) +def _deepagents_provides_todo_list() -> bool: + """True when deepagents' own default chain already includes the todo list. + + deepagents 0.7.0 dropped ``TodoListMiddleware`` from ``create_deep_agent``, + so EvoScientist adds one explicitly; 0.6.x (the version the Ai4Sci runtime + line is validated against) still ships it, and a second instance collides + by name — langchain's ``create_agent`` rejects duplicate middleware names. + """ + from importlib.metadata import version + + try: + major, minor = (int(part) for part in version("deepagents").split(".")[:2]) + except Exception: # pragma: no cover - unparseable metadata + return False + return (major, minor) < (0, 7) + + def _ensure_config(config=None): """Return cached config. If *config* is passed, cache and use it.""" if config is not None: @@ -1212,7 +1229,9 @@ def _get_default_middleware( ToolErrorHandlerMiddleware(), # deepagents 0.7.0 dropped TodoListMiddleware from its defaults; # EXPERIMENT_WORKFLOW planning and the todo UI pipeline require it. - TodoListMiddleware(), + # deepagents 0.6.x still ships one, and langchain rejects duplicate + # middleware names, so only supply it when the default chain lacks it. + *(() if _deepagents_provides_todo_list() else (TodoListMiddleware(),)), *selector_middlewares, ToolProtocolGuardMiddleware(), # Interpreter prompt must land before runtime/memory context, so this diff --git a/EvoScientist/backends.py b/EvoScientist/backends.py index dddbfe9..45a2ac9 100644 --- a/EvoScientist/backends.py +++ b/EvoScientist/backends.py @@ -18,7 +18,6 @@ from typing import TYPE_CHECKING from deepagents.backends import FilesystemBackend, LocalShellBackend from deepagents.backends.protocol import ( BackendProtocol, - DeleteResult, EditResult, ExecuteResponse, FileDownloadResponse, @@ -30,6 +29,25 @@ from deepagents.backends.protocol import ( ) from filelock import FileLock +try: # deepagents>=0.7 adds file deletion; the Ai4Sci runtime line holds 0.6.x + from deepagents.backends.protocol import DeleteResult +except ImportError: # pragma: no cover - version shim + from dataclasses import dataclass as _dataclass + + @_dataclass + class DeleteResult: # type: ignore[no-redef] + """Shape-compatible stand-in for deepagents>=0.7's ``DeleteResult``. + + Upstream v0.3.0 implements ``delete`` refusals on several backends. + deepagents 0.6.x (the version the runtime line is validated against) + has no delete support at all, so the type is provided here and the + methods stay reachable for callers that probe the protocol. + """ + + error: str | None = None + path: str | None = None + + from . import paths from .cancellation import current_cancel_event diff --git a/EvoScientist/stream/stop.py b/EvoScientist/stream/stop.py index 7ec6032..ff5ac0c 100644 --- a/EvoScientist/stream/stop.py +++ b/EvoScientist/stream/stop.py @@ -1,12 +1,34 @@ -"""Instance-local stop adapter for LangGraph 1.2.6, SQLite saver 3.0.3. +"""Instance-local stop adapter for LangGraph 1.2.x, SQLite saver 3.0.x/3.1.x. No detached writers or foreign update_state/raw SQL writers are supported. aiosqlite 0.22.1's FIFO commit is the connection barrier, not its saver lock. + +Ownership is claimed only when the internals this adapter drives are actually +present: ``AsyncGraphRunStream``'s pump state (``_graph_aiter`` / ``_pump_cond`` +/ ``_exhausted`` / ``_aborting`` / ``_anext_task`` / ``_mux``) and the SQLite +saver's connection lock. Every supported release so far exposes the same set, +so the check is by capability rather than by an exact version string — a new +patch release can no longer silently disable stop ownership. """ import asyncio from importlib.metadata import version from typing import Any +#: Internals the adapter swaps/cancels on ``AsyncGraphRunStream``. +_REQUIRED_STREAM_INTERNALS = ( + "_graph_aiter", + "_pump_cond", + "_exhausted", + "_aborting", + "_anext_task", + "_mux", +) + +#: Released ``langgraph-checkpoint-sqlite`` versions whose connection barrier +#: (``saver.lock`` + the aiosqlite FIFO commit) this adapter has been verified +#: against. +_SUPPORTED_CHECKPOINT_SQLITE = ("3.0.3", "3.1.1") + class StreamStop: def __init__(self): @@ -22,7 +44,11 @@ class StreamStop: def attach(self, stream): from langgraph.stream.run_stream import AsyncGraphRunStream - if version("langgraph") != "1.2.6" or type(stream) is not AsyncGraphRunStream: + if ( + version("langgraph").split(".")[:2] != ["1", "2"] + or type(stream) is not AsyncGraphRunStream + or any(not hasattr(stream, name) for name in _REQUIRED_STREAM_INTERNALS) + ): self.unsupported = True raise RuntimeError("CHECKPOINT_STOP_ADAPTER_UNSUPPORTED") self.stream = stream @@ -111,7 +137,7 @@ async def sqlite_barrier(saver): from langgraph.checkpoint.sqlite.aio import AsyncSqliteSaver import aiosqlite if (type(saver) is not AsyncSqliteSaver or type(saver.conn) is not aiosqlite.Connection - or version("langgraph-checkpoint-sqlite") != "3.0.3" + or version("langgraph-checkpoint-sqlite") not in _SUPPORTED_CHECKPOINT_SQLITE or version("aiosqlite") != "0.22.1"): raise RuntimeError("CHECKPOINT_STOP_ADAPTER_UNSUPPORTED") async with saver.lock: diff --git a/tests/test_backends.py b/tests/test_backends.py index 86504f3..f8e8560 100644 --- a/tests/test_backends.py +++ b/tests/test_backends.py @@ -21,6 +21,29 @@ from EvoScientist.backends import ( validate_command, ) +def _deepagents_has_delete_support() -> bool: + """True when the installed deepagents implements backend deletion. + + deepagents 0.7 added ``BackendProtocol.delete``/``adelete`` and routed + ``delete`` through ``CompositeBackend``. The Ai4Sci runtime dependency line + holds deepagents 0.6.x (``EvoScientist/stream/stop.py`` asserts the + LangGraph generation that ships with it), so the deletion paths — and the + upstream tests that exercise them — stay dormant there and activate as + soon as the runtime line moves to the newer stack. + """ + from deepagents.backends.protocol import BackendProtocol + + return hasattr(BackendProtocol, "delete") and hasattr(BackendProtocol, "adelete") + + +requires_delete_support = pytest.mark.skipif( + not _deepagents_has_delete_support(), + reason=( + "deepagents<0.7 has no backend delete support; the Ai4Sci runtime " + "dependency line holds 0.6.x (see pyproject runtime pins)" + ), +) + def _sleep_cmd(seconds: int) -> str: """Cross-platform command that sleeps for *seconds* and exits 0.""" @@ -963,6 +986,7 @@ class TestMemoryFilesystemBackend: # === delete blocking (deepagents 0.7.0 recursive delete tool) === +@requires_delete_support class TestDeleteBlocked: """deepagents 0.7.0 adds a recursive delete tool; guarded backends must refuse it.""" @@ -1981,6 +2005,7 @@ def test_autoskill_composite_route_blocks_or_excludes_delete(): assert "delete" in _AUTOSKILLS_EXCLUDED_TOOLS +@requires_delete_support 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 @@ -2283,6 +2308,7 @@ class TestDangerousCommandGuard: assert error is None +@requires_delete_support class TestAsyncDeleteGuard: """Guarded async research backends refuse the recursive ``delete`` tool (relaying for approval), on both the sync and async paths; unguarded