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.
- temporalio_deepagents-0.0.1/LICENSE +21 -0
- temporalio_deepagents-0.0.1/PKG-INFO +367 -0
- temporalio_deepagents-0.0.1/README.md +340 -0
- temporalio_deepagents-0.0.1/pyproject.toml +127 -0
- temporalio_deepagents-0.0.1/pyproject.toml.orig +112 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/__init__.py +68 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/_activity.py +363 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/_model.py +395 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/_plugin.py +307 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/_serde.py +412 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/_tools.py +679 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/py.typed +0 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/testing.py +141 -0
- temporalio_deepagents-0.0.1/src/temporalio/deepagents/workflow.py +398 -0
|
@@ -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`.
|