deepagents 0.3.6__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 +1 -1
- deepagents/graph.py +46 -12
- deepagents/middleware/__init__.py +3 -1
- deepagents/middleware/_utils.py +23 -0
- deepagents/middleware/filesystem.py +205 -84
- deepagents/middleware/memory.py +17 -15
- deepagents/middleware/skills.py +10 -9
- deepagents/middleware/subagents.py +51 -24
- deepagents/middleware/summarization.py +758 -0
- {deepagents-0.3.6.dist-info → deepagents-0.3.7.dist-info}/METADATA +5 -3
- deepagents-0.3.7.dist-info/RECORD +22 -0
- {deepagents-0.3.6.dist-info → deepagents-0.3.7.dist-info}/WHEEL +1 -1
- deepagents-0.3.6.dist-info/RECORD +0 -20
- {deepagents-0.3.6.dist-info → deepagents-0.3.7.dist-info}/top_level.txt +0 -0
|
@@ -33,12 +33,12 @@ from deepagents.backends.utils import (
|
|
|
33
33
|
sanitize_tool_call_id,
|
|
34
34
|
truncate_if_too_long,
|
|
35
35
|
)
|
|
36
|
+
from deepagents.middleware._utils import append_to_system_message
|
|
36
37
|
|
|
37
38
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
38
|
-
MAX_LINE_LENGTH = 2000
|
|
39
39
|
LINE_NUMBER_WIDTH = 6
|
|
40
40
|
DEFAULT_READ_OFFSET = 0
|
|
41
|
-
DEFAULT_READ_LIMIT =
|
|
41
|
+
DEFAULT_READ_LIMIT = 100
|
|
42
42
|
|
|
43
43
|
|
|
44
44
|
class FileData(TypedDict):
|
|
@@ -154,27 +154,24 @@ class FilesystemState(AgentState):
|
|
|
154
154
|
"""Files in the filesystem."""
|
|
155
155
|
|
|
156
156
|
|
|
157
|
-
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in
|
|
157
|
+
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in a directory.
|
|
158
158
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- You should almost ALWAYS use this tool before using the Read or Edit tools."""
|
|
159
|
+
This is useful for exploring the filesystem and finding the right file to read or edit.
|
|
160
|
+
You should almost ALWAYS use this tool before using the read_file or edit_file tools."""
|
|
161
|
+
|
|
162
|
+
READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem.
|
|
164
163
|
|
|
165
|
-
|
|
166
|
-
Assume this tool is able to read all files on the machine. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
164
|
+
Assume this tool is able to read all files. If the User provides a path to a file assume that path is valid. It is okay to read a file that does not exist; an error will be returned.
|
|
167
165
|
|
|
168
166
|
Usage:
|
|
169
|
-
-
|
|
170
|
-
- By default, it reads up to 500 lines starting from the beginning of the file
|
|
167
|
+
- By default, it reads up to 100 lines starting from the beginning of the file
|
|
171
168
|
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
172
169
|
- First scan: read_file(path, limit=100) to see file structure
|
|
173
170
|
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
174
171
|
- Only omit limit (read full file) when necessary for editing
|
|
175
172
|
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
176
|
-
- Any lines longer than 2000 characters will be truncated
|
|
177
173
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
174
|
+
- Lines longer than 5,000 characters will be split into multiple lines with continuation markers (e.g., 5.1, 5.2, etc.). When you specify a limit, these continuation lines count towards the limit.
|
|
178
175
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
179
176
|
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
180
177
|
- You should ALWAYS make sure a file has been read before editing it."""
|
|
@@ -182,61 +179,46 @@ Usage:
|
|
|
182
179
|
EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
|
|
183
180
|
|
|
184
181
|
Usage:
|
|
185
|
-
- You must
|
|
186
|
-
- When editing
|
|
187
|
-
- ALWAYS prefer editing existing files
|
|
188
|
-
- Only use emojis if the user explicitly requests it.
|
|
189
|
-
- The edit will FAIL if `old_string` is not unique in the file. Either provide a larger string with more surrounding context to make it unique or use `replace_all` to change every instance of `old_string`.
|
|
190
|
-
- Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."""
|
|
182
|
+
- You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
|
|
183
|
+
- When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
|
|
184
|
+
- ALWAYS prefer editing existing files over creating new ones.
|
|
185
|
+
- Only use emojis if the user explicitly requests it."""
|
|
191
186
|
|
|
192
187
|
|
|
193
188
|
WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
|
|
194
189
|
|
|
195
190
|
Usage:
|
|
196
|
-
- The file_path parameter must be an absolute path, not a relative path
|
|
197
|
-
- The content parameter must be a string
|
|
198
191
|
- The write_file tool will create the a new file.
|
|
199
|
-
- Prefer to edit existing files over creating new ones when possible.
|
|
200
|
-
|
|
192
|
+
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
|
|
193
|
+
"""
|
|
201
194
|
|
|
202
195
|
GLOB_TOOL_DESCRIPTION = """Find files matching a glob pattern.
|
|
203
196
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
- Supports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character)
|
|
207
|
-
- Patterns can be absolute (starting with `/`) or relative
|
|
208
|
-
- Returns a list of absolute file paths that match the pattern
|
|
197
|
+
Supports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).
|
|
198
|
+
Returns a list of absolute file paths that match the pattern.
|
|
209
199
|
|
|
210
200
|
Examples:
|
|
211
201
|
- `**/*.py` - Find all Python files
|
|
212
202
|
- `*.txt` - Find all text files in root
|
|
213
203
|
- `/subdir/**/*.md` - Find all markdown files under /subdir"""
|
|
214
204
|
|
|
215
|
-
GREP_TOOL_DESCRIPTION = """Search for a pattern
|
|
205
|
+
GREP_TOOL_DESCRIPTION = """Search for a text pattern across files.
|
|
216
206
|
|
|
217
|
-
|
|
218
|
-
- The grep tool searches for text patterns across files
|
|
219
|
-
- The pattern parameter is the text to search for (literal string, not regex)
|
|
220
|
-
- The path parameter filters which directory to search in (default is the current working directory)
|
|
221
|
-
- The glob parameter accepts a glob pattern to filter which files to search (e.g., `*.py`)
|
|
222
|
-
- The output_mode parameter controls the output format:
|
|
223
|
-
- `files_with_matches`: List only file paths containing matches (default)
|
|
224
|
-
- `content`: Show matching lines with file path and line numbers
|
|
225
|
-
- `count`: Show count of matches per file
|
|
207
|
+
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
226
208
|
|
|
227
209
|
Examples:
|
|
228
210
|
- Search all files: `grep(pattern="TODO")`
|
|
229
211
|
- Search Python files only: `grep(pattern="import", glob="*.py")`
|
|
230
212
|
- Show matching lines: `grep(pattern="error", output_mode="content")`"""
|
|
231
213
|
|
|
232
|
-
EXECUTE_TOOL_DESCRIPTION = """Executes a
|
|
214
|
+
EXECUTE_TOOL_DESCRIPTION = """Executes a shell command in an isolated sandbox environment.
|
|
233
215
|
|
|
216
|
+
Usage:
|
|
217
|
+
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
234
218
|
Before executing the command, please follow these steps:
|
|
235
|
-
|
|
236
219
|
1. Directory Verification:
|
|
237
220
|
- If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location
|
|
238
221
|
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
|
239
|
-
|
|
240
222
|
2. Command Execution:
|
|
241
223
|
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
|
242
224
|
- Examples of proper quoting:
|
|
@@ -246,9 +228,7 @@ Before executing the command, please follow these steps:
|
|
|
246
228
|
- python /path/with spaces/script.py (incorrect - will fail)
|
|
247
229
|
- After ensuring proper quoting, execute the command
|
|
248
230
|
- Capture the output of the command
|
|
249
|
-
|
|
250
231
|
Usage notes:
|
|
251
|
-
- The command parameter is required
|
|
252
232
|
- Commands run in an isolated sandbox environment
|
|
253
233
|
- Returns combined stdout/stderr output with exit code
|
|
254
234
|
- If the output is very large, it may be truncated
|
|
@@ -323,7 +303,10 @@ def _ls_tool_generator(
|
|
|
323
303
|
"""
|
|
324
304
|
tool_description = custom_description or LIST_FILES_TOOL_DESCRIPTION
|
|
325
305
|
|
|
326
|
-
def sync_ls(
|
|
306
|
+
def sync_ls(
|
|
307
|
+
runtime: ToolRuntime[None, FilesystemState],
|
|
308
|
+
path: Annotated[str, "Absolute path to the directory to list. Must be absolute, not relative."],
|
|
309
|
+
) -> str:
|
|
327
310
|
"""Synchronous wrapper for ls tool."""
|
|
328
311
|
resolved_backend = _get_backend(backend, runtime)
|
|
329
312
|
validated_path = _validate_path(path)
|
|
@@ -332,7 +315,10 @@ def _ls_tool_generator(
|
|
|
332
315
|
result = truncate_if_too_long(paths)
|
|
333
316
|
return str(result)
|
|
334
317
|
|
|
335
|
-
async def async_ls(
|
|
318
|
+
async def async_ls(
|
|
319
|
+
runtime: ToolRuntime[None, FilesystemState],
|
|
320
|
+
path: Annotated[str, "Absolute path to the directory to list. Must be absolute, not relative."],
|
|
321
|
+
) -> str:
|
|
336
322
|
"""Asynchronous wrapper for ls tool."""
|
|
337
323
|
resolved_backend = _get_backend(backend, runtime)
|
|
338
324
|
validated_path = _validate_path(path)
|
|
@@ -365,26 +351,40 @@ def _read_file_tool_generator(
|
|
|
365
351
|
tool_description = custom_description or READ_FILE_TOOL_DESCRIPTION
|
|
366
352
|
|
|
367
353
|
def sync_read_file(
|
|
368
|
-
file_path: str,
|
|
354
|
+
file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
|
|
369
355
|
runtime: ToolRuntime[None, FilesystemState],
|
|
370
|
-
offset: int = DEFAULT_READ_OFFSET,
|
|
371
|
-
limit: int = DEFAULT_READ_LIMIT,
|
|
356
|
+
offset: Annotated[int, "Line number to start reading from (0-indexed). Use for pagination of large files."] = DEFAULT_READ_OFFSET,
|
|
357
|
+
limit: Annotated[int, "Maximum number of lines to read. Use for pagination of large files."] = DEFAULT_READ_LIMIT,
|
|
372
358
|
) -> str:
|
|
373
359
|
"""Synchronous wrapper for read_file tool."""
|
|
374
360
|
resolved_backend = _get_backend(backend, runtime)
|
|
375
361
|
file_path = _validate_path(file_path)
|
|
376
|
-
|
|
362
|
+
result = resolved_backend.read(file_path, offset=offset, limit=limit)
|
|
363
|
+
|
|
364
|
+
lines = result.splitlines(keepends=True)
|
|
365
|
+
if len(lines) > limit:
|
|
366
|
+
lines = lines[:limit]
|
|
367
|
+
result = "".join(lines)
|
|
368
|
+
|
|
369
|
+
return result
|
|
377
370
|
|
|
378
371
|
async def async_read_file(
|
|
379
|
-
file_path: str,
|
|
372
|
+
file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
|
|
380
373
|
runtime: ToolRuntime[None, FilesystemState],
|
|
381
|
-
offset: int = DEFAULT_READ_OFFSET,
|
|
382
|
-
limit: int = DEFAULT_READ_LIMIT,
|
|
374
|
+
offset: Annotated[int, "Line number to start reading from (0-indexed). Use for pagination of large files."] = DEFAULT_READ_OFFSET,
|
|
375
|
+
limit: Annotated[int, "Maximum number of lines to read. Use for pagination of large files."] = DEFAULT_READ_LIMIT,
|
|
383
376
|
) -> str:
|
|
384
377
|
"""Asynchronous wrapper for read_file tool."""
|
|
385
378
|
resolved_backend = _get_backend(backend, runtime)
|
|
386
379
|
file_path = _validate_path(file_path)
|
|
387
|
-
|
|
380
|
+
result = await resolved_backend.aread(file_path, offset=offset, limit=limit)
|
|
381
|
+
|
|
382
|
+
lines = result.splitlines(keepends=True)
|
|
383
|
+
if len(lines) > limit:
|
|
384
|
+
lines = lines[:limit]
|
|
385
|
+
result = "".join(lines)
|
|
386
|
+
|
|
387
|
+
return result
|
|
388
388
|
|
|
389
389
|
return StructuredTool.from_function(
|
|
390
390
|
name="read_file",
|
|
@@ -410,8 +410,8 @@ def _write_file_tool_generator(
|
|
|
410
410
|
tool_description = custom_description or WRITE_FILE_TOOL_DESCRIPTION
|
|
411
411
|
|
|
412
412
|
def sync_write_file(
|
|
413
|
-
file_path: str,
|
|
414
|
-
content: str,
|
|
413
|
+
file_path: Annotated[str, "Absolute path where the file should be created. Must be absolute, not relative."],
|
|
414
|
+
content: Annotated[str, "The text content to write to the file. This parameter is required."],
|
|
415
415
|
runtime: ToolRuntime[None, FilesystemState],
|
|
416
416
|
) -> Command | str:
|
|
417
417
|
"""Synchronous wrapper for write_file tool."""
|
|
@@ -436,8 +436,8 @@ def _write_file_tool_generator(
|
|
|
436
436
|
return f"Updated file {res.path}"
|
|
437
437
|
|
|
438
438
|
async def async_write_file(
|
|
439
|
-
file_path: str,
|
|
440
|
-
content: str,
|
|
439
|
+
file_path: Annotated[str, "Absolute path where the file should be created. Must be absolute, not relative."],
|
|
440
|
+
content: Annotated[str, "The text content to write to the file. This parameter is required."],
|
|
441
441
|
runtime: ToolRuntime[None, FilesystemState],
|
|
442
442
|
) -> Command | str:
|
|
443
443
|
"""Asynchronous wrapper for write_file tool."""
|
|
@@ -485,12 +485,12 @@ def _edit_file_tool_generator(
|
|
|
485
485
|
tool_description = custom_description or EDIT_FILE_TOOL_DESCRIPTION
|
|
486
486
|
|
|
487
487
|
def sync_edit_file(
|
|
488
|
-
file_path: str,
|
|
489
|
-
old_string: str,
|
|
490
|
-
new_string: str,
|
|
488
|
+
file_path: Annotated[str, "Absolute path to the file to edit. Must be absolute, not relative."],
|
|
489
|
+
old_string: Annotated[str, "The exact text to find and replace. Must be unique in the file unless replace_all is True."],
|
|
490
|
+
new_string: Annotated[str, "The text to replace old_string with. Must be different from old_string."],
|
|
491
491
|
runtime: ToolRuntime[None, FilesystemState],
|
|
492
492
|
*,
|
|
493
|
-
replace_all: bool = False,
|
|
493
|
+
replace_all: Annotated[bool, "If True, replace all occurrences of old_string. If False (default), old_string must be unique."] = False,
|
|
494
494
|
) -> Command | str:
|
|
495
495
|
"""Synchronous wrapper for edit_file tool."""
|
|
496
496
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -513,12 +513,12 @@ def _edit_file_tool_generator(
|
|
|
513
513
|
return f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'"
|
|
514
514
|
|
|
515
515
|
async def async_edit_file(
|
|
516
|
-
file_path: str,
|
|
517
|
-
old_string: str,
|
|
518
|
-
new_string: str,
|
|
516
|
+
file_path: Annotated[str, "Absolute path to the file to edit. Must be absolute, not relative."],
|
|
517
|
+
old_string: Annotated[str, "The exact text to find and replace. Must be unique in the file unless replace_all is True."],
|
|
518
|
+
new_string: Annotated[str, "The text to replace old_string with. Must be different from old_string."],
|
|
519
519
|
runtime: ToolRuntime[None, FilesystemState],
|
|
520
520
|
*,
|
|
521
|
-
replace_all: bool = False,
|
|
521
|
+
replace_all: Annotated[bool, "If True, replace all occurrences of old_string. If False (default), old_string must be unique."] = False,
|
|
522
522
|
) -> Command | str:
|
|
523
523
|
"""Asynchronous wrapper for edit_file tool."""
|
|
524
524
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -563,7 +563,11 @@ def _glob_tool_generator(
|
|
|
563
563
|
"""
|
|
564
564
|
tool_description = custom_description or GLOB_TOOL_DESCRIPTION
|
|
565
565
|
|
|
566
|
-
def sync_glob(
|
|
566
|
+
def sync_glob(
|
|
567
|
+
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
|
|
568
|
+
runtime: ToolRuntime[None, FilesystemState],
|
|
569
|
+
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
|
|
570
|
+
) -> str:
|
|
567
571
|
"""Synchronous wrapper for glob tool."""
|
|
568
572
|
resolved_backend = _get_backend(backend, runtime)
|
|
569
573
|
infos = resolved_backend.glob_info(pattern, path=path)
|
|
@@ -571,7 +575,11 @@ def _glob_tool_generator(
|
|
|
571
575
|
result = truncate_if_too_long(paths)
|
|
572
576
|
return str(result)
|
|
573
577
|
|
|
574
|
-
async def async_glob(
|
|
578
|
+
async def async_glob(
|
|
579
|
+
pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
|
|
580
|
+
runtime: ToolRuntime[None, FilesystemState],
|
|
581
|
+
path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
|
|
582
|
+
) -> str:
|
|
575
583
|
"""Asynchronous wrapper for glob tool."""
|
|
576
584
|
resolved_backend = _get_backend(backend, runtime)
|
|
577
585
|
infos = await resolved_backend.aglob_info(pattern, path=path)
|
|
@@ -603,11 +611,14 @@ def _grep_tool_generator(
|
|
|
603
611
|
tool_description = custom_description or GREP_TOOL_DESCRIPTION
|
|
604
612
|
|
|
605
613
|
def sync_grep(
|
|
606
|
-
pattern: str,
|
|
614
|
+
pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
|
|
607
615
|
runtime: ToolRuntime[None, FilesystemState],
|
|
608
|
-
path: str | None = None,
|
|
609
|
-
glob: str | None = None,
|
|
610
|
-
output_mode:
|
|
616
|
+
path: Annotated[str | None, "Directory to search in. Defaults to current working directory."] = None,
|
|
617
|
+
glob: Annotated[str | None, "Glob pattern to filter which files to search (e.g., '*.py')."] = None,
|
|
618
|
+
output_mode: Annotated[
|
|
619
|
+
Literal["files_with_matches", "content", "count"],
|
|
620
|
+
"Output format: 'files_with_matches' (file paths only, default), 'content' (matching lines with context), 'count' (match counts per file).",
|
|
621
|
+
] = "files_with_matches",
|
|
611
622
|
) -> str:
|
|
612
623
|
"""Synchronous wrapper for grep tool."""
|
|
613
624
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -618,11 +629,14 @@ def _grep_tool_generator(
|
|
|
618
629
|
return truncate_if_too_long(formatted) # type: ignore[arg-type]
|
|
619
630
|
|
|
620
631
|
async def async_grep(
|
|
621
|
-
pattern: str,
|
|
632
|
+
pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
|
|
622
633
|
runtime: ToolRuntime[None, FilesystemState],
|
|
623
|
-
path: str | None = None,
|
|
624
|
-
glob: str | None = None,
|
|
625
|
-
output_mode:
|
|
634
|
+
path: Annotated[str | None, "Directory to search in. Defaults to current working directory."] = None,
|
|
635
|
+
glob: Annotated[str | None, "Glob pattern to filter which files to search (e.g., '*.py')."] = None,
|
|
636
|
+
output_mode: Annotated[
|
|
637
|
+
Literal["files_with_matches", "content", "count"],
|
|
638
|
+
"Output format: 'files_with_matches' (file paths only, default), 'content' (matching lines with context), 'count' (match counts per file).",
|
|
639
|
+
] = "files_with_matches",
|
|
626
640
|
) -> str:
|
|
627
641
|
"""Asynchronous wrapper for grep tool."""
|
|
628
642
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -679,7 +693,7 @@ def _execute_tool_generator(
|
|
|
679
693
|
tool_description = custom_description or EXECUTE_TOOL_DESCRIPTION
|
|
680
694
|
|
|
681
695
|
def sync_execute(
|
|
682
|
-
command: str,
|
|
696
|
+
command: Annotated[str, "Shell command to execute in the sandbox environment."],
|
|
683
697
|
runtime: ToolRuntime[None, FilesystemState],
|
|
684
698
|
) -> str:
|
|
685
699
|
"""Synchronous wrapper for execute tool."""
|
|
@@ -712,7 +726,7 @@ def _execute_tool_generator(
|
|
|
712
726
|
return "".join(parts)
|
|
713
727
|
|
|
714
728
|
async def async_execute(
|
|
715
|
-
command: str,
|
|
729
|
+
command: Annotated[str, "Shell command to execute in the sandbox environment."],
|
|
716
730
|
runtime: ToolRuntime[None, FilesystemState],
|
|
717
731
|
) -> str:
|
|
718
732
|
"""Asynchronous wrapper for execute tool."""
|
|
@@ -800,8 +814,9 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
800
814
|
"""Middleware for providing filesystem and optional execution tools to an agent.
|
|
801
815
|
|
|
802
816
|
This middleware adds filesystem tools to the agent: `ls`, `read_file`, `write_file`,
|
|
803
|
-
`edit_file`, `glob`, and `grep`.
|
|
804
|
-
|
|
817
|
+
`edit_file`, `glob`, and `grep`.
|
|
818
|
+
|
|
819
|
+
Files can be stored using any backend that implements the `BackendProtocol`.
|
|
805
820
|
|
|
806
821
|
If the backend implements `SandboxBackendProtocol`, an `execute` tool is also added
|
|
807
822
|
for running shell commands.
|
|
@@ -822,8 +837,6 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
822
837
|
tool_token_limit_before_evict: Token limit before evicting a tool result to the
|
|
823
838
|
filesystem.
|
|
824
839
|
|
|
825
|
-
Defaults to 20,000 tokens.
|
|
826
|
-
|
|
827
840
|
When exceeded, writes the result using the configured backend and replaces it
|
|
828
841
|
with a truncated preview and file reference.
|
|
829
842
|
|
|
@@ -933,7 +946,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
933
946
|
system_prompt = "\n\n".join(prompt_parts)
|
|
934
947
|
|
|
935
948
|
if system_prompt:
|
|
936
|
-
|
|
949
|
+
new_system_message = append_to_system_message(request.system_message, system_prompt)
|
|
950
|
+
request = request.override(system_message=new_system_message)
|
|
937
951
|
|
|
938
952
|
return handler(request)
|
|
939
953
|
|
|
@@ -980,7 +994,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
980
994
|
system_prompt = "\n\n".join(prompt_parts)
|
|
981
995
|
|
|
982
996
|
if system_prompt:
|
|
983
|
-
|
|
997
|
+
new_system_message = append_to_system_message(request.system_message, system_prompt)
|
|
998
|
+
request = request.override(system_message=new_system_message)
|
|
984
999
|
|
|
985
1000
|
return await handler(request)
|
|
986
1001
|
|
|
@@ -1054,6 +1069,66 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1054
1069
|
processed_message = ToolMessage(
|
|
1055
1070
|
content=replacement_text,
|
|
1056
1071
|
tool_call_id=message.tool_call_id,
|
|
1072
|
+
name=message.name,
|
|
1073
|
+
)
|
|
1074
|
+
return processed_message, result.files_update
|
|
1075
|
+
|
|
1076
|
+
async def _aprocess_large_message(
|
|
1077
|
+
self,
|
|
1078
|
+
message: ToolMessage,
|
|
1079
|
+
resolved_backend: BackendProtocol,
|
|
1080
|
+
) -> tuple[ToolMessage, dict[str, FileData] | None]:
|
|
1081
|
+
"""Async version of _process_large_message.
|
|
1082
|
+
|
|
1083
|
+
Uses async backend methods to avoid sync calls in async context.
|
|
1084
|
+
See _process_large_message for full documentation.
|
|
1085
|
+
"""
|
|
1086
|
+
# Early exit if eviction not configured
|
|
1087
|
+
if not self.tool_token_limit_before_evict:
|
|
1088
|
+
return message, None
|
|
1089
|
+
|
|
1090
|
+
# Convert content to string once for both size check and eviction
|
|
1091
|
+
# Special case: single text block - extract text directly for readability
|
|
1092
|
+
if (
|
|
1093
|
+
isinstance(message.content, list)
|
|
1094
|
+
and len(message.content) == 1
|
|
1095
|
+
and isinstance(message.content[0], dict)
|
|
1096
|
+
and message.content[0].get("type") == "text"
|
|
1097
|
+
and "text" in message.content[0]
|
|
1098
|
+
):
|
|
1099
|
+
content_str = str(message.content[0]["text"])
|
|
1100
|
+
elif isinstance(message.content, str):
|
|
1101
|
+
content_str = message.content
|
|
1102
|
+
else:
|
|
1103
|
+
# Multiple blocks or non-text content - stringify entire structure
|
|
1104
|
+
content_str = str(message.content)
|
|
1105
|
+
|
|
1106
|
+
# Check if content exceeds eviction threshold
|
|
1107
|
+
# Using 4 chars per token as a conservative approximation (actual ratio varies by content)
|
|
1108
|
+
# This errs on the high side to avoid premature eviction of content that might fit
|
|
1109
|
+
if len(content_str) <= 4 * self.tool_token_limit_before_evict:
|
|
1110
|
+
return message, None
|
|
1111
|
+
|
|
1112
|
+
# Write content to filesystem using async method
|
|
1113
|
+
sanitized_id = sanitize_tool_call_id(message.tool_call_id)
|
|
1114
|
+
file_path = f"/large_tool_results/{sanitized_id}"
|
|
1115
|
+
result = await resolved_backend.awrite(file_path, content_str)
|
|
1116
|
+
if result.error:
|
|
1117
|
+
return message, None
|
|
1118
|
+
|
|
1119
|
+
# Create truncated preview for the replacement message
|
|
1120
|
+
content_sample = format_content_with_line_numbers([line[:1000] for line in content_str.splitlines()[:10]], start_line=1)
|
|
1121
|
+
replacement_text = TOO_LARGE_TOOL_MSG.format(
|
|
1122
|
+
tool_call_id=message.tool_call_id,
|
|
1123
|
+
file_path=file_path,
|
|
1124
|
+
content_sample=content_sample,
|
|
1125
|
+
)
|
|
1126
|
+
|
|
1127
|
+
# Always return as plain string after eviction
|
|
1128
|
+
processed_message = ToolMessage(
|
|
1129
|
+
content=replacement_text,
|
|
1130
|
+
tool_call_id=message.tool_call_id,
|
|
1131
|
+
name=message.name,
|
|
1057
1132
|
)
|
|
1058
1133
|
return processed_message, result.files_update
|
|
1059
1134
|
|
|
@@ -1113,6 +1188,52 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1113
1188
|
return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
|
|
1114
1189
|
raise AssertionError(f"Unreachable code reached in _intercept_large_tool_result: for tool_result of type {type(tool_result)}")
|
|
1115
1190
|
|
|
1191
|
+
async def _aintercept_large_tool_result(self, tool_result: ToolMessage | Command, runtime: ToolRuntime) -> ToolMessage | Command:
|
|
1192
|
+
"""Async version of _intercept_large_tool_result.
|
|
1193
|
+
|
|
1194
|
+
Uses async backend methods to avoid sync calls in async context.
|
|
1195
|
+
See _intercept_large_tool_result for full documentation.
|
|
1196
|
+
"""
|
|
1197
|
+
if isinstance(tool_result, ToolMessage):
|
|
1198
|
+
resolved_backend = self._get_backend(runtime)
|
|
1199
|
+
processed_message, files_update = await self._aprocess_large_message(
|
|
1200
|
+
tool_result,
|
|
1201
|
+
resolved_backend,
|
|
1202
|
+
)
|
|
1203
|
+
return (
|
|
1204
|
+
Command(
|
|
1205
|
+
update={
|
|
1206
|
+
"files": files_update,
|
|
1207
|
+
"messages": [processed_message],
|
|
1208
|
+
}
|
|
1209
|
+
)
|
|
1210
|
+
if files_update is not None
|
|
1211
|
+
else processed_message
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
if isinstance(tool_result, Command):
|
|
1215
|
+
update = tool_result.update
|
|
1216
|
+
if update is None:
|
|
1217
|
+
return tool_result
|
|
1218
|
+
command_messages = update.get("messages", [])
|
|
1219
|
+
accumulated_file_updates = dict(update.get("files", {}))
|
|
1220
|
+
resolved_backend = self._get_backend(runtime)
|
|
1221
|
+
processed_messages = []
|
|
1222
|
+
for message in command_messages:
|
|
1223
|
+
if not isinstance(message, ToolMessage):
|
|
1224
|
+
processed_messages.append(message)
|
|
1225
|
+
continue
|
|
1226
|
+
|
|
1227
|
+
processed_message, files_update = await self._aprocess_large_message(
|
|
1228
|
+
message,
|
|
1229
|
+
resolved_backend,
|
|
1230
|
+
)
|
|
1231
|
+
processed_messages.append(processed_message)
|
|
1232
|
+
if files_update is not None:
|
|
1233
|
+
accumulated_file_updates.update(files_update)
|
|
1234
|
+
return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
|
|
1235
|
+
raise AssertionError(f"Unreachable code reached in _aintercept_large_tool_result: for tool_result of type {type(tool_result)}")
|
|
1236
|
+
|
|
1116
1237
|
def wrap_tool_call(
|
|
1117
1238
|
self,
|
|
1118
1239
|
request: ToolCallRequest,
|
|
@@ -1151,4 +1272,4 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1151
1272
|
return await handler(request)
|
|
1152
1273
|
|
|
1153
1274
|
tool_result = await handler(request)
|
|
1154
|
-
return self.
|
|
1275
|
+
return await self._aintercept_large_tool_result(tool_result, request.runtime)
|
deepagents/middleware/memory.py
CHANGED
|
@@ -53,7 +53,6 @@ import logging
|
|
|
53
53
|
from collections.abc import Awaitable, Callable
|
|
54
54
|
from typing import TYPE_CHECKING, Annotated, NotRequired, TypedDict
|
|
55
55
|
|
|
56
|
-
from langchain.messages import SystemMessage
|
|
57
56
|
from langchain_core.runnables import RunnableConfig
|
|
58
57
|
|
|
59
58
|
if TYPE_CHECKING:
|
|
@@ -69,11 +68,13 @@ from langchain.agents.middleware.types import (
|
|
|
69
68
|
from langchain.tools import ToolRuntime
|
|
70
69
|
from langgraph.runtime import Runtime
|
|
71
70
|
|
|
71
|
+
from deepagents.middleware._utils import append_to_system_message
|
|
72
|
+
|
|
72
73
|
logger = logging.getLogger(__name__)
|
|
73
74
|
|
|
74
75
|
|
|
75
76
|
class MemoryState(AgentState):
|
|
76
|
-
"""State schema for MemoryMiddleware
|
|
77
|
+
"""State schema for `MemoryMiddleware`.
|
|
77
78
|
|
|
78
79
|
Attributes:
|
|
79
80
|
memory_contents: Dict mapping source paths to their loaded content.
|
|
@@ -84,7 +85,7 @@ class MemoryState(AgentState):
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
class MemoryStateUpdate(TypedDict):
|
|
87
|
-
"""State update for MemoryMiddleware
|
|
88
|
+
"""State update for `MemoryMiddleware`."""
|
|
88
89
|
|
|
89
90
|
memory_contents: dict[str, str]
|
|
90
91
|
|
|
@@ -152,14 +153,15 @@ MEMORY_SYSTEM_PROMPT = """<agent_memory>
|
|
|
152
153
|
|
|
153
154
|
|
|
154
155
|
class MemoryMiddleware(AgentMiddleware):
|
|
155
|
-
"""Middleware for loading agent memory from AGENTS.md files.
|
|
156
|
+
"""Middleware for loading agent memory from `AGENTS.md` files.
|
|
156
157
|
|
|
157
158
|
Loads memory content from configured sources and injects into the system prompt.
|
|
159
|
+
|
|
158
160
|
Supports multiple sources that are combined together.
|
|
159
161
|
|
|
160
162
|
Args:
|
|
161
163
|
backend: Backend instance or factory function for file operations.
|
|
162
|
-
sources: List of MemorySource configurations specifying paths and names.
|
|
164
|
+
sources: List of `MemorySource` configurations specifying paths and names.
|
|
163
165
|
"""
|
|
164
166
|
|
|
165
167
|
state_schema = MemoryState
|
|
@@ -175,9 +177,12 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
175
177
|
Args:
|
|
176
178
|
backend: Backend instance or factory function that takes runtime
|
|
177
179
|
and returns a backend. Use a factory for StateBackend.
|
|
178
|
-
sources: List of memory file paths to load (e.g., ["~/.deepagents/AGENTS.md",
|
|
179
|
-
"./.deepagents/AGENTS.md"]).
|
|
180
|
-
|
|
180
|
+
sources: List of memory file paths to load (e.g., `["~/.deepagents/AGENTS.md",
|
|
181
|
+
"./.deepagents/AGENTS.md"]`).
|
|
182
|
+
|
|
183
|
+
Display names are automatically derived from the paths.
|
|
184
|
+
|
|
185
|
+
Sources are loaded in order.
|
|
181
186
|
"""
|
|
182
187
|
self._backend = backend
|
|
183
188
|
self.sources = sources
|
|
@@ -354,23 +359,20 @@ class MemoryMiddleware(AgentMiddleware):
|
|
|
354
359
|
return MemoryStateUpdate(memory_contents=contents)
|
|
355
360
|
|
|
356
361
|
def modify_request(self, request: ModelRequest) -> ModelRequest:
|
|
357
|
-
"""Inject memory content into the system
|
|
362
|
+
"""Inject memory content into the system message.
|
|
358
363
|
|
|
359
364
|
Args:
|
|
360
365
|
request: Model request to modify.
|
|
361
366
|
|
|
362
367
|
Returns:
|
|
363
|
-
Modified request with memory injected into system
|
|
368
|
+
Modified request with memory injected into system message.
|
|
364
369
|
"""
|
|
365
370
|
contents = request.state.get("memory_contents", {})
|
|
366
371
|
agent_memory = self._format_agent_memory(contents)
|
|
367
372
|
|
|
368
|
-
|
|
369
|
-
system_prompt = agent_memory + "\n\n" + request.system_prompt
|
|
370
|
-
else:
|
|
371
|
-
system_prompt = agent_memory
|
|
373
|
+
new_system_message = append_to_system_message(request.system_message, agent_memory)
|
|
372
374
|
|
|
373
|
-
return request.override(system_message=
|
|
375
|
+
return request.override(system_message=new_system_message)
|
|
374
376
|
|
|
375
377
|
def wrap_model_call(
|
|
376
378
|
self,
|