Skip to main content

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 the deepagents / 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 whenever model= is a name string; use create_temporal_deep_agent when you want to scope activity_options to 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_provider builds the real client. Temporal owns retries and timeouts (LLM-SDK retries are disabled).
  • Sub-agents inherit durability. Because sub-agents inherit the parent's model object 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.

Call tool_as_activity(...) at module import or worker startup on every activity worker that can receive deepagents.invoke_tool. Registration is process-local; calling it only inside a workflow does not register the tool in another process. With activity_as_tool(...), register the underlying Temporal activity on every worker polling its task queue.

Durable file and shell backends

Use TemporalBackend("name", ...) for a real-I/O backend (FilesystemBackend / LocalShellBackend / a remote sandbox). The agent's built-in file and shell tools execute as durable deepagents.backend_op Activities. Each activity worker constructs the backend from the configuration sent with the operation, so multiple worker processes and serverless workers can execute it independently:

from datetime import timedelta

from deepagents.backends import FilesystemBackend
from temporalio import workflow
from temporalio.deepagents import (
    DeepAgentsPlugin,
    TemporalBackend,
    create_temporal_deep_agent,
)
from temporalio.workflow import ActivityConfig


# Configure this plugin on every worker polling backend activities.
plugin = DeepAgentsPlugin(
    backends={
        "filesystem": lambda root_dir: FilesystemBackend(
            root_dir=root_dir, virtual_mode=True
        ),
    }
)


@workflow.defn
class FilesystemAgent:
    @workflow.run
    async def run(self, root_dir: str) -> str:
        backend = TemporalBackend(
            "filesystem",
            factory_argument=root_dir,
            backend_type=FilesystemBackend,
            activity_options=ActivityConfig(
                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

Add plugin to the Client or Worker as in the Hello world example. activity_options accepts the SDK's ActivityConfig, a typed dictionary for timeouts, retry policy and task queue routing. Existing dictionary literals also work. backend_type advertises the backend's delete and shell capabilities and method signatures without constructing it in the workflow. For a sandbox, pass metadata={"id": sandbox_id} if workflow code needs its id.

For a remote sandbox, register a factory that reconnects using a serializable sandbox ID, and pass that ID as factory_argument. This follows the MCP plugins' named factory pattern: the lambda stays on the activity worker; only its name and argument cross the activity boundary. A factory can accept one positional argument (None by default), or no arguments when everything is configured on the worker, for example {"filesystem": lambda: FilesystemBackend(root_dir="/shared")}. Factories may be synchronous or asynchronous and run once per activity attempt. Operations and retries must reconnect to the same external state. Synchronous factory construction runs in a thread with the activity context preserved; returned awaitables run on the activity event loop. Changes held only in a backend instance's memory do not survive between calls. A local filesystem path must be accessible to every activity worker that can receive the operation, or the backend activities must be routed to a task queue whose workers share that filesystem. The factory argument is recorded in workflow history; credentials and live connections belong in the worker-side factory.

TemporalBackend(existing_backend, ...) also works when the workflow and its backend activities execute in the same process. It holds the backend in a process-local registry, so use a named backend factory when activities can run in another process.

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.run must accept state_snapshot=None as 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 InMemorySaver is 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.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for temporalio-deepagents 0.0.2
File Size Uploaded
temporalio_deepagents-0.0.2.tar.gz 42.2 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for temporalio-deepagents 0.0.2
File Interpreter ABI Platform
temporalio_deepagents-0.0.2-py3-none-any.whl Python 3 none any Details

Total release size: 89.8 kB

Release files / temporalio_deepagents-0.0.2.tar.gz

Download URL temporalio_deepagents-0.0.2.tar.gz
Size 42.2 kB
Tags Source
SHA-256 checksum
How to use checksums
646dabeac2991ae054f7b02788c3a6cb8ad3b6f1c6a09cad7a92ef9be4b67351
BLAKE2b-256 checksum
How to use checksums
ed8b4ab6926de7df35a43a5af5c82e6d2b4ba71553449b875a40446a73e1a6c8
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 16, 2026.

Transparency log

Release files / temporalio_deepagents-0.0.2-py3-none-any.whl

Download URL temporalio_deepagents-0.0.2-py3-none-any.whl
Size 47.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
95a8d261cd26b4edb90985c0ad530ef4c49c3bf28f8d22cc175e3057e60a3a23
BLAKE2b-256 checksum
How to use checksums
e5916613b35490f7e141c936314bc2bd81cdde2a748294de3e4831947d2766a6
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 16, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.2 This release

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page