deepagents 0.2.7__py3-none-any.whl → 0.3.0__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/composite.py +321 -9
- deepagents/backends/filesystem.py +90 -23
- deepagents/backends/protocol.py +289 -27
- deepagents/backends/sandbox.py +24 -5
- deepagents/backends/state.py +6 -10
- deepagents/backends/store.py +72 -8
- deepagents/graph.py +18 -4
- deepagents/middleware/filesystem.py +209 -27
- deepagents/middleware/subagents.py +4 -2
- {deepagents-0.2.7.dist-info → deepagents-0.3.0.dist-info}/METADATA +5 -8
- deepagents-0.3.0.dist-info/RECORD +18 -0
- deepagents-0.2.7.dist-info/RECORD +0 -18
- {deepagents-0.2.7.dist-info → deepagents-0.3.0.dist-info}/WHEEL +0 -0
- {deepagents-0.2.7.dist-info → deepagents-0.3.0.dist-info}/top_level.txt +0 -0
deepagents/backends/protocol.py
CHANGED
|
@@ -5,14 +5,87 @@ must follow. Backends can store files in different locations (state, filesystem,
|
|
|
5
5
|
database, etc.) and provide a uniform interface for file operations.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
+
import abc
|
|
9
|
+
import asyncio
|
|
8
10
|
from collections.abc import Callable
|
|
9
11
|
from dataclasses import dataclass
|
|
10
|
-
from typing import Any,
|
|
12
|
+
from typing import Any, Literal, NotRequired, TypeAlias
|
|
11
13
|
|
|
12
14
|
from langchain.tools import ToolRuntime
|
|
15
|
+
from typing_extensions import TypedDict
|
|
16
|
+
|
|
17
|
+
FileOperationError = Literal[
|
|
18
|
+
"file_not_found", # Download: file doesn't exist
|
|
19
|
+
"permission_denied", # Both: access denied
|
|
20
|
+
"is_directory", # Download: tried to download directory as file
|
|
21
|
+
"invalid_path", # Both: path syntax malformed (parent dir missing, invalid chars)
|
|
22
|
+
]
|
|
23
|
+
"""Standardized error codes for file upload/download operations.
|
|
24
|
+
|
|
25
|
+
These represent common, recoverable errors that an LLM can understand and potentially fix:
|
|
26
|
+
- file_not_found: The requested file doesn't exist (download)
|
|
27
|
+
- parent_not_found: The parent directory doesn't exist (upload)
|
|
28
|
+
- permission_denied: Access denied for the operation
|
|
29
|
+
- is_directory: Attempted to download a directory as a file
|
|
30
|
+
- invalid_path: Path syntax is malformed or contains invalid characters
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class FileDownloadResponse:
|
|
36
|
+
"""Result of a single file download operation.
|
|
37
|
+
|
|
38
|
+
The response is designed to allow partial success in batch operations.
|
|
39
|
+
The errors are standardized using FileOperationError literals
|
|
40
|
+
for certain recoverable conditions for use cases that involve
|
|
41
|
+
LLMs performing file operations.
|
|
42
|
+
|
|
43
|
+
Attributes:
|
|
44
|
+
path: The file path that was requested. Included for easy correlation
|
|
45
|
+
when processing batch results, especially useful for error messages.
|
|
46
|
+
content: File contents as bytes on success, None on failure.
|
|
47
|
+
error: Standardized error code on failure, None on success.
|
|
48
|
+
Uses FileOperationError literal for structured, LLM-actionable error reporting.
|
|
49
|
+
|
|
50
|
+
Examples:
|
|
51
|
+
>>> # Success
|
|
52
|
+
>>> FileDownloadResponse(path="/app/config.json", content=b"{...}", error=None)
|
|
53
|
+
>>> # Failure
|
|
54
|
+
>>> FileDownloadResponse(path="/wrong/path.txt", content=None, error="file_not_found")
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
path: str
|
|
58
|
+
content: bytes | None = None
|
|
59
|
+
error: FileOperationError | None = None
|
|
60
|
+
|
|
13
61
|
|
|
62
|
+
@dataclass
|
|
63
|
+
class FileUploadResponse:
|
|
64
|
+
"""Result of a single file upload operation.
|
|
65
|
+
|
|
66
|
+
The response is designed to allow partial success in batch operations.
|
|
67
|
+
The errors are standardized using FileOperationError literals
|
|
68
|
+
for certain recoverable conditions for use cases that involve
|
|
69
|
+
LLMs performing file operations.
|
|
70
|
+
|
|
71
|
+
Attributes:
|
|
72
|
+
path: The file path that was requested. Included for easy correlation
|
|
73
|
+
when processing batch results and for clear error messages.
|
|
74
|
+
error: Standardized error code on failure, None on success.
|
|
75
|
+
Uses FileOperationError literal for structured, LLM-actionable error reporting.
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> # Success
|
|
79
|
+
>>> FileUploadResponse(path="/app/data.txt", error=None)
|
|
80
|
+
>>> # Failure
|
|
81
|
+
>>> FileUploadResponse(path="/readonly/file.txt", error="permission_denied")
|
|
82
|
+
"""
|
|
14
83
|
|
|
15
|
-
|
|
84
|
+
path: str
|
|
85
|
+
error: FileOperationError | None = None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class FileInfo(TypedDict):
|
|
16
89
|
"""Structured file listing info.
|
|
17
90
|
|
|
18
91
|
Minimal contract used across backends. Only "path" is required.
|
|
@@ -20,9 +93,9 @@ class FileInfo(TypedDict, total=False):
|
|
|
20
93
|
"""
|
|
21
94
|
|
|
22
95
|
path: str
|
|
23
|
-
is_dir: bool
|
|
24
|
-
size: int # bytes (approx)
|
|
25
|
-
modified_at: str # ISO timestamp if known
|
|
96
|
+
is_dir: NotRequired[bool]
|
|
97
|
+
size: NotRequired[int] # bytes (approx)
|
|
98
|
+
modified_at: NotRequired[str] # ISO timestamp if known
|
|
26
99
|
|
|
27
100
|
|
|
28
101
|
class GrepMatch(TypedDict):
|
|
@@ -85,8 +158,7 @@ class EditResult:
|
|
|
85
158
|
occurrences: int | None = None
|
|
86
159
|
|
|
87
160
|
|
|
88
|
-
|
|
89
|
-
class BackendProtocol(Protocol):
|
|
161
|
+
class BackendProtocol(abc.ABC):
|
|
90
162
|
"""Protocol for pluggable memory backends (single, unified).
|
|
91
163
|
|
|
92
164
|
Backends can store files in different locations (state, filesystem, database, etc.)
|
|
@@ -94,15 +166,30 @@ class BackendProtocol(Protocol):
|
|
|
94
166
|
|
|
95
167
|
All file data is represented as dicts with the following structure:
|
|
96
168
|
{
|
|
97
|
-
"content": list[str],
|
|
98
|
-
"created_at": str,
|
|
99
|
-
"modified_at": str,
|
|
169
|
+
"content": list[str], # Lines of text content
|
|
170
|
+
"created_at": str, # ISO format timestamp
|
|
171
|
+
"modified_at": str, # ISO format timestamp
|
|
100
172
|
}
|
|
101
173
|
"""
|
|
102
174
|
|
|
103
175
|
def ls_info(self, path: str) -> list["FileInfo"]:
|
|
104
|
-
"""
|
|
105
|
-
|
|
176
|
+
"""List all files in a directory with metadata.
|
|
177
|
+
|
|
178
|
+
Args:
|
|
179
|
+
path: Absolute path to the directory to list. Must start with '/'.
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
List of FileInfo dicts containing file metadata:
|
|
183
|
+
|
|
184
|
+
- `path` (required): Absolute file path
|
|
185
|
+
- `is_dir` (optional): True if directory
|
|
186
|
+
- `size` (optional): File size in bytes
|
|
187
|
+
- `modified_at` (optional): ISO 8601 timestamp
|
|
188
|
+
"""
|
|
189
|
+
|
|
190
|
+
async def als_info(self, path: str) -> list["FileInfo"]:
|
|
191
|
+
"""Async version of ls_info."""
|
|
192
|
+
return await asyncio.to_thread(self.ls_info, path)
|
|
106
193
|
|
|
107
194
|
def read(
|
|
108
195
|
self,
|
|
@@ -110,8 +197,35 @@ class BackendProtocol(Protocol):
|
|
|
110
197
|
offset: int = 0,
|
|
111
198
|
limit: int = 2000,
|
|
112
199
|
) -> str:
|
|
113
|
-
"""Read file content with line numbers
|
|
114
|
-
|
|
200
|
+
"""Read file content with line numbers.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
file_path: Absolute path to the file to read. Must start with '/'.
|
|
204
|
+
offset: Line number to start reading from (0-indexed). Default: 0.
|
|
205
|
+
limit: Maximum number of lines to read. Default: 2000.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
String containing file content formatted with line numbers (cat -n format),
|
|
209
|
+
starting at line 1. Lines longer than 2000 characters are truncated.
|
|
210
|
+
|
|
211
|
+
Returns an error string if the file doesn't exist or can't be read.
|
|
212
|
+
|
|
213
|
+
!!! note
|
|
214
|
+
- Use pagination (offset/limit) for large files to avoid context overflow
|
|
215
|
+
- First scan: `read(path, limit=100)` to see file structure
|
|
216
|
+
- Read more: `read(path, offset=100, limit=200)` for next section
|
|
217
|
+
- ALWAYS read a file before editing it
|
|
218
|
+
- If file exists but is empty, you'll receive a system reminder warning
|
|
219
|
+
"""
|
|
220
|
+
|
|
221
|
+
async def aread(
|
|
222
|
+
self,
|
|
223
|
+
file_path: str,
|
|
224
|
+
offset: int = 0,
|
|
225
|
+
limit: int = 2000,
|
|
226
|
+
) -> str:
|
|
227
|
+
"""Async version of read."""
|
|
228
|
+
return await asyncio.to_thread(self.read, file_path, offset, limit)
|
|
115
229
|
|
|
116
230
|
def grep_raw(
|
|
117
231
|
self,
|
|
@@ -119,20 +233,94 @@ class BackendProtocol(Protocol):
|
|
|
119
233
|
path: str | None = None,
|
|
120
234
|
glob: str | None = None,
|
|
121
235
|
) -> list["GrepMatch"] | str:
|
|
122
|
-
"""
|
|
123
|
-
|
|
236
|
+
"""Search for a literal text pattern in files.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
pattern: Literal string to search for (NOT regex).
|
|
240
|
+
Performs exact substring matching within file content.
|
|
241
|
+
Example: "TODO" matches any line containing "TODO"
|
|
242
|
+
|
|
243
|
+
path: Optional directory path to search in.
|
|
244
|
+
If None, searches in current working directory.
|
|
245
|
+
Example: "/workspace/src"
|
|
246
|
+
|
|
247
|
+
glob: Optional glob pattern to filter which FILES to search.
|
|
248
|
+
Filters by filename/path, not content.
|
|
249
|
+
Supports standard glob wildcards:
|
|
250
|
+
- `*` matches any characters in filename
|
|
251
|
+
- `**` matches any directories recursively
|
|
252
|
+
- `?` matches single character
|
|
253
|
+
- `[abc]` matches one character from set
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
- "*.py" - only search Python files
|
|
257
|
+
- "**/*.txt" - search all .txt files recursively
|
|
258
|
+
- "src/**/*.js" - search JS files under src/
|
|
259
|
+
- "test[0-9].txt" - search test0.txt, test1.txt, etc.
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
On success: list[GrepMatch] with structured results containing:
|
|
263
|
+
- path: Absolute file path
|
|
264
|
+
- line: Line number (1-indexed)
|
|
265
|
+
- text: Full line content containing the match
|
|
266
|
+
|
|
267
|
+
On error: str with error message (e.g., invalid path, permission denied)
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
async def agrep_raw(
|
|
271
|
+
self,
|
|
272
|
+
pattern: str,
|
|
273
|
+
path: str | None = None,
|
|
274
|
+
glob: str | None = None,
|
|
275
|
+
) -> list["GrepMatch"] | str:
|
|
276
|
+
"""Async version of grep_raw."""
|
|
277
|
+
return await asyncio.to_thread(self.grep_raw, pattern, path, glob)
|
|
124
278
|
|
|
125
279
|
def glob_info(self, pattern: str, path: str = "/") -> list["FileInfo"]:
|
|
126
|
-
"""
|
|
127
|
-
|
|
280
|
+
"""Find files matching a glob pattern.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
pattern: Glob pattern with wildcards to match file paths.
|
|
284
|
+
Supports standard glob syntax:
|
|
285
|
+
- `*` matches any characters within a filename/directory
|
|
286
|
+
- `**` matches any directories recursively
|
|
287
|
+
- `?` matches a single character
|
|
288
|
+
- `[abc]` matches one character from set
|
|
289
|
+
|
|
290
|
+
path: Base directory to search from. Default: "/" (root).
|
|
291
|
+
The pattern is applied relative to this path.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
list of FileInfo
|
|
295
|
+
"""
|
|
296
|
+
|
|
297
|
+
async def aglob_info(self, pattern: str, path: str = "/") -> list["FileInfo"]:
|
|
298
|
+
"""Async version of glob_info."""
|
|
299
|
+
return await asyncio.to_thread(self.glob_info, pattern, path)
|
|
128
300
|
|
|
129
301
|
def write(
|
|
130
302
|
self,
|
|
131
303
|
file_path: str,
|
|
132
304
|
content: str,
|
|
133
305
|
) -> WriteResult:
|
|
134
|
-
"""
|
|
135
|
-
|
|
306
|
+
"""Write content to a new file in the filesystem, error if file exists.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
file_path: Absolute path where the file should be created.
|
|
310
|
+
Must start with '/'.
|
|
311
|
+
content: String content to write to the file.
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
WriteResult
|
|
315
|
+
"""
|
|
316
|
+
|
|
317
|
+
async def awrite(
|
|
318
|
+
self,
|
|
319
|
+
file_path: str,
|
|
320
|
+
content: str,
|
|
321
|
+
) -> WriteResult:
|
|
322
|
+
"""Async version of write."""
|
|
323
|
+
return await asyncio.to_thread(self.write, file_path, content)
|
|
136
324
|
|
|
137
325
|
def edit(
|
|
138
326
|
self,
|
|
@@ -141,8 +329,78 @@ class BackendProtocol(Protocol):
|
|
|
141
329
|
new_string: str,
|
|
142
330
|
replace_all: bool = False,
|
|
143
331
|
) -> EditResult:
|
|
144
|
-
"""
|
|
145
|
-
|
|
332
|
+
"""Perform exact string replacements in an existing file.
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
file_path: Absolute path to the file to edit. Must start with '/'.
|
|
336
|
+
old_string: Exact string to search for and replace.
|
|
337
|
+
Must match exactly including whitespace and indentation.
|
|
338
|
+
new_string: String to replace old_string with.
|
|
339
|
+
Must be different from old_string.
|
|
340
|
+
replace_all: If True, replace all occurrences. If False (default),
|
|
341
|
+
old_string must be unique in the file or the edit fails.
|
|
342
|
+
|
|
343
|
+
Returns:
|
|
344
|
+
EditResult
|
|
345
|
+
"""
|
|
346
|
+
|
|
347
|
+
async def aedit(
|
|
348
|
+
self,
|
|
349
|
+
file_path: str,
|
|
350
|
+
old_string: str,
|
|
351
|
+
new_string: str,
|
|
352
|
+
replace_all: bool = False,
|
|
353
|
+
) -> EditResult:
|
|
354
|
+
"""Async version of edit."""
|
|
355
|
+
return await asyncio.to_thread(self.edit, file_path, old_string, new_string, replace_all)
|
|
356
|
+
|
|
357
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
358
|
+
"""Upload multiple files to the sandbox.
|
|
359
|
+
|
|
360
|
+
This API is designed to allow developers to use it either directly or
|
|
361
|
+
by exposing it to LLMs via custom tools.
|
|
362
|
+
|
|
363
|
+
Args:
|
|
364
|
+
files: List of (path, content) tuples to upload.
|
|
365
|
+
|
|
366
|
+
Returns:
|
|
367
|
+
List of FileUploadResponse objects, one per input file.
|
|
368
|
+
Response order matches input order (response[i] for files[i]).
|
|
369
|
+
Check the error field to determine success/failure per file.
|
|
370
|
+
|
|
371
|
+
Examples:
|
|
372
|
+
```python
|
|
373
|
+
responses = sandbox.upload_files(
|
|
374
|
+
[
|
|
375
|
+
("/app/config.json", b"{...}"),
|
|
376
|
+
("/app/data.txt", b"content"),
|
|
377
|
+
]
|
|
378
|
+
)
|
|
379
|
+
```
|
|
380
|
+
"""
|
|
381
|
+
|
|
382
|
+
async def aupload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
383
|
+
"""Async version of upload_files."""
|
|
384
|
+
return await asyncio.to_thread(self.upload_files, files)
|
|
385
|
+
|
|
386
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
387
|
+
"""Download multiple files from the sandbox.
|
|
388
|
+
|
|
389
|
+
This API is designed to allow developers to use it either directly or
|
|
390
|
+
by exposing it to LLMs via custom tools.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
paths: List of file paths to download.
|
|
394
|
+
|
|
395
|
+
Returns:
|
|
396
|
+
List of FileDownloadResponse objects, one per input path.
|
|
397
|
+
Response order matches input order (response[i] for paths[i]).
|
|
398
|
+
Check the error field to determine success/failure per file.
|
|
399
|
+
"""
|
|
400
|
+
|
|
401
|
+
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
402
|
+
"""Async version of download_files."""
|
|
403
|
+
return await asyncio.to_thread(self.download_files, paths)
|
|
146
404
|
|
|
147
405
|
|
|
148
406
|
@dataclass
|
|
@@ -162,8 +420,7 @@ class ExecuteResponse:
|
|
|
162
420
|
"""Whether the output was truncated due to backend limitations."""
|
|
163
421
|
|
|
164
422
|
|
|
165
|
-
|
|
166
|
-
class SandboxBackendProtocol(BackendProtocol, Protocol):
|
|
423
|
+
class SandboxBackendProtocol(BackendProtocol):
|
|
167
424
|
"""Protocol for sandboxed backends with isolated runtime.
|
|
168
425
|
|
|
169
426
|
Sandboxed backends run in isolated environments (e.g., separate processes,
|
|
@@ -184,12 +441,17 @@ class SandboxBackendProtocol(BackendProtocol, Protocol):
|
|
|
184
441
|
Returns:
|
|
185
442
|
ExecuteResponse with combined output, exit code, optional signal, and truncation flag.
|
|
186
443
|
"""
|
|
187
|
-
|
|
444
|
+
|
|
445
|
+
async def aexecute(
|
|
446
|
+
self,
|
|
447
|
+
command: str,
|
|
448
|
+
) -> ExecuteResponse:
|
|
449
|
+
"""Async version of execute."""
|
|
450
|
+
return await asyncio.to_thread(self.execute, command)
|
|
188
451
|
|
|
189
452
|
@property
|
|
190
453
|
def id(self) -> str:
|
|
191
|
-
"""Unique identifier for the sandbox backend."""
|
|
192
|
-
...
|
|
454
|
+
"""Unique identifier for the sandbox backend instance."""
|
|
193
455
|
|
|
194
456
|
|
|
195
457
|
BackendFactory: TypeAlias = Callable[[ToolRuntime], BackendProtocol]
|
deepagents/backends/sandbox.py
CHANGED
|
@@ -9,12 +9,15 @@ from __future__ import annotations
|
|
|
9
9
|
|
|
10
10
|
import base64
|
|
11
11
|
import json
|
|
12
|
+
import shlex
|
|
12
13
|
from abc import ABC, abstractmethod
|
|
13
14
|
|
|
14
15
|
from deepagents.backends.protocol import (
|
|
15
16
|
EditResult,
|
|
16
17
|
ExecuteResponse,
|
|
18
|
+
FileDownloadResponse,
|
|
17
19
|
FileInfo,
|
|
20
|
+
FileUploadResponse,
|
|
18
21
|
GrepMatch,
|
|
19
22
|
SandboxBackendProtocol,
|
|
20
23
|
WriteResult,
|
|
@@ -270,10 +273,10 @@ except PermissionError:
|
|
|
270
273
|
glob: str | None = None,
|
|
271
274
|
) -> list[GrepMatch] | str:
|
|
272
275
|
"""Structured search results or error string for invalid input."""
|
|
273
|
-
search_path = path or "."
|
|
276
|
+
search_path = shlex.quote(path or ".")
|
|
274
277
|
|
|
275
278
|
# Build grep command to get structured output
|
|
276
|
-
grep_opts = "-
|
|
279
|
+
grep_opts = "-rHnF" # recursive, with filename, with line number, fixed-strings (literal)
|
|
277
280
|
|
|
278
281
|
# Add glob pattern if specified
|
|
279
282
|
glob_pattern = ""
|
|
@@ -281,9 +284,9 @@ except PermissionError:
|
|
|
281
284
|
glob_pattern = f"--include='{glob}'"
|
|
282
285
|
|
|
283
286
|
# Escape pattern for shell
|
|
284
|
-
pattern_escaped =
|
|
287
|
+
pattern_escaped = shlex.quote(pattern)
|
|
285
288
|
|
|
286
|
-
cmd = f"grep {grep_opts} {glob_pattern} -e
|
|
289
|
+
cmd = f"grep {grep_opts} {glob_pattern} -e {pattern_escaped} {search_path} 2>/dev/null || true"
|
|
287
290
|
result = self.execute(cmd)
|
|
288
291
|
|
|
289
292
|
output = result.output.rstrip()
|
|
@@ -338,4 +341,20 @@ except PermissionError:
|
|
|
338
341
|
@property
|
|
339
342
|
@abstractmethod
|
|
340
343
|
def id(self) -> str:
|
|
341
|
-
"""Unique identifier for
|
|
344
|
+
"""Unique identifier for the sandbox backend."""
|
|
345
|
+
|
|
346
|
+
@abstractmethod
|
|
347
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
348
|
+
"""Upload multiple files to the sandbox.
|
|
349
|
+
|
|
350
|
+
Implementations must support partial success - catch exceptions per-file
|
|
351
|
+
and return errors in FileUploadResponse objects rather than raising.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
@abstractmethod
|
|
355
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
356
|
+
"""Download multiple files from the sandbox.
|
|
357
|
+
|
|
358
|
+
Implementations must support partial success - catch exceptions per-file
|
|
359
|
+
and return errors in FileDownloadResponse objects rather than raising.
|
|
360
|
+
"""
|
deepagents/backends/state.py
CHANGED
|
@@ -90,8 +90,6 @@ class StateBackend(BackendProtocol):
|
|
|
90
90
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
91
91
|
return infos
|
|
92
92
|
|
|
93
|
-
# Removed legacy ls() convenience to keep lean surface
|
|
94
|
-
|
|
95
93
|
def read(
|
|
96
94
|
self,
|
|
97
95
|
file_path: str,
|
|
@@ -101,9 +99,11 @@ class StateBackend(BackendProtocol):
|
|
|
101
99
|
"""Read file content with line numbers.
|
|
102
100
|
|
|
103
101
|
Args:
|
|
104
|
-
file_path: Absolute file path
|
|
105
|
-
offset: Line offset to start reading from (0-indexed)
|
|
106
|
-
limit: Maximum number of lines to
|
|
102
|
+
file_path: Absolute file path.
|
|
103
|
+
offset: Line offset to start reading from (0-indexed).
|
|
104
|
+
limit: Maximum number of lines to read.
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
107
|
Formatted file content with line numbers, or error message.
|
|
108
108
|
"""
|
|
109
109
|
files = self.runtime.state.get("files", {})
|
|
@@ -156,8 +156,6 @@ class StateBackend(BackendProtocol):
|
|
|
156
156
|
new_file_data = update_file_data(file_data, new_content)
|
|
157
157
|
return EditResult(path=file_path, files_update={file_path: new_file_data}, occurrences=int(occurrences))
|
|
158
158
|
|
|
159
|
-
# Removed legacy grep() convenience to keep lean surface
|
|
160
|
-
|
|
161
159
|
def grep_raw(
|
|
162
160
|
self,
|
|
163
161
|
pattern: str,
|
|
@@ -168,6 +166,7 @@ class StateBackend(BackendProtocol):
|
|
|
168
166
|
return grep_matches_from_files(files, pattern, path, glob)
|
|
169
167
|
|
|
170
168
|
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
169
|
+
"""Get FileInfo for files matching glob pattern."""
|
|
171
170
|
files = self.runtime.state.get("files", {})
|
|
172
171
|
result = _glob_search_files(files, pattern, path)
|
|
173
172
|
if result == "No files found":
|
|
@@ -186,6 +185,3 @@ class StateBackend(BackendProtocol):
|
|
|
186
185
|
}
|
|
187
186
|
)
|
|
188
187
|
return infos
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
# Provider classes removed: prefer callables like `lambda rt: StateBackend(rt)`
|
deepagents/backends/store.py
CHANGED
|
@@ -5,7 +5,15 @@ from typing import Any
|
|
|
5
5
|
from langgraph.config import get_config
|
|
6
6
|
from langgraph.store.base import BaseStore, Item
|
|
7
7
|
|
|
8
|
-
from deepagents.backends.protocol import
|
|
8
|
+
from deepagents.backends.protocol import (
|
|
9
|
+
BackendProtocol,
|
|
10
|
+
EditResult,
|
|
11
|
+
FileDownloadResponse,
|
|
12
|
+
FileInfo,
|
|
13
|
+
FileUploadResponse,
|
|
14
|
+
GrepMatch,
|
|
15
|
+
WriteResult,
|
|
16
|
+
)
|
|
9
17
|
from deepagents.backends.utils import (
|
|
10
18
|
_glob_search_files,
|
|
11
19
|
create_file_data,
|
|
@@ -30,17 +38,18 @@ class StoreBackend(BackendProtocol):
|
|
|
30
38
|
"""Initialize StoreBackend with runtime.
|
|
31
39
|
|
|
32
40
|
Args:
|
|
41
|
+
runtime: The ToolRuntime instance providing store access and configuration.
|
|
33
42
|
"""
|
|
34
43
|
self.runtime = runtime
|
|
35
44
|
|
|
36
45
|
def _get_store(self) -> BaseStore:
|
|
37
46
|
"""Get the store instance.
|
|
38
47
|
|
|
39
|
-
|
|
40
|
-
BaseStore instance
|
|
48
|
+
Returns:
|
|
49
|
+
BaseStore instance from the runtime.
|
|
41
50
|
|
|
42
51
|
Raises:
|
|
43
|
-
ValueError: If no store is available
|
|
52
|
+
ValueError: If no store is available in the runtime.
|
|
44
53
|
"""
|
|
45
54
|
store = self.runtime.store
|
|
46
55
|
if store is None:
|
|
@@ -240,8 +249,6 @@ class StoreBackend(BackendProtocol):
|
|
|
240
249
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
241
250
|
return infos
|
|
242
251
|
|
|
243
|
-
# Removed legacy ls() convenience to keep lean surface
|
|
244
|
-
|
|
245
252
|
def read(
|
|
246
253
|
self,
|
|
247
254
|
file_path: str,
|
|
@@ -251,8 +258,9 @@ class StoreBackend(BackendProtocol):
|
|
|
251
258
|
"""Read file content with line numbers.
|
|
252
259
|
|
|
253
260
|
Args:
|
|
254
|
-
file_path: Absolute file path
|
|
255
|
-
offset: Line offset to start reading from (0-indexed)
|
|
261
|
+
file_path: Absolute file path.
|
|
262
|
+
offset: Line offset to start reading from (0-indexed).
|
|
263
|
+
limit: Maximum number of lines to read.
|
|
256
264
|
|
|
257
265
|
Returns:
|
|
258
266
|
Formatted file content with line numbers, or error message.
|
|
@@ -376,3 +384,59 @@ class StoreBackend(BackendProtocol):
|
|
|
376
384
|
}
|
|
377
385
|
)
|
|
378
386
|
return infos
|
|
387
|
+
|
|
388
|
+
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
|
389
|
+
"""Upload multiple files to the store.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
files: List of (path, content) tuples where content is bytes.
|
|
393
|
+
|
|
394
|
+
Returns:
|
|
395
|
+
List of FileUploadResponse objects, one per input file.
|
|
396
|
+
Response order matches input order.
|
|
397
|
+
"""
|
|
398
|
+
store = self._get_store()
|
|
399
|
+
namespace = self._get_namespace()
|
|
400
|
+
responses: list[FileUploadResponse] = []
|
|
401
|
+
|
|
402
|
+
for path, content in files:
|
|
403
|
+
content_str = content.decode("utf-8")
|
|
404
|
+
# Create file data
|
|
405
|
+
file_data = create_file_data(content_str)
|
|
406
|
+
store_value = self._convert_file_data_to_store_value(file_data)
|
|
407
|
+
|
|
408
|
+
# Store the file
|
|
409
|
+
store.put(namespace, path, store_value)
|
|
410
|
+
responses.append(FileUploadResponse(path=path, error=None))
|
|
411
|
+
|
|
412
|
+
return responses
|
|
413
|
+
|
|
414
|
+
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
|
415
|
+
"""Download multiple files from the store.
|
|
416
|
+
|
|
417
|
+
Args:
|
|
418
|
+
paths: List of file paths to download.
|
|
419
|
+
|
|
420
|
+
Returns:
|
|
421
|
+
List of FileDownloadResponse objects, one per input path.
|
|
422
|
+
Response order matches input order.
|
|
423
|
+
"""
|
|
424
|
+
store = self._get_store()
|
|
425
|
+
namespace = self._get_namespace()
|
|
426
|
+
responses: list[FileDownloadResponse] = []
|
|
427
|
+
|
|
428
|
+
for path in paths:
|
|
429
|
+
item = store.get(namespace, path)
|
|
430
|
+
|
|
431
|
+
if item is None:
|
|
432
|
+
responses.append(FileDownloadResponse(path=path, content=None, error="file_not_found"))
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
file_data = self._convert_store_item_to_file_data(item)
|
|
436
|
+
# Convert file data to bytes
|
|
437
|
+
content_str = file_data_to_string(file_data)
|
|
438
|
+
content_bytes = content_str.encode("utf-8")
|
|
439
|
+
|
|
440
|
+
responses.append(FileDownloadResponse(path=path, content=content_bytes, error=None))
|
|
441
|
+
|
|
442
|
+
return responses
|
deepagents/graph.py
CHANGED
|
@@ -98,6 +98,18 @@ def create_deep_agent(
|
|
|
98
98
|
if model is None:
|
|
99
99
|
model = get_default_model()
|
|
100
100
|
|
|
101
|
+
if (
|
|
102
|
+
model.profile is not None
|
|
103
|
+
and isinstance(model.profile, dict)
|
|
104
|
+
and "max_input_tokens" in model.profile
|
|
105
|
+
and isinstance(model.profile["max_input_tokens"], int)
|
|
106
|
+
):
|
|
107
|
+
trigger = ("fraction", 0.85)
|
|
108
|
+
keep = ("fraction", 0.10)
|
|
109
|
+
else:
|
|
110
|
+
trigger = ("tokens", 170000)
|
|
111
|
+
keep = ("messages", 6)
|
|
112
|
+
|
|
101
113
|
deepagent_middleware = [
|
|
102
114
|
TodoListMiddleware(),
|
|
103
115
|
FilesystemMiddleware(backend=backend),
|
|
@@ -110,8 +122,9 @@ def create_deep_agent(
|
|
|
110
122
|
FilesystemMiddleware(backend=backend),
|
|
111
123
|
SummarizationMiddleware(
|
|
112
124
|
model=model,
|
|
113
|
-
|
|
114
|
-
|
|
125
|
+
trigger=trigger,
|
|
126
|
+
keep=keep,
|
|
127
|
+
trim_tokens_to_summarize=None,
|
|
115
128
|
),
|
|
116
129
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
117
130
|
PatchToolCallsMiddleware(),
|
|
@@ -121,8 +134,9 @@ def create_deep_agent(
|
|
|
121
134
|
),
|
|
122
135
|
SummarizationMiddleware(
|
|
123
136
|
model=model,
|
|
124
|
-
|
|
125
|
-
|
|
137
|
+
trigger=trigger,
|
|
138
|
+
keep=keep,
|
|
139
|
+
trim_tokens_to_summarize=None,
|
|
126
140
|
),
|
|
127
141
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
128
142
|
PatchToolCallsMiddleware(),
|