DeepAgentsPlugin — Temporal plugin for LangChain Deep Agents
Make a Deep Agent durable by adding
one plugin. Build your agent with create_temporal_deep_agent(...) (or vanilla
create_deep_agent(...)) inside a @workflow.defn, add
plugins=[DeepAgentsPlugin(...)] to your Client (or Worker), and each LLM call
and each I/O tool call becomes a Temporal Activity — while the agent's control
loop runs, and deterministically replays, inside the Workflow.
The code you already wrote against deepagents does not change: sub-agents,
planning/todo state, the filesystem middleware, human-in-the-loop interrupts, and
agent.ainvoke(...) all keep working. You get crash-durability, resumable
human-in-the-loop, and bounded history on top.
Release stage: Pre-release.
Install
uv add "temporalio-deepagents"
(or pip install "temporalio-deepagents"). Requires Python ≥ 3.11 (the same
floor deepagents sets).
Hello world
import asyncio
from datetime import timedelta
from deepagents import create_deep_agent # no import guard needed; see below
from temporalio import workflow
from temporalio.client import Client
from temporalio.deepagents import (
DeepAgentsPlugin,
create_temporal_deep_agent,
)
from temporalio.worker import Worker
@workflow.defn
class ResearchAgent:
@workflow.run
async def run(self, question: str) -> str:
# create_temporal_deep_agent wraps deepagents' create_deep_agent and
# scopes this agent's model-call activity options explicitly.
agent = create_temporal_deep_agent(
model="anthropic:claude-sonnet-4-5",
system_prompt="You are a careful research assistant.",
activity_options={"start_to_close_timeout": timedelta(minutes=5)},
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": question}]}
)
return result["messages"][-1].content
async def main() -> None:
# API keys live on the worker via the model provider, never in workflow
# inputs or history. The default provider is LangChain's init_chat_model.
plugin = DeepAgentsPlugin()
# Add the plugin on ONE side. The SDK propagates a Client plugin to any
# Worker built from that Client, so the Worker below inherits it.
client = await Client.connect("localhost:7233", plugins=[plugin])
worker = Worker(
client,
task_queue="deepagents-task-queue",
workflows=[ResearchAgent],
)
await worker.run()
if __name__ == "__main__":
asyncio.run(main())
Two things worth noticing:
- No
workflow.unsafe.imports_passed_through()guard. The plugin configures the workflow sandbox to pass thedeepagents/ LangChain import tree through, so workflow files import them like any other module. - Vanilla
create_deep_agent(...)also works. The plugin substitutes the durable model automatically whenevermodel=is a name string; usecreate_temporal_deep_agentwhen you want to scopeactivity_optionsto one agent instead of configuring plugin-wide defaults.
What this plugin gives you
- Drop-in durability.
create_deep_agent(...).ainvoke(...)runs unchanged inside a Workflow. The loop replays deterministically; every nondeterministic step (LLM, I/O tool, real filesystem/shell op) is an Activity. - One LLM call per Activity. The Workflow ships only the model name; the
worker's
model_providerbuilds the real client. Temporal owns retries and timeouts (LLM-SDK retries are disabled). - Sub-agents inherit durability. Because sub-agents inherit the parent's
modelobject and tools, substituting them once propagates to the whole agent tree — no per-sub-agent wiring. - Tools, real-I/O backends, human-in-the-loop, streaming, and continue-as-new each get a section below.
Configuring activity options
Per agent (recommended): create_temporal_deep_agent(..., activity_options=...)
as in Hello world above. Plugin-wide defaults use two keyed maps, because model
calls and tool calls have different timeout profiles:
from datetime import timedelta
from temporalio.deepagents import DeepAgentsPlugin
plugin = DeepAgentsPlugin(
# A single config, or a map keyed by MODEL name (thinking-mode models get
# longer timeouts than fast ones).
model_activity_options={"start_to_close_timeout": timedelta(minutes=5)},
# A single config, or a map keyed by TOOL name.
tool_activity_options={"start_to_close_timeout": timedelta(seconds=30)},
)
Tools: the Workflow-vs-Activity choice, made explicit
A tool that only mutates agent state can run in-workflow; a tool that does real I/O must not. Both directions are one call:
from datetime import timedelta
from langchain_core.tools import tool
from temporalio import activity
from temporalio.deepagents import activity_as_tool, tool_as_activity
@activity.defn
async def get_weather(city: str) -> str:
"""Return the current weather for a city."""
return f"It is sunny and 22C in {city}."
@tool
def web_search(query: str) -> str:
"""Search the web for a query."""
return f"Top result for {query!r}: ..."
# An existing Temporal activity, exposed to the agent as a tool:
weather_tool = activity_as_tool(
get_weather, start_to_close_timeout=timedelta(seconds=30)
)
# A LangChain tool whose body does I/O, moved into an activity:
search_tool = tool_as_activity(
web_search, start_to_close_timeout=timedelta(seconds=30)
)
Pass both to create_temporal_deep_agent(..., tools=[weather_tool, search_tool]). An unwrapped, non-builtin tool runs in-workflow and the plugin
warns at construction, so the choice is never silent. Deep Agents' pure
built-ins (write_todos, state-backed file tools) stay in-workflow by design.
Durable file and shell backends
Wrap a real-I/O backend (FilesystemBackend / LocalShellBackend /
StoreBackend) in TemporalBackend and the agent's built-in file and shell
tools execute as durable deepagents.backend_op Activities instead of touching
disk from workflow code:
from datetime import timedelta
from deepagents.backends import FilesystemBackend
from temporalio import workflow
from temporalio.deepagents import TemporalBackend, create_temporal_deep_agent
@workflow.defn
class FilesystemAgent:
@workflow.run
async def run(self, root_dir: str) -> str:
backend = TemporalBackend(
FilesystemBackend(root_dir=root_dir, virtual_mode=True),
activity_options={"start_to_close_timeout": timedelta(seconds=30)},
)
agent = create_temporal_deep_agent(
model="anthropic:claude-sonnet-4-5",
backend=backend,
)
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": "Take notes as you work."}]}
)
return result["messages"][-1].content
State-only backends (the default) need no wrapping — they are pure workflow state, replayed deterministically.
Human-in-the-loop
With interrupt_on=..., the agent pauses before a guarded tool and
ainvoke(...) returns the pending approval under the SDK-native
__interrupt__ key — directly in your workflow. Expose it via a Query and
resume with an Update; no shim exception, the native LangGraph resume protocol
is used as-is:
from langgraph.checkpoint.memory import InMemorySaver
from langgraph.types import Command
from temporalio import workflow
from temporalio.deepagents import create_temporal_deep_agent
@workflow.defn
class ApprovalAgent:
def __init__(self) -> None:
self._pending: str | None = None
self._decision: str | None = None
@workflow.run
async def run(self, request: str) -> str:
agent = create_temporal_deep_agent(
model="anthropic:claude-sonnet-4-5",
interrupt_on={"book_trip": True},
checkpointer=InMemorySaver(),
)
config = {"configurable": {"thread_id": workflow.info().workflow_id}}
result = await agent.ainvoke(
{"messages": [{"role": "user", "content": request}]}, config=config
)
if result.get("__interrupt__"):
self._pending = str(result["__interrupt__"][0].value)
await workflow.wait_condition(lambda: self._decision is not None)
result = await agent.ainvoke(
Command(resume={"decisions": [{"type": self._decision}]}),
config=config,
)
return result["messages"][-1].content
@workflow.query
def pending_approval(self) -> str | None:
return self._pending
@workflow.update
async def resume(self, decision: str) -> None:
self._decision = decision
A client polls pending_approval, shows it to a person, and calls the
resume update with "approve" / "reject".
Streaming
Set streaming_topic= on the plugin and model dispatch switches to a streaming
Activity that publishes chunk batches to a
temporalio.contrib.workflow_streams topic for live subscribers — while the
aggregated final message still returns to the workflow, so the durable result
is identical to the non-streaming path:
from temporalio.deepagents import DeepAgentsPlugin
plugin = DeepAgentsPlugin(streaming_topic="agent-stream")
Subscribers read the topic with WorkflowStreamClient; each item is an
AIMessageChunk in langchain_core.load.dumpd form.
Continue-as-new: what carries and what does not
Long conversations bloat workflow history. run_deep_agent(agent, input, state_snapshot=...) snapshots state and continues into a fresh run when the
turn ends with pending todos and the server recommends continuing
(workflow.info().is_continue_as_new_suggested(), the default and recommended
mode — it accounts for history length and size):
from deepagents import create_deep_agent
from temporalio import workflow
from temporalio.deepagents import run_deep_agent
@workflow.defn
class LongResearchAgent:
@workflow.run
async def run(self, input: dict, state_snapshot: dict | None = None) -> dict:
agent = create_deep_agent(model="anthropic:claude-sonnet-4-5")
return await run_deep_agent(
agent,
input,
state_snapshot=state_snapshot,
)
Pass continue_as_new_after=N instead to trigger on a fixed history-event
count.
- Carries forward: the accumulated messages. Repeated identical calls are
NOT deduplicated: a call the agent re-issues after the boundary runs its own
Activity, so a genuinely new identical request is never served a stale prior
result. Your
@workflow.runmust acceptstate_snapshot=Noneas shown. - Does not carry forward: anything held only in an in-memory checkpointer's
own structures beyond the messages/todos snapshot. The default in-workflow
InMemorySaveris rehydrated for free by deterministic replay; a durable checkpointer that does its own I/O is not replay-safe from inside a workflow, and the plugin warns if you pass one — prefer the snapshot + continue-as-new path above.
Runtime behavior
While a worker built with this plugin is running, the plugin wraps
deepagents.create_deep_agent so a bare model="provider:name" string is
auto-routed through an Activity. The wrapper only rewrites arguments when called
inside a workflow, so importing deepagents on a plain client or activity
worker is unaffected, and the original function is restored when the worker
stops. If you would rather be explicit, use create_temporal_deep_agent or
pass TemporalModel("provider:name") yourself.
Composing with other plugins
This plugin carries no tracing context of its own. For observability, compose it
with temporalio.langsmith or temporalio.contrib.opentelemetry —
registration order does not matter:
from temporalio.client import Client
from temporalio.deepagents import DeepAgentsPlugin
async def connect():
return await Client.connect(
"localhost:7233",
plugins=[
# LangSmithPlugin(), # or OpenTelemetryPlugin(), in either order
DeepAgentsPlugin(),
],
)
For agents built directly as LangGraph graphs (rather than a compiled Deep
Agent), see temporalio.langgraph.
Metadata
Release files for temporalio-deepagents 0.0.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| temporalio_deepagents-0.0.1.tar.gz | 39.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| temporalio_deepagents-0.0.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 84.0 kB
Release files / temporalio_deepagents-0.0.1.tar.gz
| Download URL | temporalio_deepagents-0.0.1.tar.gz |
|---|---|
| Size | 39.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ce7793b9ec98e96ad52768ca99a18f9e291b57024df7c06c3965b53e1739d79f
|
|
BLAKE2b-256 checksum How to use checksums |
a30ddca4dc7ade5695e97488a6e4d0d59ed489a0d1b8a9e8d4c4a99b9883ebe2
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency logRelease files / temporalio_deepagents-0.0.1-py3-none-any.whl
| Download URL | temporalio_deepagents-0.0.1-py3-none-any.whl |
|---|---|
| Size | 44.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
270f5acc74ff1ce12661bd01f385e80463c02605b14dea9782b9dd0730c15a38
|
|
BLAKE2b-256 checksum How to use checksums |
967b14b4f1e22f9eb5019b5d5349f99809a76fdeb456f6834683fffbf7559fa3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 7, 2026.
Transparency log