deepagents 0.1.4__py3-none-any.whl → 0.1.5rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- deepagents/backends/__init__.py +16 -0
- deepagents/backends/composite.py +235 -0
- deepagents/backends/filesystem.py +452 -0
- deepagents/backends/protocol.py +122 -0
- deepagents/backends/state.py +161 -0
- deepagents/backends/store.py +350 -0
- deepagents/backends/utils.py +424 -0
- deepagents/graph.py +10 -13
- deepagents/middleware/filesystem.py +249 -712
- deepagents/middleware/subagents.py +1 -1
- {deepagents-0.1.4.dist-info → deepagents-0.1.5rc1.dist-info}/METADATA +31 -11
- deepagents-0.1.5rc1.dist-info/RECORD +18 -0
- deepagents-0.1.4.dist-info/RECORD +0 -11
- {deepagents-0.1.4.dist-info → deepagents-0.1.5rc1.dist-info}/WHEEL +0 -0
- {deepagents-0.1.4.dist-info → deepagents-0.1.5rc1.dist-info}/licenses/LICENSE +0 -0
- {deepagents-0.1.4.dist-info → deepagents-0.1.5rc1.dist-info}/top_level.txt +0 -0
|
@@ -2,15 +2,11 @@
|
|
|
2
2
|
# ruff: noqa: E501
|
|
3
3
|
|
|
4
4
|
from collections.abc import Awaitable, Callable, Sequence
|
|
5
|
-
from typing import
|
|
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
|
|
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
|
-
|
|
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,
|
|
145
|
+
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem, filtering by directory.
|
|
367
146
|
|
|
368
147
|
Usage:
|
|
369
|
-
- The
|
|
370
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
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
|
-
|
|
454
|
-
|
|
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
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
|
469
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
513
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
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
|
|
300
|
+
Configured write_file tool that creates new files using the backend.
|
|
541
301
|
"""
|
|
542
|
-
tool_description =
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
-
|
|
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
|
|
598
|
-
|
|
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
|
|
342
|
+
Configured edit_file tool that performs string replacements in files using the backend.
|
|
606
343
|
"""
|
|
607
|
-
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
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
372
|
+
return edit_file
|
|
678
373
|
|
|
679
374
|
|
|
680
|
-
def
|
|
681
|
-
|
|
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
|
|
386
|
+
Configured glob tool that finds files by pattern using the backend.
|
|
689
387
|
"""
|
|
690
|
-
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
|
-
|
|
697
|
-
|
|
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
|
-
|
|
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
|
-
|
|
761
|
-
|
|
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
|
|
410
|
+
Configured grep tool that searches for patterns in files using the backend.
|
|
769
411
|
"""
|
|
770
|
-
tool_description =
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
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(
|
|
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)
|
|
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
|
|
932
|
-
and
|
|
933
|
-
|
|
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
|
-
|
|
938
|
-
|
|
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
|
|
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
|
-
#
|
|
950
|
-
agent = create_agent(middleware=[FilesystemMiddleware(
|
|
495
|
+
# Ephemeral storage only (default)
|
|
496
|
+
agent = create_agent(middleware=[FilesystemMiddleware()])
|
|
951
497
|
|
|
952
|
-
# With
|
|
953
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
986
|
-
|
|
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
|
-
|
|
989
|
-
|
|
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
|
-
|
|
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 =
|
|
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=
|
|
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 =
|
|
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=
|
|
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)
|