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.
- deepagents/__init__.py +1 -12
- deepagents/cli.py +257 -272
- deepagents/default_agent_prompt.md +0 -27
- deepagents/graph.py +16 -40
- deepagents/memory/__init__.py +17 -0
- deepagents/memory/backends/__init__.py +15 -0
- deepagents/memory/backends/composite.py +250 -0
- deepagents/memory/backends/filesystem.py +330 -0
- deepagents/memory/backends/state.py +206 -0
- deepagents/memory/backends/store.py +351 -0
- deepagents/memory/backends/utils.py +319 -0
- deepagents/memory/protocol.py +164 -0
- deepagents/middleware/__init__.py +3 -3
- deepagents/middleware/agent_memory.py +207 -0
- deepagents/middleware/filesystem.py +229 -773
- deepagents/middleware/patch_tool_calls.py +44 -0
- deepagents/middleware/subagents.py +7 -6
- deepagents/pretty_cli.py +289 -0
- {deepagents_cli-0.0.1.dist-info → deepagents_cli-0.0.3.dist-info}/METADATA +26 -30
- deepagents_cli-0.0.3.dist-info/RECORD +24 -0
- deepagents/middleware/common.py +0 -16
- deepagents/middleware/local_filesystem.py +0 -741
- deepagents/prompts.py +0 -327
- deepagents/skills.py +0 -85
- deepagents_cli-0.0.1.dist-info/RECORD +0 -17
- {deepagents_cli-0.0.1.dist-info → deepagents_cli-0.0.3.dist-info}/WHEEL +0 -0
- {deepagents_cli-0.0.1.dist-info → deepagents_cli-0.0.3.dist-info}/entry_points.txt +0 -0
- {deepagents_cli-0.0.1.dist-info → deepagents_cli-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {deepagents_cli-0.0.1.dist-info → deepagents_cli-0.0.3.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
|
|
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
|
-
|
|
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,
|
|
138
|
+
LIST_FILES_TOOL_DESCRIPTION = """Lists all files in the filesystem, filtering by directory.
|
|
365
139
|
|
|
366
140
|
Usage:
|
|
367
|
-
- The
|
|
368
|
-
-
|
|
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 =
|
|
374
|
-
|
|
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
|
-
|
|
396
|
-
All file paths must start with a /.
|
|
179
|
+
GLOB_TOOL_DESCRIPTION = """Find files matching a glob pattern.
|
|
397
180
|
|
|
398
|
-
|
|
399
|
-
-
|
|
400
|
-
-
|
|
401
|
-
-
|
|
402
|
-
|
|
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
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
|
|
410
|
-
|
|
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
|
-
|
|
413
|
-
|
|
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
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
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
|
|
430
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
448
|
-
|
|
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
|
-
|
|
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
|
|
476
|
-
|
|
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
|
-
|
|
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
|
-
|
|
259
|
+
Configured read_file tool that reads files using the backend.
|
|
483
260
|
"""
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
-
|
|
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
|
|
512
|
-
|
|
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
|
|
288
|
+
Configured write_file tool that creates new files using the backend.
|
|
520
289
|
"""
|
|
521
|
-
tool_description =
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
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
|
|
302
|
+
return write_file
|
|
574
303
|
|
|
575
304
|
|
|
576
|
-
def
|
|
577
|
-
|
|
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
|
|
316
|
+
Configured edit_file tool that performs string replacements in files using the backend.
|
|
585
317
|
"""
|
|
586
|
-
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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
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
|
|
333
|
+
return edit_file
|
|
657
334
|
|
|
658
335
|
|
|
659
|
-
def
|
|
660
|
-
|
|
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
|
|
347
|
+
Configured glob tool that finds files by pattern using the backend.
|
|
668
348
|
"""
|
|
669
|
-
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
|
-
|
|
676
|
-
|
|
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
|
-
|
|
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
|
-
|
|
734
|
-
|
|
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
|
|
370
|
+
Configured grep tool that searches for patterns in files using the backend.
|
|
742
371
|
"""
|
|
743
|
-
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
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
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
|
-
|
|
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(
|
|
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)
|
|
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
|
|
895
|
-
and
|
|
896
|
-
|
|
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
|
-
|
|
901
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
#
|
|
914
|
-
agent = create_agent(middleware=[FilesystemMiddleware(
|
|
451
|
+
# Ephemeral storage only (default)
|
|
452
|
+
agent = create_agent(middleware=[FilesystemMiddleware()])
|
|
915
453
|
|
|
916
|
-
# With
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
agent = create_agent(middleware=[FilesystemMiddleware(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1009
|
-
|
|
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 =
|
|
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=
|
|
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 =
|
|
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=
|
|
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)
|