deepagents-cli 0.0.1__py3-none-any.whl → 0.0.3__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.

Potentially problematic release.


This version of deepagents-cli might be problematic. Click here for more details.

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