deepagents 0.2.4__py3-none-any.whl → 0.2.6__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/__init__.py +1 -1
- deepagents/backends/composite.py +66 -41
- deepagents/backends/filesystem.py +92 -86
- deepagents/backends/protocol.py +87 -13
- deepagents/backends/sandbox.py +341 -0
- deepagents/backends/state.py +59 -58
- deepagents/backends/store.py +73 -74
- deepagents/backends/utils.py +7 -21
- deepagents/graph.py +8 -4
- deepagents/middleware/filesystem.py +271 -66
- deepagents/middleware/resumable_shell.py +5 -4
- deepagents/middleware/subagents.py +8 -6
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/METADATA +5 -10
- deepagents-0.2.6.dist-info/RECORD +19 -0
- deepagents-0.2.4.dist-info/RECORD +0 -19
- deepagents-0.2.4.dist-info/licenses/LICENSE +0 -21
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/WHEEL +0 -0
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/top_level.txt +0 -0
deepagents/backends/state.py
CHANGED
|
@@ -1,44 +1,38 @@
|
|
|
1
1
|
"""StateBackend: Store files in LangGraph agent state (ephemeral)."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from typing import Any, Literal, Optional, TYPE_CHECKING
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
5
4
|
|
|
6
|
-
from
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
from langgraph.types import Command
|
|
10
|
-
|
|
11
|
-
from .utils import (
|
|
5
|
+
from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
|
|
6
|
+
from deepagents.backends.utils import (
|
|
7
|
+
_glob_search_files,
|
|
12
8
|
create_file_data,
|
|
13
|
-
update_file_data,
|
|
14
9
|
file_data_to_string,
|
|
15
10
|
format_read_response,
|
|
16
|
-
perform_string_replacement,
|
|
17
|
-
_glob_search_files,
|
|
18
11
|
grep_matches_from_files,
|
|
12
|
+
perform_string_replacement,
|
|
13
|
+
update_file_data,
|
|
19
14
|
)
|
|
20
|
-
from deepagents.backends.utils import FileInfo, GrepMatch
|
|
21
|
-
from deepagents.backends.protocol import WriteResult, EditResult
|
|
22
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from langchain.tools import ToolRuntime
|
|
23
18
|
|
|
24
|
-
|
|
19
|
+
|
|
20
|
+
class StateBackend(BackendProtocol):
|
|
25
21
|
"""Backend that stores files in agent state (ephemeral).
|
|
26
|
-
|
|
22
|
+
|
|
27
23
|
Uses LangGraph's state management and checkpointing. Files persist within
|
|
28
24
|
a conversation thread but not across threads. State is automatically
|
|
29
25
|
checkpointed after each agent step.
|
|
30
|
-
|
|
26
|
+
|
|
31
27
|
Special handling: Since LangGraph state must be updated via Command objects
|
|
32
28
|
(not direct mutation), operations return Command objects instead of None.
|
|
33
29
|
This is indicated by the uses_state=True flag.
|
|
34
30
|
"""
|
|
35
|
-
|
|
31
|
+
|
|
36
32
|
def __init__(self, runtime: "ToolRuntime"):
|
|
37
|
-
"""Initialize StateBackend with runtime.
|
|
38
|
-
|
|
39
|
-
Args:"""
|
|
33
|
+
"""Initialize StateBackend with runtime."""
|
|
40
34
|
self.runtime = runtime
|
|
41
|
-
|
|
35
|
+
|
|
42
36
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
43
37
|
"""List files and directories in the specified directory (non-recursive).
|
|
44
38
|
|
|
@@ -62,7 +56,7 @@ class StateBackend:
|
|
|
62
56
|
continue
|
|
63
57
|
|
|
64
58
|
# Get the relative path after the directory
|
|
65
|
-
relative = k[len(normalized_path):]
|
|
59
|
+
relative = k[len(normalized_path) :]
|
|
66
60
|
|
|
67
61
|
# If relative path contains '/', it's in a subdirectory
|
|
68
62
|
if "/" in relative:
|
|
@@ -73,35 +67,39 @@ class StateBackend:
|
|
|
73
67
|
|
|
74
68
|
# This is a file directly in the current directory
|
|
75
69
|
size = len("\n".join(fd.get("content", [])))
|
|
76
|
-
infos.append(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
70
|
+
infos.append(
|
|
71
|
+
{
|
|
72
|
+
"path": k,
|
|
73
|
+
"is_dir": False,
|
|
74
|
+
"size": int(size),
|
|
75
|
+
"modified_at": fd.get("modified_at", ""),
|
|
76
|
+
}
|
|
77
|
+
)
|
|
82
78
|
|
|
83
79
|
# Add directories to the results
|
|
84
80
|
for subdir in sorted(subdirs):
|
|
85
|
-
infos.append(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
infos.append(
|
|
82
|
+
{
|
|
83
|
+
"path": subdir,
|
|
84
|
+
"is_dir": True,
|
|
85
|
+
"size": 0,
|
|
86
|
+
"modified_at": "",
|
|
87
|
+
}
|
|
88
|
+
)
|
|
91
89
|
|
|
92
90
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
93
91
|
return infos
|
|
94
92
|
|
|
95
93
|
# Removed legacy ls() convenience to keep lean surface
|
|
96
|
-
|
|
94
|
+
|
|
97
95
|
def read(
|
|
98
|
-
self,
|
|
96
|
+
self,
|
|
99
97
|
file_path: str,
|
|
100
98
|
offset: int = 0,
|
|
101
99
|
limit: int = 2000,
|
|
102
100
|
) -> str:
|
|
103
101
|
"""Read file content with line numbers.
|
|
104
|
-
|
|
102
|
+
|
|
105
103
|
Args:
|
|
106
104
|
file_path: Absolute file path
|
|
107
105
|
offset: Line offset to start reading from (0-indexed)
|
|
@@ -110,14 +108,14 @@ class StateBackend:
|
|
|
110
108
|
"""
|
|
111
109
|
files = self.runtime.state.get("files", {})
|
|
112
110
|
file_data = files.get(file_path)
|
|
113
|
-
|
|
111
|
+
|
|
114
112
|
if file_data is None:
|
|
115
113
|
return f"Error: File '{file_path}' not found"
|
|
116
|
-
|
|
114
|
+
|
|
117
115
|
return format_read_response(file_data, offset, limit)
|
|
118
|
-
|
|
116
|
+
|
|
119
117
|
def write(
|
|
120
|
-
self,
|
|
118
|
+
self,
|
|
121
119
|
file_path: str,
|
|
122
120
|
content: str,
|
|
123
121
|
) -> WriteResult:
|
|
@@ -125,15 +123,15 @@ class StateBackend:
|
|
|
125
123
|
Returns WriteResult with files_update to update LangGraph state.
|
|
126
124
|
"""
|
|
127
125
|
files = self.runtime.state.get("files", {})
|
|
128
|
-
|
|
126
|
+
|
|
129
127
|
if file_path in files:
|
|
130
128
|
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.")
|
|
131
|
-
|
|
129
|
+
|
|
132
130
|
new_file_data = create_file_data(content)
|
|
133
131
|
return WriteResult(path=file_path, files_update={file_path: new_file_data})
|
|
134
|
-
|
|
132
|
+
|
|
135
133
|
def edit(
|
|
136
|
-
self,
|
|
134
|
+
self,
|
|
137
135
|
file_path: str,
|
|
138
136
|
old_string: str,
|
|
139
137
|
new_string: str,
|
|
@@ -144,31 +142,31 @@ class StateBackend:
|
|
|
144
142
|
"""
|
|
145
143
|
files = self.runtime.state.get("files", {})
|
|
146
144
|
file_data = files.get(file_path)
|
|
147
|
-
|
|
145
|
+
|
|
148
146
|
if file_data is None:
|
|
149
147
|
return EditResult(error=f"Error: File '{file_path}' not found")
|
|
150
|
-
|
|
148
|
+
|
|
151
149
|
content = file_data_to_string(file_data)
|
|
152
150
|
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
|
153
|
-
|
|
151
|
+
|
|
154
152
|
if isinstance(result, str):
|
|
155
153
|
return EditResult(error=result)
|
|
156
|
-
|
|
154
|
+
|
|
157
155
|
new_content, occurrences = result
|
|
158
156
|
new_file_data = update_file_data(file_data, new_content)
|
|
159
157
|
return EditResult(path=file_path, files_update={file_path: new_file_data}, occurrences=int(occurrences))
|
|
160
|
-
|
|
158
|
+
|
|
161
159
|
# Removed legacy grep() convenience to keep lean surface
|
|
162
160
|
|
|
163
161
|
def grep_raw(
|
|
164
162
|
self,
|
|
165
163
|
pattern: str,
|
|
166
164
|
path: str = "/",
|
|
167
|
-
glob:
|
|
165
|
+
glob: str | None = None,
|
|
168
166
|
) -> list[GrepMatch] | str:
|
|
169
167
|
files = self.runtime.state.get("files", {})
|
|
170
168
|
return grep_matches_from_files(files, pattern, path, glob)
|
|
171
|
-
|
|
169
|
+
|
|
172
170
|
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
173
171
|
files = self.runtime.state.get("files", {})
|
|
174
172
|
result = _glob_search_files(files, pattern, path)
|
|
@@ -179,12 +177,15 @@ class StateBackend:
|
|
|
179
177
|
for p in paths:
|
|
180
178
|
fd = files.get(p)
|
|
181
179
|
size = len("\n".join(fd.get("content", []))) if fd else 0
|
|
182
|
-
infos.append(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
180
|
+
infos.append(
|
|
181
|
+
{
|
|
182
|
+
"path": p,
|
|
183
|
+
"is_dir": False,
|
|
184
|
+
"size": int(size),
|
|
185
|
+
"modified_at": fd.get("modified_at", "") if fd else "",
|
|
186
|
+
}
|
|
187
|
+
)
|
|
188
188
|
return infos
|
|
189
189
|
|
|
190
|
+
|
|
190
191
|
# Provider classes removed: prefer callables like `lambda rt: StateBackend(rt)`
|
deepagents/backends/store.py
CHANGED
|
@@ -1,48 +1,44 @@
|
|
|
1
1
|
"""StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread)."""
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
from typing import Any, Optional, TYPE_CHECKING
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from langchain.tools import ToolRuntime
|
|
3
|
+
from typing import Any
|
|
8
4
|
|
|
9
5
|
from langgraph.config import get_config
|
|
10
6
|
from langgraph.store.base import BaseStore, Item
|
|
11
|
-
from deepagents.backends.protocol import WriteResult, EditResult
|
|
12
7
|
|
|
8
|
+
from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
|
|
13
9
|
from deepagents.backends.utils import (
|
|
10
|
+
_glob_search_files,
|
|
14
11
|
create_file_data,
|
|
15
|
-
update_file_data,
|
|
16
12
|
file_data_to_string,
|
|
17
13
|
format_read_response,
|
|
18
|
-
perform_string_replacement,
|
|
19
|
-
_glob_search_files,
|
|
20
14
|
grep_matches_from_files,
|
|
15
|
+
perform_string_replacement,
|
|
16
|
+
update_file_data,
|
|
21
17
|
)
|
|
22
|
-
from deepagents.backends.utils import FileInfo, GrepMatch
|
|
23
18
|
|
|
24
19
|
|
|
25
|
-
class StoreBackend:
|
|
20
|
+
class StoreBackend(BackendProtocol):
|
|
26
21
|
"""Backend that stores files in LangGraph's BaseStore (persistent).
|
|
27
|
-
|
|
22
|
+
|
|
28
23
|
Uses LangGraph's Store for persistent, cross-conversation storage.
|
|
29
24
|
Files are organized via namespaces and persist across all threads.
|
|
30
|
-
|
|
25
|
+
|
|
31
26
|
The namespace can include an optional assistant_id for multi-agent isolation.
|
|
32
27
|
"""
|
|
28
|
+
|
|
33
29
|
def __init__(self, runtime: "ToolRuntime"):
|
|
34
30
|
"""Initialize StoreBackend with runtime.
|
|
35
|
-
|
|
36
|
-
Args:"""
|
|
37
|
-
self.runtime = runtime
|
|
38
31
|
|
|
32
|
+
Args:
|
|
33
|
+
"""
|
|
34
|
+
self.runtime = runtime
|
|
39
35
|
|
|
40
36
|
def _get_store(self) -> BaseStore:
|
|
41
37
|
"""Get the store instance.
|
|
42
|
-
|
|
38
|
+
|
|
43
39
|
Args:Returns:
|
|
44
40
|
BaseStore instance
|
|
45
|
-
|
|
41
|
+
|
|
46
42
|
Raises:
|
|
47
43
|
ValueError: If no store is available or runtime not provided
|
|
48
44
|
"""
|
|
@@ -51,15 +47,15 @@ class StoreBackend:
|
|
|
51
47
|
msg = "Store is required but not available in runtime"
|
|
52
48
|
raise ValueError(msg)
|
|
53
49
|
return store
|
|
54
|
-
|
|
50
|
+
|
|
55
51
|
def _get_namespace(self) -> tuple[str, ...]:
|
|
56
52
|
"""Get the namespace for store operations.
|
|
57
|
-
|
|
53
|
+
|
|
58
54
|
Preference order:
|
|
59
55
|
1) Use `self.runtime.config` if present (tests pass this explicitly).
|
|
60
56
|
2) Fallback to `langgraph.config.get_config()` if available.
|
|
61
57
|
3) Default to ("filesystem",).
|
|
62
|
-
|
|
58
|
+
|
|
63
59
|
If an assistant_id is available in the config metadata, return
|
|
64
60
|
(assistant_id, "filesystem") to provide per-assistant isolation.
|
|
65
61
|
"""
|
|
@@ -88,16 +84,16 @@ class StoreBackend:
|
|
|
88
84
|
if assistant_id:
|
|
89
85
|
return (assistant_id, namespace)
|
|
90
86
|
return (namespace,)
|
|
91
|
-
|
|
87
|
+
|
|
92
88
|
def _convert_store_item_to_file_data(self, store_item: Item) -> dict[str, Any]:
|
|
93
89
|
"""Convert a store Item to FileData format.
|
|
94
|
-
|
|
90
|
+
|
|
95
91
|
Args:
|
|
96
92
|
store_item: The store Item containing file data.
|
|
97
|
-
|
|
93
|
+
|
|
98
94
|
Returns:
|
|
99
95
|
FileData dict with content, created_at, and modified_at fields.
|
|
100
|
-
|
|
96
|
+
|
|
101
97
|
Raises:
|
|
102
98
|
ValueError: If required fields are missing or have incorrect types.
|
|
103
99
|
"""
|
|
@@ -115,13 +111,13 @@ class StoreBackend:
|
|
|
115
111
|
"created_at": store_item.value["created_at"],
|
|
116
112
|
"modified_at": store_item.value["modified_at"],
|
|
117
113
|
}
|
|
118
|
-
|
|
114
|
+
|
|
119
115
|
def _convert_file_data_to_store_value(self, file_data: dict[str, Any]) -> dict[str, Any]:
|
|
120
116
|
"""Convert FileData to a dict suitable for store.put().
|
|
121
|
-
|
|
117
|
+
|
|
122
118
|
Args:
|
|
123
119
|
file_data: The FileData to convert.
|
|
124
|
-
|
|
120
|
+
|
|
125
121
|
Returns:
|
|
126
122
|
Dictionary with content, created_at, and modified_at fields.
|
|
127
123
|
"""
|
|
@@ -177,7 +173,7 @@ class StoreBackend:
|
|
|
177
173
|
offset += page_size
|
|
178
174
|
|
|
179
175
|
return all_items
|
|
180
|
-
|
|
176
|
+
|
|
181
177
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
182
178
|
"""List files and directories in the specified directory (non-recursive).
|
|
183
179
|
|
|
@@ -206,7 +202,7 @@ class StoreBackend:
|
|
|
206
202
|
continue
|
|
207
203
|
|
|
208
204
|
# Get the relative path after the directory
|
|
209
|
-
relative = str(item.key)[len(normalized_path):]
|
|
205
|
+
relative = str(item.key)[len(normalized_path) :]
|
|
210
206
|
|
|
211
207
|
# If relative path contains '/', it's in a subdirectory
|
|
212
208
|
if "/" in relative:
|
|
@@ -221,58 +217,62 @@ class StoreBackend:
|
|
|
221
217
|
except ValueError:
|
|
222
218
|
continue
|
|
223
219
|
size = len("\n".join(fd.get("content", [])))
|
|
224
|
-
infos.append(
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
220
|
+
infos.append(
|
|
221
|
+
{
|
|
222
|
+
"path": item.key,
|
|
223
|
+
"is_dir": False,
|
|
224
|
+
"size": int(size),
|
|
225
|
+
"modified_at": fd.get("modified_at", ""),
|
|
226
|
+
}
|
|
227
|
+
)
|
|
230
228
|
|
|
231
229
|
# Add directories to the results
|
|
232
230
|
for subdir in sorted(subdirs):
|
|
233
|
-
infos.append(
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
231
|
+
infos.append(
|
|
232
|
+
{
|
|
233
|
+
"path": subdir,
|
|
234
|
+
"is_dir": True,
|
|
235
|
+
"size": 0,
|
|
236
|
+
"modified_at": "",
|
|
237
|
+
}
|
|
238
|
+
)
|
|
239
239
|
|
|
240
240
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
241
241
|
return infos
|
|
242
242
|
|
|
243
243
|
# Removed legacy ls() convenience to keep lean surface
|
|
244
|
-
|
|
244
|
+
|
|
245
245
|
def read(
|
|
246
|
-
self,
|
|
246
|
+
self,
|
|
247
247
|
file_path: str,
|
|
248
248
|
offset: int = 0,
|
|
249
249
|
limit: int = 2000,
|
|
250
250
|
) -> str:
|
|
251
251
|
"""Read file content with line numbers.
|
|
252
|
-
|
|
252
|
+
|
|
253
253
|
Args:
|
|
254
254
|
file_path: Absolute file path
|
|
255
255
|
offset: Line offset to start reading from (0-indexed)limit: Maximum number of lines to read
|
|
256
|
-
|
|
256
|
+
|
|
257
257
|
Returns:
|
|
258
258
|
Formatted file content with line numbers, or error message.
|
|
259
259
|
"""
|
|
260
260
|
store = self._get_store()
|
|
261
261
|
namespace = self._get_namespace()
|
|
262
|
-
item:
|
|
263
|
-
|
|
262
|
+
item: Item | None = store.get(namespace, file_path)
|
|
263
|
+
|
|
264
264
|
if item is None:
|
|
265
265
|
return f"Error: File '{file_path}' not found"
|
|
266
|
-
|
|
266
|
+
|
|
267
267
|
try:
|
|
268
268
|
file_data = self._convert_store_item_to_file_data(item)
|
|
269
269
|
except ValueError as e:
|
|
270
270
|
return f"Error: {e}"
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
return format_read_response(file_data, offset, limit)
|
|
273
|
-
|
|
273
|
+
|
|
274
274
|
def write(
|
|
275
|
-
self,
|
|
275
|
+
self,
|
|
276
276
|
file_path: str,
|
|
277
277
|
content: str,
|
|
278
278
|
) -> WriteResult:
|
|
@@ -281,20 +281,20 @@ class StoreBackend:
|
|
|
281
281
|
"""
|
|
282
282
|
store = self._get_store()
|
|
283
283
|
namespace = self._get_namespace()
|
|
284
|
-
|
|
284
|
+
|
|
285
285
|
# Check if file exists
|
|
286
286
|
existing = store.get(namespace, file_path)
|
|
287
287
|
if existing is not None:
|
|
288
288
|
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.")
|
|
289
|
-
|
|
289
|
+
|
|
290
290
|
# Create new file
|
|
291
291
|
file_data = create_file_data(content)
|
|
292
292
|
store_value = self._convert_file_data_to_store_value(file_data)
|
|
293
293
|
store.put(namespace, file_path, store_value)
|
|
294
294
|
return WriteResult(path=file_path, files_update=None)
|
|
295
|
-
|
|
295
|
+
|
|
296
296
|
def edit(
|
|
297
|
-
self,
|
|
297
|
+
self,
|
|
298
298
|
file_path: str,
|
|
299
299
|
old_string: str,
|
|
300
300
|
new_string: str,
|
|
@@ -305,38 +305,38 @@ class StoreBackend:
|
|
|
305
305
|
"""
|
|
306
306
|
store = self._get_store()
|
|
307
307
|
namespace = self._get_namespace()
|
|
308
|
-
|
|
308
|
+
|
|
309
309
|
# Get existing file
|
|
310
310
|
item = store.get(namespace, file_path)
|
|
311
311
|
if item is None:
|
|
312
312
|
return EditResult(error=f"Error: File '{file_path}' not found")
|
|
313
|
-
|
|
313
|
+
|
|
314
314
|
try:
|
|
315
315
|
file_data = self._convert_store_item_to_file_data(item)
|
|
316
316
|
except ValueError as e:
|
|
317
317
|
return EditResult(error=f"Error: {e}")
|
|
318
|
-
|
|
318
|
+
|
|
319
319
|
content = file_data_to_string(file_data)
|
|
320
320
|
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
|
321
|
-
|
|
321
|
+
|
|
322
322
|
if isinstance(result, str):
|
|
323
323
|
return EditResult(error=result)
|
|
324
|
-
|
|
324
|
+
|
|
325
325
|
new_content, occurrences = result
|
|
326
326
|
new_file_data = update_file_data(file_data, new_content)
|
|
327
|
-
|
|
327
|
+
|
|
328
328
|
# Update file in store
|
|
329
329
|
store_value = self._convert_file_data_to_store_value(new_file_data)
|
|
330
330
|
store.put(namespace, file_path, store_value)
|
|
331
331
|
return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
|
|
332
|
-
|
|
332
|
+
|
|
333
333
|
# Removed legacy grep() convenience to keep lean surface
|
|
334
334
|
|
|
335
335
|
def grep_raw(
|
|
336
336
|
self,
|
|
337
337
|
pattern: str,
|
|
338
338
|
path: str = "/",
|
|
339
|
-
glob:
|
|
339
|
+
glob: str | None = None,
|
|
340
340
|
) -> list[GrepMatch] | str:
|
|
341
341
|
store = self._get_store()
|
|
342
342
|
namespace = self._get_namespace()
|
|
@@ -348,7 +348,7 @@ class StoreBackend:
|
|
|
348
348
|
except ValueError:
|
|
349
349
|
continue
|
|
350
350
|
return grep_matches_from_files(files, pattern, path, glob)
|
|
351
|
-
|
|
351
|
+
|
|
352
352
|
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
353
353
|
store = self._get_store()
|
|
354
354
|
namespace = self._get_namespace()
|
|
@@ -367,13 +367,12 @@ class StoreBackend:
|
|
|
367
367
|
for p in paths:
|
|
368
368
|
fd = files.get(p)
|
|
369
369
|
size = len("\n".join(fd.get("content", []))) if fd else 0
|
|
370
|
-
infos.append(
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
370
|
+
infos.append(
|
|
371
|
+
{
|
|
372
|
+
"path": p,
|
|
373
|
+
"is_dir": False,
|
|
374
|
+
"size": int(size),
|
|
375
|
+
"modified_at": fd.get("modified_at", "") if fd else "",
|
|
376
|
+
}
|
|
377
|
+
)
|
|
376
378
|
return infos
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
# Provider classes removed: prefer callables like `lambda rt: StoreBackend(rt)`
|
deepagents/backends/utils.py
CHANGED
|
@@ -8,36 +8,22 @@ enable composition without fragile string parsing.
|
|
|
8
8
|
import re
|
|
9
9
|
from datetime import UTC, datetime
|
|
10
10
|
from pathlib import Path
|
|
11
|
-
from typing import Any, Literal
|
|
11
|
+
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
|
|
17
|
+
|
|
15
18
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
16
19
|
MAX_LINE_LENGTH = 10000
|
|
17
20
|
LINE_NUMBER_WIDTH = 6
|
|
18
21
|
TOOL_RESULT_TOKEN_LIMIT = 20000 # Same threshold as eviction
|
|
19
22
|
TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]"
|
|
20
23
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
Minimal contract used across backends. Only "path" is required.
|
|
26
|
-
Other fields are best-effort and may be absent depending on backend.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
|
-
path: str
|
|
30
|
-
is_dir: bool
|
|
31
|
-
size: int # bytes (approx)
|
|
32
|
-
modified_at: str # ISO timestamp if known
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
class GrepMatch(TypedDict):
|
|
36
|
-
"""Structured grep match entry."""
|
|
37
|
-
|
|
38
|
-
path: str
|
|
39
|
-
line: int
|
|
40
|
-
text: str
|
|
24
|
+
# Re-export protocol types for backwards compatibility
|
|
25
|
+
FileInfo = _FileInfo
|
|
26
|
+
GrepMatch = _GrepMatch
|
|
41
27
|
|
|
42
28
|
|
|
43
29
|
def sanitize_tool_call_id(tool_call_id: str) -> str:
|
deepagents/graph.py
CHANGED
|
@@ -17,7 +17,7 @@ from langgraph.graph.state import CompiledStateGraph
|
|
|
17
17
|
from langgraph.store.base import BaseStore
|
|
18
18
|
from langgraph.types import Checkpointer
|
|
19
19
|
|
|
20
|
-
from deepagents.backends.protocol import
|
|
20
|
+
from deepagents.backends.protocol import BackendFactory, BackendProtocol
|
|
21
21
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
22
22
|
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
|
|
23
23
|
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
@@ -57,9 +57,12 @@ def create_deep_agent(
|
|
|
57
57
|
"""Create a deep agent.
|
|
58
58
|
|
|
59
59
|
This agent will by default have access to a tool to write todos (write_todos),
|
|
60
|
-
|
|
60
|
+
seven file and execution tools: ls, read_file, write_file, edit_file, glob, grep, execute,
|
|
61
61
|
and a tool to call subagents.
|
|
62
62
|
|
|
63
|
+
The execute tool allows running shell commands if the backend implements SandboxBackendProtocol.
|
|
64
|
+
For non-sandbox backends, the execute tool will return an error message.
|
|
65
|
+
|
|
63
66
|
Args:
|
|
64
67
|
model: The model to use. Defaults to Claude Sonnet 4.
|
|
65
68
|
tools: The tools the agent should have access to.
|
|
@@ -80,8 +83,9 @@ def create_deep_agent(
|
|
|
80
83
|
context_schema: The schema of the deep agent.
|
|
81
84
|
checkpointer: Optional checkpointer for persisting agent state between runs.
|
|
82
85
|
store: Optional store for persistent storage (required if backend uses StoreBackend).
|
|
83
|
-
backend: Optional backend for file storage. Pass either a Backend instance
|
|
84
|
-
callable factory like `lambda rt: StateBackend(rt)`.
|
|
86
|
+
backend: Optional backend for file storage and execution. Pass either a Backend instance
|
|
87
|
+
or a callable factory like `lambda rt: StateBackend(rt)`. For execution support,
|
|
88
|
+
use a backend that implements SandboxBackendProtocol.
|
|
85
89
|
interrupt_on: Optional Dict[str, bool | InterruptOnConfig] mapping tool names to
|
|
86
90
|
interrupt configs.
|
|
87
91
|
debug: Whether to enable debug mode. Passed through to create_agent.
|