deepagents 0.1.4__py3-none-any.whl → 0.1.5rc2__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.
@@ -2,15 +2,11 @@
2
2
  # ruff: noqa: E501
3
3
 
4
4
  from collections.abc import Awaitable, Callable, Sequence
5
- from typing import TYPE_CHECKING, Annotated, Any
5
+ from typing import Annotated
6
6
  from typing_extensions import NotRequired
7
7
 
8
- if TYPE_CHECKING:
9
- from langgraph.runtime import Runtime
10
-
11
8
  import os
12
- from datetime import UTC, datetime
13
- from typing import TYPE_CHECKING, Literal
9
+ from typing import Literal, Optional
14
10
 
15
11
  from langchain.agents.middleware.types import (
16
12
  AgentMiddleware,
@@ -22,18 +18,28 @@ from langchain.tools import ToolRuntime
22
18
  from langchain.tools.tool_node import ToolCallRequest
23
19
  from langchain_core.messages import ToolMessage
24
20
  from langchain_core.tools import BaseTool, tool
25
- from langgraph.config import get_config
26
- from langgraph.runtime import Runtime
27
- from langgraph.store.base import BaseStore, Item
28
21
  from langgraph.types import Command
29
22
  from typing_extensions import TypedDict
30
23
 
31
- MEMORIES_PREFIX = "/memories/"
24
+ from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
25
+ from deepagents.backends import StateBackend
26
+ from deepagents.backends.utils import (
27
+ create_file_data,
28
+ update_file_data,
29
+ format_content_with_line_numbers,
30
+ format_grep_matches,
31
+ truncate_if_too_long,
32
+ )
33
+
32
34
  EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
33
35
  MAX_LINE_LENGTH = 2000
34
36
  LINE_NUMBER_WIDTH = 6
35
37
  DEFAULT_READ_OFFSET = 0
36
38
  DEFAULT_READ_LIMIT = 2000
39
+ BACKEND_TYPES = (
40
+ BackendProtocol
41
+ | BackendFactory
42
+ )
37
43
 
38
44
 
39
45
  class FileData(TypedDict):
@@ -74,10 +80,8 @@ def _file_data_reducer(left: dict[str, FileData] | None, right: dict[str, FileDa
74
80
  ```
75
81
  """
76
82
  if left is None:
77
- # Filter out None values when initializing
78
83
  return {k: v for k, v in right.items() if v is not None}
79
84
 
80
- # Merge, filtering out None values (deletions)
81
85
  result = {**left}
82
86
  for key, value in right.items():
83
87
  if value is None:
@@ -115,247 +119,22 @@ def _validate_path(path: str, *, allowed_prefixes: Sequence[str] | None = None)
115
119
  validate_path("/etc/file.txt", allowed_prefixes=["/data/"]) # Raises ValueError
116
120
  ```
117
121
  """
118
- # Reject paths with traversal attempts
119
122
  if ".." in path or path.startswith("~"):
120
123
  msg = f"Path traversal not allowed: {path}"
121
124
  raise ValueError(msg)
122
125
 
123
- # Normalize path (resolve ., //, etc.)
124
126
  normalized = os.path.normpath(path)
125
-
126
- # Convert to forward slashes for consistency
127
127
  normalized = normalized.replace("\\", "/")
128
128
 
129
- # Ensure path starts with /
130
129
  if not normalized.startswith("/"):
131
130
  normalized = f"/{normalized}"
132
131
 
133
- # Check allowed prefixes if specified
134
132
  if allowed_prefixes is not None and not any(normalized.startswith(prefix) for prefix in allowed_prefixes):
135
133
  msg = f"Path must start with one of {allowed_prefixes}: {path}"
136
134
  raise ValueError(msg)
137
135
 
138
136
  return normalized
139
137
 
140
-
141
- def _format_content_with_line_numbers(
142
- content: str | list[str],
143
- *,
144
- format_style: Literal["pipe", "tab"] = "pipe",
145
- start_line: int = 1,
146
- ) -> str:
147
- r"""Format file content with line numbers for display.
148
-
149
- Converts file content to a numbered format similar to `cat -n` output,
150
- with support for two different formatting styles.
151
-
152
- Args:
153
- content: File content as a string or list of lines.
154
- format_style: Format style for line numbers:
155
- - `"pipe"`: Compact format like `"1|content"`
156
- - `"tab"`: Right-aligned format like `" 1\tcontent"` (lines truncated at 2000 chars)
157
- start_line: Starting line number (default: 1).
158
-
159
- Returns:
160
- Formatted content with line numbers prepended to each line.
161
-
162
- Example:
163
- ```python
164
- content = "Hello\nWorld"
165
- format_content_with_line_numbers(content, format_style="pipe")
166
- # Returns: "1|Hello\n2|World"
167
-
168
- format_content_with_line_numbers(content, format_style="tab", start_line=10)
169
- # Returns: " 10\tHello\n 11\tWorld"
170
- ```
171
- """
172
- if isinstance(content, str):
173
- lines = content.split("\n")
174
- # Remove trailing empty line from split
175
- if lines and lines[-1] == "":
176
- lines = lines[:-1]
177
- else:
178
- lines = content
179
-
180
- if format_style == "pipe":
181
- return "\n".join(f"{i + start_line}|{line}" for i, line in enumerate(lines))
182
-
183
- # Tab format with defined width and line truncation
184
- return "\n".join(f"{i + start_line:{LINE_NUMBER_WIDTH}d}\t{line[:MAX_LINE_LENGTH]}" for i, line in enumerate(lines))
185
-
186
-
187
- def _create_file_data(
188
- content: str | list[str],
189
- *,
190
- created_at: str | None = None,
191
- ) -> FileData:
192
- r"""Create a FileData object with automatic timestamp generation.
193
-
194
- Args:
195
- content: File content as a string or list of lines.
196
- created_at: Optional creation timestamp in ISO 8601 format.
197
- If `None`, uses the current UTC time.
198
-
199
- Returns:
200
- FileData object with content and timestamps.
201
-
202
- Example:
203
- ```python
204
- file_data = create_file_data("Hello\nWorld")
205
- # Returns: {"content": ["Hello", "World"], "created_at": "2024-...",
206
- # "modified_at": "2024-..."}
207
- ```
208
- """
209
- lines = content.split("\n") if isinstance(content, str) else content
210
- # Split any lines exceeding MAX_LINE_LENGTH into chunks
211
- lines = [line[i:i+MAX_LINE_LENGTH] for line in lines for i in range(0, len(line) or 1, MAX_LINE_LENGTH)]
212
- now = datetime.now(UTC).isoformat()
213
-
214
- return {
215
- "content": lines,
216
- "created_at": created_at or now,
217
- "modified_at": now,
218
- }
219
-
220
-
221
- def _update_file_data(
222
- file_data: FileData,
223
- content: str | list[str],
224
- ) -> FileData:
225
- """Update FileData with new content while preserving creation timestamp.
226
-
227
- Args:
228
- file_data: Existing FileData object to update.
229
- content: New file content as a string or list of lines.
230
-
231
- Returns:
232
- Updated FileData object with new content and updated `modified_at`
233
- timestamp. The `created_at` timestamp is preserved from the original.
234
-
235
- Example:
236
- ```python
237
- original = create_file_data("Hello")
238
- updated = update_file_data(original, "Hello World")
239
- # updated["created_at"] == original["created_at"]
240
- # updated["modified_at"] > original["modified_at"]
241
- ```
242
- """
243
- lines = content.split("\n") if isinstance(content, str) else content
244
- # Split any lines exceeding MAX_LINE_LENGTH into chunks
245
- lines = [line[i:i+MAX_LINE_LENGTH] for line in lines for i in range(0, len(line) or 1, MAX_LINE_LENGTH)]
246
- now = datetime.now(UTC).isoformat()
247
-
248
- return {
249
- "content": lines,
250
- "created_at": file_data["created_at"],
251
- "modified_at": now,
252
- }
253
-
254
-
255
- def _file_data_to_string(file_data: FileData) -> str:
256
- r"""Convert FileData to plain string content.
257
-
258
- Joins the lines stored in FileData with newline characters to produce
259
- a single string representation of the file content.
260
-
261
- Args:
262
- file_data: FileData object containing lines of content.
263
-
264
- Returns:
265
- File content as a single string with lines joined by newlines.
266
-
267
- Example:
268
- ```python
269
- file_data = {
270
- "content": ["Hello", "World"],
271
- "created_at": "...",
272
- "modified_at": "...",
273
- }
274
- file_data_to_string(file_data) # Returns: "Hello\nWorld"
275
- ```
276
- """
277
- return "\n".join(file_data["content"])
278
-
279
-
280
- def _check_empty_content(content: str) -> str | None:
281
- """Check if file content is empty and return a warning message.
282
-
283
- Args:
284
- content: File content to check.
285
-
286
- Returns:
287
- Warning message string if content is empty or contains only whitespace,
288
- `None` otherwise.
289
-
290
- Example:
291
- ```python
292
- check_empty_content("") # Returns: "System reminder: File exists but has empty contents"
293
- check_empty_content(" ") # Returns: "System reminder: File exists but has empty contents"
294
- check_empty_content("Hello") # Returns: None
295
- ```
296
- """
297
- if not content or content.strip() == "":
298
- return EMPTY_CONTENT_WARNING
299
- return None
300
-
301
-
302
- def _has_memories_prefix(file_path: str) -> bool:
303
- """Check if a file path is in the longterm memory filesystem.
304
-
305
- Longterm memory files are distinguished by the `/memories/` path prefix.
306
-
307
- Args:
308
- file_path: File path to check.
309
-
310
- Returns:
311
- `True` if the file path starts with `/memories/`, `False` otherwise.
312
-
313
- Example:
314
- ```python
315
- has_memories_prefix("/memories/notes.txt") # Returns: True
316
- has_memories_prefix("/temp/file.txt") # Returns: False
317
- ```
318
- """
319
- return file_path.startswith(MEMORIES_PREFIX)
320
-
321
-
322
- def _append_memories_prefix(file_path: str) -> str:
323
- """Add the longterm memory prefix to a file path.
324
-
325
- Args:
326
- file_path: File path to prefix.
327
-
328
- Returns:
329
- File path with `/memories` prepended.
330
-
331
- Example:
332
- ```python
333
- append_memories_prefix("/notes.txt") # Returns: "/memories/notes.txt"
334
- ```
335
- """
336
- return f"/memories{file_path}"
337
-
338
-
339
- def _strip_memories_prefix(file_path: str) -> str:
340
- """Remove the longterm memory prefix from a file path.
341
-
342
- Args:
343
- file_path: File path potentially containing the memories prefix.
344
-
345
- Returns:
346
- File path with `/memories` removed if present at the start.
347
-
348
- Example:
349
- ```python
350
- strip_memories_prefix("/memories/notes.txt") # Returns: "/notes.txt"
351
- strip_memories_prefix("/notes.txt") # Returns: "/notes.txt"
352
- ```
353
- """
354
- if file_path.startswith(MEMORIES_PREFIX):
355
- return file_path[len(MEMORIES_PREFIX) - 1 :] # Keep the leading slash
356
- return file_path
357
-
358
-
359
138
  class FilesystemState(AgentState):
360
139
  """State for the filesystem middleware."""
361
140
 
@@ -363,14 +142,13 @@ class FilesystemState(AgentState):
363
142
  """Files in the filesystem."""
364
143
 
365
144
 
366
- LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem, optionally filtering by directory.
145
+ LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem, filtering by directory.
367
146
 
368
147
  Usage:
369
- - The list_files tool will return a list of all files in the filesystem.
370
- - You can optionally provide a path parameter to list files in a specific directory.
148
+ - The path parameter must be an absolute path, not a relative path
149
+ - The list_files tool will return a list of all files in the specified directory.
371
150
  - This is very useful for exploring the file system and finding the right file to read or edit.
372
151
  - You should almost ALWAYS use this tool before using the Read or Edit tools."""
373
- LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = f"\n- Files from the longterm filesystem will be prefixed with the {MEMORIES_PREFIX} path."
374
152
 
375
153
  READ_FILE_TOOL_DESCRIPTION = """Reads a file from the filesystem. You can access any file directly by using this tool.
376
154
  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.
@@ -384,7 +162,6 @@ Usage:
384
162
  - 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.
385
163
  - If you read a file that exists but has empty contents you will receive a system reminder warning in place of file contents.
386
164
  - You should ALWAYS make sure a file has been read before editing it."""
387
- READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = f"\n- file_paths prefixed with the {MEMORIES_PREFIX} path will be read from the longterm filesystem."
388
165
 
389
166
  EDIT_FILE_TOOL_DESCRIPTION = """Performs exact string replacements in files.
390
167
 
@@ -395,9 +172,7 @@ Usage:
395
172
  - Only use emojis if the user explicitly requests it. Avoid adding emojis to files unless asked.
396
173
  - 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`.
397
174
  - Use `replace_all` for replacing and renaming strings across the file. This parameter is useful if you want to rename a variable for instance."""
398
- EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = (
399
- f"\n- You can edit files in the longterm filesystem by prefixing the filename with the {MEMORIES_PREFIX} path."
400
- )
175
+
401
176
 
402
177
  WRITE_FILE_TOOL_DESCRIPTION = """Writes to a new file in the filesystem.
403
178
 
@@ -405,487 +180,253 @@ Usage:
405
180
  - The file_path parameter must be an absolute path, not a relative path
406
181
  - The content parameter must be a string
407
182
  - The write_file tool will create the a new file.
408
- - Prefer to edit existing files over creating new ones when possible.
409
- - file_paths prefixed with the /memories/ path will be written to the longterm filesystem."""
410
- WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = (
411
- f"\n- file_paths prefixed with the {MEMORIES_PREFIX} path will be written to the longterm filesystem."
412
- )
413
-
414
- FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
183
+ - Prefer to edit existing files over creating new ones when possible."""
415
184
 
416
- You have access to a filesystem which you can interact with using these tools.
417
- All file paths must start with a /.
418
-
419
- - ls: list all files in the filesystem
420
- - read_file: read a file from the filesystem
421
- - write_file: write to a file in the filesystem
422
- - edit_file: edit a file in the filesystem"""
423
- FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT = f"""
424
185
 
425
- You also have access to a longterm filesystem in which you can store files that you want to keep around for longer than the current conversation.
426
- In order to interact with the longterm filesystem, you can use those same tools, but filenames must be prefixed with the {MEMORIES_PREFIX} path.
427
- Remember, to interact with the longterm filesystem, you must prefix the filename with the {MEMORIES_PREFIX} path."""
186
+ GLOB_TOOL_DESCRIPTION = """Find files matching a glob pattern.
428
187
 
188
+ Usage:
189
+ - The glob tool finds files by matching patterns with wildcards
190
+ - Supports standard glob patterns: `*` (any characters), `**` (any directories), `?` (single character)
191
+ - Patterns can be absolute (starting with `/`) or relative
192
+ - Returns a list of absolute file paths that match the pattern
429
193
 
430
- def _get_namespace() -> tuple[str] | tuple[str, str]:
431
- """Get the namespace for longterm filesystem storage.
432
-
433
- Returns a tuple for organizing files in the store. If an assistant_id is available
434
- in the config metadata, returns a 2-tuple of (assistant_id, "filesystem") to provide
435
- per-assistant isolation. Otherwise, returns a 1-tuple of ("filesystem",) for shared storage.
194
+ Examples:
195
+ - `**/*.py` - Find all Python files
196
+ - `*.txt` - Find all text files in root
197
+ - `/subdir/**/*.md` - Find all markdown files under /subdir"""
436
198
 
437
- Returns:
438
- Namespace tuple for store operations, either `(assistant_id, "filesystem")` or `("filesystem",)`.
439
- """
440
- namespace = "filesystem"
441
- config = get_config()
442
- if config is None:
443
- return (namespace,)
444
- assistant_id = config.get("metadata", {}).get("assistant_id")
445
- if assistant_id is None:
446
- return (namespace,)
447
- return (assistant_id, "filesystem")
199
+ GREP_TOOL_DESCRIPTION = """Search for a pattern in files.
448
200
 
201
+ Usage:
202
+ - The grep tool searches for text patterns across files
203
+ - The pattern parameter is the text to search for (literal string, not regex)
204
+ - The path parameter filters which directory to search in (default is the current working directory)
205
+ - The glob parameter accepts a glob pattern to filter which files to search (e.g., `*.py`)
206
+ - The output_mode parameter controls the output format:
207
+ - `files_with_matches`: List only file paths containing matches (default)
208
+ - `content`: Show matching lines with file path and line numbers
209
+ - `count`: Show count of matches per file
210
+
211
+ Examples:
212
+ - Search all files: `grep(pattern="TODO")`
213
+ - Search Python files only: `grep(pattern="import", glob="*.py")`
214
+ - Show matching lines: `grep(pattern="error", output_mode="content")`"""
215
+
216
+ FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`
449
217
 
450
- def _get_store(runtime: ToolRuntime[None, FilesystemState]) -> BaseStore:
451
- """Get the store from the runtime, raising an error if unavailable.
218
+ You have access to a filesystem which you can interact with using these tools.
219
+ All file paths must start with a /.
452
220
 
453
- Args:
454
- runtime: The LangGraph runtime containing the store.
221
+ - ls: list files in a directory (requires absolute path)
222
+ - read_file: read a file from the filesystem
223
+ - write_file: write to a file in the filesystem
224
+ - edit_file: edit a file in the filesystem
225
+ - glob: find files matching a pattern (e.g., "**/*.py")
226
+ - grep: search for text within files"""
455
227
 
456
- Returns:
457
- The BaseStore instance for longterm file storage.
458
228
 
459
- Raises:
460
- ValueError: If longterm memory is enabled but no store is available in runtime.
461
- """
462
- if runtime.store is None:
463
- msg = "Longterm memory is enabled, but no store is available"
464
- raise ValueError(msg)
465
- return runtime.store
229
+ def _get_backend(backend: BACKEND_TYPES, runtime: ToolRuntime) -> BackendProtocol:
230
+ if callable(backend):
231
+ return backend(runtime)
232
+ return backend
466
233
 
467
234
 
468
- def _convert_store_item_to_file_data(store_item: Item) -> FileData:
469
- """Convert a store Item to FileData format.
235
+ def _ls_tool_generator(
236
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
237
+ custom_description: str | None = None,
238
+ ) -> BaseTool:
239
+ """Generate the ls (list files) tool.
470
240
 
471
241
  Args:
472
- store_item: The store Item containing file data.
242
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
243
+ custom_description: Optional custom description for the tool.
473
244
 
474
245
  Returns:
475
- FileData with content, created_at, and modified_at fields.
476
-
477
- Raises:
478
- ValueError: If required fields are missing or have incorrect types.
246
+ Configured ls tool that lists files using the backend.
479
247
  """
480
- if "content" not in store_item.value or not isinstance(store_item.value["content"], list):
481
- msg = f"Store item does not contain valid content field. Got: {store_item.value.keys()}"
482
- raise ValueError(msg)
483
- if "created_at" not in store_item.value or not isinstance(store_item.value["created_at"], str):
484
- msg = f"Store item does not contain valid created_at field. Got: {store_item.value.keys()}"
485
- raise ValueError(msg)
486
- if "modified_at" not in store_item.value or not isinstance(store_item.value["modified_at"], str):
487
- msg = f"Store item does not contain valid modified_at field. Got: {store_item.value.keys()}"
488
- raise ValueError(msg)
489
- return FileData(
490
- content=store_item.value["content"],
491
- created_at=store_item.value["created_at"],
492
- modified_at=store_item.value["modified_at"],
493
- )
248
+ tool_description = custom_description or LIST_FILES_TOOL_DESCRIPTION
494
249
 
250
+ @tool(description=tool_description)
251
+ def ls(runtime: ToolRuntime[None, FilesystemState], path: str) -> list[str]:
252
+ resolved_backend = _get_backend(backend, runtime)
253
+ validated_path = _validate_path(path)
254
+ infos = resolved_backend.ls_info(validated_path)
255
+ return [fi.get("path", "") for fi in infos]
495
256
 
496
- def _convert_file_data_to_store_item(file_data: FileData) -> dict[str, Any]:
497
- """Convert FileData to a dict suitable for store.put().
498
-
499
- Args:
500
- file_data: The FileData to convert.
501
-
502
- Returns:
503
- Dictionary with content, created_at, and modified_at fields.
504
- """
505
- return {
506
- "content": file_data["content"],
507
- "created_at": file_data["created_at"],
508
- "modified_at": file_data["modified_at"],
509
- }
257
+ return ls
510
258
 
511
259
 
512
- def _get_file_data_from_state(state: FilesystemState, file_path: str) -> FileData:
513
- """Retrieve file data from the agent's state.
260
+ def _read_file_tool_generator(
261
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
262
+ custom_description: str | None = None,
263
+ ) -> BaseTool:
264
+ """Generate the read_file tool.
514
265
 
515
266
  Args:
516
- state: The current filesystem state.
517
- file_path: The path of the file to retrieve.
267
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
268
+ custom_description: Optional custom description for the tool.
518
269
 
519
270
  Returns:
520
- The FileData for the requested file.
521
-
522
- Raises:
523
- ValueError: If the file is not found in state.
271
+ Configured read_file tool that reads files using the backend.
524
272
  """
525
- mock_filesystem = state.get("files", {})
526
- if file_path not in mock_filesystem:
527
- msg = f"File '{file_path}' not found"
528
- raise ValueError(msg)
529
- return mock_filesystem[file_path]
273
+ tool_description = custom_description or READ_FILE_TOOL_DESCRIPTION
274
+
275
+ @tool(description=tool_description)
276
+ def read_file(
277
+ file_path: str,
278
+ runtime: ToolRuntime[None, FilesystemState],
279
+ offset: int = DEFAULT_READ_OFFSET,
280
+ limit: int = DEFAULT_READ_LIMIT,
281
+ ) -> str:
282
+ resolved_backend = _get_backend(backend, runtime)
283
+ file_path = _validate_path(file_path)
284
+ return resolved_backend.read(file_path, offset=offset, limit=limit)
530
285
 
286
+ return read_file
531
287
 
532
- def _ls_tool_generator(custom_description: str | None = None, *, long_term_memory: bool) -> BaseTool:
533
- """Generate the ls (list files) tool.
288
+
289
+ def _write_file_tool_generator(
290
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
291
+ custom_description: str | None = None,
292
+ ) -> BaseTool:
293
+ """Generate the write_file tool.
534
294
 
535
295
  Args:
296
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
536
297
  custom_description: Optional custom description for the tool.
537
- long_term_memory: Whether to enable longterm memory support.
538
298
 
539
299
  Returns:
540
- Configured ls tool that lists files from state and optionally from longterm store.
300
+ Configured write_file tool that creates new files using the backend.
541
301
  """
542
- tool_description = LIST_FILES_TOOL_DESCRIPTION
543
- if custom_description:
544
- tool_description = custom_description
545
- elif long_term_memory:
546
- tool_description += LIST_FILES_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
547
-
548
- def _get_filenames_from_state(state: FilesystemState) -> list[str]:
549
- """Extract list of filenames from the filesystem state.
550
-
551
- Args:
552
- state: The current filesystem state.
553
-
554
- Returns:
555
- List of file paths in the state.
556
- """
557
- files_dict = state.get("files", {})
558
- return list(files_dict.keys())
559
-
560
- def _filter_files_by_path(filenames: list[str], path: str | None) -> list[str]:
561
- """Filter filenames by path prefix.
562
-
563
- Args:
564
- filenames: List of file paths to filter.
565
- path: Optional path prefix to filter by.
302
+ tool_description = custom_description or WRITE_FILE_TOOL_DESCRIPTION
303
+
304
+ @tool(description=tool_description)
305
+ def write_file(
306
+ file_path: str,
307
+ content: str,
308
+ runtime: ToolRuntime[None, FilesystemState],
309
+ ) -> Command | str:
310
+ resolved_backend = _get_backend(backend, runtime)
311
+ file_path = _validate_path(file_path)
312
+ res: WriteResult = resolved_backend.write(file_path, content)
313
+ if res.error:
314
+ return res.error
315
+ # If backend returns state update, wrap into Command with ToolMessage
316
+ if res.files_update is not None:
317
+ return Command(update={
318
+ "files": res.files_update,
319
+ "messages": [
320
+ ToolMessage(
321
+ content=f"Updated file {res.path}",
322
+ tool_call_id=runtime.tool_call_id,
323
+ )
324
+ ],
325
+ })
326
+ return f"Updated file {res.path}"
566
327
 
567
- Returns:
568
- Filtered list of file paths matching the prefix.
569
- """
570
- if path is None:
571
- return filenames
572
- normalized_path = _validate_path(path)
573
- return [f for f in filenames if f.startswith(normalized_path)]
574
-
575
- if long_term_memory:
576
-
577
- @tool(description=tool_description)
578
- def ls(runtime: ToolRuntime[None, FilesystemState], path: str | None = None) -> list[str]:
579
- files = _get_filenames_from_state(runtime.state)
580
- # Add filenames from longterm memory
581
- store = _get_store(runtime)
582
- namespace = _get_namespace()
583
- longterm_files = store.search(namespace)
584
- longterm_files_prefixed = [_append_memories_prefix(f.key) for f in longterm_files]
585
- files.extend(longterm_files_prefixed)
586
- return _filter_files_by_path(files, path)
587
- else:
588
-
589
- @tool(description=tool_description)
590
- def ls(runtime: ToolRuntime[None, FilesystemState], path: str | None = None) -> list[str]:
591
- files = _get_filenames_from_state(runtime.state)
592
- return _filter_files_by_path(files, path)
593
-
594
- return ls
328
+ return write_file
595
329
 
596
330
 
597
- def _read_file_tool_generator(custom_description: str | None = None, *, long_term_memory: bool) -> BaseTool:
598
- """Generate the read_file tool.
331
+ def _edit_file_tool_generator(
332
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
333
+ custom_description: str | None = None,
334
+ ) -> BaseTool:
335
+ """Generate the edit_file tool.
599
336
 
600
337
  Args:
338
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
601
339
  custom_description: Optional custom description for the tool.
602
- long_term_memory: Whether to enable longterm memory support.
603
340
 
604
341
  Returns:
605
- Configured read_file tool that reads files from state and optionally from longterm store.
342
+ Configured edit_file tool that performs string replacements in files using the backend.
606
343
  """
607
- tool_description = READ_FILE_TOOL_DESCRIPTION
608
- if custom_description:
609
- tool_description = custom_description
610
- elif long_term_memory:
611
- tool_description += READ_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
344
+ tool_description = custom_description or EDIT_FILE_TOOL_DESCRIPTION
612
345
 
613
- def _read_file_data_content(file_data: FileData, offset: int, limit: int) -> str:
614
- """Read and format file content with line numbers.
615
-
616
- Args:
617
- file_data: The file data to read.
618
- offset: Line offset to start reading from (0-indexed).
619
- limit: Maximum number of lines to read.
620
-
621
- Returns:
622
- Formatted file content with line numbers, or an error message.
623
- """
624
- content = _file_data_to_string(file_data)
625
- empty_msg = _check_empty_content(content)
626
- if empty_msg:
627
- return empty_msg
628
- lines = content.splitlines()
629
- start_idx = offset
630
- end_idx = min(start_idx + limit, len(lines))
631
- if start_idx >= len(lines):
632
- return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
633
- selected_lines = lines[start_idx:end_idx]
634
- return _format_content_with_line_numbers(selected_lines, format_style="tab", start_line=start_idx + 1)
635
-
636
- if long_term_memory:
637
-
638
- @tool(description=tool_description)
639
- def read_file(
640
- file_path: str,
641
- runtime: ToolRuntime[None, FilesystemState],
642
- offset: int = DEFAULT_READ_OFFSET,
643
- limit: int = DEFAULT_READ_LIMIT,
644
- ) -> str:
645
- file_path = _validate_path(file_path)
646
- if _has_memories_prefix(file_path):
647
- stripped_file_path = _strip_memories_prefix(file_path)
648
- store = _get_store(runtime)
649
- namespace = _get_namespace()
650
- item: Item | None = store.get(namespace, stripped_file_path)
651
- if item is None:
652
- return f"Error: File '{file_path}' not found"
653
- file_data = _convert_store_item_to_file_data(item)
654
- else:
655
- try:
656
- file_data = _get_file_data_from_state(runtime.state, file_path)
657
- except ValueError as e:
658
- return str(e)
659
- return _read_file_data_content(file_data, offset, limit)
660
-
661
- else:
662
-
663
- @tool(description=tool_description)
664
- def read_file(
665
- file_path: str,
666
- runtime: ToolRuntime[None, FilesystemState],
667
- offset: int = DEFAULT_READ_OFFSET,
668
- limit: int = DEFAULT_READ_LIMIT,
669
- ) -> str:
670
- file_path = _validate_path(file_path)
671
- try:
672
- file_data = _get_file_data_from_state(runtime.state, file_path)
673
- except ValueError as e:
674
- return str(e)
675
- return _read_file_data_content(file_data, offset, limit)
346
+ @tool(description=tool_description)
347
+ def edit_file(
348
+ file_path: str,
349
+ old_string: str,
350
+ new_string: str,
351
+ runtime: ToolRuntime[None, FilesystemState],
352
+ *,
353
+ replace_all: bool = False,
354
+ ) -> Command | str:
355
+ resolved_backend = _get_backend(backend, runtime)
356
+ file_path = _validate_path(file_path)
357
+ res: EditResult = resolved_backend.edit(file_path, old_string, new_string, replace_all=replace_all)
358
+ if res.error:
359
+ return res.error
360
+ if res.files_update is not None:
361
+ return Command(update={
362
+ "files": res.files_update,
363
+ "messages": [
364
+ ToolMessage(
365
+ content=f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'",
366
+ tool_call_id=runtime.tool_call_id,
367
+ )
368
+ ],
369
+ })
370
+ return f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'"
676
371
 
677
- return read_file
372
+ return edit_file
678
373
 
679
374
 
680
- def _write_file_tool_generator(custom_description: str | None = None, *, long_term_memory: bool) -> BaseTool:
681
- """Generate the write_file tool.
375
+ def _glob_tool_generator(
376
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
377
+ custom_description: str | None = None,
378
+ ) -> BaseTool:
379
+ """Generate the glob tool.
682
380
 
683
381
  Args:
382
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
684
383
  custom_description: Optional custom description for the tool.
685
- long_term_memory: Whether to enable longterm memory support.
686
384
 
687
385
  Returns:
688
- Configured write_file tool that creates new files in state or longterm store.
386
+ Configured glob tool that finds files by pattern using the backend.
689
387
  """
690
- tool_description = WRITE_FILE_TOOL_DESCRIPTION
691
- if custom_description:
692
- tool_description = custom_description
693
- elif long_term_memory:
694
- tool_description += WRITE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
388
+ tool_description = custom_description or GLOB_TOOL_DESCRIPTION
695
389
 
696
- def _write_file_to_state(state: FilesystemState, tool_call_id: str, file_path: str, content: str) -> Command | str:
697
- """Write a new file to the filesystem state.
390
+ @tool(description=tool_description)
391
+ def glob(pattern: str, runtime: ToolRuntime[None, FilesystemState], path: str = "/") -> list[str]:
392
+ resolved_backend = _get_backend(backend, runtime)
393
+ infos = resolved_backend.glob_info(pattern, path=path)
394
+ return [fi.get("path", "") for fi in infos]
698
395
 
699
- Args:
700
- state: The current filesystem state.
701
- tool_call_id: ID of the tool call for generating ToolMessage.
702
- file_path: The path where the file should be written.
703
- content: The content to write to the file.
396
+ return glob
704
397
 
705
- Returns:
706
- Command to update state with new file, or error string if file exists.
707
- """
708
- mock_filesystem = state.get("files", {})
709
- existing = mock_filesystem.get(file_path)
710
- if existing:
711
- return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
712
- new_file_data = _create_file_data(content)
713
- return Command(
714
- update={
715
- "files": {file_path: new_file_data},
716
- "messages": [ToolMessage(f"Updated file {file_path}", tool_call_id=tool_call_id)],
717
- }
718
- )
719
-
720
- if long_term_memory:
721
-
722
- @tool(description=tool_description)
723
- def write_file(
724
- file_path: str,
725
- content: str,
726
- runtime: ToolRuntime[None, FilesystemState],
727
- ) -> Command | str:
728
- file_path = _validate_path(file_path)
729
- if not runtime.tool_call_id:
730
- value_error_msg = "Tool call ID is required for write_file invocation"
731
- raise ValueError(value_error_msg)
732
- if _has_memories_prefix(file_path):
733
- stripped_file_path = _strip_memories_prefix(file_path)
734
- store = _get_store(runtime)
735
- namespace = _get_namespace()
736
- if store.get(namespace, stripped_file_path) is not None:
737
- return f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path."
738
- new_file_data = _create_file_data(content)
739
- store.put(namespace, stripped_file_path, _convert_file_data_to_store_item(new_file_data))
740
- return f"Updated longterm memories file {file_path}"
741
- return _write_file_to_state(runtime.state, runtime.tool_call_id, file_path, content)
742
-
743
- else:
744
-
745
- @tool(description=tool_description)
746
- def write_file(
747
- file_path: str,
748
- content: str,
749
- runtime: ToolRuntime[None, FilesystemState],
750
- ) -> Command | str:
751
- file_path = _validate_path(file_path)
752
- if not runtime.tool_call_id:
753
- value_error_msg = "Tool call ID is required for write_file invocation"
754
- raise ValueError(value_error_msg)
755
- return _write_file_to_state(runtime.state, runtime.tool_call_id, file_path, content)
756
-
757
- return write_file
758
398
 
759
-
760
- def _edit_file_tool_generator(custom_description: str | None = None, *, long_term_memory: bool) -> BaseTool:
761
- """Generate the edit_file tool.
399
+ def _grep_tool_generator(
400
+ backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
401
+ custom_description: str | None = None,
402
+ ) -> BaseTool:
403
+ """Generate the grep tool.
762
404
 
763
405
  Args:
406
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
764
407
  custom_description: Optional custom description for the tool.
765
- long_term_memory: Whether to enable longterm memory support.
766
408
 
767
409
  Returns:
768
- Configured edit_file tool that performs string replacements in files.
410
+ Configured grep tool that searches for patterns in files using the backend.
769
411
  """
770
- tool_description = EDIT_FILE_TOOL_DESCRIPTION
771
- if custom_description:
772
- tool_description = custom_description
773
- elif long_term_memory:
774
- tool_description += EDIT_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT
775
-
776
- def _perform_file_edit(
777
- file_data: FileData,
778
- old_string: str,
779
- new_string: str,
780
- *,
781
- replace_all: bool = False,
782
- ) -> tuple[FileData, str] | str:
783
- """Perform string replacement on file data.
784
-
785
- Args:
786
- file_data: The file data to edit.
787
- old_string: String to find and replace.
788
- new_string: Replacement string.
789
- replace_all: If True, replace all occurrences.
790
-
791
- Returns:
792
- Tuple of (updated_file_data, success_message) on success,
793
- or error string on failure.
794
- """
795
- content = _file_data_to_string(file_data)
796
- occurrences = content.count(old_string)
797
- if occurrences == 0:
798
- return f"Error: String not found in file: '{old_string}'"
799
- if occurrences > 1 and not replace_all:
800
- return f"Error: String '{old_string}' appears {occurrences} times in file. Use replace_all=True to replace all instances, or provide a more specific string with surrounding context."
801
- new_content = content.replace(old_string, new_string)
802
- new_file_data = _update_file_data(file_data, new_content)
803
- result_msg = f"Successfully replaced {occurrences} instance(s) of the string"
804
- return new_file_data, result_msg
805
-
806
- if long_term_memory:
807
-
808
- @tool(description=tool_description)
809
- def edit_file(
810
- file_path: str,
811
- old_string: str,
812
- new_string: str,
813
- runtime: ToolRuntime[None, FilesystemState],
814
- *,
815
- replace_all: bool = False,
816
- ) -> Command | str:
817
- file_path = _validate_path(file_path)
818
- is_longterm_memory = _has_memories_prefix(file_path)
819
-
820
- # Retrieve file data from appropriate storage
821
- if is_longterm_memory:
822
- stripped_file_path = _strip_memories_prefix(file_path)
823
- store = _get_store(runtime)
824
- namespace = _get_namespace()
825
- item: Item | None = store.get(namespace, stripped_file_path)
826
- if item is None:
827
- return f"Error: File '{file_path}' not found"
828
- file_data = _convert_store_item_to_file_data(item)
829
- else:
830
- try:
831
- file_data = _get_file_data_from_state(runtime.state, file_path)
832
- except ValueError as e:
833
- return str(e)
834
-
835
- # Perform the edit
836
- result = _perform_file_edit(file_data, old_string, new_string, replace_all=replace_all)
837
- if isinstance(result, str): # Error message
838
- return result
839
-
840
- new_file_data, result_msg = result
841
- full_msg = f"{result_msg} in '{file_path}'"
842
-
843
- # Save to appropriate storage
844
- if is_longterm_memory:
845
- store.put(namespace, stripped_file_path, _convert_file_data_to_store_item(new_file_data))
846
- return full_msg
847
-
848
- return Command(
849
- update={
850
- "files": {file_path: new_file_data},
851
- "messages": [ToolMessage(full_msg, tool_call_id=runtime.tool_call_id)],
852
- }
853
- )
854
- else:
855
-
856
- @tool(description=tool_description)
857
- def edit_file(
858
- file_path: str,
859
- old_string: str,
860
- new_string: str,
861
- runtime: ToolRuntime[None, FilesystemState],
862
- *,
863
- replace_all: bool = False,
864
- ) -> Command | str:
865
- file_path = _validate_path(file_path)
866
-
867
- # Retrieve file data from state
868
- try:
869
- file_data = _get_file_data_from_state(runtime.state, file_path)
870
- except ValueError as e:
871
- return str(e)
872
-
873
- # Perform the edit
874
- result = _perform_file_edit(file_data, old_string, new_string, replace_all=replace_all)
875
- if isinstance(result, str): # Error message
876
- return result
877
-
878
- new_file_data, result_msg = result
879
- full_msg = f"{result_msg} in '{file_path}'"
880
-
881
- return Command(
882
- update={
883
- "files": {file_path: new_file_data},
884
- "messages": [ToolMessage(full_msg, tool_call_id=runtime.tool_call_id)],
885
- }
886
- )
887
-
888
- return edit_file
412
+ tool_description = custom_description or GREP_TOOL_DESCRIPTION
413
+
414
+ @tool(description=tool_description)
415
+ def grep(
416
+ pattern: str,
417
+ runtime: ToolRuntime[None, FilesystemState],
418
+ path: Optional[str] = None,
419
+ glob: str | None = None,
420
+ output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
421
+ ) -> str:
422
+ resolved_backend = _get_backend(backend, runtime)
423
+ raw = resolved_backend.grep_raw(pattern, path=path, glob=glob)
424
+ if isinstance(raw, str):
425
+ return raw
426
+ formatted = format_grep_matches(raw, output_mode)
427
+ return truncate_if_too_long(formatted) # type: ignore[arg-type]
428
+
429
+ return grep
889
430
 
890
431
 
891
432
  TOOL_GENERATORS = {
@@ -893,24 +434,29 @@ TOOL_GENERATORS = {
893
434
  "read_file": _read_file_tool_generator,
894
435
  "write_file": _write_file_tool_generator,
895
436
  "edit_file": _edit_file_tool_generator,
437
+ "glob": _glob_tool_generator,
438
+ "grep": _grep_tool_generator,
896
439
  }
897
440
 
898
441
 
899
- def _get_filesystem_tools(custom_tool_descriptions: dict[str, str] | None = None, *, long_term_memory: bool) -> list[BaseTool]:
442
+ def _get_filesystem_tools(
443
+ backend: BackendProtocol,
444
+ custom_tool_descriptions: dict[str, str] | None = None,
445
+ ) -> list[BaseTool]:
900
446
  """Get filesystem tools.
901
447
 
902
448
  Args:
449
+ backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
903
450
  custom_tool_descriptions: Optional custom descriptions for tools.
904
- long_term_memory: Whether to enable longterm memory support.
905
451
 
906
452
  Returns:
907
- List of configured filesystem tools (ls, read_file, write_file, edit_file).
453
+ List of configured filesystem tools (ls, read_file, write_file, edit_file, glob, grep).
908
454
  """
909
455
  if custom_tool_descriptions is None:
910
456
  custom_tool_descriptions = {}
911
457
  tools = []
912
458
  for tool_name, tool_generator in TOOL_GENERATORS.items():
913
- tool = tool_generator(custom_tool_descriptions.get(tool_name), long_term_memory=long_term_memory)
459
+ tool = tool_generator(backend, custom_tool_descriptions.get(tool_name))
914
460
  tools.append(tool)
915
461
  return tools
916
462
 
@@ -928,29 +474,33 @@ Here are the first 10 lines of the result:
928
474
  class FilesystemMiddleware(AgentMiddleware):
929
475
  """Middleware for providing filesystem tools to an agent.
930
476
 
931
- This middleware adds four filesystem tools to the agent: ls, read_file, write_file,
932
- and edit_file. Files can be stored in two locations:
933
- - Short-term: In the agent's state (ephemeral, lasts only for the conversation)
934
- - Long-term: In a persistent store (persists across conversations when enabled)
477
+ This middleware adds six filesystem tools to the agent: ls, read_file, write_file,
478
+ edit_file, glob, and grep. Files can be stored using any backend that implements
479
+ the BackendProtocol.
935
480
 
936
481
  Args:
937
- long_term_memory: Whether to enable longterm memory support.
938
- system_prompt_extension: Optional custom system prompt override.
482
+ backend: Backend for file storage. If not provided, defaults to StateBackend
483
+ (ephemeral storage in agent state). For persistent storage or hybrid setups,
484
+ use CompositeBackend with custom routes.
485
+ system_prompt: Optional custom system prompt override.
939
486
  custom_tool_descriptions: Optional custom tool descriptions override.
940
-
941
- Raises:
942
- ValueError: If longterm memory is enabled but no store is available.
487
+ tool_token_limit_before_evict: Optional token limit before evicting a tool result to the filesystem.
943
488
 
944
489
  Example:
945
490
  ```python
946
- from langchain.agents.middleware.filesystem import FilesystemMiddleware
491
+ from deepagents.middleware.filesystem import FilesystemMiddleware
492
+ from deepagents.memory.backends import StateBackend, StoreBackend, CompositeBackend
947
493
  from langchain.agents import create_agent
948
494
 
949
- # Short-term memory only
950
- agent = create_agent(middleware=[FilesystemMiddleware(long_term_memory=False)])
495
+ # Ephemeral storage only (default)
496
+ agent = create_agent(middleware=[FilesystemMiddleware()])
951
497
 
952
- # With long-term memory
953
- agent = create_agent(middleware=[FilesystemMiddleware(long_term_memory=True)])
498
+ # With hybrid storage (ephemeral + persistent /memories/)
499
+ backend = CompositeBackend(
500
+ default=StateBackend(),
501
+ routes={"/memories/": StoreBackend()}
502
+ )
503
+ agent = create_agent(middleware=[FilesystemMiddleware(memory_backend=backend)])
954
504
  ```
955
505
  """
956
506
 
@@ -959,7 +509,7 @@ class FilesystemMiddleware(AgentMiddleware):
959
509
  def __init__(
960
510
  self,
961
511
  *,
962
- long_term_memory: bool = False,
512
+ backend: BACKEND_TYPES | None = None,
963
513
  system_prompt: str | None = None,
964
514
  custom_tool_descriptions: dict[str, str] | None = None,
965
515
  tool_token_limit_before_evict: int | None = 20000,
@@ -967,38 +517,20 @@ class FilesystemMiddleware(AgentMiddleware):
967
517
  """Initialize the filesystem middleware.
968
518
 
969
519
  Args:
970
- long_term_memory: Whether to enable longterm memory support.
520
+ backend: Backend for file storage, or a factory callable. Defaults to StateBackend if not provided.
971
521
  system_prompt: Optional custom system prompt override.
972
522
  custom_tool_descriptions: Optional custom tool descriptions override.
973
523
  tool_token_limit_before_evict: Optional token limit before evicting a tool result to the filesystem.
974
524
  """
975
- self.long_term_memory = long_term_memory
976
525
  self.tool_token_limit_before_evict = tool_token_limit_before_evict
977
- self.system_prompt = FILESYSTEM_SYSTEM_PROMPT
978
- if system_prompt is not None:
979
- self.system_prompt = system_prompt
980
- elif long_term_memory:
981
- self.system_prompt += FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT
982
-
983
- self.tools = _get_filesystem_tools(custom_tool_descriptions, long_term_memory=long_term_memory)
984
526
 
985
- def before_agent(self, state: AgentState, runtime: Runtime[Any]) -> dict[str, Any] | None: # noqa: ARG002
986
- """Validate that store is available if longterm memory is enabled.
527
+ # Use provided backend or default to StateBackend factory
528
+ self.backend = backend if backend is not None else (lambda rt: StateBackend(rt))
987
529
 
988
- Args:
989
- state: The state of the agent.
990
- runtime: The LangGraph runtime.
991
-
992
- Returns:
993
- The unmodified model request.
530
+ # Set system prompt (allow full override)
531
+ self.system_prompt = system_prompt if system_prompt is not None else FILESYSTEM_SYSTEM_PROMPT
994
532
 
995
- Raises:
996
- ValueError: If long_term_memory is True but runtime.store is None.
997
- """
998
- if self.long_term_memory and runtime.store is None:
999
- msg = "Longterm memory is enabled, but no store is available"
1000
- raise ValueError(msg)
1001
- return None
533
+ self.tools = _get_filesystem_tools(self.backend, custom_tool_descriptions)
1002
534
 
1003
535
  def wrap_model_call(
1004
536
  self,
@@ -1041,14 +573,14 @@ class FilesystemMiddleware(AgentMiddleware):
1041
573
  content = tool_result.content
1042
574
  if self.tool_token_limit_before_evict and len(content) > 4 * self.tool_token_limit_before_evict:
1043
575
  file_path = f"/large_tool_results/{tool_result.tool_call_id}"
1044
- file_data = _create_file_data(content)
576
+ file_data = create_file_data(content)
1045
577
  state_update = {
1046
578
  "messages": [
1047
579
  ToolMessage(
1048
580
  TOO_LARGE_TOOL_MSG.format(
1049
581
  tool_call_id=tool_result.tool_call_id,
1050
582
  file_path=file_path,
1051
- content_sample=_format_content_with_line_numbers(file_data["content"][:10], format_style="tab", start_line=1),
583
+ content_sample=format_content_with_line_numbers(file_data["content"][:10], start_line=1),
1052
584
  ),
1053
585
  tool_call_id=tool_result.tool_call_id,
1054
586
  )
@@ -1069,13 +601,13 @@ class FilesystemMiddleware(AgentMiddleware):
1069
601
  content = message.content
1070
602
  if len(content) > 4 * self.tool_token_limit_before_evict:
1071
603
  file_path = f"/large_tool_results/{message.tool_call_id}"
1072
- file_data = _create_file_data(content)
604
+ file_data = create_file_data(content)
1073
605
  edited_message_updates.append(
1074
606
  ToolMessage(
1075
607
  TOO_LARGE_TOOL_MSG.format(
1076
608
  tool_call_id=message.tool_call_id,
1077
609
  file_path=file_path,
1078
- content_sample=_format_content_with_line_numbers(file_data["content"][:10], format_style="tab", start_line=1),
610
+ content_sample=format_content_with_line_numbers(file_data["content"][:10], start_line=1),
1079
611
  ),
1080
612
  tool_call_id=message.tool_call_id,
1081
613
  )
@@ -1100,7 +632,6 @@ class FilesystemMiddleware(AgentMiddleware):
1100
632
  Returns:
1101
633
  The raw ToolMessage, or a pseudo tool message with the ToolResult in state.
1102
634
  """
1103
- # If no token limit specified, or if it is a filesystem tool, do not evict
1104
635
  if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOL_GENERATORS:
1105
636
  return handler(request)
1106
637
 
@@ -1121,9 +652,15 @@ class FilesystemMiddleware(AgentMiddleware):
1121
652
  Returns:
1122
653
  The raw ToolMessage, or a pseudo tool message with the ToolResult in state.
1123
654
  """
1124
- # If no token limit specified, or if it is a filesystem tool, do not evict
1125
655
  if self.tool_token_limit_before_evict is None or request.tool_call["name"] in TOOL_GENERATORS:
1126
656
  return await handler(request)
1127
657
 
1128
658
  tool_result = await handler(request)
1129
659
  return self._intercept_large_tool_result(tool_result)
660
+
661
+ # Back-compat aliases expected by some tests
662
+ def _create_file_data(content: str):
663
+ return create_file_data(content)
664
+
665
+ def _update_file_data(file_data: dict, content: str):
666
+ return update_file_data(file_data, content)