deepagents 0.3.5__py3-none-any.whl → 0.3.7__py3-none-any.whl
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.
- deepagents/backends/filesystem.py +148 -22
- deepagents/backends/sandbox.py +1 -1
- deepagents/backends/store.py +85 -0
- deepagents/backends/utils.py +2 -3
- deepagents/graph.py +63 -12
- deepagents/middleware/__init__.py +3 -1
- deepagents/middleware/_utils.py +23 -0
- deepagents/middleware/filesystem.py +223 -92
- deepagents/middleware/memory.py +17 -15
- deepagents/middleware/skills.py +10 -9
- deepagents/middleware/subagents.py +100 -33
- deepagents/middleware/summarization.py +758 -0
- {deepagents-0.3.5.dist-info → deepagents-0.3.7.dist-info}/METADATA +52 -39
- deepagents-0.3.7.dist-info/RECORD +22 -0
- {deepagents-0.3.5.dist-info → deepagents-0.3.7.dist-info}/WHEEL +1 -1
- deepagents-0.3.5.dist-info/RECORD +0 -20
- {deepagents-0.3.5.dist-info → deepagents-0.3.7.dist-info}/top_level.txt +0 -0
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
"""FilesystemBackend
|
|
2
|
-
|
|
3
|
-
Security and search upgrades:
|
|
4
|
-
- Secure path resolution with root containment when in virtual_mode (sandboxed to cwd)
|
|
5
|
-
- Prevent symlink-following on file I/O using O_NOFOLLOW when available
|
|
6
|
-
- Ripgrep-powered grep with JSON parsing, plus Python fallback with regex
|
|
7
|
-
and optional glob include filtering, while preserving virtual path behavior
|
|
8
|
-
"""
|
|
1
|
+
"""`FilesystemBackend`: Read and write files directly from the filesystem."""
|
|
9
2
|
|
|
10
3
|
import json
|
|
11
4
|
import os
|
|
@@ -38,6 +31,39 @@ class FilesystemBackend(BackendProtocol):
|
|
|
38
31
|
Files are accessed using their actual filesystem paths. Relative paths are
|
|
39
32
|
resolved relative to the current working directory. Content is read/written
|
|
40
33
|
as plain text, and metadata (timestamps) are derived from filesystem stats.
|
|
34
|
+
|
|
35
|
+
!!! warning "Security Warning"
|
|
36
|
+
|
|
37
|
+
This backend grants agents direct filesystem read/write access. Use with
|
|
38
|
+
caution and only in appropriate environments.
|
|
39
|
+
|
|
40
|
+
**Appropriate use cases:**
|
|
41
|
+
|
|
42
|
+
- Local development CLIs (coding assistants, development tools)
|
|
43
|
+
- CI/CD pipelines (see security considerations below)
|
|
44
|
+
|
|
45
|
+
**Inappropriate use cases:**
|
|
46
|
+
|
|
47
|
+
- Web servers or HTTP APIs - use `StateBackend`, `StoreBackend`, or
|
|
48
|
+
`SandboxBackend` instead
|
|
49
|
+
|
|
50
|
+
**Security risks:**
|
|
51
|
+
|
|
52
|
+
- Agents can read any accessible file, including secrets (API keys,
|
|
53
|
+
credentials, `.env` files)
|
|
54
|
+
- Combined with network tools, secrets may be exfiltrated via SSRF attacks
|
|
55
|
+
- File modifications are permanent and irreversible
|
|
56
|
+
|
|
57
|
+
**Recommended safeguards:**
|
|
58
|
+
|
|
59
|
+
1. Enable Human-in-the-Loop (HITL) middleware to review sensitive operations
|
|
60
|
+
2. Exclude secrets from accessible filesystem paths (especially in CI/CD)
|
|
61
|
+
3. Use `SandboxBackend` for production environments requiring filesystem
|
|
62
|
+
interaction
|
|
63
|
+
4. **Always** use `virtual_mode=True` with `root_dir` to enable path-based
|
|
64
|
+
access restrictions (blocks `..`, `~`, and absolute paths outside root).
|
|
65
|
+
Note that the default (`virtual_mode=False`) provides no security even with
|
|
66
|
+
`root_dir` set.
|
|
41
67
|
"""
|
|
42
68
|
|
|
43
69
|
def __init__(
|
|
@@ -49,9 +75,35 @@ class FilesystemBackend(BackendProtocol):
|
|
|
49
75
|
"""Initialize filesystem backend.
|
|
50
76
|
|
|
51
77
|
Args:
|
|
52
|
-
root_dir: Optional root directory for file operations.
|
|
53
|
-
|
|
54
|
-
|
|
78
|
+
root_dir: Optional root directory for file operations.
|
|
79
|
+
|
|
80
|
+
- If not provided, defaults to the current working directory.
|
|
81
|
+
- When `virtual_mode=False` (default): Only affects relative path
|
|
82
|
+
resolution. Provides **no security** - agents can access any file
|
|
83
|
+
using absolute paths or `..` sequences.
|
|
84
|
+
- When `virtual_mode=True`: All paths are restricted to this
|
|
85
|
+
directory with traversal protection enabled.
|
|
86
|
+
|
|
87
|
+
virtual_mode: Enable path-based access restrictions.
|
|
88
|
+
|
|
89
|
+
When `True`, all paths are treated as virtual paths anchored to
|
|
90
|
+
`root_dir`. Path traversal (`..`, `~`) is blocked and all resolved paths
|
|
91
|
+
are verified to remain within `root_dir`.
|
|
92
|
+
|
|
93
|
+
When `False` (default), **no security is provided**:
|
|
94
|
+
|
|
95
|
+
- Absolute paths (e.g., `/etc/passwd`) bypass `root_dir` entirely
|
|
96
|
+
- Relative paths with `..` can escape `root_dir`
|
|
97
|
+
- Agents have unrestricted filesystem access
|
|
98
|
+
|
|
99
|
+
**Security note:** `virtual_mode=True` provides path-based access
|
|
100
|
+
control, not process isolation. It restricts which files can be
|
|
101
|
+
accessed via paths, but does not sandbox the Python process itself.
|
|
102
|
+
|
|
103
|
+
max_file_size_mb: Maximum file size in megabytes for operations like
|
|
104
|
+
grep's Python fallback search.
|
|
105
|
+
|
|
106
|
+
Files exceeding this limit are skipped during search. Defaults to 10 MB.
|
|
55
107
|
"""
|
|
56
108
|
self.cwd = Path(root_dir).resolve() if root_dir else Path.cwd()
|
|
57
109
|
self.virtual_mode = virtual_mode
|
|
@@ -60,16 +112,22 @@ class FilesystemBackend(BackendProtocol):
|
|
|
60
112
|
def _resolve_path(self, key: str) -> Path:
|
|
61
113
|
"""Resolve a file path with security checks.
|
|
62
114
|
|
|
63
|
-
When virtual_mode=True
|
|
64
|
-
self.cwd
|
|
65
|
-
|
|
115
|
+
When `virtual_mode=True`, treat incoming paths as virtual absolute paths under
|
|
116
|
+
`self.cwd`, disallow traversal (`..`, `~`) and ensure resolved path stays within
|
|
117
|
+
root.
|
|
118
|
+
|
|
119
|
+
When `virtual_mode=False`, preserve legacy behavior: absolute paths are allowed
|
|
66
120
|
as-is; relative paths resolve under cwd.
|
|
67
121
|
|
|
68
122
|
Args:
|
|
69
|
-
key: File path (absolute, relative, or virtual when virtual_mode=True)
|
|
123
|
+
key: File path (absolute, relative, or virtual when `virtual_mode=True`).
|
|
70
124
|
|
|
71
125
|
Returns:
|
|
72
|
-
Resolved absolute Path object
|
|
126
|
+
Resolved absolute `Path` object.
|
|
127
|
+
|
|
128
|
+
Raises:
|
|
129
|
+
ValueError: If path traversal is attempted in `virtual_mode` or if the
|
|
130
|
+
resolved path escapes the root directory.
|
|
73
131
|
"""
|
|
74
132
|
if self.virtual_mode:
|
|
75
133
|
vpath = key if key.startswith("/") else "/" + key
|
|
@@ -94,8 +152,9 @@ class FilesystemBackend(BackendProtocol):
|
|
|
94
152
|
path: Absolute directory path to list files from.
|
|
95
153
|
|
|
96
154
|
Returns:
|
|
97
|
-
List of FileInfo
|
|
98
|
-
|
|
155
|
+
List of `FileInfo`-like dicts for files and directories directly in the
|
|
156
|
+
directory. Directories have a trailing `/` in their path and
|
|
157
|
+
`is_dir=True`.
|
|
99
158
|
"""
|
|
100
159
|
dir_path = self._resolve_path(path)
|
|
101
160
|
if not dir_path.exists() or not dir_path.is_dir():
|
|
@@ -242,7 +301,14 @@ class FilesystemBackend(BackendProtocol):
|
|
|
242
301
|
content: str,
|
|
243
302
|
) -> WriteResult:
|
|
244
303
|
"""Create a new file with content.
|
|
245
|
-
|
|
304
|
+
|
|
305
|
+
Args:
|
|
306
|
+
file_path: Path where the new file will be created.
|
|
307
|
+
content: Text content to write to the file.
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
`WriteResult` with path on success, or error message if the file
|
|
311
|
+
already exists or write fails. External storage sets `files_update=None`.
|
|
246
312
|
"""
|
|
247
313
|
resolved_path = self._resolve_path(file_path)
|
|
248
314
|
|
|
@@ -273,7 +339,18 @@ class FilesystemBackend(BackendProtocol):
|
|
|
273
339
|
replace_all: bool = False,
|
|
274
340
|
) -> EditResult:
|
|
275
341
|
"""Edit a file by replacing string occurrences.
|
|
276
|
-
|
|
342
|
+
|
|
343
|
+
Args:
|
|
344
|
+
file_path: Path to the file to edit.
|
|
345
|
+
old_string: The text to search for and replace.
|
|
346
|
+
new_string: The replacement text.
|
|
347
|
+
replace_all: If `True`, replace all occurrences. If `False` (default),
|
|
348
|
+
replace only if exactly one occurrence exists.
|
|
349
|
+
|
|
350
|
+
Returns:
|
|
351
|
+
`EditResult` with path and occurrence count on success, or error
|
|
352
|
+
message if file not found or replacement fails. External storage sets
|
|
353
|
+
`files_update=None`.
|
|
277
354
|
"""
|
|
278
355
|
resolved_path = self._resolve_path(file_path)
|
|
279
356
|
|
|
@@ -311,6 +388,19 @@ class FilesystemBackend(BackendProtocol):
|
|
|
311
388
|
path: str | None = None,
|
|
312
389
|
glob: str | None = None,
|
|
313
390
|
) -> list[GrepMatch] | str:
|
|
391
|
+
"""Search for a regex pattern in files.
|
|
392
|
+
|
|
393
|
+
Uses ripgrep if available, falling back to Python regex search.
|
|
394
|
+
|
|
395
|
+
Args:
|
|
396
|
+
pattern: Regular expression pattern to search for.
|
|
397
|
+
path: Directory or file path to search in. Defaults to current directory.
|
|
398
|
+
glob: Optional glob pattern to filter which files to search.
|
|
399
|
+
|
|
400
|
+
Returns:
|
|
401
|
+
List of GrepMatch dicts containing path, line number, and matched text.
|
|
402
|
+
Returns an error string if the regex pattern is invalid.
|
|
403
|
+
"""
|
|
314
404
|
# Validate regex
|
|
315
405
|
try:
|
|
316
406
|
re.compile(pattern)
|
|
@@ -338,6 +428,17 @@ class FilesystemBackend(BackendProtocol):
|
|
|
338
428
|
return matches
|
|
339
429
|
|
|
340
430
|
def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]] | None:
|
|
431
|
+
"""Search using ripgrep with JSON output parsing.
|
|
432
|
+
|
|
433
|
+
Args:
|
|
434
|
+
pattern: Regex pattern to search for.
|
|
435
|
+
base_full: Resolved base path to search in.
|
|
436
|
+
include_glob: Optional glob pattern to filter files.
|
|
437
|
+
|
|
438
|
+
Returns:
|
|
439
|
+
Dict mapping file paths to list of `(line_number, line_text)` tuples.
|
|
440
|
+
Returns `None` if ripgrep is unavailable or times out.
|
|
441
|
+
"""
|
|
341
442
|
cmd = ["rg", "--json"]
|
|
342
443
|
if include_glob:
|
|
343
444
|
cmd.extend(["--glob", include_glob])
|
|
@@ -383,6 +484,18 @@ class FilesystemBackend(BackendProtocol):
|
|
|
383
484
|
return results
|
|
384
485
|
|
|
385
486
|
def _python_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]]:
|
|
487
|
+
"""Fallback search using Python regex when ripgrep is unavailable.
|
|
488
|
+
|
|
489
|
+
Recursively searches files, respecting `max_file_size_bytes` limit.
|
|
490
|
+
|
|
491
|
+
Args:
|
|
492
|
+
pattern: Regex pattern to search for.
|
|
493
|
+
base_full: Resolved base path to search in.
|
|
494
|
+
include_glob: Optional glob pattern to filter files by name.
|
|
495
|
+
|
|
496
|
+
Returns:
|
|
497
|
+
Dict mapping file paths to list of `(line_number, line_text)` tuples.
|
|
498
|
+
"""
|
|
386
499
|
try:
|
|
387
500
|
regex = re.compile(pattern)
|
|
388
501
|
except re.error:
|
|
@@ -392,7 +505,10 @@ class FilesystemBackend(BackendProtocol):
|
|
|
392
505
|
root = base_full if base_full.is_dir() else base_full.parent
|
|
393
506
|
|
|
394
507
|
for fp in root.rglob("*"):
|
|
395
|
-
|
|
508
|
+
try:
|
|
509
|
+
if not fp.is_file():
|
|
510
|
+
continue
|
|
511
|
+
except (PermissionError, OSError):
|
|
396
512
|
continue
|
|
397
513
|
if include_glob and not wcglob.globmatch(fp.name, include_glob, flags=wcglob.BRACE):
|
|
398
514
|
continue
|
|
@@ -419,6 +535,16 @@ class FilesystemBackend(BackendProtocol):
|
|
|
419
535
|
return results
|
|
420
536
|
|
|
421
537
|
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
538
|
+
"""Find files matching a glob pattern.
|
|
539
|
+
|
|
540
|
+
Args:
|
|
541
|
+
pattern: Glob pattern to match files against (e.g., `'*.py'`, `'**/*.txt'`).
|
|
542
|
+
path: Base directory to search from. Defaults to root (`/`).
|
|
543
|
+
|
|
544
|
+
Returns:
|
|
545
|
+
List of `FileInfo` dicts for matching files, sorted by path. Each dict
|
|
546
|
+
contains `path`, `is_dir`, `size`, and `modified_at` fields.
|
|
547
|
+
"""
|
|
422
548
|
if pattern.startswith("/"):
|
|
423
549
|
pattern = pattern.lstrip("/")
|
|
424
550
|
|
|
@@ -432,7 +558,7 @@ class FilesystemBackend(BackendProtocol):
|
|
|
432
558
|
for matched_path in search_path.rglob(pattern):
|
|
433
559
|
try:
|
|
434
560
|
is_file = matched_path.is_file()
|
|
435
|
-
except OSError:
|
|
561
|
+
except (PermissionError, OSError):
|
|
436
562
|
continue
|
|
437
563
|
if not is_file:
|
|
438
564
|
continue
|
deepagents/backends/sandbox.py
CHANGED
deepagents/backends/store.py
CHANGED
|
@@ -279,6 +279,30 @@ class StoreBackend(BackendProtocol):
|
|
|
279
279
|
|
|
280
280
|
return format_read_response(file_data, offset, limit)
|
|
281
281
|
|
|
282
|
+
async def aread(
|
|
283
|
+
self,
|
|
284
|
+
file_path: str,
|
|
285
|
+
offset: int = 0,
|
|
286
|
+
limit: int = 2000,
|
|
287
|
+
) -> str:
|
|
288
|
+
"""Async version of read using native store async methods.
|
|
289
|
+
|
|
290
|
+
This avoids sync calls in async context by using store.aget directly.
|
|
291
|
+
"""
|
|
292
|
+
store = self._get_store()
|
|
293
|
+
namespace = self._get_namespace()
|
|
294
|
+
item: Item | None = await store.aget(namespace, file_path)
|
|
295
|
+
|
|
296
|
+
if item is None:
|
|
297
|
+
return f"Error: File '{file_path}' not found"
|
|
298
|
+
|
|
299
|
+
try:
|
|
300
|
+
file_data = self._convert_store_item_to_file_data(item)
|
|
301
|
+
except ValueError as e:
|
|
302
|
+
return f"Error: {e}"
|
|
303
|
+
|
|
304
|
+
return format_read_response(file_data, offset, limit)
|
|
305
|
+
|
|
282
306
|
def write(
|
|
283
307
|
self,
|
|
284
308
|
file_path: str,
|
|
@@ -301,6 +325,29 @@ class StoreBackend(BackendProtocol):
|
|
|
301
325
|
store.put(namespace, file_path, store_value)
|
|
302
326
|
return WriteResult(path=file_path, files_update=None)
|
|
303
327
|
|
|
328
|
+
async def awrite(
|
|
329
|
+
self,
|
|
330
|
+
file_path: str,
|
|
331
|
+
content: str,
|
|
332
|
+
) -> WriteResult:
|
|
333
|
+
"""Async version of write using native store async methods.
|
|
334
|
+
|
|
335
|
+
This avoids sync calls in async context by using store.aget/aput directly.
|
|
336
|
+
"""
|
|
337
|
+
store = self._get_store()
|
|
338
|
+
namespace = self._get_namespace()
|
|
339
|
+
|
|
340
|
+
# Check if file exists using async method
|
|
341
|
+
existing = await store.aget(namespace, file_path)
|
|
342
|
+
if existing is not None:
|
|
343
|
+
return WriteResult(error=f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path.")
|
|
344
|
+
|
|
345
|
+
# Create new file using async method
|
|
346
|
+
file_data = create_file_data(content)
|
|
347
|
+
store_value = self._convert_file_data_to_store_value(file_data)
|
|
348
|
+
await store.aput(namespace, file_path, store_value)
|
|
349
|
+
return WriteResult(path=file_path, files_update=None)
|
|
350
|
+
|
|
304
351
|
def edit(
|
|
305
352
|
self,
|
|
306
353
|
file_path: str,
|
|
@@ -338,6 +385,44 @@ class StoreBackend(BackendProtocol):
|
|
|
338
385
|
store.put(namespace, file_path, store_value)
|
|
339
386
|
return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
|
|
340
387
|
|
|
388
|
+
async def aedit(
|
|
389
|
+
self,
|
|
390
|
+
file_path: str,
|
|
391
|
+
old_string: str,
|
|
392
|
+
new_string: str,
|
|
393
|
+
replace_all: bool = False,
|
|
394
|
+
) -> EditResult:
|
|
395
|
+
"""Async version of edit using native store async methods.
|
|
396
|
+
|
|
397
|
+
This avoids sync calls in async context by using store.aget/aput directly.
|
|
398
|
+
"""
|
|
399
|
+
store = self._get_store()
|
|
400
|
+
namespace = self._get_namespace()
|
|
401
|
+
|
|
402
|
+
# Get existing file using async method
|
|
403
|
+
item = await store.aget(namespace, file_path)
|
|
404
|
+
if item is None:
|
|
405
|
+
return EditResult(error=f"Error: File '{file_path}' not found")
|
|
406
|
+
|
|
407
|
+
try:
|
|
408
|
+
file_data = self._convert_store_item_to_file_data(item)
|
|
409
|
+
except ValueError as e:
|
|
410
|
+
return EditResult(error=f"Error: {e}")
|
|
411
|
+
|
|
412
|
+
content = file_data_to_string(file_data)
|
|
413
|
+
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
|
414
|
+
|
|
415
|
+
if isinstance(result, str):
|
|
416
|
+
return EditResult(error=result)
|
|
417
|
+
|
|
418
|
+
new_content, occurrences = result
|
|
419
|
+
new_file_data = update_file_data(file_data, new_content)
|
|
420
|
+
|
|
421
|
+
# Update file in store using async method
|
|
422
|
+
store_value = self._convert_file_data_to_store_value(new_file_data)
|
|
423
|
+
await store.aput(namespace, file_path, store_value)
|
|
424
|
+
return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
|
|
425
|
+
|
|
341
426
|
# Removed legacy grep() convenience to keep lean surface
|
|
342
427
|
|
|
343
428
|
def grep_raw(
|
deepagents/backends/utils.py
CHANGED
|
@@ -12,11 +12,10 @@ from typing import Any, Literal
|
|
|
12
12
|
|
|
13
13
|
import wcmatch.glob as wcglob
|
|
14
14
|
|
|
15
|
-
from deepagents.backends.protocol import FileInfo as _FileInfo
|
|
16
|
-
from deepagents.backends.protocol import GrepMatch as _GrepMatch
|
|
15
|
+
from deepagents.backends.protocol import FileInfo as _FileInfo, GrepMatch as _GrepMatch
|
|
17
16
|
|
|
18
17
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
19
|
-
MAX_LINE_LENGTH =
|
|
18
|
+
MAX_LINE_LENGTH = 5000
|
|
20
19
|
LINE_NUMBER_WIDTH = 6
|
|
21
20
|
TOOL_RESULT_TOKEN_LIMIT = 20000 # Same threshold as eviction
|
|
22
21
|
TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]"
|
deepagents/graph.py
CHANGED
|
@@ -5,13 +5,13 @@ from typing import Any
|
|
|
5
5
|
|
|
6
6
|
from langchain.agents import create_agent
|
|
7
7
|
from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig, TodoListMiddleware
|
|
8
|
-
from langchain.agents.middleware.summarization import SummarizationMiddleware
|
|
9
8
|
from langchain.agents.middleware.types import AgentMiddleware
|
|
10
9
|
from langchain.agents.structured_output import ResponseFormat
|
|
11
10
|
from langchain.chat_models import init_chat_model
|
|
12
11
|
from langchain_anthropic import ChatAnthropic
|
|
13
12
|
from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
|
|
14
13
|
from langchain_core.language_models import BaseChatModel
|
|
14
|
+
from langchain_core.messages import SystemMessage
|
|
15
15
|
from langchain_core.tools import BaseTool
|
|
16
16
|
from langgraph.cache.base import BaseCache
|
|
17
17
|
from langgraph.graph.state import CompiledStateGraph
|
|
@@ -25,6 +25,7 @@ from deepagents.middleware.memory import MemoryMiddleware
|
|
|
25
25
|
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
|
|
26
26
|
from deepagents.middleware.skills import SkillsMiddleware
|
|
27
27
|
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
28
|
+
from deepagents.middleware.summarization import SummarizationMiddleware
|
|
28
29
|
|
|
29
30
|
BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
|
|
30
31
|
|
|
@@ -37,7 +38,7 @@ def get_default_model() -> ChatAnthropic:
|
|
|
37
38
|
"""
|
|
38
39
|
return ChatAnthropic(
|
|
39
40
|
model_name="claude-sonnet-4-5-20250929",
|
|
40
|
-
max_tokens=20000,
|
|
41
|
+
max_tokens=20000, # type: ignore[call-arg]
|
|
41
42
|
)
|
|
42
43
|
|
|
43
44
|
|
|
@@ -45,7 +46,7 @@ def create_deep_agent(
|
|
|
45
46
|
model: str | BaseChatModel | None = None,
|
|
46
47
|
tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
|
|
47
48
|
*,
|
|
48
|
-
system_prompt: str | None = None,
|
|
49
|
+
system_prompt: str | SystemMessage | None = None,
|
|
49
50
|
middleware: Sequence[AgentMiddleware] = (),
|
|
50
51
|
subagents: list[SubAgent | CompiledSubAgent] | None = None,
|
|
51
52
|
skills: list[str] | None = None,
|
|
@@ -62,19 +63,36 @@ def create_deep_agent(
|
|
|
62
63
|
) -> CompiledStateGraph:
|
|
63
64
|
"""Create a deep agent.
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
66
|
+
!!! warning "Deep agents require a LLM that supports tool calling!"
|
|
67
|
+
|
|
68
|
+
By default, this agent has access to the following tools:
|
|
69
|
+
|
|
70
|
+
- `write_todos`: manage a todo list
|
|
71
|
+
- `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`: file operations
|
|
72
|
+
- `execute`: run shell commands
|
|
73
|
+
- `task`: call subagents
|
|
68
74
|
|
|
69
75
|
The `execute` tool allows running shell commands if the backend implements `SandboxBackendProtocol`.
|
|
70
76
|
For non-sandbox backends, the `execute` tool will return an error message.
|
|
71
77
|
|
|
72
78
|
Args:
|
|
73
|
-
model: The model to use.
|
|
79
|
+
model: The model to use.
|
|
80
|
+
|
|
81
|
+
Defaults to `claude-sonnet-4-5-20250929`.
|
|
82
|
+
|
|
83
|
+
Use the `provider:model` format (e.g., `openai:gpt-5`) to quickly switch between models.
|
|
74
84
|
tools: The tools the agent should have access to.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
85
|
+
|
|
86
|
+
In addition to custom tools you provide, deep agents include built-in tools for planning,
|
|
87
|
+
file management, and subagent spawning.
|
|
88
|
+
system_prompt: Custom system instructions to prepend before the base deep agent
|
|
89
|
+
prompt.
|
|
90
|
+
|
|
91
|
+
If a string, it's concatenated with the base prompt.
|
|
92
|
+
middleware: Additional middleware to apply after the standard middleware stack
|
|
93
|
+
(`TodoListMiddleware`, `FilesystemMiddleware`, `SubAgentMiddleware`,
|
|
94
|
+
`SummarizationMiddleware`, `AnthropicPromptCachingMiddleware`,
|
|
95
|
+
`PatchToolCallsMiddleware`).
|
|
78
96
|
subagents: The subagents to use.
|
|
79
97
|
|
|
80
98
|
Each subagent should be a `dict` with the following keys:
|
|
@@ -93,7 +111,10 @@ def create_deep_agent(
|
|
|
93
111
|
to the backend's `root_dir`. Later sources override earlier ones for skills with the
|
|
94
112
|
same name (last one wins).
|
|
95
113
|
memory: Optional list of memory file paths (`AGENTS.md` files) to load
|
|
96
|
-
(e.g., `["/memory/AGENTS.md"]`).
|
|
114
|
+
(e.g., `["/memory/AGENTS.md"]`).
|
|
115
|
+
|
|
116
|
+
Display names are automatically derived from paths.
|
|
117
|
+
|
|
97
118
|
Memory is loaded at agent startup and added into the system prompt.
|
|
98
119
|
response_format: A structured output response format to use for the agent.
|
|
99
120
|
context_schema: The schema of the deep agent.
|
|
@@ -104,6 +125,10 @@ def create_deep_agent(
|
|
|
104
125
|
Pass either a `Backend` instance or a callable factory like `lambda rt: StateBackend(rt)`.
|
|
105
126
|
For execution support, use a backend that implements `SandboxBackendProtocol`.
|
|
106
127
|
interrupt_on: Mapping of tool names to interrupt configs.
|
|
128
|
+
|
|
129
|
+
Pass to pause agent execution at specified tool calls for human approval or modification.
|
|
130
|
+
|
|
131
|
+
Example: `interrupt_on={"edit_file": True}` pauses before every edit.
|
|
107
132
|
debug: Whether to enable debug mode. Passed through to `create_agent`.
|
|
108
133
|
name: The name of the agent. Passed through to `create_agent`.
|
|
109
134
|
cache: The cache to use for the agent. Passed through to `create_agent`.
|
|
@@ -124,9 +149,17 @@ def create_deep_agent(
|
|
|
124
149
|
):
|
|
125
150
|
trigger = ("fraction", 0.85)
|
|
126
151
|
keep = ("fraction", 0.10)
|
|
152
|
+
truncate_args_settings = {
|
|
153
|
+
"trigger": ("fraction", 0.85),
|
|
154
|
+
"keep": ("fraction", 0.10),
|
|
155
|
+
}
|
|
127
156
|
else:
|
|
128
157
|
trigger = ("tokens", 170000)
|
|
129
158
|
keep = ("messages", 6)
|
|
159
|
+
truncate_args_settings = {
|
|
160
|
+
"trigger": ("messages", 20),
|
|
161
|
+
"keep": ("messages", 20),
|
|
162
|
+
}
|
|
130
163
|
|
|
131
164
|
# Build middleware stack for subagents (includes skills if provided)
|
|
132
165
|
subagent_middleware: list[AgentMiddleware] = [
|
|
@@ -142,9 +175,11 @@ def create_deep_agent(
|
|
|
142
175
|
FilesystemMiddleware(backend=backend),
|
|
143
176
|
SummarizationMiddleware(
|
|
144
177
|
model=model,
|
|
178
|
+
backend=backend,
|
|
145
179
|
trigger=trigger,
|
|
146
180
|
keep=keep,
|
|
147
181
|
trim_tokens_to_summarize=None,
|
|
182
|
+
truncate_args_settings=truncate_args_settings,
|
|
148
183
|
),
|
|
149
184
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
150
185
|
PatchToolCallsMiddleware(),
|
|
@@ -172,9 +207,11 @@ def create_deep_agent(
|
|
|
172
207
|
),
|
|
173
208
|
SummarizationMiddleware(
|
|
174
209
|
model=model,
|
|
210
|
+
backend=backend,
|
|
175
211
|
trigger=trigger,
|
|
176
212
|
keep=keep,
|
|
177
213
|
trim_tokens_to_summarize=None,
|
|
214
|
+
truncate_args_settings=truncate_args_settings,
|
|
178
215
|
),
|
|
179
216
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
180
217
|
PatchToolCallsMiddleware(),
|
|
@@ -185,9 +222,23 @@ def create_deep_agent(
|
|
|
185
222
|
if interrupt_on is not None:
|
|
186
223
|
deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
|
|
187
224
|
|
|
225
|
+
# Combine system_prompt with BASE_AGENT_PROMPT
|
|
226
|
+
if system_prompt is None:
|
|
227
|
+
final_system_prompt: str | SystemMessage = BASE_AGENT_PROMPT
|
|
228
|
+
elif isinstance(system_prompt, SystemMessage):
|
|
229
|
+
# SystemMessage: append BASE_AGENT_PROMPT to content_blocks
|
|
230
|
+
new_content = [
|
|
231
|
+
*system_prompt.content_blocks,
|
|
232
|
+
{"type": "text", "text": f"\n\n{BASE_AGENT_PROMPT}"},
|
|
233
|
+
]
|
|
234
|
+
final_system_prompt = SystemMessage(content=new_content)
|
|
235
|
+
else:
|
|
236
|
+
# String: simple concatenation
|
|
237
|
+
final_system_prompt = system_prompt + "\n\n" + BASE_AGENT_PROMPT
|
|
238
|
+
|
|
188
239
|
return create_agent(
|
|
189
240
|
model,
|
|
190
|
-
system_prompt=
|
|
241
|
+
system_prompt=final_system_prompt,
|
|
191
242
|
tools=tools,
|
|
192
243
|
middleware=deepagent_middleware,
|
|
193
244
|
response_format=response_format,
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
"""Middleware for the
|
|
1
|
+
"""Middleware for the agent."""
|
|
2
2
|
|
|
3
3
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
4
|
from deepagents.middleware.memory import MemoryMiddleware
|
|
5
5
|
from deepagents.middleware.skills import SkillsMiddleware
|
|
6
6
|
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
7
|
+
from deepagents.middleware.summarization import SummarizationMiddleware
|
|
7
8
|
|
|
8
9
|
__all__ = [
|
|
9
10
|
"CompiledSubAgent",
|
|
@@ -12,4 +13,5 @@ __all__ = [
|
|
|
12
13
|
"SkillsMiddleware",
|
|
13
14
|
"SubAgent",
|
|
14
15
|
"SubAgentMiddleware",
|
|
16
|
+
"SummarizationMiddleware",
|
|
15
17
|
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Utility functions for middleware."""
|
|
2
|
+
|
|
3
|
+
from langchain_core.messages import SystemMessage
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def append_to_system_message(
|
|
7
|
+
system_message: SystemMessage | None,
|
|
8
|
+
text: str,
|
|
9
|
+
) -> SystemMessage:
|
|
10
|
+
"""Append text to a system message.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
system_message: Existing system message or None.
|
|
14
|
+
text: Text to add to the system message.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
New SystemMessage with the text appended.
|
|
18
|
+
"""
|
|
19
|
+
new_content: list[str | dict[str, str]] = list(system_message.content_blocks) if system_message else []
|
|
20
|
+
if new_content:
|
|
21
|
+
text = f"\n\n{text}"
|
|
22
|
+
new_content.append({"type": "text", "text": text})
|
|
23
|
+
return SystemMessage(content=new_content)
|