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
|
@@ -20,10 +20,8 @@ from langgraph.types import Command
|
|
|
20
20
|
from typing_extensions import TypedDict
|
|
21
21
|
|
|
22
22
|
from deepagents.backends import StateBackend
|
|
23
|
-
|
|
24
|
-
# Re-export type here for backwards compatibility
|
|
25
|
-
from deepagents.backends.protocol import BACKEND_TYPES as BACKEND_TYPES
|
|
26
23
|
from deepagents.backends.protocol import (
|
|
24
|
+
BACKEND_TYPES as BACKEND_TYPES, # Re-export for backwards compatibility
|
|
27
25
|
BackendProtocol,
|
|
28
26
|
EditResult,
|
|
29
27
|
SandboxBackendProtocol,
|
|
@@ -35,12 +33,12 @@ from deepagents.backends.utils import (
|
|
|
35
33
|
sanitize_tool_call_id,
|
|
36
34
|
truncate_if_too_long,
|
|
37
35
|
)
|
|
36
|
+
from deepagents.middleware._utils import append_to_system_message
|
|
38
37
|
|
|
39
38
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
40
|
-
MAX_LINE_LENGTH = 2000
|
|
41
39
|
LINE_NUMBER_WIDTH = 6
|
|
42
40
|
DEFAULT_READ_OFFSET = 0
|
|
43
|
-
DEFAULT_READ_LIMIT =
|
|
41
|
+
DEFAULT_READ_LIMIT = 100
|
|
44
42
|
|
|
45
43
|
|
|
46
44
|
class FileData(TypedDict):
|
|
@@ -156,27 +154,24 @@ class FilesystemState(AgentState):
|
|
|
156
154
|
"""Files in the filesystem."""
|
|
157
155
|
|
|
158
156
|
|
|
159
|
-
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in
|
|
157
|
+
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in a directory.
|
|
160
158
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
- The list_files tool will return a list of all files in the specified directory.
|
|
164
|
-
- This is very useful for exploring the file system and finding the right file to read or edit.
|
|
165
|
-
- 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."""
|
|
166
161
|
|
|
167
|
-
READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem.
|
|
168
|
-
|
|
162
|
+
READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem.
|
|
163
|
+
|
|
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.
|
|
169
165
|
|
|
170
166
|
Usage:
|
|
171
|
-
-
|
|
172
|
-
- 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
|
|
173
168
|
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
174
169
|
- First scan: read_file(path, limit=100) to see file structure
|
|
175
170
|
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
176
171
|
- Only omit limit (read full file) when necessary for editing
|
|
177
172
|
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
178
|
-
- Any lines longer than 2000 characters will be truncated
|
|
179
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.
|
|
180
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.
|
|
181
176
|
- If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
|
|
182
177
|
- You should ALWAYS make sure a file has been read before editing it."""
|
|
@@ -184,61 +179,46 @@ Usage:
|
|
|
184
179
|
EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
|
|
185
180
|
|
|
186
181
|
Usage:
|
|
187
|
-
- You must
|
|
188
|
-
- When editing
|
|
189
|
-
- ALWAYS prefer editing existing files
|
|
190
|
-
- Only use emojis if the user explicitly requests it.
|
|
191
|
-
- 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`.
|
|
192
|
-
- 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."""
|
|
193
186
|
|
|
194
187
|
|
|
195
188
|
WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
|
|
196
189
|
|
|
197
190
|
Usage:
|
|
198
|
-
- The file_path parameter must be an absolute path, not a relative path
|
|
199
|
-
- The content parameter must be a string
|
|
200
191
|
- The write_file tool will create the a new file.
|
|
201
|
-
- Prefer to edit existing files over creating new ones when possible.
|
|
202
|
-
|
|
192
|
+
- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
|
|
193
|
+
"""
|
|
203
194
|
|
|
204
195
|
GLOB_TOOL_DESCRIPTION = """Find files matching a glob pattern.
|
|
205
196
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
- Supports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character)
|
|
209
|
-
- Patterns can be absolute (starting with `/`) or relative
|
|
210
|
-
- 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.
|
|
211
199
|
|
|
212
200
|
Examples:
|
|
213
201
|
- `**/*.py` - Find all Python files
|
|
214
202
|
- `*.txt` - Find all text files in root
|
|
215
203
|
- `/subdir/**/*.md` - Find all markdown files under /subdir"""
|
|
216
204
|
|
|
217
|
-
GREP_TOOL_DESCRIPTION = """Search for a pattern
|
|
205
|
+
GREP_TOOL_DESCRIPTION = """Search for a text pattern across files.
|
|
218
206
|
|
|
219
|
-
|
|
220
|
-
- The grep tool searches for text patterns across files
|
|
221
|
-
- The pattern parameter is the text to search for (literal string, not regex)
|
|
222
|
-
- The path parameter filters which directory to search in (default is the current working directory)
|
|
223
|
-
- The glob parameter accepts a glob pattern to filter which files to search (e.g., `*.py`)
|
|
224
|
-
- The output_mode parameter controls the output format:
|
|
225
|
-
- `files_with_matches`: List only file paths containing matches (default)
|
|
226
|
-
- `content`: Show matching lines with file path and line numbers
|
|
227
|
-
- `count`: Show count of matches per file
|
|
207
|
+
Searches for literal text (not regex) and returns matching files or content based on output_mode.
|
|
228
208
|
|
|
229
209
|
Examples:
|
|
230
210
|
- Search all files: `grep(pattern="TODO")`
|
|
231
211
|
- Search Python files only: `grep(pattern="import", glob="*.py")`
|
|
232
212
|
- Show matching lines: `grep(pattern="error", output_mode="content")`"""
|
|
233
213
|
|
|
234
|
-
EXECUTE_TOOL_DESCRIPTION = """Executes a
|
|
214
|
+
EXECUTE_TOOL_DESCRIPTION = """Executes a shell command in an isolated sandbox environment.
|
|
235
215
|
|
|
216
|
+
Usage:
|
|
217
|
+
Executes a given command in the sandbox environment with proper handling and security measures.
|
|
236
218
|
Before executing the command, please follow these steps:
|
|
237
|
-
|
|
238
219
|
1. Directory Verification:
|
|
239
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
|
|
240
221
|
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
|
241
|
-
|
|
242
222
|
2. Command Execution:
|
|
243
223
|
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
|
244
224
|
- Examples of proper quoting:
|
|
@@ -248,9 +228,7 @@ Before executing the command, please follow these steps:
|
|
|
248
228
|
- python /path/with spaces/script.py (incorrect - will fail)
|
|
249
229
|
- After ensuring proper quoting, execute the command
|
|
250
230
|
- Capture the output of the command
|
|
251
|
-
|
|
252
231
|
Usage notes:
|
|
253
|
-
- The command parameter is required
|
|
254
232
|
- Commands run in an isolated sandbox environment
|
|
255
233
|
- Returns combined stdout/stderr output with exit code
|
|
256
234
|
- If the output is very large, it may be truncated
|
|
@@ -325,7 +303,10 @@ def _ls_tool_generator(
|
|
|
325
303
|
"""
|
|
326
304
|
tool_description = custom_description or LIST_FILES_TOOL_DESCRIPTION
|
|
327
305
|
|
|
328
|
-
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:
|
|
329
310
|
"""Synchronous wrapper for ls tool."""
|
|
330
311
|
resolved_backend = _get_backend(backend, runtime)
|
|
331
312
|
validated_path = _validate_path(path)
|
|
@@ -334,7 +315,10 @@ def _ls_tool_generator(
|
|
|
334
315
|
result = truncate_if_too_long(paths)
|
|
335
316
|
return str(result)
|
|
336
317
|
|
|
337
|
-
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:
|
|
338
322
|
"""Asynchronous wrapper for ls tool."""
|
|
339
323
|
resolved_backend = _get_backend(backend, runtime)
|
|
340
324
|
validated_path = _validate_path(path)
|
|
@@ -367,26 +351,40 @@ def _read_file_tool_generator(
|
|
|
367
351
|
tool_description = custom_description or READ_FILE_TOOL_DESCRIPTION
|
|
368
352
|
|
|
369
353
|
def sync_read_file(
|
|
370
|
-
file_path: str,
|
|
354
|
+
file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
|
|
371
355
|
runtime: ToolRuntime[None, FilesystemState],
|
|
372
|
-
offset: int = DEFAULT_READ_OFFSET,
|
|
373
|
-
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,
|
|
374
358
|
) -> str:
|
|
375
359
|
"""Synchronous wrapper for read_file tool."""
|
|
376
360
|
resolved_backend = _get_backend(backend, runtime)
|
|
377
361
|
file_path = _validate_path(file_path)
|
|
378
|
-
|
|
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
|
|
379
370
|
|
|
380
371
|
async def async_read_file(
|
|
381
|
-
file_path: str,
|
|
372
|
+
file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
|
|
382
373
|
runtime: ToolRuntime[None, FilesystemState],
|
|
383
|
-
offset: int = DEFAULT_READ_OFFSET,
|
|
384
|
-
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,
|
|
385
376
|
) -> str:
|
|
386
377
|
"""Asynchronous wrapper for read_file tool."""
|
|
387
378
|
resolved_backend = _get_backend(backend, runtime)
|
|
388
379
|
file_path = _validate_path(file_path)
|
|
389
|
-
|
|
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
|
|
390
388
|
|
|
391
389
|
return StructuredTool.from_function(
|
|
392
390
|
name="read_file",
|
|
@@ -412,8 +410,8 @@ def _write_file_tool_generator(
|
|
|
412
410
|
tool_description = custom_description or WRITE_FILE_TOOL_DESCRIPTION
|
|
413
411
|
|
|
414
412
|
def sync_write_file(
|
|
415
|
-
file_path: str,
|
|
416
|
-
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."],
|
|
417
415
|
runtime: ToolRuntime[None, FilesystemState],
|
|
418
416
|
) -> Command | str:
|
|
419
417
|
"""Synchronous wrapper for write_file tool."""
|
|
@@ -438,8 +436,8 @@ def _write_file_tool_generator(
|
|
|
438
436
|
return f"Updated file {res.path}"
|
|
439
437
|
|
|
440
438
|
async def async_write_file(
|
|
441
|
-
file_path: str,
|
|
442
|
-
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."],
|
|
443
441
|
runtime: ToolRuntime[None, FilesystemState],
|
|
444
442
|
) -> Command | str:
|
|
445
443
|
"""Asynchronous wrapper for write_file tool."""
|
|
@@ -487,12 +485,12 @@ def _edit_file_tool_generator(
|
|
|
487
485
|
tool_description = custom_description or EDIT_FILE_TOOL_DESCRIPTION
|
|
488
486
|
|
|
489
487
|
def sync_edit_file(
|
|
490
|
-
file_path: str,
|
|
491
|
-
old_string: str,
|
|
492
|
-
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."],
|
|
493
491
|
runtime: ToolRuntime[None, FilesystemState],
|
|
494
492
|
*,
|
|
495
|
-
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,
|
|
496
494
|
) -> Command | str:
|
|
497
495
|
"""Synchronous wrapper for edit_file tool."""
|
|
498
496
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -515,12 +513,12 @@ def _edit_file_tool_generator(
|
|
|
515
513
|
return f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'"
|
|
516
514
|
|
|
517
515
|
async def async_edit_file(
|
|
518
|
-
file_path: str,
|
|
519
|
-
old_string: str,
|
|
520
|
-
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."],
|
|
521
519
|
runtime: ToolRuntime[None, FilesystemState],
|
|
522
520
|
*,
|
|
523
|
-
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,
|
|
524
522
|
) -> Command | str:
|
|
525
523
|
"""Asynchronous wrapper for edit_file tool."""
|
|
526
524
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -565,7 +563,11 @@ def _glob_tool_generator(
|
|
|
565
563
|
"""
|
|
566
564
|
tool_description = custom_description or GLOB_TOOL_DESCRIPTION
|
|
567
565
|
|
|
568
|
-
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:
|
|
569
571
|
"""Synchronous wrapper for glob tool."""
|
|
570
572
|
resolved_backend = _get_backend(backend, runtime)
|
|
571
573
|
infos = resolved_backend.glob_info(pattern, path=path)
|
|
@@ -573,7 +575,11 @@ def _glob_tool_generator(
|
|
|
573
575
|
result = truncate_if_too_long(paths)
|
|
574
576
|
return str(result)
|
|
575
577
|
|
|
576
|
-
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:
|
|
577
583
|
"""Asynchronous wrapper for glob tool."""
|
|
578
584
|
resolved_backend = _get_backend(backend, runtime)
|
|
579
585
|
infos = await resolved_backend.aglob_info(pattern, path=path)
|
|
@@ -605,11 +611,14 @@ def _grep_tool_generator(
|
|
|
605
611
|
tool_description = custom_description or GREP_TOOL_DESCRIPTION
|
|
606
612
|
|
|
607
613
|
def sync_grep(
|
|
608
|
-
pattern: str,
|
|
614
|
+
pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
|
|
609
615
|
runtime: ToolRuntime[None, FilesystemState],
|
|
610
|
-
path: str | None = None,
|
|
611
|
-
glob: str | None = None,
|
|
612
|
-
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",
|
|
613
622
|
) -> str:
|
|
614
623
|
"""Synchronous wrapper for grep tool."""
|
|
615
624
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -620,11 +629,14 @@ def _grep_tool_generator(
|
|
|
620
629
|
return truncate_if_too_long(formatted) # type: ignore[arg-type]
|
|
621
630
|
|
|
622
631
|
async def async_grep(
|
|
623
|
-
pattern: str,
|
|
632
|
+
pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
|
|
624
633
|
runtime: ToolRuntime[None, FilesystemState],
|
|
625
|
-
path: str | None = None,
|
|
626
|
-
glob: str | None = None,
|
|
627
|
-
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",
|
|
628
640
|
) -> str:
|
|
629
641
|
"""Asynchronous wrapper for grep tool."""
|
|
630
642
|
resolved_backend = _get_backend(backend, runtime)
|
|
@@ -681,7 +693,7 @@ def _execute_tool_generator(
|
|
|
681
693
|
tool_description = custom_description or EXECUTE_TOOL_DESCRIPTION
|
|
682
694
|
|
|
683
695
|
def sync_execute(
|
|
684
|
-
command: str,
|
|
696
|
+
command: Annotated[str, "Shell command to execute in the sandbox environment."],
|
|
685
697
|
runtime: ToolRuntime[None, FilesystemState],
|
|
686
698
|
) -> str:
|
|
687
699
|
"""Synchronous wrapper for execute tool."""
|
|
@@ -714,7 +726,7 @@ def _execute_tool_generator(
|
|
|
714
726
|
return "".join(parts)
|
|
715
727
|
|
|
716
728
|
async def async_execute(
|
|
717
|
-
command: str,
|
|
729
|
+
command: Annotated[str, "Shell command to execute in the sandbox environment."],
|
|
718
730
|
runtime: ToolRuntime[None, FilesystemState],
|
|
719
731
|
) -> str:
|
|
720
732
|
"""Asynchronous wrapper for execute tool."""
|
|
@@ -801,21 +813,32 @@ Here are the first 10 lines of the result:
|
|
|
801
813
|
class FilesystemMiddleware(AgentMiddleware):
|
|
802
814
|
"""Middleware for providing filesystem and optional execution tools to an agent.
|
|
803
815
|
|
|
804
|
-
This middleware adds filesystem tools to the agent: ls
|
|
805
|
-
edit_file
|
|
806
|
-
|
|
816
|
+
This middleware adds filesystem tools to the agent: `ls`, `read_file`, `write_file`,
|
|
817
|
+
`edit_file`, `glob`, and `grep`.
|
|
818
|
+
|
|
819
|
+
Files can be stored using any backend that implements the `BackendProtocol`.
|
|
807
820
|
|
|
808
|
-
If the backend implements SandboxBackendProtocol
|
|
821
|
+
If the backend implements `SandboxBackendProtocol`, an `execute` tool is also added
|
|
809
822
|
for running shell commands.
|
|
810
823
|
|
|
824
|
+
This middleware also automatically evicts large tool results to the file system when
|
|
825
|
+
they exceed a token threshold, preventing context window saturation.
|
|
826
|
+
|
|
811
827
|
Args:
|
|
812
|
-
backend: Backend for file storage and optional execution.
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
828
|
+
backend: Backend for file storage and optional execution.
|
|
829
|
+
|
|
830
|
+
If not provided, defaults to `StateBackend` (ephemeral storage in agent state).
|
|
831
|
+
|
|
832
|
+
For persistent storage or hybrid setups, use `CompositeBackend` with custom routes.
|
|
833
|
+
|
|
834
|
+
For execution support, use a backend that implements `SandboxBackendProtocol`.
|
|
816
835
|
system_prompt: Optional custom system prompt override.
|
|
817
836
|
custom_tool_descriptions: Optional custom tool descriptions override.
|
|
818
|
-
tool_token_limit_before_evict:
|
|
837
|
+
tool_token_limit_before_evict: Token limit before evicting a tool result to the
|
|
838
|
+
filesystem.
|
|
839
|
+
|
|
840
|
+
When exceeded, writes the result using the configured backend and replaces it
|
|
841
|
+
with a truncated preview and file reference.
|
|
819
842
|
|
|
820
843
|
Example:
|
|
821
844
|
```python
|
|
@@ -923,7 +946,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
923
946
|
system_prompt = "\n\n".join(prompt_parts)
|
|
924
947
|
|
|
925
948
|
if system_prompt:
|
|
926
|
-
|
|
949
|
+
new_system_message = append_to_system_message(request.system_message, system_prompt)
|
|
950
|
+
request = request.override(system_message=new_system_message)
|
|
927
951
|
|
|
928
952
|
return handler(request)
|
|
929
953
|
|
|
@@ -970,7 +994,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
970
994
|
system_prompt = "\n\n".join(prompt_parts)
|
|
971
995
|
|
|
972
996
|
if system_prompt:
|
|
973
|
-
|
|
997
|
+
new_system_message = append_to_system_message(request.system_message, system_prompt)
|
|
998
|
+
request = request.override(system_message=new_system_message)
|
|
974
999
|
|
|
975
1000
|
return await handler(request)
|
|
976
1001
|
|
|
@@ -1044,6 +1069,66 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1044
1069
|
processed_message = ToolMessage(
|
|
1045
1070
|
content=replacement_text,
|
|
1046
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,
|
|
1047
1132
|
)
|
|
1048
1133
|
return processed_message, result.files_update
|
|
1049
1134
|
|
|
@@ -1103,6 +1188,52 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1103
1188
|
return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
|
|
1104
1189
|
raise AssertionError(f"Unreachable code reached in _intercept_large_tool_result: for tool_result of type {type(tool_result)}")
|
|
1105
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
|
+
|
|
1106
1237
|
def wrap_tool_call(
|
|
1107
1238
|
self,
|
|
1108
1239
|
request: ToolCallRequest,
|
|
@@ -1141,4 +1272,4 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
1141
1272
|
return await handler(request)
|
|
1142
1273
|
|
|
1143
1274
|
tool_result = await handler(request)
|
|
1144
|
-
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,
|