[AI-250] Add Deep Agents plugin - #1644
Open
DABH wants to merge 12 commits into
Open
Conversation
temporalio.contrib.deepagents makes LangChain Deep Agents durable: unmodified create_deep_agent(...).ainvoke(...) code runs inside a workflow with plugins=[DeepAgentsPlugin()], routing every model turn, I/O tool call, and backend file/shell op through Temporal activities while the agent loop replays deterministically. Includes explicit per-tool workflow-vs-activity choice (activity_as_tool / tool_as_activity), TemporalBackend interception of the full backend protocol (sync + async + execute) with typed protocol-dataclass round-trip, continue-as-new state carry via run_deep_agent, streaming through workflow_streams, and native LangGraph interrupt/resume for human-in-the-loop. Ships as the temporalio[deepagents] extra (Python >= 3.11, matching deepagents' own floor).
deepagents' BackendProtocol implements every async method's DEFAULT as asyncio.to_thread(sync_twin, ...), and neither StateBackend nor FilesystemBackend overrides any of them. The deterministic workflow event loop has no thread executor, so an agent's first built-in tool call against an unwrapped in-workflow backend — e.g. a model spontaneously invoking grep on the default state backend — failed the workflow task with NotImplementedError. Scripted-model tests never invoke built-ins on the default backend, so only a live-model run surfaced it. The plugin now patches the protocol's async defaults at worker start (same seam pattern as the resolve_model patch): inside a workflow the sync twin runs inline, which for state-only backends is deterministic and semantically identical to the upstream default; outside a workflow the upstream thread-hop default is untouched, as are subclasses that override an async method natively.
tool_as_activity returned the ToolMessage assembled inside the invoke_tool activity, whose tool_call_id is workflow-generated — the activity cannot know the id the model minted for the call. A real provider (Anthropic) rejects the next model turn with 400 "unexpected tool_use_id found in tool_result blocks" because the tool_result does not pair with any tool_use in the previous message. Offline fakes never validate the pairing, so only a live-model run surfaced it. The wrapper now returns the tool result CONTENT and lets the tool node stamp the model's own tool_call_id — the same path unwrapped tools take. activity_as_tool already returned raw content and is unaffected. The regression test records the fake model's second-turn request and asserts the tool_result rides under the scripted id.
Two CI-only failure classes the local scoped runs missed: - basedpyright fails on warnings repo-wide. Replace deprecated typing aliases (Mapping/Sequence/AsyncIterator/Iterator/List/Optional/Union) with collections.abc / PEP 604 forms, type the test fixtures the way the rest of the suite does, drop unused imports/params, and keep the interpreter-floor warning behind a module constant so newer runtimes do not narrow it into unreachable code. - Python 3.10 jobs cannot install deepagents (its floor is 3.11), so pyright cannot resolve those imports there. Runtime imports of deepagents/langchain in module code go through importlib (attribute access on ModuleType is dynamic; monkeypatch writes use setattr), and the deepagents test modules carry a file-level pyright directive alongside their existing importorskip guards. langchain-core stays statically imported — it resolves everywhere via the langgraph extra.
The API-docs build rejects an inline literal whose end-string is followed by a letter (``Serializable``s), and cannot resolve a :class: link to the package re-export, so both become plain prose / literals. On 3.10, where the real deepagents package cannot install, basedpyright resolves `from deepagents import ...` in tests/contrib/deepagents/ implicitly relative to the same-named test directory — extend the existing file-level directive; Python 3 has no implicit relative imports at runtime and collection is already importorskip-gated.
The previous round's file-level directive used reportImplicitRelativeImport, which is basedpyright-only vocabulary — plain pyright hard-errors on unknown rules in pyright comments, taking every lint leg down. Rather than juggle two checkers' rule sets, drop the static `from deepagents import ...` lines from the tests entirely: symbols now bind off the module object pytest.importorskip already returns, which is dynamically typed for every checker, resolves nothing against the same-named test directory on 3.10, and lets the directives (and the now-moot protocol cast) be deleted outright.
There was a problem hiding this comment.
Pull request overview
Adds a new temporalio.contrib.deepagents integration that makes LangChain Deep Agents durable inside Temporal Workflows by routing model/tool/backend I/O through Activities while keeping the agent loop deterministic and replay-safe in-workflow.
Changes:
- Introduces
DeepAgentsPluginplus workflow-side helpers (run_deep_agent, dispatch choke points) and worker-side activities for model/tool/backend execution. - Adds serialization/caching utilities for LangChain objects and deepagents backend dataclasses, plus sandbox passthrough configuration.
- Adds a comprehensive pytest suite under
tests/contrib/deepagents/and wires new packaging extras/deps (temporalio[deepagents], Python ≥ 3.11).
Reviewed changes
Copilot reviewed 24 out of 27 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| uv.lock | Adds locked dependencies for the new deepagents extra and related LangChain packages. |
| pyproject.toml | Adds the deepagents extra + dev deps gated to Python ≥ 3.11. |
| tests/contrib/deepagents/init.py | New test package marker for the deepagents contrib tests. |
| tests/contrib/deepagents/helpers.py | Adds shared helper to count scheduled activities from workflow history. |
| tests/contrib/deepagents/test_backends.py | Tests TemporalBackend routing (including deepagents FilesystemBackend async protocol + replay). |
| tests/contrib/deepagents/test_checkpointer.py | Tests durable-checkpointer warning semantics and replay rehydration behavior. |
| tests/contrib/deepagents/test_continue_as_new.py | E2E tests for run_deep_agent(..., continue_as_new_after=...) snapshot/carry and cache reuse. |
| tests/contrib/deepagents/test_failures.py | Tests HTTP-error→retry translation and stable workflow failure typing. |
| tests/contrib/deepagents/test_hitl.py | Tests HITL interrupt surfaced via Query + resume via Update/Command protocol. |
| tests/contrib/deepagents/test_model_activity.py | Tests TemporalModel calls becoming a single model activity (and per-model overrides). |
| tests/contrib/deepagents/test_native_e2e.py | End-to-end “plugin only” durability test for unmodified create_deep_agent(...).ainvoke(...). |
| tests/contrib/deepagents/test_readme.py | Ensures README fenced python snippets compile. |
| tests/contrib/deepagents/test_replay.py | Records history and replays it through Replayer with the plugin configured. |
| tests/contrib/deepagents/test_side_effects.py | Forces replay (max_cached_workflows=0) and asserts bounded/expected activity scheduling. |
| tests/contrib/deepagents/test_streaming.py | Tests streaming dispatch via invoke_model_streaming and batch interval wiring. |
| tests/contrib/deepagents/test_subagents.py | Tests sub-agent model calls still route via activities. |
| tests/contrib/deepagents/test_tools.py | Tests activity_as_tool and tool_as_activity routing + tool_call_id pairing behavior. |
| temporalio/contrib/deepagents/init.py | Lazy public surface for the plugin and helpers without requiring LangChain at import time. |
| temporalio/contrib/deepagents/README.md | Documents installation and usage patterns (hello world, CAN snapshotting, streaming, composition). |
| temporalio/contrib/deepagents/_activity.py | Implements the four activities and boundary dataclasses, plus error translation and heartbeating. |
| temporalio/contrib/deepagents/_model.py | Implements TemporalModel and patches deepagents model resolution inside workflows. |
| temporalio/contrib/deepagents/_plugin.py | Wires activities, data converter, sandbox passthrough, run-context patching, and failure typing. |
| temporalio/contrib/deepagents/_serde.py | Provides LangChain object (de)serialization, runnable config stripping, result cache, passthrough list. |
| temporalio/contrib/deepagents/_tools.py | Implements tool/backend seams (activity_as_tool, tool_as_activity, TemporalBackend) and registries. |
| temporalio/contrib/deepagents/testing.py | Provides offline testing helpers (scripted fake model and mock model provider). |
| temporalio/contrib/deepagents/workflow.py | Provides workflow-side dispatch choke points, durable-checkpointer warning, and CAN runner. |
| temporalio/contrib/deepagents/py.typed | Marks the package as typed for downstream type checkers. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Await the cancelled heartbeat task so no pending task outlives an activity at event-loop shutdown. - Catch only ImportError when installing the create_deep_agent patches and include the original error in the warning; genuine installation bugs now surface at worker startup. - Keep the LangSmith aio_to_thread override installed for the process lifetime: the override slot is global and resetting it would strip a composed contrib.langsmith plugin's identical override. - Unregister a TemporalBackend's registry entry when the wrapper is garbage-collected, identity-guarded so a replay's re-registration of the same deterministic ref survives the evicted wrapper's cleanup.
# Conflicts: # uv.lock
…DME overhaul - Add create_temporal_deep_agent(), a thin wrapper over create_deep_agent that scopes activity_options to one agent's model calls — the explicit, other-plugins-shaped construction path; drop-in vanilla create_deep_agent still works. - run_deep_agent now defaults continue_as_new_after=None to the server's is_continue_as_new_suggested() signal (accounts for history size, not just count); fixed thresholds remain available. - README: uv add install, guard-free imports (the plugin's sandbox passthrough already made imports_passed_through unnecessary — now proven by a test that imports deepagents bare in a workflow module), dedicated sections with snippets for tools/backends/HITL/streaming/options, and the plugin-ordering claim removed from the composition section.
On 3.10 (deepagents not installed) pyright mis-resolves the static `from deepagents import create_deep_agent` inside the new factory and reports the symbol uncallable; use the module-attribute access pattern _model.py already relies on. On 3.14 and latest-deps, basedpyright gates on warnings and flagged the sandbox-passthrough test's deliberately-bare `import deepagents` as unused — the noqa only covered ruff; add the pyright ignore alongside it.
…-CAN test Pyright on 3.10 (deepagents absent) resolves the module attribute itself as an uncallable object, so bind deepagents.create_deep_agent through an Any-typed local before calling. The suggested-mode continue-as-new e2e requires the local dev server's low suggestContinueAsNew threshold, so skip it under the time-skipping environment, following the existing env_type gating precedent in tests/worker/test_workflow.py.
brianstrauch
approved these changes
Jul 28, 2026
basedpyright resolves a static `import deepagents` from inside this same-named package directory (and the same-named test directory) as implicitly relative when the real package is absent (Python 3.10 CI) — which is also what produced the earlier uncallable-object error: the name resolved to our own lazy __getattr__. Load the module through importlib.import_module in the factory, and rule-ignore the deliberately-bare import in the sandbox-passthrough test. Also replace the :class: link to the lazily re-exported TemporalModel with a plain literal — pydoctor cannot resolve lazy re-exports as link targets and the API-docs step gates on it.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
temporalio.contrib.deepagents— durable execution for LangChain Deep Agents. Unmodified agent code (create_deep_agent(...).ainvoke(...)) runs inside a workflow withplugins=[DeepAgentsPlugin()]as the only change; the plugin hooks the SDK's native runtime so the agent loop replays deterministically in the workflow while everything effectful becomes a Temporal Activity.deepagents.invoke_model(orinvoke_model_streamingwhen astreaming_topicis configured, publishing chunk batches viacontrib.workflow_streamswhile returning the identical durable result). Workflows ship only a model name; the worker'smodel_providerreconstructs the real model — no credentials cross the boundary.activity_as_toolsurfaces an existing@activity.defnto the agent;tool_as_activityroutes an I/O LangChain tool throughdeepagents.invoke_tool. Pure state-mutating built-ins stay in-workflow.TemporalBackendwraps a real backend (e.g.FilesystemBackend) so the agent's built-in file/shell tools execute asdeepagents.backend_opactivities. It intercepts the full backend protocol — sync and async twins (read/aread, …) plusexecute/aexecute— and round-trips the protocol's result dataclasses (WriteResult/ReadResult/…) across the activity boundary as real typed objects.run_deep_agent(..., continue_as_new_after=N)carries the conversation and the model/tool result cache across continue-as-new; completed calls are not re-run.Command(resume=...)protocol maps onto Temporal Queries/Updates; no custom shim.exclude_unsetto keep payloads small) and an explicit transitivepassthrough_moduleslist.Tests
25 tests under
tests/contrib/deepagents/, all against a real dev server via the sharedenv/clientfixtures, offline (scriptedtesting.mock_model_provider, no API keys):create_deep_agentcode + plugin only; tool-loop routing asserts exact activity counts.FilesystemBackendops undermax_cached_workflows=0(replay by construction) — the built-inwrite_file/read_filetools cross the activity boundary and the write really lands on disk activity-side.ApplicationErrortype (429/400/503 +Retry-Afterhandling), continue-as-new conversation carry, sub-agent durability inheritance, streaming, checkpointer warning semantics, README snippets compile.Gates run locally:
pyright,basedpyright,mypy --namespace-packages --check-untyped-defs,pydocstyle,ruff check --select I,ruff format --check— all clean on the new code.Packaging
New
deepagentsextra (temporalio[deepagents]), with every dependency gated onpython_version >= '3.11'(deepagents' own floor; the SDK's 3.10 floor is unaffected). Test deps added to the dev group with the same markers; the test modules skip cleanly on 3.10.Deliberately deferred
ContextHubBackend/LangSmithSandbox(hosted services), stateful MCP sessions, and sub-agents as child workflows (v1 runs sub-agents in-workflow; they inherit the durable model, so their calls are already activities).Related
Samples: temporalio/samples-python#328 (draft; its
deepagentsdependency group picks up this extra once released).