deepagents 0.3.7a1__tar.gz → 0.3.8__tar.gz

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.
Files changed (27) hide show
  1. {deepagents-0.3.7a1 → deepagents-0.3.8}/PKG-INFO +1 -1
  2. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/filesystem.py +55 -7
  3. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/graph.py +29 -10
  4. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/__init__.py +3 -1
  5. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/filesystem.py +111 -81
  6. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/memory.py +11 -7
  7. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/skills.py +4 -2
  8. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/subagents.py +35 -19
  9. deepagents-0.3.8/deepagents/middleware/summarization.py +758 -0
  10. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents.egg-info/PKG-INFO +1 -1
  11. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents.egg-info/SOURCES.txt +2 -1
  12. {deepagents-0.3.7a1 → deepagents-0.3.8}/pyproject.toml +1 -1
  13. {deepagents-0.3.7a1 → deepagents-0.3.8}/README.md +0 -0
  14. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/__init__.py +0 -0
  15. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/__init__.py +0 -0
  16. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/composite.py +0 -0
  17. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/protocol.py +0 -0
  18. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/sandbox.py +0 -0
  19. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/state.py +0 -0
  20. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/store.py +0 -0
  21. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/backends/utils.py +0 -0
  22. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/_utils.py +0 -0
  23. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents/middleware/patch_tool_calls.py +0 -0
  24. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents.egg-info/dependency_links.txt +0 -0
  25. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents.egg-info/requires.txt +0 -0
  26. {deepagents-0.3.7a1 → deepagents-0.3.8}/deepagents.egg-info/top_level.txt +0 -0
  27. {deepagents-0.3.7a1 → deepagents-0.3.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.3.7a1
3
+ Version: 0.3.8
4
4
  Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
@@ -31,6 +31,39 @@ class FilesystemBackend(BackendProtocol):
31
31
  Files are accessed using their actual filesystem paths. Relative paths are
32
32
  resolved relative to the current working directory. Content is read/written
33
33
  as plain text, and metadata (timestamps) are derived from filesystem stats.
34
+
35
+ !!! warning "Security Warning"
36
+
37
+ This backend grants agents direct filesystem read/write access. Use with
38
+ caution and only in appropriate environments.
39
+
40
+ **Appropriate use cases:**
41
+
42
+ - Local development CLIs (coding assistants, development tools)
43
+ - CI/CD pipelines (see security considerations below)
44
+
45
+ **Inappropriate use cases:**
46
+
47
+ - Web servers or HTTP APIs - use `StateBackend`, `StoreBackend`, or
48
+ `SandboxBackend` instead
49
+
50
+ **Security risks:**
51
+
52
+ - Agents can read any accessible file, including secrets (API keys,
53
+ credentials, `.env` files)
54
+ - Combined with network tools, secrets may be exfiltrated via SSRF attacks
55
+ - File modifications are permanent and irreversible
56
+
57
+ **Recommended safeguards:**
58
+
59
+ 1. Enable Human-in-the-Loop (HITL) middleware to review sensitive operations
60
+ 2. Exclude secrets from accessible filesystem paths (especially in CI/CD)
61
+ 3. Use `SandboxBackend` for production environments requiring filesystem
62
+ interaction
63
+ 4. **Always** use `virtual_mode=True` with `root_dir` to enable path-based
64
+ access restrictions (blocks `..`, `~`, and absolute paths outside root).
65
+ Note that the default (`virtual_mode=False`) provides no security even with
66
+ `root_dir` set.
34
67
  """
35
68
 
36
69
  def __init__(
@@ -44,14 +77,29 @@ class FilesystemBackend(BackendProtocol):
44
77
  Args:
45
78
  root_dir: Optional root directory for file operations.
46
79
 
47
- If provided, all file paths will be resolved relative to this directory.
48
- If not provided, uses the current working directory.
49
- virtual_mode: Enables sandboxed operation where all paths are treated as
50
- virtual paths rooted at `root_dir`.
80
+ - If not provided, defaults to the current working directory.
81
+ - When `virtual_mode=False` (default): Only affects relative path
82
+ resolution. Provides **no security** - agents can access any file
83
+ using absolute paths or `..` sequences.
84
+ - When `virtual_mode=True`: All paths are restricted to this
85
+ directory with traversal protection enabled.
86
+
87
+ virtual_mode: Enable path-based access restrictions.
88
+
89
+ When `True`, all paths are treated as virtual paths anchored to
90
+ `root_dir`. Path traversal (`..`, `~`) is blocked and all resolved paths
91
+ are verified to remain within `root_dir`.
92
+
93
+ When `False` (default), **no security is provided**:
94
+
95
+ - Absolute paths (e.g., `/etc/passwd`) bypass `root_dir` entirely
96
+ - Relative paths with `..` can escape `root_dir`
97
+ - Agents have unrestricted filesystem access
98
+
99
+ **Security note:** `virtual_mode=True` provides path-based access
100
+ control, not process isolation. It restricts which files can be
101
+ accessed via paths, but does not sandbox the Python process itself.
51
102
 
52
- Path traversal (using `..` or `~`) is disallowed and all resolved paths
53
- must remain within the root directory. When `False` (default), absolute
54
- paths are allowed as-is and relative paths resolve under cwd.
55
103
  max_file_size_mb: Maximum file size in megabytes for operations like
56
104
  grep's Python fallback search.
57
105
 
@@ -5,7 +5,6 @@ from typing import Any
5
5
 
6
6
  from langchain.agents import create_agent
7
7
  from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig, TodoListMiddleware
8
- from langchain.agents.middleware.summarization import SummarizationMiddleware
9
8
  from langchain.agents.middleware.types import AgentMiddleware
10
9
  from langchain.agents.structured_output import ResponseFormat
11
10
  from langchain.chat_models import init_chat_model
@@ -26,6 +25,7 @@ from deepagents.middleware.memory import MemoryMiddleware
26
25
  from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
27
26
  from deepagents.middleware.skills import SkillsMiddleware
28
27
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
28
+ from deepagents.middleware.summarization import SummarizationMiddleware
29
29
 
30
30
  BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
31
31
 
@@ -38,7 +38,7 @@ def get_default_model() -> ChatAnthropic:
38
38
  """
39
39
  return ChatAnthropic(
40
40
  model_name="claude-sonnet-4-5-20250929",
41
- max_tokens=20000,
41
+ max_tokens=20000, # type: ignore[call-arg]
42
42
  )
43
43
 
44
44
 
@@ -63,11 +63,14 @@ def create_deep_agent(
63
63
  ) -> CompiledStateGraph:
64
64
  """Create a deep agent.
65
65
 
66
- Deep agents require a LLM that supports tool calling.
66
+ !!! warning "Deep agents require a LLM that supports tool calling!"
67
67
 
68
- This agent will by default have access to a tool to write todos (`write_todos`),
69
- seven file and execution tools: `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `execute`,
70
- and a tool to call subagents (`task`).
68
+ By default, this agent has access to the following tools:
69
+
70
+ - `write_todos`: manage a todo list
71
+ - `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`: file operations
72
+ - `execute`: run shell commands
73
+ - `task`: call subagents
71
74
 
72
75
  The `execute` tool allows running shell commands if the backend implements `SandboxBackendProtocol`.
73
76
  For non-sandbox backends, the `execute` tool will return an error message.
@@ -82,10 +85,14 @@ def create_deep_agent(
82
85
 
83
86
  In addition to custom tools you provide, deep agents include built-in tools for planning,
84
87
  file management, and subagent spawning.
85
- system_prompt: The additional instructions the agent should have.
86
-
87
- Will go in the system prompt. Can be a string or a `SystemMessage`.
88
- middleware: Additional middleware to apply after standard middleware.
88
+ system_prompt: Custom system instructions to prepend before the base deep agent
89
+ prompt.
90
+
91
+ If a string, it's concatenated with the base prompt.
92
+ middleware: Additional middleware to apply after the standard middleware stack
93
+ (`TodoListMiddleware`, `FilesystemMiddleware`, `SubAgentMiddleware`,
94
+ `SummarizationMiddleware`, `AnthropicPromptCachingMiddleware`,
95
+ `PatchToolCallsMiddleware`).
89
96
  subagents: The subagents to use.
90
97
 
91
98
  Each subagent should be a `dict` with the following keys:
@@ -142,9 +149,17 @@ def create_deep_agent(
142
149
  ):
143
150
  trigger = ("fraction", 0.85)
144
151
  keep = ("fraction", 0.10)
152
+ truncate_args_settings = {
153
+ "trigger": ("fraction", 0.85),
154
+ "keep": ("fraction", 0.10),
155
+ }
145
156
  else:
146
157
  trigger = ("tokens", 170000)
147
158
  keep = ("messages", 6)
159
+ truncate_args_settings = {
160
+ "trigger": ("messages", 20),
161
+ "keep": ("messages", 20),
162
+ }
148
163
 
149
164
  # Build middleware stack for subagents (includes skills if provided)
150
165
  subagent_middleware: list[AgentMiddleware] = [
@@ -160,9 +175,11 @@ def create_deep_agent(
160
175
  FilesystemMiddleware(backend=backend),
161
176
  SummarizationMiddleware(
162
177
  model=model,
178
+ backend=backend,
163
179
  trigger=trigger,
164
180
  keep=keep,
165
181
  trim_tokens_to_summarize=None,
182
+ truncate_args_settings=truncate_args_settings,
166
183
  ),
167
184
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
168
185
  PatchToolCallsMiddleware(),
@@ -190,9 +207,11 @@ def create_deep_agent(
190
207
  ),
191
208
  SummarizationMiddleware(
192
209
  model=model,
210
+ backend=backend,
193
211
  trigger=trigger,
194
212
  keep=keep,
195
213
  trim_tokens_to_summarize=None,
214
+ truncate_args_settings=truncate_args_settings,
196
215
  ),
197
216
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
198
217
  PatchToolCallsMiddleware(),
@@ -1,9 +1,10 @@
1
- """Middleware for the DeepAgent."""
1
+ """Middleware for the agent."""
2
2
 
3
3
  from deepagents.middleware.filesystem import FilesystemMiddleware
4
4
  from deepagents.middleware.memory import MemoryMiddleware
5
5
  from deepagents.middleware.skills import SkillsMiddleware
6
6
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
7
+ from deepagents.middleware.summarization import SummarizationMiddleware
7
8
 
8
9
  __all__ = [
9
10
  "CompiledSubAgent",
@@ -12,4 +13,5 @@ __all__ = [
12
13
  "SkillsMiddleware",
13
14
  "SubAgent",
14
15
  "SubAgentMiddleware",
16
+ "SummarizationMiddleware",
15
17
  ]
@@ -20,8 +20,9 @@ from langgraph.types import Command
20
20
  from typing_extensions import TypedDict
21
21
 
22
22
  from deepagents.backends import StateBackend
23
+ from deepagents.backends.composite import CompositeBackend
23
24
  from deepagents.backends.protocol import (
24
- BACKEND_TYPES as BACKEND_TYPES, # Re-export for backwards compatibility
25
+ BACKEND_TYPES as BACKEND_TYPES, # Re-export type here for backwards compatibility
25
26
  BackendProtocol,
26
27
  EditResult,
27
28
  SandboxBackendProtocol,
@@ -154,19 +155,16 @@ class FilesystemState(AgentState):
154
155
  """Files in the filesystem."""
155
156
 
156
157
 
157
- LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem, filtering by directory.
158
+ LIST_FILES_TOOL_DESCRIPTION = """Lists all files in a directory.
158
159
 
159
- Usage:
160
- - The path parameter must be an absolute path, not a relative path
161
- - The list_files tool will return a list of all files in the specified directory.
162
- - This is very useful for exploring the file system and finding the right file to read or edit.
163
- - You should almost ALWAYS use this tool before using the Read or Edit tools."""
160
+ This is useful for exploring the filesystem and finding the right file to read or edit.
161
+ You should almost ALWAYS use this tool before using the read_file or edit_file tools."""
162
+
163
+ READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem.
164
164
 
165
- READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem. You can access any file directly by using this tool.
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.
165
+ 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
166
 
168
167
  Usage:
169
- - The file_path parameter must be an absolute path, not a relative path
170
168
  - By default, it reads up to 100 lines starting from the beginning of the file
171
169
  - **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
172
170
  - First scan: read_file(path, limit=100) to see file structure
@@ -182,61 +180,46 @@ Usage:
182
180
  EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
183
181
 
184
182
  Usage:
185
- - You must use your `Read` tool at least once in the conversation before editing. This tool will error if you attempt an edit without reading the file.
186
- - When editing text from Read tool output, ensure you preserve the exact indentation (tabs/spaces) as it appears AFTER the line number prefix. The line number prefix format is: spaces + line number + tab. Everything after that tab is the actual file content to match. Never include any part of the line number prefix in the old_string or new_string.
187
- - ALWAYS prefer editing existing files. NEVER write new files unless explicitly required.
188
- - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
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."""
183
+ - You must read the file before editing. This tool will error if you attempt an edit without reading the file first.
184
+ - When editing, preserve the exact indentation (tabs/spaces) from the read output. Never include line number prefixes in old_string or new_string.
185
+ - ALWAYS prefer editing existing files over creating new ones.
186
+ - Only use emojis if the user explicitly requests it."""
191
187
 
192
188
 
193
189
  WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
194
190
 
195
191
  Usage:
196
- - The file_path parameter must be an absolute path, not a relative path
197
- - The content parameter must be a string
198
192
  - The write_file tool will create the a new file.
199
- - Prefer to edit existing files over creating new ones when possible."""
200
-
193
+ - Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.
194
+ """
201
195
 
202
196
  GLOB_TOOL_DESCRIPTION = """Find files matching a glob pattern.
203
197
 
204
- Usage:
205
- - The glob tool finds files by matching patterns with wildcards
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
198
+ Supports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character).
199
+ Returns a list of absolute file paths that match the pattern.
209
200
 
210
201
  Examples:
211
202
  - `**/*.py` - Find all Python files
212
203
  - `*.txt` - Find all text files in root
213
204
  - `/subdir/**/*.md` - Find all markdown files under /subdir"""
214
205
 
215
- GREP_TOOL_DESCRIPTION = """Search for a pattern in files.
206
+ GREP_TOOL_DESCRIPTION = """Search for a text pattern across files.
216
207
 
217
- Usage:
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
208
+ Searches for literal text (not regex) and returns matching files or content based on output_mode.
226
209
 
227
210
  Examples:
228
211
  - Search all files: `grep(pattern="TODO")`
229
212
  - Search Python files only: `grep(pattern="import", glob="*.py")`
230
213
  - Show matching lines: `grep(pattern="error", output_mode="content")`"""
231
214
 
232
- EXECUTE_TOOL_DESCRIPTION = """Executes a given command in the sandbox environment with proper handling and security measures.
215
+ EXECUTE_TOOL_DESCRIPTION = """Executes a shell command in an isolated sandbox environment.
233
216
 
217
+ Usage:
218
+ Executes a given command in the sandbox environment with proper handling and security measures.
234
219
  Before executing the command, please follow these steps:
235
-
236
220
  1. Directory Verification:
237
221
  - 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
222
  - For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
239
-
240
223
  2. Command Execution:
241
224
  - Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
242
225
  - Examples of proper quoting:
@@ -246,9 +229,7 @@ Before executing the command, please follow these steps:
246
229
  - python /path/with spaces/script.py (incorrect - will fail)
247
230
  - After ensuring proper quoting, execute the command
248
231
  - Capture the output of the command
249
-
250
232
  Usage notes:
251
- - The command parameter is required
252
233
  - Commands run in an isolated sandbox environment
253
234
  - Returns combined stdout/stderr output with exit code
254
235
  - If the output is very large, it may be truncated
@@ -323,7 +304,10 @@ def _ls_tool_generator(
323
304
  """
324
305
  tool_description = custom_description or LIST_FILES_TOOL_DESCRIPTION
325
306
 
326
- def sync_ls(runtime: ToolRuntime[None, FilesystemState], path: str) -> str:
307
+ def sync_ls(
308
+ runtime: ToolRuntime[None, FilesystemState],
309
+ path: Annotated[str, "Absolute path to the directory to list. Must be absolute, not relative."],
310
+ ) -> str:
327
311
  """Synchronous wrapper for ls tool."""
328
312
  resolved_backend = _get_backend(backend, runtime)
329
313
  validated_path = _validate_path(path)
@@ -332,7 +316,10 @@ def _ls_tool_generator(
332
316
  result = truncate_if_too_long(paths)
333
317
  return str(result)
334
318
 
335
- async def async_ls(runtime: ToolRuntime[None, FilesystemState], path: str) -> str:
319
+ async def async_ls(
320
+ runtime: ToolRuntime[None, FilesystemState],
321
+ path: Annotated[str, "Absolute path to the directory to list. Must be absolute, not relative."],
322
+ ) -> str:
336
323
  """Asynchronous wrapper for ls tool."""
337
324
  resolved_backend = _get_backend(backend, runtime)
338
325
  validated_path = _validate_path(path)
@@ -365,10 +352,10 @@ def _read_file_tool_generator(
365
352
  tool_description = custom_description or READ_FILE_TOOL_DESCRIPTION
366
353
 
367
354
  def sync_read_file(
368
- file_path: str,
355
+ file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
369
356
  runtime: ToolRuntime[None, FilesystemState],
370
- offset: int = DEFAULT_READ_OFFSET,
371
- limit: int = DEFAULT_READ_LIMIT,
357
+ offset: Annotated[int, "Line number to start reading from (0-indexed). Use for pagination of large files."] = DEFAULT_READ_OFFSET,
358
+ limit: Annotated[int, "Maximum number of lines to read. Use for pagination of large files."] = DEFAULT_READ_LIMIT,
372
359
  ) -> str:
373
360
  """Synchronous wrapper for read_file tool."""
374
361
  resolved_backend = _get_backend(backend, runtime)
@@ -383,10 +370,10 @@ def _read_file_tool_generator(
383
370
  return result
384
371
 
385
372
  async def async_read_file(
386
- file_path: str,
373
+ file_path: Annotated[str, "Absolute path to the file to read. Must be absolute, not relative."],
387
374
  runtime: ToolRuntime[None, FilesystemState],
388
- offset: int = DEFAULT_READ_OFFSET,
389
- limit: int = DEFAULT_READ_LIMIT,
375
+ offset: Annotated[int, "Line number to start reading from (0-indexed). Use for pagination of large files."] = DEFAULT_READ_OFFSET,
376
+ limit: Annotated[int, "Maximum number of lines to read. Use for pagination of large files."] = DEFAULT_READ_LIMIT,
390
377
  ) -> str:
391
378
  """Asynchronous wrapper for read_file tool."""
392
379
  resolved_backend = _get_backend(backend, runtime)
@@ -424,8 +411,8 @@ def _write_file_tool_generator(
424
411
  tool_description = custom_description or WRITE_FILE_TOOL_DESCRIPTION
425
412
 
426
413
  def sync_write_file(
427
- file_path: str,
428
- content: str,
414
+ file_path: Annotated[str, "Absolute path where the file should be created. Must be absolute, not relative."],
415
+ content: Annotated[str, "The text content to write to the file. This parameter is required."],
429
416
  runtime: ToolRuntime[None, FilesystemState],
430
417
  ) -> Command | str:
431
418
  """Synchronous wrapper for write_file tool."""
@@ -450,8 +437,8 @@ def _write_file_tool_generator(
450
437
  return f"Updated file {res.path}"
451
438
 
452
439
  async def async_write_file(
453
- file_path: str,
454
- content: str,
440
+ file_path: Annotated[str, "Absolute path where the file should be created. Must be absolute, not relative."],
441
+ content: Annotated[str, "The text content to write to the file. This parameter is required."],
455
442
  runtime: ToolRuntime[None, FilesystemState],
456
443
  ) -> Command | str:
457
444
  """Asynchronous wrapper for write_file tool."""
@@ -499,12 +486,12 @@ def _edit_file_tool_generator(
499
486
  tool_description = custom_description or EDIT_FILE_TOOL_DESCRIPTION
500
487
 
501
488
  def sync_edit_file(
502
- file_path: str,
503
- old_string: str,
504
- new_string: str,
489
+ file_path: Annotated[str, "Absolute path to the file to edit. Must be absolute, not relative."],
490
+ old_string: Annotated[str, "The exact text to find and replace. Must be unique in the file unless replace_all is True."],
491
+ new_string: Annotated[str, "The text to replace old_string with. Must be different from old_string."],
505
492
  runtime: ToolRuntime[None, FilesystemState],
506
493
  *,
507
- replace_all: bool = False,
494
+ replace_all: Annotated[bool, "If True, replace all occurrences of old_string. If False (default), old_string must be unique."] = False,
508
495
  ) -> Command | str:
509
496
  """Synchronous wrapper for edit_file tool."""
510
497
  resolved_backend = _get_backend(backend, runtime)
@@ -527,12 +514,12 @@ def _edit_file_tool_generator(
527
514
  return f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'"
528
515
 
529
516
  async def async_edit_file(
530
- file_path: str,
531
- old_string: str,
532
- new_string: str,
517
+ file_path: Annotated[str, "Absolute path to the file to edit. Must be absolute, not relative."],
518
+ old_string: Annotated[str, "The exact text to find and replace. Must be unique in the file unless replace_all is True."],
519
+ new_string: Annotated[str, "The text to replace old_string with. Must be different from old_string."],
533
520
  runtime: ToolRuntime[None, FilesystemState],
534
521
  *,
535
- replace_all: bool = False,
522
+ replace_all: Annotated[bool, "If True, replace all occurrences of old_string. If False (default), old_string must be unique."] = False,
536
523
  ) -> Command | str:
537
524
  """Asynchronous wrapper for edit_file tool."""
538
525
  resolved_backend = _get_backend(backend, runtime)
@@ -577,7 +564,11 @@ def _glob_tool_generator(
577
564
  """
578
565
  tool_description = custom_description or GLOB_TOOL_DESCRIPTION
579
566
 
580
- def sync_glob(pattern: str, runtime: ToolRuntime[None, FilesystemState], path: str = "/") -> str:
567
+ def sync_glob(
568
+ pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
569
+ runtime: ToolRuntime[None, FilesystemState],
570
+ path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
571
+ ) -> str:
581
572
  """Synchronous wrapper for glob tool."""
582
573
  resolved_backend = _get_backend(backend, runtime)
583
574
  infos = resolved_backend.glob_info(pattern, path=path)
@@ -585,7 +576,11 @@ def _glob_tool_generator(
585
576
  result = truncate_if_too_long(paths)
586
577
  return str(result)
587
578
 
588
- async def async_glob(pattern: str, runtime: ToolRuntime[None, FilesystemState], path: str = "/") -> str:
579
+ async def async_glob(
580
+ pattern: Annotated[str, "Glob pattern to match files (e.g., '**/*.py', '*.txt', '/subdir/**/*.md')."],
581
+ runtime: ToolRuntime[None, FilesystemState],
582
+ path: Annotated[str, "Base directory to search from. Defaults to root '/'."] = "/",
583
+ ) -> str:
589
584
  """Asynchronous wrapper for glob tool."""
590
585
  resolved_backend = _get_backend(backend, runtime)
591
586
  infos = await resolved_backend.aglob_info(pattern, path=path)
@@ -617,11 +612,14 @@ def _grep_tool_generator(
617
612
  tool_description = custom_description or GREP_TOOL_DESCRIPTION
618
613
 
619
614
  def sync_grep(
620
- pattern: str,
615
+ pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
621
616
  runtime: ToolRuntime[None, FilesystemState],
622
- path: str | None = None,
623
- glob: str | None = None,
624
- output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
617
+ path: Annotated[str | None, "Directory to search in. Defaults to current working directory."] = None,
618
+ glob: Annotated[str | None, "Glob pattern to filter which files to search (e.g., '*.py')."] = None,
619
+ output_mode: Annotated[
620
+ Literal["files_with_matches", "content", "count"],
621
+ "Output format: 'files_with_matches' (file paths only, default), 'content' (matching lines with context), 'count' (match counts per file).",
622
+ ] = "files_with_matches",
625
623
  ) -> str:
626
624
  """Synchronous wrapper for grep tool."""
627
625
  resolved_backend = _get_backend(backend, runtime)
@@ -632,11 +630,14 @@ def _grep_tool_generator(
632
630
  return truncate_if_too_long(formatted) # type: ignore[arg-type]
633
631
 
634
632
  async def async_grep(
635
- pattern: str,
633
+ pattern: Annotated[str, "Text pattern to search for (literal string, not regex)."],
636
634
  runtime: ToolRuntime[None, FilesystemState],
637
- path: str | None = None,
638
- glob: str | None = None,
639
- output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
635
+ path: Annotated[str | None, "Directory to search in. Defaults to current working directory."] = None,
636
+ glob: Annotated[str | None, "Glob pattern to filter which files to search (e.g., '*.py')."] = None,
637
+ output_mode: Annotated[
638
+ Literal["files_with_matches", "content", "count"],
639
+ "Output format: 'files_with_matches' (file paths only, default), 'content' (matching lines with context), 'count' (match counts per file).",
640
+ ] = "files_with_matches",
640
641
  ) -> str:
641
642
  """Asynchronous wrapper for grep tool."""
642
643
  resolved_backend = _get_backend(backend, runtime)
@@ -666,9 +667,6 @@ def _supports_execution(backend: BackendProtocol) -> bool:
666
667
  Returns:
667
668
  True if the backend supports execution, False otherwise.
668
669
  """
669
- # Import here to avoid circular dependency
670
- from deepagents.backends.composite import CompositeBackend
671
-
672
670
  # For CompositeBackend, check the default backend
673
671
  if isinstance(backend, CompositeBackend):
674
672
  return isinstance(backend.default, SandboxBackendProtocol)
@@ -693,7 +691,7 @@ def _execute_tool_generator(
693
691
  tool_description = custom_description or EXECUTE_TOOL_DESCRIPTION
694
692
 
695
693
  def sync_execute(
696
- command: str,
694
+ command: Annotated[str, "Shell command to execute in the sandbox environment."],
697
695
  runtime: ToolRuntime[None, FilesystemState],
698
696
  ) -> str:
699
697
  """Synchronous wrapper for execute tool."""
@@ -726,7 +724,7 @@ def _execute_tool_generator(
726
724
  return "".join(parts)
727
725
 
728
726
  async def async_execute(
729
- command: str,
727
+ command: Annotated[str, "Shell command to execute in the sandbox environment."],
730
728
  runtime: ToolRuntime[None, FilesystemState],
731
729
  ) -> str:
732
730
  """Asynchronous wrapper for execute tool."""
@@ -766,6 +764,37 @@ def _execute_tool_generator(
766
764
  )
767
765
 
768
766
 
767
+ # Tools that should be excluded from the large result eviction logic.
768
+ #
769
+ # This tuple contains tools that should NOT have their results evicted to the filesystem
770
+ # when they exceed token limits. Tools are excluded for different reasons:
771
+ #
772
+ # 1. Tools with built-in truncation (ls, glob, grep):
773
+ # These tools truncate their own output when it becomes too large. When these tools
774
+ # produce truncated output due to many matches, it typically indicates the query
775
+ # needs refinement rather than full result preservation. In such cases, the truncated
776
+ # matches are potentially more like noise and the LLM should be prompted to narrow
777
+ # its search criteria instead.
778
+ #
779
+ # 2. Tools with problematic truncation behavior (read_file):
780
+ # read_file is tricky to handle as the failure mode here is single long lines
781
+ # (e.g., imagine a jsonl file with very long payloads on each line). If we try to
782
+ # truncate the result of read_file, the agent may then attempt to re-read the
783
+ # truncated file using read_file again, which won't help.
784
+ #
785
+ # 3. Tools that never exceed limits (edit_file, write_file):
786
+ # These tools return minimal confirmation messages and are never expected to produce
787
+ # output large enough to exceed token limits, so checking them would be unnecessary.
788
+ TOOLS_EXCLUDED_FROM_EVICTION = (
789
+ "ls",
790
+ "glob",
791
+ "grep",
792
+ "read_file",
793
+ "edit_file",
794
+ "write_file",
795
+ )
796
+
797
+
769
798
  TOOL_GENERATORS = {
770
799
  "ls": _ls_tool_generator,
771
800
  "read_file": _read_file_tool_generator,
@@ -814,8 +843,9 @@ class FilesystemMiddleware(AgentMiddleware):
814
843
  """Middleware for providing filesystem and optional execution tools to an agent.
815
844
 
816
845
  This middleware adds filesystem tools to the agent: `ls`, `read_file`, `write_file`,
817
- `edit_file`, `glob`, and `grep`. Files can be stored using any backend that implements
818
- the `BackendProtocol`.
846
+ `edit_file`, `glob`, and `grep`.
847
+
848
+ Files can be stored using any backend that implements the `BackendProtocol`.
819
849
 
820
850
  If the backend implements `SandboxBackendProtocol`, an `execute` tool is also added
821
851
  for running shell commands.
@@ -836,8 +866,6 @@ class FilesystemMiddleware(AgentMiddleware):
836
866
  tool_token_limit_before_evict: Token limit before evicting a tool result to the
837
867
  filesystem.
838
868
 
839
- Defaults to 20,000 tokens.
840
-
841
869
  When exceeded, writes the result using the configured backend and replaces it
842
870
  with a truncated preview and file reference.
843
871
 
@@ -1070,6 +1098,7 @@ class FilesystemMiddleware(AgentMiddleware):
1070
1098
  processed_message = ToolMessage(
1071
1099
  content=replacement_text,
1072
1100
  tool_call_id=message.tool_call_id,
1101
+ name=message.name,
1073
1102
  )
1074
1103
  return processed_message, result.files_update
1075
1104
 
@@ -1128,6 +1157,7 @@ class FilesystemMiddleware(AgentMiddleware):
1128
1157
  processed_message = ToolMessage(
1129
1158
  content=replacement_text,
1130
1159
  tool_call_id=message.tool_call_id,
1160
+ name=message.name,
1131
1161
  )
1132
1162
  return processed_message, result.files_update
1133
1163
 
@@ -1247,7 +1277,7 @@ class FilesystemMiddleware(AgentMiddleware):
1247
1277
  Returns:
1248
1278
  The raw ToolMessage, or a pseudo tool message with the ToolResult in state.
1249
1279
  """
1250
- if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOL_GENERATORS:
1280
+ if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOLS_EXCLUDED_FROM_EVICTION:
1251
1281
  return handler(request)
1252
1282
 
1253
1283
  tool_result = handler(request)
@@ -1267,7 +1297,7 @@ class FilesystemMiddleware(AgentMiddleware):
1267
1297
  Returns:
1268
1298
  The raw ToolMessage, or a pseudo tool message with the ToolResult in state.
1269
1299
  """
1270
- if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOL_GENERATORS:
1300
+ if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOLS_EXCLUDED_FROM_EVICTION:
1271
1301
  return await handler(request)
1272
1302
 
1273
1303
  tool_result = await handler(request)