temporalio-deepagents 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 temporal.io
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,367 @@
1
+ Metadata-Version: 2.4
2
+ Name: temporalio-deepagents
3
+ Version: 0.0.1
4
+ Summary: Temporal integration for deepagents
5
+ Author: Temporal Technologies Inc
6
+ Author-email: Temporal Technologies Inc <sdk@temporal.io>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Classifier: Programming Language :: Python :: 3.13
13
+ Classifier: Programming Language :: Python :: 3.14
14
+ Classifier: Typing :: Typed
15
+ Requires-Dist: temporalio>=1.34.0,<2
16
+ Requires-Dist: deepagents>=0.7,<0.8
17
+ Requires-Dist: langchain>=1.3.14,<2
18
+ Requires-Dist: langchain-core>=1.5.0,<2
19
+ Requires-Dist: langgraph>=1.1.0
20
+ Requires-Dist: langsmith>=0.10.9,<0.13
21
+ Requires-Dist: pydantic>=2.0.0,<3
22
+ Requires-Python: >=3.11
23
+ Project-URL: Homepage, https://github.com/temporalio/ai-integrations/tree/main/python/deepagents
24
+ Project-URL: Repository, https://github.com/temporalio/ai-integrations
25
+ Project-URL: Documentation, https://docs.temporal.io/develop/python/integrations/deepagents
26
+ Description-Content-Type: text/markdown
27
+
28
+ # DeepAgentsPlugin — Temporal plugin for LangChain Deep Agents
29
+
30
+ Make a [Deep Agent](https://github.com/langchain-ai/deepagents) durable by adding
31
+ one plugin. Build your agent with `create_temporal_deep_agent(...)` (or vanilla
32
+ `create_deep_agent(...)`) inside a `@workflow.defn`, add
33
+ `plugins=[DeepAgentsPlugin(...)]` to your Client (or Worker), and each LLM call
34
+ and each I/O tool call becomes a Temporal Activity — while the agent's control
35
+ loop runs, and deterministically replays, inside the Workflow.
36
+
37
+ The code you already wrote against `deepagents` does not change: sub-agents,
38
+ planning/todo state, the filesystem middleware, human-in-the-loop interrupts, and
39
+ `agent.ainvoke(...)` all keep working. You get crash-durability, resumable
40
+ human-in-the-loop, and bounded history on top.
41
+
42
+ > Release stage: [Pre-release](https://docs.temporal.io/develop/python/integrations/deepagents).
43
+
44
+ ## Install
45
+
46
+ ```bash
47
+ uv add "temporalio-deepagents"
48
+ ```
49
+
50
+ (or `pip install "temporalio-deepagents"`). Requires Python ≥ 3.11 (the same
51
+ floor `deepagents` sets).
52
+
53
+ ## Hello world
54
+
55
+ ```python
56
+ import asyncio
57
+ from datetime import timedelta
58
+
59
+ from deepagents import create_deep_agent # no import guard needed; see below
60
+ from temporalio import workflow
61
+ from temporalio.client import Client
62
+ from temporalio.deepagents import (
63
+ DeepAgentsPlugin,
64
+ create_temporal_deep_agent,
65
+ )
66
+ from temporalio.worker import Worker
67
+
68
+
69
+ @workflow.defn
70
+ class ResearchAgent:
71
+ @workflow.run
72
+ async def run(self, question: str) -> str:
73
+ # create_temporal_deep_agent wraps deepagents' create_deep_agent and
74
+ # scopes this agent's model-call activity options explicitly.
75
+ agent = create_temporal_deep_agent(
76
+ model="anthropic:claude-sonnet-4-5",
77
+ system_prompt="You are a careful research assistant.",
78
+ activity_options={"start_to_close_timeout": timedelta(minutes=5)},
79
+ )
80
+ result = await agent.ainvoke(
81
+ {"messages": [{"role": "user", "content": question}]}
82
+ )
83
+ return result["messages"][-1].content
84
+
85
+
86
+ async def main() -> None:
87
+ # API keys live on the worker via the model provider, never in workflow
88
+ # inputs or history. The default provider is LangChain's init_chat_model.
89
+ plugin = DeepAgentsPlugin()
90
+ # Add the plugin on ONE side. The SDK propagates a Client plugin to any
91
+ # Worker built from that Client, so the Worker below inherits it.
92
+ client = await Client.connect("localhost:7233", plugins=[plugin])
93
+ worker = Worker(
94
+ client,
95
+ task_queue="deepagents-task-queue",
96
+ workflows=[ResearchAgent],
97
+ )
98
+ await worker.run()
99
+
100
+
101
+ if __name__ == "__main__":
102
+ asyncio.run(main())
103
+ ```
104
+
105
+ Two things worth noticing:
106
+
107
+ - **No `workflow.unsafe.imports_passed_through()` guard.** The plugin
108
+ configures the workflow sandbox to pass the `deepagents` / LangChain import
109
+ tree through, so workflow files import them like any other module.
110
+ - **Vanilla `create_deep_agent(...)` also works.** The plugin substitutes the
111
+ durable model automatically whenever `model=` is a name string; use
112
+ `create_temporal_deep_agent` when you want to scope `activity_options` to one
113
+ agent instead of configuring plugin-wide defaults.
114
+
115
+ ## What this plugin gives you
116
+
117
+ - **Drop-in durability.** `create_deep_agent(...).ainvoke(...)` runs unchanged
118
+ inside a Workflow. The loop replays deterministically; every nondeterministic
119
+ step (LLM, I/O tool, real filesystem/shell op) is an Activity.
120
+ - **One LLM call per Activity.** The Workflow ships only the model *name*; the
121
+ worker's `model_provider` builds the real client. Temporal owns retries and
122
+ timeouts (LLM-SDK retries are disabled).
123
+ - **Sub-agents inherit durability.** Because sub-agents inherit the parent's
124
+ `model` object and tools, substituting them once propagates to the whole agent
125
+ tree — no per-sub-agent wiring.
126
+ - Tools, real-I/O backends, human-in-the-loop, streaming, and continue-as-new
127
+ each get a section below.
128
+
129
+ ## Configuring activity options
130
+
131
+ Per agent (recommended): `create_temporal_deep_agent(..., activity_options=...)`
132
+ as in Hello world above. Plugin-wide defaults use two keyed maps, because model
133
+ calls and tool calls have different timeout profiles:
134
+
135
+ ```python
136
+ from datetime import timedelta
137
+
138
+ from temporalio.deepagents import DeepAgentsPlugin
139
+
140
+ plugin = DeepAgentsPlugin(
141
+ # A single config, or a map keyed by MODEL name (thinking-mode models get
142
+ # longer timeouts than fast ones).
143
+ model_activity_options={"start_to_close_timeout": timedelta(minutes=5)},
144
+ # A single config, or a map keyed by TOOL name.
145
+ tool_activity_options={"start_to_close_timeout": timedelta(seconds=30)},
146
+ )
147
+ ```
148
+
149
+ ## Tools: the Workflow-vs-Activity choice, made explicit
150
+
151
+ A tool that only mutates agent state can run in-workflow; a tool that does real
152
+ I/O must not. Both directions are one call:
153
+
154
+ ```python
155
+ from datetime import timedelta
156
+
157
+ from langchain_core.tools import tool
158
+ from temporalio import activity
159
+ from temporalio.deepagents import activity_as_tool, tool_as_activity
160
+
161
+
162
+ @activity.defn
163
+ async def get_weather(city: str) -> str:
164
+ """Return the current weather for a city."""
165
+ return f"It is sunny and 22C in {city}."
166
+
167
+
168
+ @tool
169
+ def web_search(query: str) -> str:
170
+ """Search the web for a query."""
171
+ return f"Top result for {query!r}: ..."
172
+
173
+
174
+ # An existing Temporal activity, exposed to the agent as a tool:
175
+ weather_tool = activity_as_tool(
176
+ get_weather, start_to_close_timeout=timedelta(seconds=30)
177
+ )
178
+ # A LangChain tool whose body does I/O, moved into an activity:
179
+ search_tool = tool_as_activity(
180
+ web_search, start_to_close_timeout=timedelta(seconds=30)
181
+ )
182
+ ```
183
+
184
+ Pass both to `create_temporal_deep_agent(..., tools=[weather_tool,
185
+ search_tool])`. An unwrapped, non-builtin tool runs in-workflow and the plugin
186
+ warns at construction, so the choice is never silent. Deep Agents' pure
187
+ built-ins (`write_todos`, state-backed file tools) stay in-workflow by design.
188
+
189
+ ## Durable file and shell backends
190
+
191
+ Wrap a real-I/O backend (`FilesystemBackend` / `LocalShellBackend` /
192
+ `StoreBackend`) in `TemporalBackend` and the agent's *built-in* file and shell
193
+ tools execute as durable `deepagents.backend_op` Activities instead of touching
194
+ disk from workflow code:
195
+
196
+ ```python
197
+ from datetime import timedelta
198
+
199
+ from deepagents.backends import FilesystemBackend
200
+ from temporalio import workflow
201
+ from temporalio.deepagents import TemporalBackend, create_temporal_deep_agent
202
+
203
+
204
+ @workflow.defn
205
+ class FilesystemAgent:
206
+ @workflow.run
207
+ async def run(self, root_dir: str) -> str:
208
+ backend = TemporalBackend(
209
+ FilesystemBackend(root_dir=root_dir, virtual_mode=True),
210
+ activity_options={"start_to_close_timeout": timedelta(seconds=30)},
211
+ )
212
+ agent = create_temporal_deep_agent(
213
+ model="anthropic:claude-sonnet-4-5",
214
+ backend=backend,
215
+ )
216
+ result = await agent.ainvoke(
217
+ {"messages": [{"role": "user", "content": "Take notes as you work."}]}
218
+ )
219
+ return result["messages"][-1].content
220
+ ```
221
+
222
+ State-only backends (the default) need no wrapping — they are pure workflow
223
+ state, replayed deterministically.
224
+
225
+ ## Human-in-the-loop
226
+
227
+ With `interrupt_on=...`, the agent pauses before a guarded tool and
228
+ `ainvoke(...)` returns the pending approval under the SDK-native
229
+ `__interrupt__` key — directly in your workflow. Expose it via a Query and
230
+ resume with an Update; no shim exception, the native LangGraph resume protocol
231
+ is used as-is:
232
+
233
+ ```python
234
+ from langgraph.checkpoint.memory import InMemorySaver
235
+ from langgraph.types import Command
236
+ from temporalio import workflow
237
+ from temporalio.deepagents import create_temporal_deep_agent
238
+
239
+
240
+ @workflow.defn
241
+ class ApprovalAgent:
242
+ def __init__(self) -> None:
243
+ self._pending: str | None = None
244
+ self._decision: str | None = None
245
+
246
+ @workflow.run
247
+ async def run(self, request: str) -> str:
248
+ agent = create_temporal_deep_agent(
249
+ model="anthropic:claude-sonnet-4-5",
250
+ interrupt_on={"book_trip": True},
251
+ checkpointer=InMemorySaver(),
252
+ )
253
+ config = {"configurable": {"thread_id": workflow.info().workflow_id}}
254
+ result = await agent.ainvoke(
255
+ {"messages": [{"role": "user", "content": request}]}, config=config
256
+ )
257
+ if result.get("__interrupt__"):
258
+ self._pending = str(result["__interrupt__"][0].value)
259
+ await workflow.wait_condition(lambda: self._decision is not None)
260
+ result = await agent.ainvoke(
261
+ Command(resume={"decisions": [{"type": self._decision}]}),
262
+ config=config,
263
+ )
264
+ return result["messages"][-1].content
265
+
266
+ @workflow.query
267
+ def pending_approval(self) -> str | None:
268
+ return self._pending
269
+
270
+ @workflow.update
271
+ async def resume(self, decision: str) -> None:
272
+ self._decision = decision
273
+ ```
274
+
275
+ A client polls `pending_approval`, shows it to a person, and calls the
276
+ `resume` update with `"approve"` / `"reject"`.
277
+
278
+ ## Streaming
279
+
280
+ Set `streaming_topic=` on the plugin and model dispatch switches to a streaming
281
+ Activity that publishes chunk batches to a
282
+ `temporalio.contrib.workflow_streams` topic for live subscribers — while the
283
+ aggregated final message still returns to the workflow, so the durable result
284
+ is identical to the non-streaming path:
285
+
286
+ ```python
287
+ from temporalio.deepagents import DeepAgentsPlugin
288
+
289
+ plugin = DeepAgentsPlugin(streaming_topic="agent-stream")
290
+ ```
291
+
292
+ Subscribers read the topic with `WorkflowStreamClient`; each item is an
293
+ `AIMessageChunk` in `langchain_core.load.dumpd` form.
294
+
295
+ ## Continue-as-new: what carries and what does not
296
+
297
+ Long conversations bloat workflow history. `run_deep_agent(agent, input,
298
+ state_snapshot=...)` snapshots state and continues into a fresh run when the
299
+ turn ends with pending todos and the server recommends continuing
300
+ (`workflow.info().is_continue_as_new_suggested()`, the default and recommended
301
+ mode — it accounts for history length *and* size):
302
+
303
+ ```python
304
+ from deepagents import create_deep_agent
305
+ from temporalio import workflow
306
+ from temporalio.deepagents import run_deep_agent
307
+
308
+
309
+ @workflow.defn
310
+ class LongResearchAgent:
311
+ @workflow.run
312
+ async def run(self, input: dict, state_snapshot: dict | None = None) -> dict:
313
+ agent = create_deep_agent(model="anthropic:claude-sonnet-4-5")
314
+ return await run_deep_agent(
315
+ agent,
316
+ input,
317
+ state_snapshot=state_snapshot,
318
+ )
319
+ ```
320
+
321
+ Pass `continue_as_new_after=N` instead to trigger on a fixed history-event
322
+ count.
323
+
324
+ - **Carries forward:** the accumulated messages. Repeated identical calls are
325
+ NOT deduplicated: a call the agent re-issues after the boundary runs its own
326
+ Activity, so a genuinely new identical request is never served a stale prior
327
+ result. Your `@workflow.run` must accept `state_snapshot=None` as shown.
328
+ - **Does not carry forward:** anything held only in an in-memory checkpointer's
329
+ own structures beyond the messages/todos snapshot. The default in-workflow
330
+ `InMemorySaver` is rehydrated for free by deterministic replay; a durable
331
+ checkpointer that does its own I/O is not replay-safe from inside a workflow,
332
+ and the plugin warns if you pass one — prefer the snapshot + continue-as-new
333
+ path above.
334
+
335
+ ## Runtime behavior
336
+
337
+ While a worker built with this plugin is running, the plugin wraps
338
+ `deepagents.create_deep_agent` so a bare `model="provider:name"` string is
339
+ auto-routed through an Activity. The wrapper only rewrites arguments when called
340
+ *inside a workflow*, so importing `deepagents` on a plain client or activity
341
+ worker is unaffected, and the original function is restored when the worker
342
+ stops. If you would rather be explicit, use `create_temporal_deep_agent` or
343
+ pass `TemporalModel("provider:name")` yourself.
344
+
345
+ ## Composing with other plugins
346
+
347
+ This plugin carries no tracing context of its own. For observability, compose it
348
+ with `temporalio.langsmith` or `temporalio.contrib.opentelemetry` —
349
+ registration order does not matter:
350
+
351
+ ```python
352
+ from temporalio.client import Client
353
+ from temporalio.deepagents import DeepAgentsPlugin
354
+
355
+
356
+ async def connect():
357
+ return await Client.connect(
358
+ "localhost:7233",
359
+ plugins=[
360
+ # LangSmithPlugin(), # or OpenTelemetryPlugin(), in either order
361
+ DeepAgentsPlugin(),
362
+ ],
363
+ )
364
+ ```
365
+
366
+ For agents built directly as LangGraph graphs (rather than a compiled Deep
367
+ Agent), see `temporalio.langgraph`.