From 6e2c94c6225a1d00a8153791f2d2eef1f19eae6c Mon Sep 17 00:00:00 2001 From: DABH Date: Wed, 16 Sep 2026 01:25:56 -0500 Subject: [PATCH 1/6] Bump deepagents to 0.7 and mirror optional backend ops in TemporalBackend The deepagents extra now requires deepagents>=0.7.12,<0.8 (0.7.14 exists but is inside the two-week exclude-newer window). deepagents 0.7 needs langsmith>=0.11.2, so the langsmith extra allows langsmith<0.13; the RunTree.patch overrides follow langsmith 0.12's Optional exclude_inputs so LangSmith's own default applies on every supported version. deepagents 0.7 made delete an optional protocol op and checks support on the wrapper's class (type(backend).delete against the protocol default), which raised AttributeError for TemporalBackend's __getattr__-based ops. The I/O ops are now real class-level methods, and the wrapper picks a subclass mirroring the inner backend's delete support, so deepagents offers the delete tool exactly when the unwrapped backend would. --- CHANGELOG.md | 9 + pyproject.toml | 8 +- temporalio/contrib/deepagents/_tools.py | 78 +++++- temporalio/contrib/langsmith/_interceptor.py | 11 +- tests/contrib/deepagents/test_backends.py | 42 +++- uv.lock | 240 ++++++++++++++----- 6 files changed, 307 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 87dafcbe2..5ca40d33a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,12 +22,21 @@ to include examples, links to docs, or any other relevant information. ### Changed +- The `deepagents` extra now requires `deepagents>=0.7.12,<0.8` (was `<0.7`). Because + deepagents 0.7 requires `langsmith>=0.11.2`, the `langsmith` extra now allows + `langsmith<0.13` (was `<0.9`). + ### Deprecated ### :boom: Breaking Changes ### Fixed +- `temporalio.contrib.deepagents.TemporalBackend` mirrors the wrapped backend's optional + `delete` support at class level, which deepagents 0.7 checks before offering the delete + tool; `delete` / `adelete` on a delete-capable backend run as `deepagents.backend_op` + Activities like every other op. + ### Security ## [1.33.0] - 2026-09-14 diff --git a/pyproject.toml b/pyproject.toml index b7077785f..2cbccee9f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -31,9 +31,9 @@ pydantic = ["pydantic>=2.0.0,<3"] openai-agents = ["openai-agents>=0.19.2,<0.20", "mcp>=1.9.4, <2"] google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"] langgraph = ["langgraph>=1.1.0"] -langsmith = ["langsmith>=0.7.34,<0.9"] +langsmith = ["langsmith>=0.7.34,<0.13"] deepagents = [ - "deepagents>=0.6.12,<0.7; python_version >= '3.11'", + "deepagents>=0.7.12,<0.8; python_version >= '3.11'", "langchain>=1.3.11,<2; python_version >= '3.11'", "langchain-core>=1.4.8,<2; python_version >= '3.11'", ] @@ -96,8 +96,8 @@ dev = [ "pytest-xdist>=3.6,<4", "moto[s3,server]>=5", "langgraph>=1.1.0", - "langsmith>=0.7.34,<0.9", - "deepagents>=0.6.12,<0.7; python_version >= '3.11'", + "langsmith>=0.7.34,<0.13", + "deepagents>=0.7.12,<0.8; python_version >= '3.11'", "langchain>=1.3.11,<2; python_version >= '3.11'", "langchain-core>=1.4.8,<2; python_version >= '3.11'", "langchain-anthropic>=1.4.7; python_version >= '3.11'", diff --git a/temporalio/contrib/deepagents/_tools.py b/temporalio/contrib/deepagents/_tools.py index b0d8d705f..1c992b647 100644 --- a/temporalio/contrib/deepagents/_tools.py +++ b/temporalio/contrib/deepagents/_tools.py @@ -386,6 +386,7 @@ def _is_coroutine(fn: Any) -> bool: "adownload_files": "download_files", "aupload_files": "upload_files", "aexecute": "execute", + "adelete": "delete", } _original_backend_async_defaults: dict[str, Any] = {} @@ -462,6 +463,7 @@ def uninstall_backend_async_patch() -> None: "download_files", "upload_files", "execute", + "delete", # Async twins (what FilesystemMiddleware actually calls). "als", "als_info", @@ -475,8 +477,15 @@ def uninstall_backend_async_patch() -> None: "adownload_files", "aupload_files", "aexecute", + "adelete", ) +# ``delete`` is optional in the deepagents protocol, and deepagents decides +# whether a backend has it from the CLASS (``type(backend).delete`` compared +# against the protocol default), not the instance. The wrapper therefore has to +# mirror the inner backend at class level; see :func:`_wrapper_class_for`. +_OPTIONAL_OPS = frozenset({"delete", "adelete"}) + class TemporalBackend: """Route a real-I/O backend's operations through Temporal activities. @@ -491,6 +500,18 @@ class TemporalBackend: metadata / configuration the agent reads (but that does no I/O) still works. """ + def __new__( + cls, + inner: Any, + *, + activity_options: Mapping[str, Any] | None = None, + ) -> "TemporalBackend": + """Pick the wrapper class that mirrors ``inner``'s optional capabilities.""" + target: type[TemporalBackend] = ( + _wrapper_class_for(inner) if cls is TemporalBackend else cls + ) + return object.__new__(target) + def __init__( self, inner: Any, @@ -533,14 +554,55 @@ async def _dispatch(self, op: str, *args: Any, **kwargs: Any) -> Any: return _serde.load_backend_result(output.result) def __getattr__(self, name: str) -> Any: - """Bound-method access for a known I/O op returns an activity dispatcher. + """Forward anything that is not an I/O op to the inner backend unchanged.""" + return getattr(self._inner, name) - Everything else forwards to the inner backend unchanged. - """ - if name in _BACKEND_OPS: - async def _op(*args: Any, **kwargs: Any) -> Any: - return await self._dispatch(name, *args, **kwargs) +def _make_backend_op(name: str) -> Callable[..., Any]: + async def _op(self: TemporalBackend, *args: Any, **kwargs: Any) -> Any: + return await self._dispatch(name, *args, **kwargs) - return _op - return getattr(self._inner, name) + _op.__name__ = _op.__qualname__ = name + return _op + + +# The I/O ops are real class-level methods, not ``__getattr__`` products, so +# class-based capability checks in deepagents see them. +for _op_name in _BACKEND_OPS: + if _op_name not in _OPTIONAL_OPS: + setattr(TemporalBackend, _op_name, _make_backend_op(_op_name)) + +_wrapper_classes: dict[bool, type[TemporalBackend]] = {} + + +def _wrapper_class_for(inner: Any) -> type[TemporalBackend]: + """Return the ``TemporalBackend`` subclass whose optional ops mirror ``inner``. + + A delete-capable inner backend gets a class whose ``delete`` / ``adelete`` + dispatch activities like every other op. Any other inner backend keeps the + protocol defaults for both, so deepagents disables its delete tool exactly + as it would for the unwrapped backend. Without deepagents installed there + is no protocol to mirror and the base class is used as-is. + """ + try: + protocol = importlib.import_module("deepagents.backends.protocol") + except ImportError: + return TemporalBackend + default_delete = protocol.BackendProtocol.delete + supports = getattr(type(inner), "delete", default_delete) is not default_delete + cls = _wrapper_classes.get(supports) + if cls is None: + namespace: dict[str, Any] = { + "__module__": __name__, + "__qualname__": TemporalBackend.__qualname__, + "__doc__": TemporalBackend.__doc__, + } + if supports: + namespace["delete"] = _make_backend_op("delete") + namespace["adelete"] = _make_backend_op("adelete") + else: + namespace["delete"] = default_delete + namespace["adelete"] = protocol.BackendProtocol.adelete + cls = type(TemporalBackend.__name__, (TemporalBackend,), namespace) + _wrapper_classes[supports] = cls + return cls diff --git a/temporalio/contrib/langsmith/_interceptor.py b/temporalio/contrib/langsmith/_interceptor.py index a7eea0714..1e2cd147f 100644 --- a/temporalio/contrib/langsmith/_interceptor.py +++ b/temporalio/contrib/langsmith/_interceptor.py @@ -336,8 +336,13 @@ def end(self, **kwargs: Any) -> None: kwargs.setdefault("end_time", temporalio.workflow.now()) self._run.end(**kwargs) - def patch(self, *, exclude_inputs: bool = False) -> None: - """Patch the run to LangSmith, skipping during replay.""" + def patch(self, *, exclude_inputs: bool | None = None) -> None: + """Patch the run to LangSmith, skipping during replay. + + ``exclude_inputs`` is forwarded untouched so LangSmith's own default + applies (a plain ``False`` before 0.12; the + ``LANGSMITH_EXCLUDE_INPUTS_ON_PATCH`` setting from 0.12 on). + """ if temporalio.workflow.in_workflow(): if _is_replaying(): return @@ -394,7 +399,7 @@ def post(self, exclude_child_runs: bool = True) -> NoReturn: """Factory must never be posted.""" raise RuntimeError("_RootReplaySafeRunTreeFactory must never be posted") - def patch(self, *, exclude_inputs: bool = False) -> NoReturn: + def patch(self, *, exclude_inputs: bool | None = None) -> NoReturn: """Factory must never be patched.""" raise RuntimeError("_RootReplaySafeRunTreeFactory must never be patched") diff --git a/tests/contrib/deepagents/test_backends.py b/tests/contrib/deepagents/test_backends.py index d21f812a0..61b35c204 100644 --- a/tests/contrib/deepagents/test_backends.py +++ b/tests/contrib/deepagents/test_backends.py @@ -57,6 +57,14 @@ def read(self, file_path: str) -> str: async def aread(self, file_path: str) -> str: return f"acontents of {file_path}" + # ``delete`` is optional in the protocol (deepagents >= 0.7); a backend that + # has it must still cross the activity boundary like every other op. + async def adelete(self, file_path: str) -> str: + return f"deleted {file_path}" + + def delete(self, file_path: str) -> str: + return f"deleted {file_path}" + @workflow.defn class BackendWorkflow: @@ -68,7 +76,8 @@ async def run(self, path: str) -> str: ) sync_out = await backend.read(path) async_out = await backend.aread(path) - return f"{sync_out}|{async_out}" + deleted = await backend.adelete(path) + return f"{sync_out}|{async_out}|{deleted}" # Bind deepagents symbols off the module importorskip returns: a static @@ -136,10 +145,35 @@ async def test_temporal_backend_op_activity(env: WorkflowEnvironment) -> None: ) out = await handle.result() - assert out == "contents of notes.txt|acontents of notes.txt" + assert out == "contents of notes.txt|acontents of notes.txt|deleted notes.txt" counts = await count_scheduled_activities(handle) - # One activity per op — the sync read AND the async aread both cross. - assert counts[BACKEND_OP] == 2, counts + # One activity per op — the sync read, the async aread, and the optional + # adelete all cross. + assert counts[BACKEND_OP] == 3, counts + + +def test_temporal_backend_mirrors_inner_delete_support() -> None: + # deepagents decides delete support from the wrapper CLASS, so a wrapper + # around a delete-capable backend must advertise it and one around a + # backend without delete must not (or the agent gets a delete tool that + # can only fail). + protocol = pytest.importorskip("deepagents.backends.protocol") + + class NoDelete: + def read(self, file_path: str) -> str: + return f"contents of {file_path}" + + with_delete = TemporalBackend(RecordingBackend()) + without_delete = TemporalBackend(NoDelete()) + assert protocol._supports_delete(with_delete) is True + assert protocol._supports_delete(without_delete) is False + assert isinstance(with_delete, TemporalBackend) + assert isinstance(without_delete, TemporalBackend) + # The real deepagents backends resolve the same way wrapped or not. + state_backend = StateBackend() + assert protocol._supports_delete( + TemporalBackend(state_backend) + ) is protocol._supports_delete(state_backend) def test_temporal_backend_unregisters_on_gc() -> None: diff --git a/uv.lock b/uv.lock index ab1c17005..7562ad66a 100644 --- a/uv.lock +++ b/uv.lock @@ -4,12 +4,13 @@ requires-python = ">=3.10" resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", "python_full_version < '3.11'", ] [options] -exclude-newer = "2026-08-31T19:12:49.465398Z" +exclude-newer = "0001-01-01T00:00:00Z" # This has no effect and is included for backwards compatibility when using relative exclude-newer values. exclude-newer-span = "P2W" [[package]] @@ -254,21 +255,20 @@ wheels = [ [[package]] name = "anthropic" -version = "0.117.0" +version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anyio", marker = "python_full_version >= '3.11'" }, - { name = "distro", marker = "python_full_version >= '3.11'" }, - { name = "docstring-parser", marker = "python_full_version >= '3.11'" }, - { name = "httpx", marker = "python_full_version >= '3.11'" }, - { name = "jiter", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, - { name = "sniffio", marker = "python_full_version >= '3.11'" }, - { name = "typing-extensions", marker = "python_full_version >= '3.11'" }, + { name = "anyio" }, + { name = "docstring-parser" }, + { name = "httpx2" }, + { name = "jiter" }, + { name = "pydantic" }, + { 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/b4/50/463166f02179ab279edb61de1589a6f69cb3838d6a2fb6f2c92a3f8042f1/anthropic-1.3.0.tar.gz", hash = "sha256:6873492a77ede8849a161ab1bc78bc9a1e492a006d0b5bb4c57ac77845df838a", size = 1148177, upload-time = "2026-09-01T17:37:10.392Z" } 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/2c/5d/7863a9961d320c23787c7b594956afe4e878f9c0ae2376b11a20e416791d/anthropic-1.3.0-py3-none-any.whl", hash = "sha256:e7e7dbebf9f3c84a23954ab989378af6ae10a4d1804c81e9fea4b5ced695ce75", size = 1296959, upload-time = "2026-09-01T17:37:08.525Z" }, ] [[package]] @@ -939,19 +939,20 @@ wheels = [ [[package]] name = "deepagents" -version = "0.6.12" +version = "0.7.12" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain", marker = "python_full_version >= '3.11'" }, - { name = "langchain-anthropic", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "langchain-google-genai", marker = "python_full_version >= '3.11'" }, - { name = "langsmith", marker = "python_full_version >= '3.11'" }, - { name = "wcmatch", marker = "python_full_version >= '3.11'" }, + { name = "langchain" }, + { name = "langchain-anthropic" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "langchain-google-genai" }, + { name = "langsmith" }, + { name = "packaging" }, + { 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/b9/af/35d8adf1181a27f2d98f535d81246d73328297a5daf99c4130a5c667494e/deepagents-0.7.12.tar.gz", hash = "sha256:e5968af37f505d79bea2bf7217dad0bdaff6689950e33a1a0f82c25f7ac9fd7e", size = 297304, upload-time = "2026-09-01T18:50:18.6Z" } 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/8b/1f/1b0080e0c76c5dfe00dfb752ebee4d3ff0778e56463fd9df19dbf8357fcd/deepagents-0.7.12-py3-none-any.whl", hash = "sha256:5df1818b515bb83098057b59a6f3dcbdd632657e17560c49a516e8d13b6b768b", size = 324239, upload-time = "2026-09-01T18:50:17.335Z" }, ] [[package]] @@ -1022,7 +1023,7 @@ name = "exceptiongroup" version = "1.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } wheels = [ @@ -1590,6 +1591,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, ] +[[package]] +name = "httpcore2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "h11" }, + { name = "truststore" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/74/d370e55600d9bcfa0d9794b0166126d49291a3d2b20c268fc98c453a4948/httpcore2-2.12.0-py3-none-any.whl", hash = "sha256:7e04258ce01013d7d615e5b910a3b27fac937d7a95038227e79652b4ba3b4ceb", size = 83074, upload-time = "2026-08-18T13:22:05.854Z" }, +] + [[package]] name = "httpx" version = "0.28.1" @@ -1614,6 +1628,32 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fd/6668e5aec43ab844de6fc74927e155a3b37bf40d7c3790e49fc0406b6578/httpx_sse-0.4.3-py3-none-any.whl", hash = "sha256:0ac1c9fe3c0afad2e0ebb25a934a59f4c7823b60792691f779fad2c5568830fc", size = 8960, upload-time = "2025-10-10T21:48:21.158Z" }, ] +[[package]] +name = "httpx2" +version = "2.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio", marker = "sys_platform != 'emscripten'" }, + { name = "httpcore2", marker = "sys_platform != 'emscripten'" }, + { name = "httpx2-jsfetch", marker = "python_full_version >= '3.12' and sys_platform == 'emscripten'" }, + { name = "idna" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/f8/579a8b51e42e38ee32647df9f08aa25643ae788e275cc625b199829c4671/httpx2-2.12.0.tar.gz", hash = "sha256:7631fe9887a8a2275f4a2540e053aa670fcc50742864a9ae7c66e609fdcf12cf", size = 100040, upload-time = "2026-08-18T13:22:09.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/95/411ba65569158e862368917aaf56597f3e5fa3b91b0502919638465a08f3/httpx2-2.12.0-py3-none-any.whl", hash = "sha256:cc8b6eecb8661c146b8f89a60e97456ee086e91a784ed31ac450c3a9e613dd36", size = 95427, upload-time = "2026-08-18T13:22:06.834Z" }, +] + +[[package]] +name = "httpx2-jsfetch" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/c4/0e5636363151a2a1795e0a77617168b9ca438e1748ec05fc9b5687f93d64/httpx2_jsfetch-1.0.tar.gz", hash = "sha256:70a0e3eabfef7cce5ad9c629f7d01ca05e418f586646f4ddf14782e4c1454c60", size = 6872, upload-time = "2026-08-07T00:13:07.492Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/43/832f631d32e4f1211caa2ba368317739fe71f0b8530e4c9d15dc454bac2a/httpx2_jsfetch-1.0-py3-none-any.whl", hash = "sha256:cb916b707601e69a07721aabc8f3f6659be3a6893bc1ff5c6f9e02241df2da32", size = 6382, upload-time = "2026-08-07T00:13:06.567Z" }, +] + [[package]] name = "huggingface-hub" version = "1.23.0" @@ -1968,36 +2008,39 @@ wheels = [ [[package]] name = "langchain" -version = "1.3.14" +version = "1.3.18" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "langgraph", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "langgraph", version = "1.2.11", source = { registry = "https://pypi.org/simple" } }, + { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/29/68/a6dbad9c22df4087a0f9e79ddd46226c442b30128bfeee538d5889492a73/langchain-1.3.14.tar.gz", hash = "sha256:1b6696c72ba3bbbce54d745e0180742c9f6ece8bbc59ed5a46c3e20b9a435929", size = 645181, upload-time = "2026-07-16T13:28:18.29Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/e9/0e5425e522b624d7c5c0911957b467b82890b36b0099bf727951f2ee3059/langchain-1.3.18.tar.gz", hash = "sha256:74ce99294f6f2c82ee64c3df39daa6a22085ee1449a718065b3604a88d78bf4d", size = 637052, upload-time = "2026-08-27T17:33:12.702Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a9/ec/0f942e78a621f8e3162ff1ed24284f469aaf51fb4607ee5831c626f2b2bc/langchain-1.3.14-py3-none-any.whl", hash = "sha256:4d10dbe91005952cddd56d0dc77aa108964da6bae90ab20063653957e901f782", size = 139560, upload-time = "2026-07-16T13:28:16.498Z" }, + { url = "https://files.pythonhosted.org/packages/f7/04/374f6014ed6959dbdab92962c2b09e4d0223ed6a82f65694870b46d2c13f/langchain-1.3.18-py3-none-any.whl", hash = "sha256:f29cbef985848e5cfff5398f3c8c1568994a3a6f2a8f4fa720df30b6e0668b9c", size = 148007, upload-time = "2026-08-27T17:33:11.23Z" }, ] [[package]] name = "langchain-anthropic" -version = "1.4.8" +version = "1.7.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "anthropic", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "anthropic" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" } }, + { 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/56/fc/52f6d1d6069bafb08626e204c89c49c8dd4a536eedbb94f0b7e78668594d/langchain_anthropic-1.7.0.tar.gz", hash = "sha256:d48e3c118ff8d3eea83f17b50234a2d2ff491a2375d565f212eb990e7e3856cb", size = 750068, upload-time = "2026-08-27T15:23:59.261Z" } 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/8e/ce/e4367713080bc750e1dba845409c058f196543d1a0dc99683e2ef062f581/langchain_anthropic-1.7.0-py3-none-any.whl", hash = "sha256:68b34369aa01dad0c67bc690b8c47e09d06bd30fb28f320ad19ddc71c4445dc0", size = 60475, upload-time = "2026-08-27T15:23:57.989Z" }, ] [[package]] name = "langchain-core" version = "1.4.9" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ { name = "jsonpatch" }, { name = "langchain-protocol" }, @@ -2014,19 +2057,46 @@ 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" }, ] +[[package]] +name = "langchain-core" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", +] +dependencies = [ + { name = "httpx" }, + { name = "jsonpatch" }, + { name = "langchain-protocol" }, + { name = "langsmith" }, + { name = "packaging" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "tenacity" }, + { name = "typing-extensions" }, + { name = "uuid-utils" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/12/aff76ca89c219ebe6f9dd3c5dbc4e3b1cf5450e9fc7037dccad23d45cd7a/langchain_core-1.6.1.tar.gz", hash = "sha256:1b156cb395aac4f009a8a1b38a574c7d948fe2d5f74c96e0d8a5017b4149e04f", size = 1003359, upload-time = "2026-08-27T19:31:14.956Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/25/f50dd65673c819aa33d3c34df58c115dbb6ec627d19f93e6e401dd0fc8d7/langchain_core-1.6.1-py3-none-any.whl", hash = "sha256:954a84132a5cb0435d27b910e336347b6744ecc18fbeef1e2de7029a0959841a", size = 571478, upload-time = "2026-08-27T19:31:13.34Z" }, +] + [[package]] name = "langchain-google-genai" -version = "4.2.7" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "filetype", marker = "python_full_version >= '3.11'" }, - { name = "google-genai", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "pydantic", marker = "python_full_version >= '3.11'" }, + { name = "filetype" }, + { name = "google-genai" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" } }, + { 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/cf/4e/41798c80b574d958d189e049f13d64eb9246a66623465290f2cbfd641759/langchain_google_genai-4.4.0.tar.gz", hash = "sha256:7871beec56ac07b719f77c46997845db6ff2267b817bffb0f97877053b0895d7", size = 378415, upload-time = "2026-09-01T20:15:45.816Z" } 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/2c/10/83bb535e78c2cf767a6193c2f3b00b2894c1581a36ef85911ebaa3b9a891/langchain_google_genai-4.4.0-py3-none-any.whl", hash = "sha256:8e23a1307bd2158590bbf9d99f1d658fc84a9d9ddb77fb1b372f8875a2bafbbf", size = 81617, upload-time = "2026-09-01T20:15:44.533Z" }, ] [[package]] @@ -2045,8 +2115,11 @@ wheels = [ name = "langgraph" version = "1.2.9" source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11'", +] dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", version = "1.4.9", source = { registry = "https://pypi.org/simple" } }, { name = "langgraph-checkpoint" }, { name = "langgraph-prebuilt" }, { name = "langgraph-sdk" }, @@ -2058,12 +2131,36 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/16/0b8dc48823f1326f3e0c8012a3c07a40da6f194299e2ec080df236287baf/langgraph-1.2.9-py3-none-any.whl", hash = "sha256:c2d98ad94333937922ba04148641c1da2bfe45b5b8e55d7b6dcb0bb2df809e76", size = 247473, upload-time = "2026-07-10T01:30:13.733Z" }, ] +[[package]] +name = "langgraph" +version = "1.2.11" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14'", + "python_full_version == '3.13.*'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", +] +dependencies = [ + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" } }, + { name = "langgraph-checkpoint" }, + { name = "langgraph-prebuilt" }, + { name = "langgraph-sdk" }, + { name = "pydantic" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/56/0d/c8e7ee98896659e1b6555db0ab115a9ca899844744645d5d894032bab1d7/langgraph-1.2.11.tar.gz", hash = "sha256:9ecfe11e50d338b34b15cf4d8a442642de103e8ae6971320efba84e4542eb363", size = 725753, upload-time = "2026-08-11T14:00:36.945Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/7f/c5c30e4be99ff821029c7ac872a480676bb179c9f3df85ea3f38d13f86d4/langgraph-1.2.11-py3-none-any.whl", hash = "sha256:8bab70de7b2d00b5300fb289bcf38d8b241400f3184c1e95e8ce706fb0e8686b", size = 248854, upload-time = "2026-08-11T14:00:35.494Z" }, +] + [[package]] name = "langgraph-checkpoint" version = "4.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "ormsgpack" }, ] sdist = { url = "https://files.pythonhosted.org/packages/83/47/886af6f886f0bff2273164a45f008694e48a96ff3cd25ff0228f2aa9480e/langgraph_checkpoint-4.1.1.tar.gz", hash = "sha256:6c2bdb530c91f91d7d9c1bd100925d0fc4f498d418c17f3587d1526279482a25", size = 184020, upload-time = "2026-05-22T16:57:38.503Z" } @@ -2076,7 +2173,8 @@ name = "langgraph-prebuilt" version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "langchain-core" }, + { name = "langchain-core", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "langgraph-checkpoint" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/66/ed9b93f56bc17ef22d551892f0ac2b225a97fe0fcf23a511b857f70d590b/langgraph_prebuilt-1.1.0.tar.gz", hash = "sha256:3c579cf6eed2d17f9c157c2d0fcaddcd8688524e7022d3b22b37a3bf4589d528", size = 178833, upload-time = "2026-05-12T03:37:49.332Z" } @@ -2090,7 +2188,8 @@ version = "0.4.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, - { name = "langchain-core" }, + { name = "langchain-core", version = "1.4.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "langchain-protocol" }, { name = "orjson" }, { name = "websockets" }, @@ -2102,23 +2201,27 @@ wheels = [ [[package]] name = "langsmith" -version = "0.8.18" +version = "0.12.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "httpx" }, + { name = "anyio" }, + { name = "distro" }, + { name = "httpx2" }, { name = "orjson", marker = "platform_python_implementation != 'PyPy'" }, { name = "packaging" }, { name = "pydantic" }, { name = "requests" }, { name = "requests-toolbelt" }, + { name = "sniffio" }, + { name = "typing-extensions" }, { name = "uuid-utils" }, { name = "websockets" }, { name = "xxhash" }, { name = "zstandard" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/9a/d9/a6681aa9847bbbc5ec21abe20a5e233b94e5edcfe39624db607ac7e8ccb4/langsmith-0.8.18.tar.gz", hash = "sha256:32dde9c0e67e053e0fb738921fc8ced768af7b8fa83d7a0e3fd63597cf8776dd", size = 4526988, upload-time = "2026-06-19T13:12:17.123Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/9a/b07aa9c457265e4bd04a868afbd35b0b6b12d655459bf274f7dba0b70be5/langsmith-0.12.1.tar.gz", hash = "sha256:8916c1a8daa4282511f311f569fd5cb2f0aba8d89a4d1761620ed37b57f72c00", size = 4851951, upload-time = "2026-09-01T17:43:55.828Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/03/70/0e0cc80a3b064c8d6c8d697c3125ed86e39d5a7393ec6dc8b07cb1cf13c4/langsmith-0.8.18-py3-none-any.whl", hash = "sha256:3940183349993faef48e6c7d08e4822ee9cefd906b362d0e3c2d650314d2f282", size = 508108, upload-time = "2026-06-19T13:12:15.348Z" }, + { url = "https://files.pythonhosted.org/packages/e9/61/a61db7922016b77afe212c97744cd8b0aad54430a1d3df23fc792fb963bf/langsmith-0.12.1-py3-none-any.whl", hash = "sha256:34574d4411947f62825e8368fd172cad2505ddfb73bf5d8ed7c0e7a41e4c4879", size = 758328, upload-time = "2026-09-01T17:43:53.319Z" }, ] [[package]] @@ -2721,7 +2824,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } wheels = [ @@ -2838,7 +2942,7 @@ wheels = [ [package.optional-dependencies] litellm = [ - { name = "litellm", marker = "python_full_version < '3.14'" }, + { name = "litellm" }, ] [[package]] @@ -4368,7 +4472,8 @@ source = { registry = "https://pypi.org/simple" } resolution-markers = [ "python_full_version >= '3.14'", "python_full_version == '3.13.*'", - "python_full_version >= '3.11' and python_full_version < '3.13'", + "python_full_version == '3.12.*' and sys_platform == 'emscripten'", + "(python_full_version >= '3.11' and python_full_version < '3.13' and sys_platform != 'emscripten') or (python_full_version == '3.11.*' and sys_platform == 'emscripten')", ] sdist = { url = "https://files.pythonhosted.org/packages/aa/2a/9618a122aeb2a169a28b03889a2995fe297588964333d4a7d67bdf46e147/rpds_py-2026.6.3.tar.gz", hash = "sha256:1cebd1337c242e4ec2293e541f712b2da849b29f48f0c293684b71c0632625d4", size = 64051, upload-time = "2026-06-30T07:17:53.009Z" } wheels = [ @@ -4720,7 +4825,7 @@ cloud-run-worker-otel = [ deepagents = [ { name = "deepagents", marker = "python_full_version >= '3.11'" }, { name = "langchain", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] google-adk = [ { name = "google-adk" }, @@ -4740,7 +4845,8 @@ lambda-worker-otel = [ { name = "opentelemetry-semantic-conventions" }, ] langgraph = [ - { name = "langgraph" }, + { name = "langgraph", version = "1.2.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "langgraph", version = "1.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] langsmith = [ { name = "langsmith" }, @@ -4772,8 +4878,9 @@ dev = [ { name = "httpx" }, { name = "langchain", marker = "python_full_version >= '3.11'" }, { name = "langchain-anthropic", marker = "python_full_version >= '3.11'" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'" }, - { name = "langgraph" }, + { name = "langchain-core", version = "1.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "langgraph", version = "1.2.9", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "langgraph", version = "1.2.11", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "langsmith" }, { name = "litellm" }, { name = "maturin" }, @@ -4811,14 +4918,14 @@ dev = [ [package.metadata] requires-dist = [ { name = "aioboto3", marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, - { name = "deepagents", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=0.6.12,<0.7" }, + { name = "deepagents", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=0.7.12,<0.8" }, { name = "google-adk", marker = "extra == 'google-adk'", specifier = ">=2.2.0,<3" }, { name = "google-genai", marker = "extra == 'google-genai'", specifier = ">=2.21.0,<3.0.0" }, { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, { name = "langchain", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.3.11,<2" }, { name = "langchain-core", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.4.8,<2" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.1.0" }, - { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.9" }, + { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.13" }, { name = "mcp", marker = "extra == 'google-adk'", specifier = ">=1.24,<2" }, { name = "mcp", marker = "extra == 'openai-agents'", specifier = ">=1.9.4,<2" }, { name = "nexus-rpc", specifier = "==1.4.0" }, @@ -4850,7 +4957,7 @@ dev = [ { name = "basedpyright", specifier = "==1.34.0" }, { name = "cibuildwheel", specifier = ">=2.22.0,<3" }, { name = "cryptography", specifier = ">=46" }, - { name = "deepagents", marker = "python_full_version >= '3.11'", specifier = ">=0.6.12,<0.7" }, + { name = "deepagents", marker = "python_full_version >= '3.11'", specifier = ">=0.7.12,<0.8" }, { name = "googleapis-common-protos", specifier = ">=1.75.0,<2" }, { name = "grpcio-tools", specifier = ">=1.48.2,<2" }, { name = "httpx", specifier = ">=0.28.1" }, @@ -4858,7 +4965,7 @@ dev = [ { name = "langchain-anthropic", marker = "python_full_version >= '3.11'", specifier = ">=1.4.7" }, { name = "langchain-core", marker = "python_full_version >= '3.11'", specifier = ">=1.4.8,<2" }, { name = "langgraph", specifier = ">=1.1.0" }, - { name = "langsmith", specifier = ">=0.7.34,<0.9" }, + { name = "langsmith", specifier = ">=0.7.34,<0.13" }, { name = "litellm", specifier = ">=1.83.0" }, { name = "maturin", specifier = ">=1.8.2" }, { name = "mcp", specifier = ">=1.9.4,<2" }, @@ -5064,6 +5171,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/22/2a/5e5e750890ada51017d18d0d4c30da696e5b5bd3180947729927628fc3cb/tqdm-4.68.4-py3-none-any.whl", hash = "sha256:5168118b2368f48c561afda8020fd79195b1bdb0bdf8086b88442c267a315dc2", size = 676612, upload-time = "2026-07-07T09:58:16.256Z" }, ] +[[package]] +name = "truststore" +version = "0.10.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/a3/1585216310e344e8102c22482f6060c7a6ea0322b63e026372e6dcefcfd6/truststore-0.10.4.tar.gz", hash = "sha256:9d91bd436463ad5e4ee4aba766628dd6cd7010cf3e2461756b3303710eebc301", size = 26169, upload-time = "2025-08-12T18:49:02.73Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/19/97/56608b2249fe206a67cd573bc93cd9896e1efb9e98bce9c163bcdc704b88/truststore-0.10.4-py3-none-any.whl", hash = "sha256:adaeaecf1cbb5f4de3b1959b42d41f6fab57b2b1666adb59e89cb0b53361d981", size = 18660, upload-time = "2025-08-12T18:49:01.46Z" }, +] + [[package]] name = "twine" version = "4.0.2" @@ -5377,7 +5493,7 @@ name = "wcmatch" version = "11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "bracex", marker = "python_full_version >= '3.11'" }, + { name = "bracex" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/25/1da725838132221e33568973da484ff43813662ccc06ebf7f6e3abddfcd5/wcmatch-11.0.tar.gz", hash = "sha256:55d95c2447789712774b198ceec72939e88b5618f1f8f0a9b605bf7740b63b96", size = 141360, upload-time = "2026-07-10T05:50:24.183Z" } wheels = [ From b9c13f110a57dd02cd2a893808dcf1b0470631d8 Mon Sep 17 00:00:00 2001 From: DABH Date: Thu, 17 Sep 2026 11:13:46 -0500 Subject: [PATCH 2/6] Mirror execution support and subclasses in TemporalBackend; pin patch default Per review. deepagents decides execution support with a nominal isinstance check against SandboxBackendProtocol (an ABC subclass, not a structural Protocol), so no wrapper was ever mistaken for shell-capable; the real gap ran the other way: a wrapped LocalShellBackend lost its shell tool. The mirror class now derives from SandboxBackendProtocol only when deepagents reports the inner backend as execution-capable, with execute/aexecute dispatchers and id forwarded; plain backends stay non-sandbox classes. Mirroring also applies to user subclasses of TemporalBackend (generated as a subclass of theirs; ops they define are left alone) instead of being skipped. The langsmith RunTree.patch tests now pin that a no-argument call forwards exclude_inputs=None so LangSmith's own default applies. --- CHANGELOG.md | 8 +- temporalio/contrib/deepagents/_tools.py | 92 +++++++++++++------ tests/contrib/deepagents/test_backends.py | 86 +++++++++++++++++ tests/contrib/langsmith/test_background_io.py | 17 +++- 4 files changed, 170 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48eb8525b..72f1f90e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,9 +34,11 @@ to include examples, links to docs, or any other relevant information. ### Fixed - `temporalio.contrib.deepagents.TemporalBackend` mirrors the wrapped backend's optional - `delete` support at class level, which deepagents 0.7 checks before offering the delete - tool; `delete` / `adelete` on a delete-capable backend run as `deepagents.backend_op` - Activities like every other op. + capabilities at class level, where deepagents 0.7 checks them: `delete` / `adelete` are + offered (and run as `deepagents.backend_op` Activities) only for a delete-capable backend, + and `execute` / `aexecute` only for an execution-capable one, so a wrapped filesystem or + store backend no longer advertises a shell tool that could only fail. User subclasses of + `TemporalBackend` get the same mirroring. - Current workflow and activity payload converter accessors now return the configured converter without SDK-internal transfer type conversion. diff --git a/temporalio/contrib/deepagents/_tools.py b/temporalio/contrib/deepagents/_tools.py index 1c992b647..b13e5c632 100644 --- a/temporalio/contrib/deepagents/_tools.py +++ b/temporalio/contrib/deepagents/_tools.py @@ -480,11 +480,13 @@ def uninstall_backend_async_patch() -> None: "adelete", ) -# ``delete`` is optional in the deepagents protocol, and deepagents decides -# whether a backend has it from the CLASS (``type(backend).delete`` compared -# against the protocol default), not the instance. The wrapper therefore has to -# mirror the inner backend at class level; see :func:`_wrapper_class_for`. -_OPTIONAL_OPS = frozenset({"delete", "adelete"}) +# Capability-gated ops. deepagents decides whether a backend has ``delete`` from +# the CLASS (``type(backend).delete`` against the protocol default) and whether +# it can ``execute`` from a nominal ``isinstance`` check against +# ``SandboxBackendProtocol`` (an ABC subclass, not a structural Protocol). +# Neither can be answered per instance, so the wrapper mirrors the inner +# backend at class level; see :func:`_wrapper_class_for`. +_OPTIONAL_OPS = frozenset({"delete", "adelete", "execute", "aexecute"}) class TemporalBackend: @@ -506,9 +508,16 @@ def __new__( *, activity_options: Mapping[str, Any] | None = None, ) -> "TemporalBackend": - """Pick the wrapper class that mirrors ``inner``'s optional capabilities.""" + """Pick the wrapper class that mirrors ``inner``'s optional capabilities. + + Applies to subclasses too: a user's ``class MyBackend(TemporalBackend)`` + gets a mirrored subclass of ``MyBackend``, so ``isinstance`` and any + methods it defines are preserved. + """ target: type[TemporalBackend] = ( - _wrapper_class_for(inner) if cls is TemporalBackend else cls + cls + if getattr(cls, "_temporal_mirror_of", None) is not None + else _wrapper_class_for(inner, cls) ) return object.__new__(target) @@ -572,37 +581,62 @@ async def _op(self: TemporalBackend, *args: Any, **kwargs: Any) -> Any: if _op_name not in _OPTIONAL_OPS: setattr(TemporalBackend, _op_name, _make_backend_op(_op_name)) -_wrapper_classes: dict[bool, type[TemporalBackend]] = {} +_wrapper_classes: dict[tuple[type, bool, bool], type[TemporalBackend]] = {} -def _wrapper_class_for(inner: Any) -> type[TemporalBackend]: - """Return the ``TemporalBackend`` subclass whose optional ops mirror ``inner``. +def _wrapper_class_for( + inner: Any, base: type[TemporalBackend] +) -> type[TemporalBackend]: + """Return the subclass of ``base`` whose optional capabilities mirror ``inner``. - A delete-capable inner backend gets a class whose ``delete`` / ``adelete`` - dispatch activities like every other op. Any other inner backend keeps the - protocol defaults for both, so deepagents disables its delete tool exactly - as it would for the unwrapped backend. Without deepagents installed there - is no protocol to mirror and the base class is used as-is. + ``delete`` / ``adelete``: a delete-capable inner backend gets dispatchers + like every other op; any other inner backend keeps the protocol defaults, + so deepagents disables its delete tool exactly as it would for the + unwrapped backend. Execution: deepagents offers its shell tool only to a + nominal ``SandboxBackendProtocol`` instance, so when deepagents reports the + inner backend as execution-capable the mirror also derives from that class + (with ``execute`` / ``aexecute`` dispatchers and ``id`` forwarded); a wrapper + around a plain filesystem or store backend stays a non-sandbox class. An op + that ``base`` already defines (a user subclass) is left alone. Without + deepagents installed there is nothing to mirror and ``base`` is used as-is. """ try: protocol = importlib.import_module("deepagents.backends.protocol") + filesystem = importlib.import_module("deepagents.middleware.filesystem") except ImportError: - return TemporalBackend + return base default_delete = protocol.BackendProtocol.delete - supports = getattr(type(inner), "delete", default_delete) is not default_delete - cls = _wrapper_classes.get(supports) + has_delete = getattr(type(inner), "delete", default_delete) is not default_delete + has_execute = bool(filesystem.supports_execution(inner)) + key = (base, has_delete, has_execute) + cls = _wrapper_classes.get(key) if cls is None: namespace: dict[str, Any] = { - "__module__": __name__, - "__qualname__": TemporalBackend.__qualname__, - "__doc__": TemporalBackend.__doc__, + "__module__": base.__module__, + "__qualname__": base.__qualname__, + "__doc__": base.__doc__, + "_temporal_mirror_of": base, } - if supports: - namespace["delete"] = _make_backend_op("delete") - namespace["adelete"] = _make_backend_op("adelete") - else: - namespace["delete"] = default_delete - namespace["adelete"] = protocol.BackendProtocol.adelete - cls = type(TemporalBackend.__name__, (TemporalBackend,), namespace) - _wrapper_classes[supports] = cls + for op in ("delete", "adelete"): + if hasattr(base, op): + continue + namespace[op] = ( + _make_backend_op(op) + if has_delete + else getattr(protocol.BackendProtocol, op) + ) + bases: tuple[type, ...] = (base,) + if has_execute: + sandbox_cls = protocol.SandboxBackendProtocol + if not issubclass(base, sandbox_cls): + bases = (base, sandbox_cls) + for op in ("execute", "aexecute"): + if not hasattr(base, op): + namespace[op] = _make_backend_op(op) + if not hasattr(base, "id"): + # The sandbox class defines ``id``; keep reading the inner + # backend's, which ``__getattr__`` would otherwise have served. + namespace["id"] = property(lambda self: self._inner.id) + cls = type(base.__name__, bases, namespace) + _wrapper_classes[key] = cls return cls diff --git a/tests/contrib/deepagents/test_backends.py b/tests/contrib/deepagents/test_backends.py index 61b35c204..3de2132c7 100644 --- a/tests/contrib/deepagents/test_backends.py +++ b/tests/contrib/deepagents/test_backends.py @@ -88,6 +88,7 @@ async def run(self, path: str) -> str: _backends_mod = pytest.importorskip("deepagents.backends") create_deep_agent = _deepagents_mod.create_deep_agent FilesystemBackend = _backends_mod.FilesystemBackend +LocalShellBackend = _backends_mod.LocalShellBackend StateBackend = _backends_mod.StateBackend @@ -176,6 +177,91 @@ def read(self, file_path: str) -> str: ) is protocol._supports_delete(state_backend) +def test_temporal_backend_mirrors_inner_execution_support(tmp_path: Path) -> None: + # deepagents offers its shell tool when the backend passes an isinstance + # check against the sandbox protocol, i.e. when execute/aexecute exist on + # the object. A wrapper must only grow them when the inner backend is + # execution-capable, or a plain filesystem backend gets a shell tool whose + # every call fails in the activity. + supports_execution = pytest.importorskip( + "deepagents.middleware.filesystem" + ).supports_execution + + plain = TemporalBackend( + FilesystemBackend(root_dir=str(tmp_path), virtual_mode=True) + ) + assert supports_execution(plain) is False + assert not hasattr(plain, "aexecute") + + shell_inner = LocalShellBackend(root_dir=str(tmp_path)) + shell = TemporalBackend(shell_inner) + assert supports_execution(shell) is True + # The op is the activity dispatcher, not the inner backend's own method. + assert getattr(type(shell), "aexecute") is not type(shell_inner).aexecute + assert supports_execution(shell_inner) is True + + +def test_temporal_backend_subclass_keeps_capability_mirroring() -> None: + protocol = pytest.importorskip("deepagents.backends.protocol") + + class MyBackend(TemporalBackend): + def extra(self) -> str: + return "extra" + + class NoDelete: + def read(self, file_path: str) -> str: + return f"contents of {file_path}" + + class OwnDelete(TemporalBackend): + def delete(self, file_path: str) -> str: + return f"own {file_path}" + + wrapped = MyBackend(RecordingBackend()) + assert isinstance(wrapped, MyBackend) + assert wrapped.extra() == "extra" + assert protocol._supports_delete(wrapped) is True + assert protocol._supports_delete(MyBackend(NoDelete())) is False + # An op the subclass defines itself is left alone. + own = OwnDelete(NoDelete()) + assert getattr(type(own), "delete") is OwnDelete.delete + + +@workflow.defn +class ShellBackendWorkflow: + @workflow.run + async def run(self, root_dir: str) -> str: + backend = TemporalBackend( + LocalShellBackend(root_dir=root_dir), + activity_options={"start_to_close_timeout": timedelta(seconds=30)}, + ) + result = await backend.aexecute("echo shell-ok") + return str(getattr(result, "output", result)) + + +@pytest.mark.asyncio +async def test_temporal_backend_execute_runs_as_activity( + env: WorkflowEnvironment, tmp_path: Path +) -> None: + plugin = DeepAgentsPlugin() + async with Worker( + env.client, + task_queue="da-shell-backend", + workflows=[ShellBackendWorkflow], + plugins=[plugin], + ): + handle = await env.client.start_workflow( + ShellBackendWorkflow.run, + str(tmp_path), + id=f"da-shell-backend-{uuid.uuid4()}", + task_queue="da-shell-backend", + ) + out = await handle.result() + + assert "shell-ok" in out + counts = await count_scheduled_activities(handle) + assert counts[BACKEND_OP] == 1, counts + + def test_temporal_backend_unregisters_on_gc() -> None: # A wrapper is typically constructed per workflow run; its registry entry # must not outlive it, or a long-lived worker leaks one entry per run. diff --git a/tests/contrib/langsmith/test_background_io.py b/tests/contrib/langsmith/test_background_io.py index 79c48eeef..972ba6e16 100644 --- a/tests/contrib/langsmith/test_background_io.py +++ b/tests/contrib/langsmith/test_background_io.py @@ -235,7 +235,9 @@ def test_patch_submits_to_executor_in_workflow( tree.patch() executor.shutdown(wait=True) - mock_run.patch.assert_called_once() + # No argument means "LangSmith's default": forwarded as None, never + # resolved to a bool here (0.12 reads LANGSMITH_EXCLUDE_INPUTS_ON_PATCH). + mock_run.patch.assert_called_once_with(exclude_inputs=None) @patch(_PATCH_IN_WORKFLOW, return_value=False) def test_post_delegates_directly_outside_workflow(self, _mock_in_wf: Any) -> None: @@ -259,6 +261,19 @@ def test_patch_delegates_directly_outside_workflow(self, _mock_in_wf: Any) -> No mock_run.patch.assert_called_once_with(exclude_inputs=True) + @patch(_PATCH_IN_WORKFLOW, return_value=False) + def test_patch_forwards_langsmith_default_outside_workflow( + self, _mock_in_wf: Any + ) -> None: + """No argument is forwarded as None so LangSmith's own default applies.""" + executor = _make_executor() + mock_run = _make_mock_run() + tree = _ReplaySafeRunTree(mock_run, executor=executor) + + tree.patch() + + mock_run.patch.assert_called_once_with(exclude_inputs=None) + @patch(_PATCH_IS_REPLAYING, return_value=False) @patch(_PATCH_IN_WORKFLOW, return_value=True) def test_post_error_logged_via_done_callback( From 2e4ee103bd80fb2fdc891ffbfb4b7f179b8eed58 Mon Sep 17 00:00:00 2001 From: DABH Date: Sat, 19 Sep 2026 23:15:54 -0500 Subject: [PATCH 3/6] Forward execute timeout through TemporalBackend dispatchers deepagents gates the execute tool's per-command timeout on execute_accepts_timeout(type(backend)), which inspects the signature of the class's execute for a timeout parameter and, unlike the max_count probe, does not accept **kwargs as a stand-in. The wrapper's dispatchers were (*args, **kwargs), so a wrapped LocalShellBackend read False and refused a timeout its unwrapped self accepts. The optional-op dispatchers built in _wrapper_class_for now carry the inner method's signature via __signature__, so every signature probe answers as it does for the inner backend (including a sandbox that does not take timeout). The wrapper-class cache is keyed by the inner class as well, and uses dict.setdefault so concurrent workflow threads building the same key share one class. --- CHANGELOG.md | 4 + temporalio/contrib/deepagents/_tools.py | 61 ++++++++--- tests/contrib/deepagents/test_backends.py | 122 +++++++++++++++++++++- 3 files changed, 174 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d28148a69..32faf7dbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,6 +43,10 @@ to include examples, links to docs, or any other relevant information. and `execute` / `aexecute` only for an execution-capable one, so a wrapped filesystem or store backend no longer advertises a shell tool that could only fail. User subclasses of `TemporalBackend` get the same mirroring. +- `temporalio.contrib.deepagents.TemporalBackend` now forwards the per-command `timeout` of + deepagents' `execute` tool for a wrapped sandbox backend such as `LocalShellBackend`: its + `execute` / `aexecute` dispatchers carry the wrapped backend's signatures, which deepagents + inspects to decide whether a backend accepts `timeout`. - `contrib.deepagents`: prevent duplicate input messages after continue-as-new. - `DataConverter.payload_converter` and current workflow and activity payload converter accessors now return the configured converter without SDK-internal transfer type conversion. diff --git a/temporalio/contrib/deepagents/_tools.py b/temporalio/contrib/deepagents/_tools.py index b13e5c632..05b3da8b8 100644 --- a/temporalio/contrib/deepagents/_tools.py +++ b/temporalio/contrib/deepagents/_tools.py @@ -27,6 +27,7 @@ from __future__ import annotations import importlib +import inspect import threading import uuid as _uuid import warnings @@ -485,7 +486,10 @@ def uninstall_backend_async_patch() -> None: # it can ``execute`` from a nominal ``isinstance`` check against # ``SandboxBackendProtocol`` (an ABC subclass, not a structural Protocol). # Neither can be answered per instance, so the wrapper mirrors the inner -# backend at class level; see :func:`_wrapper_class_for`. +# backend at class level; see :func:`_wrapper_class_for`. Some probes read the +# method's SIGNATURE as well (``execute_accepts_timeout`` looks for a +# ``timeout`` parameter on ``type(backend).execute``), so these dispatchers +# also carry the inner method's signature; see :func:`_make_backend_op`. _OPTIONAL_OPS = frozenset({"delete", "adelete", "execute", "aexecute"}) @@ -567,11 +571,34 @@ def __getattr__(self, name: str) -> Any: return getattr(self._inner, name) -def _make_backend_op(name: str) -> Callable[..., Any]: +def _make_backend_op(name: str, mirror: Any = None) -> Callable[..., Any]: + """Build the activity dispatcher for backend op ``name``. + + ``mirror`` is the inner backend's own method for the op. When given, the + dispatcher advertises that method's signature (through ``__signature__``; + it still accepts and forwards any arguments). deepagents probes some + capabilities by signature: ``execute_accepts_timeout`` looks for a + ``timeout`` parameter on ``type(backend).execute`` and, unlike the + ``max_count`` probe, does not take ``**kwargs`` as a stand-in, so a bare + ``(*args, **kwargs)`` dispatcher made a wrapped ``LocalShellBackend`` + refuse the per-command ``timeout`` its unwrapped self accepts. Carrying the + inner signature keeps every such probe answering exactly as it does for the + inner backend, including for a sandbox that does *not* accept ``timeout`` + (deepagents then declines the call up front instead of the activity failing + on an unexpected keyword). + """ + async def _op(self: TemporalBackend, *args: Any, **kwargs: Any) -> Any: return await self._dispatch(name, *args, **kwargs) _op.__name__ = _op.__qualname__ = name + if mirror is not None: + try: + signature = inspect.signature(mirror) + except (TypeError, ValueError): + pass + else: + setattr(_op, "__signature__", signature) return _op @@ -581,7 +608,7 @@ async def _op(self: TemporalBackend, *args: Any, **kwargs: Any) -> Any: if _op_name not in _OPTIONAL_OPS: setattr(TemporalBackend, _op_name, _make_backend_op(_op_name)) -_wrapper_classes: dict[tuple[type, bool, bool], type[TemporalBackend]] = {} +_wrapper_classes: dict[tuple[type, type, bool, bool], type[TemporalBackend]] = {} def _wrapper_class_for( @@ -596,19 +623,26 @@ def _wrapper_class_for( nominal ``SandboxBackendProtocol`` instance, so when deepagents reports the inner backend as execution-capable the mirror also derives from that class (with ``execute`` / ``aexecute`` dispatchers and ``id`` forwarded); a wrapper - around a plain filesystem or store backend stays a non-sandbox class. An op - that ``base`` already defines (a user subclass) is left alone. Without - deepagents installed there is nothing to mirror and ``base`` is used as-is. + around a plain filesystem or store backend stays a non-sandbox class. The + dispatchers built here carry the inner methods' signatures, which deepagents + reads for finer probes such as ``execute_accepts_timeout``; see + :func:`_make_backend_op`. An op that ``base`` already defines (a user + subclass) is left alone. Without deepagents installed there is nothing to + mirror and ``base`` is used as-is. """ try: protocol = importlib.import_module("deepagents.backends.protocol") filesystem = importlib.import_module("deepagents.middleware.filesystem") except ImportError: return base + inner_cls = type(inner) default_delete = protocol.BackendProtocol.delete - has_delete = getattr(type(inner), "delete", default_delete) is not default_delete + has_delete = getattr(inner_cls, "delete", default_delete) is not default_delete has_execute = bool(filesystem.supports_execution(inner)) - key = (base, has_delete, has_execute) + # Keyed by the inner CLASS too, since the dispatchers carry its method + # signatures. ``has_execute`` stays in the key: for a ``CompositeBackend`` + # deepagents answers it per instance, from the default backend. + key = (base, inner_cls, has_delete, has_execute) cls = _wrapper_classes.get(key) if cls is None: namespace: dict[str, Any] = { @@ -621,7 +655,7 @@ def _wrapper_class_for( if hasattr(base, op): continue namespace[op] = ( - _make_backend_op(op) + _make_backend_op(op, mirror=getattr(inner_cls, op, None)) if has_delete else getattr(protocol.BackendProtocol, op) ) @@ -632,11 +666,14 @@ def _wrapper_class_for( bases = (base, sandbox_cls) for op in ("execute", "aexecute"): if not hasattr(base, op): - namespace[op] = _make_backend_op(op) + namespace[op] = _make_backend_op( + op, mirror=getattr(inner_cls, op, None) + ) if not hasattr(base, "id"): # The sandbox class defines ``id``; keep reading the inner # backend's, which ``__getattr__`` would otherwise have served. namespace["id"] = property(lambda self: self._inner.id) - cls = type(base.__name__, bases, namespace) - _wrapper_classes[key] = cls + # Workflow tasks run on a thread pool, so two runs can build the same + # key at once; the first class stored wins for both callers. + cls = _wrapper_classes.setdefault(key, type(base.__name__, bases, namespace)) return cls diff --git a/tests/contrib/deepagents/test_backends.py b/tests/contrib/deepagents/test_backends.py index 3de2132c7..997f1cc0a 100644 --- a/tests/contrib/deepagents/test_backends.py +++ b/tests/contrib/deepagents/test_backends.py @@ -13,6 +13,7 @@ from __future__ import annotations import gc +import inspect import sys import uuid from datetime import timedelta @@ -201,6 +202,45 @@ def test_temporal_backend_mirrors_inner_execution_support(tmp_path: Path) -> Non assert supports_execution(shell_inner) is True +def test_temporal_backend_mirrors_execute_timeout_support(tmp_path: Path) -> None: + # deepagents gates the execute tool's per-command ``timeout`` on a + # SIGNATURE probe: ``execute_accepts_timeout(type(backend))`` looks for a + # ``timeout`` parameter on ``execute`` and, unlike the ``max_count`` probe, + # does not take ``**kwargs`` as a stand-in. A bare ``(*args, **kwargs)`` + # dispatcher read False, so a wrapped LocalShellBackend refused a timeout + # its unwrapped self accepts. + protocol = pytest.importorskip("deepagents.backends.protocol") + + shell_inner = LocalShellBackend(root_dir=str(tmp_path)) + shell = TemporalBackend(shell_inner) + assert protocol.execute_accepts_timeout(type(shell_inner)) is True + assert protocol.execute_accepts_timeout(type(shell)) is True + for op in ("execute", "aexecute"): + assert inspect.signature(getattr(type(shell), op)) == inspect.signature( + getattr(type(shell_inner), op) + ) + # The ``max_count`` probe, which does accept ``**kwargs``, still passes. + assert protocol._method_accepts_max_count(type(shell), "grep") is True + + # A sandbox whose execute takes no timeout (deepagents' "older backend + # package" case) must read False wrapped too: deepagents then declines the + # call up front instead of the forwarded keyword failing inside the + # activity. Built with type(): a class statement cannot name its base off + # the importorskip module for the type checkers. + def execute_without_timeout(_self: Any, command: str) -> str: + return command + + NoTimeoutSandbox = type( + "NoTimeoutSandbox", + (protocol.SandboxBackendProtocol,), + {"execute": execute_without_timeout}, + ) + no_timeout = TemporalBackend(NoTimeoutSandbox()) + assert protocol.execute_accepts_timeout(NoTimeoutSandbox) is False + assert protocol.execute_accepts_timeout(type(no_timeout)) is False + assert type(no_timeout) is not type(shell) + + def test_temporal_backend_subclass_keeps_capability_mirroring() -> None: protocol = pytest.importorskip("deepagents.backends.protocol") @@ -235,7 +275,10 @@ async def run(self, root_dir: str) -> str: activity_options={"start_to_close_timeout": timedelta(seconds=30)}, ) result = await backend.aexecute("echo shell-ok") - return str(getattr(result, "output", result)) + # The per-command timeout deepagents forwards to a timeout-capable + # sandbox crosses the activity boundary along with the command. + timed = await backend.aexecute("echo shell-timeout-ok", timeout=5) + return f"{getattr(result, 'output', result)}|{getattr(timed, 'output', timed)}" @pytest.mark.asyncio @@ -258,8 +301,85 @@ async def test_temporal_backend_execute_runs_as_activity( out = await handle.result() assert "shell-ok" in out + assert "shell-timeout-ok" in out + counts = await count_scheduled_activities(handle) + # One activity per execute call; the second carried ``timeout=5``. + assert counts[BACKEND_OP] == 2, counts + + +@workflow.defn +class ShellAgentWorkflow: + @workflow.run + async def run(self, root_dir: str) -> str: + backend = TemporalBackend( + LocalShellBackend(root_dir=root_dir), + activity_options={"start_to_close_timeout": timedelta(seconds=30)}, + ) + agent = create_deep_agent( + model="anthropic:claude-sonnet-4-5", + backend=backend, + system_prompt="Run the command, then report its output.", + ) + result = await agent.ainvoke( + {"messages": [{"role": "user", "content": "Run it."}]} + ) + # The execute tool's own message says whether deepagents ran the + # command or declined the timeout up front. + return "\n".join(str(m.content) for m in result["messages"] if m.type == "tool") + + +@pytest.mark.asyncio +async def test_agent_execute_tool_forwards_timeout_through_backend( + env: WorkflowEnvironment, tmp_path: Path +) -> None: + """deepagents' built-in ``execute`` tool, called WITH a per-command + ``timeout``, runs through a TemporalBackend-wrapped LocalShellBackend. + + Regression: deepagents gates the timeout on + ``execute_accepts_timeout(type(backend))``, a signature probe for a + ``timeout`` parameter on ``execute``. The dispatcher's bare + ``(*args, **kwargs)`` read False, so the tool answered "does not support + per-command timeout overrides" for a backend that accepts one unwrapped. + """ + from langchain_core.messages import AIMessage + + from temporalio.contrib.deepagents.testing import mock_model_provider + + execute_turn = AIMessage( + content="", + tool_calls=[ + { + "name": "execute", + "args": {"command": "echo tool-timeout-ok", "timeout": 5}, + "id": "call-execute", + } + ], + ) + final = AIMessage(content="Done.") + plugin = DeepAgentsPlugin( + model_provider=mock_model_provider([execute_turn, final]), + ) + async with Worker( + env.client, + task_queue="da-shell-agent", + workflows=[ShellAgentWorkflow], + plugins=[plugin], + max_cached_workflows=0, + ): + handle = await env.client.start_workflow( + ShellAgentWorkflow.run, + str(tmp_path), + id=f"da-shell-agent-{uuid.uuid4()}", + task_queue="da-shell-agent", + ) + out = await handle.result() + + assert "tool-timeout-ok" in out, out + assert "does not support per-command timeout" not in out, out counts = await count_scheduled_activities(handle) + # The command ran as an activity, not in the workflow. assert counts[BACKEND_OP] == 1, counts + assert counts["deepagents.invoke_model"] == 2, counts def test_temporal_backend_unregisters_on_gc() -> None: From b1c904e5ba59592f5f664c7abf532a585e795fd1 Mon Sep 17 00:00:00 2001 From: DABH Date: Sat, 19 Sep 2026 23:15:54 -0500 Subject: [PATCH 4/6] Loosen deepagents floor and align langchain floors deepagents 0.7.0 already has everything the plugin relies on (SandboxBackendProtocol, supports_execution, execute_accepts_timeout, _supports_delete, the optional delete/adelete), and the deepagents test suite passes against it, so the extra allows >=0.7,<0.8 instead of the 0.7.12 that exclude-newer happened to resolve. The langchain / langchain-core (and dev langchain-anthropic) floors were carried over from the deepagents 0.6.12 requirements and sat below what any 0.7.x pulls in; they now match deepagents 0.7.0's own floors (langchain>=1.3.14, langchain-core>=1.5.0, langchain-anthropic>=1.5.3). Only the requires-dist metadata in uv.lock changes; resolved versions are unchanged. --- CHANGELOG.md | 4 ++-- pyproject.toml | 14 +++++++------- uv.lock | 14 +++++++------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 32faf7dbc..8ba746293 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,8 +27,8 @@ to include examples, links to docs, or any other relevant information. - Added the `temporalio.contrib.gcp.cloud_run.id` module with the `CloudRunIdPlugin` client plugin to set the worker identity on Cloud Run. ### Changed -- The `deepagents` extra now requires `deepagents>=0.7.12,<0.8` (was `<0.7`). Because - deepagents 0.7 requires `langsmith>=0.11.2`, the `langsmith` extra now allows +- The `deepagents` extra now requires `deepagents>=0.7,<0.8` (was `<0.7`). Because + deepagents 0.7 requires `langsmith>=0.10.9`, the `langsmith` extra now allows `langsmith<0.13` (was `<0.9`). ### Deprecated diff --git a/pyproject.toml b/pyproject.toml index 0c2a7d1e7..3c861c302 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,9 +33,9 @@ google-adk = ["google-adk>=2.2.0,<3", "mcp>=1.24,<2"] langgraph = ["langgraph>=1.1.0"] langsmith = ["langsmith>=0.7.34,<0.13"] deepagents = [ - "deepagents>=0.7.12,<0.8; python_version >= '3.11'", - "langchain>=1.3.11,<2; python_version >= '3.11'", - "langchain-core>=1.4.8,<2; python_version >= '3.11'", + "deepagents>=0.7,<0.8; python_version >= '3.11'", + "langchain>=1.3.14,<2; python_version >= '3.11'", + "langchain-core>=1.5.0,<2; python_version >= '3.11'", ] lambda-worker-otel = [ "opentelemetry-api>=1.26,<2", @@ -97,10 +97,10 @@ dev = [ "moto[s3,server]>=5", "langgraph>=1.1.0", "langsmith>=0.7.34,<0.13", - "deepagents>=0.7.12,<0.8; python_version >= '3.11'", - "langchain>=1.3.11,<2; python_version >= '3.11'", - "langchain-core>=1.4.8,<2; python_version >= '3.11'", - "langchain-anthropic>=1.4.7; python_version >= '3.11'", + "deepagents>=0.7,<0.8; python_version >= '3.11'", + "langchain>=1.3.14,<2; python_version >= '3.11'", + "langchain-core>=1.5.0,<2; python_version >= '3.11'", + "langchain-anthropic>=1.5.3; python_version >= '3.11'", "setuptools<82", "opentelemetry-exporter-otlp-proto-grpc>=1.11.1,<2", "opentelemetry-semantic-conventions>=0.40b0,<1", diff --git a/uv.lock b/uv.lock index 8ffdf00fa..aee295d34 100644 --- a/uv.lock +++ b/uv.lock @@ -4919,12 +4919,12 @@ dev = [ [package.metadata] requires-dist = [ { name = "aioboto3", marker = "extra == 'aioboto3'", specifier = ">=10.4.0" }, - { name = "deepagents", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=0.7.12,<0.8" }, + { name = "deepagents", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=0.7,<0.8" }, { name = "google-adk", marker = "extra == 'google-adk'", specifier = ">=2.2.0,<3" }, { name = "google-genai", marker = "extra == 'google-genai'", specifier = ">=2.21.0,<3.0.0" }, { name = "grpcio", marker = "extra == 'grpc'", specifier = ">=1.48.2,<2" }, - { name = "langchain", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.3.11,<2" }, - { name = "langchain-core", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.4.8,<2" }, + { name = "langchain", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.3.14,<2" }, + { name = "langchain-core", marker = "python_full_version >= '3.11' and extra == 'deepagents'", specifier = ">=1.5.0,<2" }, { name = "langgraph", marker = "extra == 'langgraph'", specifier = ">=1.1.0" }, { name = "langsmith", marker = "extra == 'langsmith'", specifier = ">=0.7.34,<0.13" }, { name = "mcp", marker = "extra == 'google-adk'", specifier = ">=1.24,<2" }, @@ -4958,13 +4958,13 @@ dev = [ { name = "basedpyright", specifier = "==1.34.0" }, { name = "cibuildwheel", specifier = ">=2.22.0,<3" }, { name = "cryptography", specifier = ">=46" }, - { name = "deepagents", marker = "python_full_version >= '3.11'", specifier = ">=0.7.12,<0.8" }, + { name = "deepagents", marker = "python_full_version >= '3.11'", specifier = ">=0.7,<0.8" }, { name = "googleapis-common-protos", specifier = ">=1.75.0,<2" }, { name = "grpcio-tools", specifier = ">=1.48.2,<2" }, { name = "httpx", specifier = ">=0.28.1" }, - { name = "langchain", marker = "python_full_version >= '3.11'", specifier = ">=1.3.11,<2" }, - { name = "langchain-anthropic", marker = "python_full_version >= '3.11'", specifier = ">=1.4.7" }, - { name = "langchain-core", marker = "python_full_version >= '3.11'", specifier = ">=1.4.8,<2" }, + { name = "langchain", marker = "python_full_version >= '3.11'", specifier = ">=1.3.14,<2" }, + { name = "langchain-anthropic", marker = "python_full_version >= '3.11'", specifier = ">=1.5.3" }, + { name = "langchain-core", marker = "python_full_version >= '3.11'", specifier = ">=1.5.0,<2" }, { name = "langgraph", specifier = ">=1.1.0" }, { name = "langsmith", specifier = ">=0.7.34,<0.13" }, { name = "litellm", specifier = ">=1.83.0" }, From d1f1ae40a6663876d1c9af4bc93cf771982cc745 Mon Sep 17 00:00:00 2001 From: DABH Date: Sat, 19 Sep 2026 23:15:54 -0500 Subject: [PATCH 5/6] Fix langsmith version in comments The Optional exclude_inputs default and LANGSMITH_EXCLUDE_INPUTS_ON_PATCH landed in langsmith 0.11.0 (0.10.9 still has exclude_inputs: bool = False), not 0.12. --- temporalio/contrib/langsmith/_interceptor.py | 4 ++-- tests/contrib/langsmith/test_background_io.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/temporalio/contrib/langsmith/_interceptor.py b/temporalio/contrib/langsmith/_interceptor.py index 1e2cd147f..46d460d4b 100644 --- a/temporalio/contrib/langsmith/_interceptor.py +++ b/temporalio/contrib/langsmith/_interceptor.py @@ -340,8 +340,8 @@ def patch(self, *, exclude_inputs: bool | None = None) -> None: """Patch the run to LangSmith, skipping during replay. ``exclude_inputs`` is forwarded untouched so LangSmith's own default - applies (a plain ``False`` before 0.12; the - ``LANGSMITH_EXCLUDE_INPUTS_ON_PATCH`` setting from 0.12 on). + applies (a plain ``False`` before 0.11; the + ``LANGSMITH_EXCLUDE_INPUTS_ON_PATCH`` setting from 0.11 on). """ if temporalio.workflow.in_workflow(): if _is_replaying(): diff --git a/tests/contrib/langsmith/test_background_io.py b/tests/contrib/langsmith/test_background_io.py index 972ba6e16..4ddd61b3d 100644 --- a/tests/contrib/langsmith/test_background_io.py +++ b/tests/contrib/langsmith/test_background_io.py @@ -236,7 +236,7 @@ def test_patch_submits_to_executor_in_workflow( executor.shutdown(wait=True) # No argument means "LangSmith's default": forwarded as None, never - # resolved to a bool here (0.12 reads LANGSMITH_EXCLUDE_INPUTS_ON_PATCH). + # resolved to a bool here (0.11 reads LANGSMITH_EXCLUDE_INPUTS_ON_PATCH). mock_run.patch.assert_called_once_with(exclude_inputs=None) @patch(_PATCH_IN_WORKFLOW, return_value=False) From 6cc6bad70777367ec1f614e4cf9abb53928b235a Mon Sep 17 00:00:00 2001 From: David Hyde Date: Sun, 20 Sep 2026 00:51:31 -0500 Subject: [PATCH 6/6] clean up readme --- CHANGELOG.md | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cc29cc238..ee61c44c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -56,16 +56,10 @@ to include examples, links to docs, or any other relevant information. ### Fixed -- `temporalio.contrib.deepagents.TemporalBackend` mirrors the wrapped backend's optional - capabilities at class level, where deepagents 0.7 checks them: `delete` / `adelete` are - offered (and run as `deepagents.backend_op` Activities) only for a delete-capable backend, - and `execute` / `aexecute` only for an execution-capable one, so a wrapped filesystem or - store backend no longer advertises a shell tool that could only fail. User subclasses of - `TemporalBackend` get the same mirroring. +- `temporalio.contrib.deepagents.TemporalBackend` is fixed for deepagents 0.7 compatibility + (e.g., adding `delete` / `adelete`). - `temporalio.contrib.deepagents.TemporalBackend` now forwards the per-command `timeout` of - deepagents' `execute` tool for a wrapped sandbox backend such as `LocalShellBackend`: its - `execute` / `aexecute` dispatchers carry the wrapped backend's signatures, which deepagents - inspects to decide whether a backend accepts `timeout`. + deepagents' `execute` tool for a wrapped sandbox backend such as `LocalShellBackend`. - `GoogleAdkPlugin` now passes the optional `anthropic`, `litellm`, and `openai` SDKs through the workflow sandbox. - `contrib.deepagents`: prevent duplicate input messages after continue-as-new.