deepagents 0.2.1rc2__tar.gz → 0.2.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepagents-0.2.1rc2/src/deepagents.egg-info → deepagents-0.2.3}/PKG-INFO +1 -1
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/pyproject.toml +7 -2
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/utils.py +51 -52
- deepagents-0.2.3/src/deepagents/default_agent_prompt.md +110 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/graph.py +2 -2
- deepagents-0.2.3/src/deepagents/middleware/__init__.py +13 -0
- deepagents-0.2.3/src/deepagents/middleware/agent_memory.py +222 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/filesystem.py +7 -3
- deepagents-0.2.3/src/deepagents/middleware/resumable_shell.py +85 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3/src/deepagents.egg-info}/PKG-INFO +1 -1
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/SOURCES.txt +3 -0
- deepagents-0.2.1rc2/src/deepagents/middleware/__init__.py +0 -6
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/LICENSE +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/README.md +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/setup.cfg +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/__init__.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/__init__.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/composite.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/filesystem.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/protocol.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/state.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/store.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/patch_tool_calls.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/subagents.py +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/requires.txt +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/top_level.txt +0 -0
- {deepagents-0.2.1rc2 → deepagents-0.2.3}/tests/test_middleware.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepagents"
|
|
3
|
-
version = "0.2.
|
|
3
|
+
version = "0.2.3"
|
|
4
4
|
description = "General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -42,7 +42,7 @@ include = ["deepagents*"]
|
|
|
42
42
|
"" = "src"
|
|
43
43
|
|
|
44
44
|
[tool.setuptools.package-data]
|
|
45
|
-
"*" = ["py.typed"]
|
|
45
|
+
"*" = ["py.typed", "*.md"]
|
|
46
46
|
|
|
47
47
|
[tool.ruff]
|
|
48
48
|
line-length = 150
|
|
@@ -93,3 +93,8 @@ enable_error_code = ["deprecated"]
|
|
|
93
93
|
# Optional: reduce strictness if needed
|
|
94
94
|
disallow_any_generics = false
|
|
95
95
|
warn_return_any = false
|
|
96
|
+
|
|
97
|
+
[tool.uv.workspace]
|
|
98
|
+
members = [
|
|
99
|
+
"libs/deepagents-cli",
|
|
100
|
+
]
|
|
@@ -6,10 +6,11 @@ enable composition without fragile string parsing.
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import re
|
|
9
|
-
import wcmatch.glob as wcglob
|
|
10
9
|
from datetime import UTC, datetime
|
|
11
10
|
from pathlib import Path
|
|
12
|
-
from typing import Any, Literal, TypedDict
|
|
11
|
+
from typing import Any, Literal, TypedDict
|
|
12
|
+
|
|
13
|
+
import wcmatch.glob as wcglob
|
|
13
14
|
|
|
14
15
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
15
16
|
MAX_LINE_LENGTH = 10000
|
|
@@ -24,6 +25,7 @@ class FileInfo(TypedDict, total=False):
|
|
|
24
25
|
Minimal contract used across backends. Only "path" is required.
|
|
25
26
|
Other fields are best-effort and may be absent depending on backend.
|
|
26
27
|
"""
|
|
28
|
+
|
|
27
29
|
path: str
|
|
28
30
|
is_dir: bool
|
|
29
31
|
size: int # bytes (approx)
|
|
@@ -32,14 +34,15 @@ class FileInfo(TypedDict, total=False):
|
|
|
32
34
|
|
|
33
35
|
class GrepMatch(TypedDict):
|
|
34
36
|
"""Structured grep match entry."""
|
|
37
|
+
|
|
35
38
|
path: str
|
|
36
39
|
line: int
|
|
37
40
|
text: str
|
|
38
41
|
|
|
39
42
|
|
|
40
43
|
def sanitize_tool_call_id(tool_call_id: str) -> str:
|
|
41
|
-
"""Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
42
|
-
|
|
44
|
+
r"""Sanitize tool_call_id to prevent path traversal and separator issues.
|
|
45
|
+
|
|
43
46
|
Replaces dangerous characters (., /, \) with underscores.
|
|
44
47
|
"""
|
|
45
48
|
sanitized = tool_call_id.replace(".", "_").replace("/", "_").replace("\\", "_")
|
|
@@ -94,10 +97,10 @@ def format_content_with_line_numbers(
|
|
|
94
97
|
|
|
95
98
|
def check_empty_content(content: str) -> str | None:
|
|
96
99
|
"""Check if content is empty and return warning message.
|
|
97
|
-
|
|
100
|
+
|
|
98
101
|
Args:
|
|
99
102
|
content: Content to check
|
|
100
|
-
|
|
103
|
+
|
|
101
104
|
Returns:
|
|
102
105
|
Warning message if empty, None otherwise
|
|
103
106
|
"""
|
|
@@ -108,10 +111,10 @@ def check_empty_content(content: str) -> str | None:
|
|
|
108
111
|
|
|
109
112
|
def file_data_to_string(file_data: dict[str, Any]) -> str:
|
|
110
113
|
"""Convert FileData to plain string content.
|
|
111
|
-
|
|
114
|
+
|
|
112
115
|
Args:
|
|
113
116
|
file_data: FileData dict with 'content' key
|
|
114
|
-
|
|
117
|
+
|
|
115
118
|
Returns:
|
|
116
119
|
Content as string with lines joined by newlines
|
|
117
120
|
"""
|
|
@@ -164,12 +167,12 @@ def format_read_response(
|
|
|
164
167
|
limit: int,
|
|
165
168
|
) -> str:
|
|
166
169
|
"""Format file data for read response with line numbers.
|
|
167
|
-
|
|
170
|
+
|
|
168
171
|
Args:
|
|
169
172
|
file_data: FileData dict
|
|
170
173
|
offset: Line offset (0-indexed)
|
|
171
174
|
limit: Maximum number of lines
|
|
172
|
-
|
|
175
|
+
|
|
173
176
|
Returns:
|
|
174
177
|
Formatted content or error message
|
|
175
178
|
"""
|
|
@@ -177,14 +180,14 @@ def format_read_response(
|
|
|
177
180
|
empty_msg = check_empty_content(content)
|
|
178
181
|
if empty_msg:
|
|
179
182
|
return empty_msg
|
|
180
|
-
|
|
183
|
+
|
|
181
184
|
lines = content.splitlines()
|
|
182
185
|
start_idx = offset
|
|
183
186
|
end_idx = min(start_idx + limit, len(lines))
|
|
184
|
-
|
|
187
|
+
|
|
185
188
|
if start_idx >= len(lines):
|
|
186
189
|
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
|
187
|
-
|
|
190
|
+
|
|
188
191
|
selected_lines = lines[start_idx:end_idx]
|
|
189
192
|
return format_content_with_line_numbers(selected_lines, start_line=start_idx + 1)
|
|
190
193
|
|
|
@@ -196,24 +199,24 @@ def perform_string_replacement(
|
|
|
196
199
|
replace_all: bool,
|
|
197
200
|
) -> tuple[str, int] | str:
|
|
198
201
|
"""Perform string replacement with occurrence validation.
|
|
199
|
-
|
|
202
|
+
|
|
200
203
|
Args:
|
|
201
204
|
content: Original content
|
|
202
205
|
old_string: String to replace
|
|
203
206
|
new_string: Replacement string
|
|
204
207
|
replace_all: Whether to replace all occurrences
|
|
205
|
-
|
|
208
|
+
|
|
206
209
|
Returns:
|
|
207
210
|
Tuple of (new_content, occurrences) on success, or error message string
|
|
208
211
|
"""
|
|
209
212
|
occurrences = content.count(old_string)
|
|
210
|
-
|
|
213
|
+
|
|
211
214
|
if occurrences == 0:
|
|
212
215
|
return f"Error: String not found in file: '{old_string}'"
|
|
213
|
-
|
|
216
|
+
|
|
214
217
|
if occurrences > 1 and not replace_all:
|
|
215
218
|
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."
|
|
216
|
-
|
|
219
|
+
|
|
217
220
|
new_content = content.replace(old_string, new_string)
|
|
218
221
|
return new_content, occurrences
|
|
219
222
|
|
|
@@ -225,33 +228,33 @@ def truncate_if_too_long(result: list[str] | str) -> list[str] | str:
|
|
|
225
228
|
if total_chars > TOOL_RESULT_TOKEN_LIMIT * 4:
|
|
226
229
|
return result[: len(result) * TOOL_RESULT_TOKEN_LIMIT * 4 // total_chars] + [TRUNCATION_GUIDANCE]
|
|
227
230
|
return result
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
231
|
+
# string
|
|
232
|
+
if len(result) > TOOL_RESULT_TOKEN_LIMIT * 4:
|
|
233
|
+
return result[: TOOL_RESULT_TOKEN_LIMIT * 4] + "\n" + TRUNCATION_GUIDANCE
|
|
234
|
+
return result
|
|
232
235
|
|
|
233
236
|
|
|
234
237
|
def _validate_path(path: str | None) -> str:
|
|
235
238
|
"""Validate and normalize a path.
|
|
236
|
-
|
|
239
|
+
|
|
237
240
|
Args:
|
|
238
241
|
path: Path to validate
|
|
239
|
-
|
|
242
|
+
|
|
240
243
|
Returns:
|
|
241
244
|
Normalized path starting with /
|
|
242
|
-
|
|
245
|
+
|
|
243
246
|
Raises:
|
|
244
247
|
ValueError: If path is invalid
|
|
245
248
|
"""
|
|
246
249
|
path = path or "/"
|
|
247
250
|
if not path or path.strip() == "":
|
|
248
251
|
raise ValueError("Path cannot be empty")
|
|
249
|
-
|
|
252
|
+
|
|
250
253
|
normalized = path if path.startswith("/") else "/" + path
|
|
251
|
-
|
|
254
|
+
|
|
252
255
|
if not normalized.endswith("/"):
|
|
253
256
|
normalized += "/"
|
|
254
|
-
|
|
257
|
+
|
|
255
258
|
return normalized
|
|
256
259
|
|
|
257
260
|
|
|
@@ -261,16 +264,16 @@ def _glob_search_files(
|
|
|
261
264
|
path: str = "/",
|
|
262
265
|
) -> str:
|
|
263
266
|
"""Search files dict for paths matching glob pattern.
|
|
264
|
-
|
|
267
|
+
|
|
265
268
|
Args:
|
|
266
269
|
files: Dictionary of file paths to FileData.
|
|
267
270
|
pattern: Glob pattern (e.g., "*.py", "**/*.ts").
|
|
268
271
|
path: Base path to search from.
|
|
269
|
-
|
|
272
|
+
|
|
270
273
|
Returns:
|
|
271
274
|
Newline-separated file paths, sorted by modification time (most recent first).
|
|
272
275
|
Returns "No files found" if no matches.
|
|
273
|
-
|
|
276
|
+
|
|
274
277
|
Example:
|
|
275
278
|
```python
|
|
276
279
|
files = {"/src/main.py": FileData(...), "/test.py": FileData(...)}
|
|
@@ -313,29 +316,28 @@ def _format_grep_results(
|
|
|
313
316
|
output_mode: Literal["files_with_matches", "content", "count"],
|
|
314
317
|
) -> str:
|
|
315
318
|
"""Format grep search results based on output mode.
|
|
316
|
-
|
|
319
|
+
|
|
317
320
|
Args:
|
|
318
321
|
results: Dictionary mapping file paths to list of (line_num, line_content) tuples
|
|
319
322
|
output_mode: Output format - "files_with_matches", "content", or "count"
|
|
320
|
-
|
|
323
|
+
|
|
321
324
|
Returns:
|
|
322
325
|
Formatted string output
|
|
323
326
|
"""
|
|
324
327
|
if output_mode == "files_with_matches":
|
|
325
328
|
return "\n".join(sorted(results.keys()))
|
|
326
|
-
|
|
329
|
+
if output_mode == "count":
|
|
327
330
|
lines = []
|
|
328
331
|
for file_path in sorted(results.keys()):
|
|
329
332
|
count = len(results[file_path])
|
|
330
333
|
lines.append(f"{file_path}: {count}")
|
|
331
334
|
return "\n".join(lines)
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
return "\n".join(lines)
|
|
335
|
+
lines = []
|
|
336
|
+
for file_path in sorted(results.keys()):
|
|
337
|
+
lines.append(f"{file_path}:")
|
|
338
|
+
for line_num, line in results[file_path]:
|
|
339
|
+
lines.append(f" {line_num}: {line}")
|
|
340
|
+
return "\n".join(lines)
|
|
339
341
|
|
|
340
342
|
|
|
341
343
|
def _grep_search_files(
|
|
@@ -346,17 +348,17 @@ def _grep_search_files(
|
|
|
346
348
|
output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
|
|
347
349
|
) -> str:
|
|
348
350
|
"""Search file contents for regex pattern.
|
|
349
|
-
|
|
351
|
+
|
|
350
352
|
Args:
|
|
351
353
|
files: Dictionary of file paths to FileData.
|
|
352
354
|
pattern: Regex pattern to search for.
|
|
353
355
|
path: Base path to search from.
|
|
354
356
|
glob: Optional glob pattern to filter files (e.g., "*.py").
|
|
355
357
|
output_mode: Output format - "files_with_matches", "content", or "count".
|
|
356
|
-
|
|
358
|
+
|
|
357
359
|
Returns:
|
|
358
360
|
Formatted search results. Returns "No matches found" if no results.
|
|
359
|
-
|
|
361
|
+
|
|
360
362
|
Example:
|
|
361
363
|
```python
|
|
362
364
|
files = {"/file.py": FileData(content=["import os", "print('hi')"], ...)}
|
|
@@ -394,6 +396,7 @@ def _grep_search_files(
|
|
|
394
396
|
|
|
395
397
|
# -------- Structured helpers for composition --------
|
|
396
398
|
|
|
399
|
+
|
|
397
400
|
def grep_matches_from_files(
|
|
398
401
|
files: dict[str, Any],
|
|
399
402
|
pattern: str,
|
|
@@ -419,11 +422,7 @@ def grep_matches_from_files(
|
|
|
419
422
|
filtered = {fp: fd for fp, fd in files.items() if fp.startswith(normalized_path)}
|
|
420
423
|
|
|
421
424
|
if glob:
|
|
422
|
-
filtered = {
|
|
423
|
-
fp: fd
|
|
424
|
-
for fp, fd in filtered.items()
|
|
425
|
-
if wcglob.globmatch(Path(fp).name, glob, flags=wcglob.BRACE)
|
|
426
|
-
}
|
|
425
|
+
filtered = {fp: fd for fp, fd in filtered.items() if wcglob.globmatch(Path(fp).name, glob, flags=wcglob.BRACE)}
|
|
427
426
|
|
|
428
427
|
matches: list[GrepMatch] = []
|
|
429
428
|
for file_path, file_data in filtered.items():
|
|
@@ -433,16 +432,16 @@ def grep_matches_from_files(
|
|
|
433
432
|
return matches
|
|
434
433
|
|
|
435
434
|
|
|
436
|
-
def build_grep_results_dict(matches:
|
|
435
|
+
def build_grep_results_dict(matches: list[GrepMatch]) -> dict[str, list[tuple[int, str]]]:
|
|
437
436
|
"""Group structured matches into the legacy dict form used by formatters."""
|
|
438
|
-
grouped:
|
|
437
|
+
grouped: dict[str, list[tuple[int, str]]] = {}
|
|
439
438
|
for m in matches:
|
|
440
439
|
grouped.setdefault(m["path"], []).append((m["line"], m["text"]))
|
|
441
440
|
return grouped
|
|
442
441
|
|
|
443
442
|
|
|
444
443
|
def format_grep_matches(
|
|
445
|
-
matches:
|
|
444
|
+
matches: list[GrepMatch],
|
|
446
445
|
output_mode: Literal["files_with_matches", "content", "count"],
|
|
447
446
|
) -> str:
|
|
448
447
|
"""Format structured grep matches using existing formatting logic."""
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
You are an AI assistant that helps users with various tasks including coding, research, and analysis.
|
|
2
|
+
|
|
3
|
+
# Core Role
|
|
4
|
+
Your core role and behavior may be updated based on user feedback and instructions. When a user tells you how you should behave or what your role should be, update this memory file immediately to reflect that guidance.
|
|
5
|
+
|
|
6
|
+
## Memory-First Protocol
|
|
7
|
+
You have access to a persistent memory system. ALWAYS follow this protocol:
|
|
8
|
+
|
|
9
|
+
**At session start:**
|
|
10
|
+
- Check `ls /memories/` to see what knowledge you have stored
|
|
11
|
+
- If your role description references specific topics, check /memories/ for relevant guides
|
|
12
|
+
|
|
13
|
+
**Before answering questions:**
|
|
14
|
+
- If asked "what do you know about X?" or "how do I do Y?" → Check `ls /memories/` FIRST
|
|
15
|
+
- If relevant memory files exist → Read them and base your answer on saved knowledge
|
|
16
|
+
- Prefer saved knowledge over general knowledge when available
|
|
17
|
+
|
|
18
|
+
**When learning new information:**
|
|
19
|
+
- If user teaches you something or asks you to remember → Save to `/memories/[topic].md`
|
|
20
|
+
- Use descriptive filenames: `/memories/deep-agents-guide.md` not `/memories/notes.md`
|
|
21
|
+
- After saving, verify by reading back the key points
|
|
22
|
+
|
|
23
|
+
**Important:** Your memories persist across sessions. Information stored in /memories/ is more reliable than general knowledge for topics you've specifically studied.
|
|
24
|
+
|
|
25
|
+
# Tone and Style
|
|
26
|
+
Be concise and direct. Answer in fewer than 4 lines unless the user asks for detail.
|
|
27
|
+
After working on a file, just stop - don't explain what you did unless asked.
|
|
28
|
+
Avoid unnecessary introductions or conclusions.
|
|
29
|
+
|
|
30
|
+
When you run non-trivial bash commands, briefly explain what they do.
|
|
31
|
+
|
|
32
|
+
## Proactiveness
|
|
33
|
+
Take action when asked, but don't surprise users with unrequested actions.
|
|
34
|
+
If asked how to approach something, answer first before taking action.
|
|
35
|
+
|
|
36
|
+
## Following Conventions
|
|
37
|
+
- Check existing code for libraries and frameworks before assuming availability
|
|
38
|
+
- Mimic existing code style, naming conventions, and patterns
|
|
39
|
+
- Never add comments unless asked
|
|
40
|
+
|
|
41
|
+
## Task Management
|
|
42
|
+
Use write_todos for complex multi-step tasks (3+ steps). Mark tasks in_progress before starting, completed immediately after finishing.
|
|
43
|
+
For simple 1-2 step tasks, just do them without todos.
|
|
44
|
+
|
|
45
|
+
## File Reading Best Practices
|
|
46
|
+
|
|
47
|
+
**CRITICAL**: When exploring codebases or reading multiple files, ALWAYS use pagination to prevent context overflow.
|
|
48
|
+
|
|
49
|
+
**Pattern for codebase exploration:**
|
|
50
|
+
1. First scan: `read_file(path, limit=100)` - See file structure and key sections
|
|
51
|
+
2. Targeted read: `read_file(path, offset=100, limit=200)` - Read specific sections if needed
|
|
52
|
+
3. Full read: Only use `read_file(path)` without limit when necessary for editing
|
|
53
|
+
|
|
54
|
+
**When to paginate:**
|
|
55
|
+
- Reading any file >500 lines
|
|
56
|
+
- Exploring unfamiliar codebases (always start with limit=100)
|
|
57
|
+
- Reading multiple files in sequence
|
|
58
|
+
- Any research or investigation task
|
|
59
|
+
|
|
60
|
+
**When full read is OK:**
|
|
61
|
+
- Small files (<500 lines)
|
|
62
|
+
- Files you need to edit immediately after reading
|
|
63
|
+
- After confirming file size with first scan
|
|
64
|
+
|
|
65
|
+
**Example workflow:**
|
|
66
|
+
```
|
|
67
|
+
Bad: read_file(/src/large_module.py) # Floods context with 2000+ lines
|
|
68
|
+
Good: read_file(/src/large_module.py, limit=100) # Scan structure first
|
|
69
|
+
read_file(/src/large_module.py, offset=100, limit=100) # Read relevant section
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Working with Subagents (task tool)
|
|
73
|
+
When delegating to subagents:
|
|
74
|
+
- **Use filesystem for large I/O**: If input instructions are large (>500 words) OR expected output is large, communicate via files
|
|
75
|
+
- Write input context/instructions to a file, tell subagent to read it
|
|
76
|
+
- Ask subagent to write their output to a file, then read it after they return
|
|
77
|
+
- This prevents token bloat and keeps context manageable in both directions
|
|
78
|
+
- **Parallelize independent work**: When tasks are independent, spawn parallel subagents to work simultaneously
|
|
79
|
+
- **Clear specifications**: Tell subagent exactly what format/structure you need in their response or output file
|
|
80
|
+
- **Main agent synthesizes**: Subagents gather/execute, main agent integrates results into final deliverable
|
|
81
|
+
|
|
82
|
+
## Tools
|
|
83
|
+
|
|
84
|
+
### execute_bash
|
|
85
|
+
Execute shell commands. Always quote paths with spaces.
|
|
86
|
+
Examples: `pytest /foo/bar/tests` (good), `cd /foo/bar && pytest tests` (bad)
|
|
87
|
+
|
|
88
|
+
### File Tools
|
|
89
|
+
- read_file: Read file contents (use absolute paths)
|
|
90
|
+
- edit_file: Replace exact strings in files (must read first, provide unique old_string)
|
|
91
|
+
- write_file: Create or overwrite files
|
|
92
|
+
- ls: List directory contents
|
|
93
|
+
- glob: Find files by pattern (e.g., "**/*.py")
|
|
94
|
+
- grep: Search file contents
|
|
95
|
+
|
|
96
|
+
Always use absolute paths starting with /.
|
|
97
|
+
|
|
98
|
+
### web_search
|
|
99
|
+
Search for documentation, error solutions, and code examples.
|
|
100
|
+
|
|
101
|
+
### http_request
|
|
102
|
+
Make HTTP requests to APIs (GET, POST, etc.).
|
|
103
|
+
|
|
104
|
+
## Code References
|
|
105
|
+
When referencing code, use format: `file_path:line_number`
|
|
106
|
+
|
|
107
|
+
## Documentation
|
|
108
|
+
- Do NOT create excessive markdown summary/documentation files after completing work
|
|
109
|
+
- Focus on the work itself, not documenting what you did
|
|
110
|
+
- Only create documentation when explicitly requested
|
|
@@ -123,10 +123,10 @@ def create_deep_agent(
|
|
|
123
123
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
124
124
|
PatchToolCallsMiddleware(),
|
|
125
125
|
]
|
|
126
|
+
if middleware:
|
|
127
|
+
deepagent_middleware.extend(middleware)
|
|
126
128
|
if interrupt_on is not None:
|
|
127
129
|
deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
|
|
128
|
-
if middleware is not None:
|
|
129
|
-
deepagent_middleware.extend(middleware)
|
|
130
130
|
|
|
131
131
|
return create_agent(
|
|
132
132
|
model,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Middleware for the DeepAgent."""
|
|
2
|
+
|
|
3
|
+
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
|
+
from deepagents.middleware.resumable_shell import ResumableShellToolMiddleware
|
|
5
|
+
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CompiledSubAgent",
|
|
9
|
+
"FilesystemMiddleware",
|
|
10
|
+
"ResumableShellToolMiddleware",
|
|
11
|
+
"SubAgent",
|
|
12
|
+
"SubAgentMiddleware",
|
|
13
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Middleware for loading agent-specific long-term memory into the system prompt."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from langgraph.runtime import Runtime
|
|
8
|
+
|
|
9
|
+
from langchain.agents.middleware.types import (
|
|
10
|
+
AgentMiddleware,
|
|
11
|
+
AgentState,
|
|
12
|
+
ModelRequest,
|
|
13
|
+
ModelResponse,
|
|
14
|
+
)
|
|
15
|
+
from typing_extensions import NotRequired, TypedDict
|
|
16
|
+
|
|
17
|
+
from deepagents.backends.protocol import BackendProtocol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentMemoryState(AgentState):
|
|
21
|
+
"""State for the agent memory middleware."""
|
|
22
|
+
|
|
23
|
+
agent_memory: NotRequired[str | None]
|
|
24
|
+
"""Long-term memory content for the agent."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AGENT_MEMORY_FILE_PATH = "/agent.md"
|
|
28
|
+
|
|
29
|
+
# Long-term Memory Documentation
|
|
30
|
+
LONGTERM_MEMORY_SYSTEM_PROMPT = """
|
|
31
|
+
|
|
32
|
+
## Long-term Memory
|
|
33
|
+
|
|
34
|
+
You have access to a long-term memory system using the {memory_path} path prefix.
|
|
35
|
+
Files stored in {memory_path} persist across sessions and conversations.
|
|
36
|
+
|
|
37
|
+
Your system prompt is loaded from {memory_path}agent.md at startup. You can update your own instructions by editing this file.
|
|
38
|
+
|
|
39
|
+
**When to CHECK/READ memories (CRITICAL - do this FIRST):**
|
|
40
|
+
- **At the start of ANY new session**: Run `ls {memory_path}` to see what you know
|
|
41
|
+
- **BEFORE answering questions**: If asked "what do you know about X?" or "how do I do Y?", check `ls {memory_path}` for relevant files FIRST
|
|
42
|
+
- **When user asks you to do something**: Check if you have guides, examples, or patterns in {memory_path} before proceeding
|
|
43
|
+
- **When user references past work or conversations**: Search {memory_path} for related content
|
|
44
|
+
- **If you're unsure**: Check your memories rather than guessing or using only general knowledge
|
|
45
|
+
|
|
46
|
+
**Memory-first response pattern:**
|
|
47
|
+
1. User asks a question → Run `ls {memory_path}` to check for relevant files
|
|
48
|
+
2. If relevant files exist → Read them with `read_file {memory_path}[filename]`
|
|
49
|
+
3. Base your answer on saved knowledge (from memories) supplemented by general knowledge
|
|
50
|
+
4. If no relevant memories exist → Use general knowledge, then consider if this is worth saving
|
|
51
|
+
|
|
52
|
+
**When to update memories:**
|
|
53
|
+
- **IMMEDIATELY when the user describes your role or how you should behave** (e.g., "you are a web researcher", "you are an expert in X")
|
|
54
|
+
- **IMMEDIATELY when the user gives feedback on your work** - Before continuing, update memories to capture what was wrong and how to do it better
|
|
55
|
+
- When the user explicitly asks you to remember something
|
|
56
|
+
- When patterns or preferences emerge (coding styles, conventions, workflows)
|
|
57
|
+
- After significant work where context would help in future sessions
|
|
58
|
+
|
|
59
|
+
**Learning from feedback:**
|
|
60
|
+
- When user says something is better/worse, capture WHY and encode it as a pattern
|
|
61
|
+
- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions
|
|
62
|
+
- When user says "you should remember X" or "be careful about Y", treat this as HIGH PRIORITY - update memories IMMEDIATELY
|
|
63
|
+
- Look for the underlying principle behind corrections, not just the specific mistake
|
|
64
|
+
- If it's something you "should have remembered", identify where that instruction should live permanently
|
|
65
|
+
|
|
66
|
+
**What to store where:**
|
|
67
|
+
- **{memory_path}agent.md**: Update this to modify your core instructions and behavioral patterns
|
|
68
|
+
- **Other {memory_path} files**: Use for project-specific context, reference information, or structured notes
|
|
69
|
+
- If you create additional memory files, add references to them in {memory_path}agent.md so you remember to consult them
|
|
70
|
+
|
|
71
|
+
The portion of your system prompt that comes from {memory_path}agent.md is marked with `<agent_memory>` tags so you can identify what instructions come from your persistent memory.
|
|
72
|
+
|
|
73
|
+
Example: `ls {memory_path}` to see what memories you have
|
|
74
|
+
Example: `read_file '{memory_path}deep-agents-guide.md'` to recall saved knowledge
|
|
75
|
+
Example: `edit_file('{memory_path}agent.md', ...)` to update your instructions
|
|
76
|
+
Example: `write_file('{memory_path}project_context.md', ...)` for project-specific notes, then reference it in agent.md
|
|
77
|
+
|
|
78
|
+
Remember: To interact with the longterm filesystem, you must prefix the filename with the {memory_path} path."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_MEMORY_SNIPPET = """<agent_memory>
|
|
82
|
+
{agent_memory}
|
|
83
|
+
</agent_memory>
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
class AgentMemoryMiddleware(AgentMiddleware):
|
|
87
|
+
"""Middleware for loading agent-specific long-term memory.
|
|
88
|
+
|
|
89
|
+
This middleware loads the agent's long-term memory from a file (agent.md)
|
|
90
|
+
and injects it into the system prompt. The memory is loaded once at the
|
|
91
|
+
start of the conversation and stored in state.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
backend: Backend to use for loading the agent memory file.
|
|
95
|
+
system_prompt_template: Optional custom template for how to inject
|
|
96
|
+
the agent memory into the system prompt. Use {agent_memory} as
|
|
97
|
+
a placeholder. Defaults to a simple section header.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
```python
|
|
101
|
+
from deepagents.middleware.agent_memory import AgentMemoryMiddleware
|
|
102
|
+
from deepagents.memory.backends import FilesystemBackend
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
# Set up backend pointing to agent's directory
|
|
106
|
+
agent_dir = Path.home() / ".deepagents" / "my-agent"
|
|
107
|
+
backend = FilesystemBackend(root_dir=agent_dir)
|
|
108
|
+
|
|
109
|
+
# Create middleware
|
|
110
|
+
middleware = AgentMemoryMiddleware(backend=backend)
|
|
111
|
+
```
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
state_schema = AgentMemoryState
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
backend: BackendProtocol,
|
|
120
|
+
memory_path: str,
|
|
121
|
+
system_prompt_template: str | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Initialize the agent memory middleware.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
backend: Backend to use for loading the agent memory file.
|
|
127
|
+
system_prompt_template: Optional custom template for injecting
|
|
128
|
+
agent memory into system prompt.
|
|
129
|
+
"""
|
|
130
|
+
self.backend = backend
|
|
131
|
+
self.memory_path = memory_path
|
|
132
|
+
self.system_prompt_template = system_prompt_template or DEFAULT_MEMORY_SNIPPET
|
|
133
|
+
|
|
134
|
+
def before_agent(
|
|
135
|
+
self,
|
|
136
|
+
state: AgentMemoryState,
|
|
137
|
+
runtime,
|
|
138
|
+
) -> AgentMemoryState:
|
|
139
|
+
"""Load agent memory from file before agent execution.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
state: Current agent state.
|
|
143
|
+
handler: Handler function to call after loading memory.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Updated state with agent_memory populated.
|
|
147
|
+
"""
|
|
148
|
+
# Only load memory if it hasn't been loaded yet
|
|
149
|
+
if "agent_memory" not in state or state.get("agent_memory") is None:
|
|
150
|
+
file_data = self.backend.read(AGENT_MEMORY_FILE_PATH)
|
|
151
|
+
return {"agent_memory": file_data}
|
|
152
|
+
|
|
153
|
+
async def abefore_agent(
|
|
154
|
+
self,
|
|
155
|
+
state: AgentMemoryState,
|
|
156
|
+
runtime,
|
|
157
|
+
) -> AgentMemoryState:
|
|
158
|
+
"""(async) Load agent memory from file before agent execution.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
state: Current agent state.
|
|
162
|
+
handler: Handler function to call after loading memory.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Updated state with agent_memory populated.
|
|
166
|
+
"""
|
|
167
|
+
# Only load memory if it hasn't been loaded yet
|
|
168
|
+
if "agent_memory" not in state or state.get("agent_memory") is None:
|
|
169
|
+
file_data = self.backend.read(AGENT_MEMORY_FILE_PATH)
|
|
170
|
+
return {"agent_memory": file_data}
|
|
171
|
+
|
|
172
|
+
def wrap_model_call(
|
|
173
|
+
self,
|
|
174
|
+
request: ModelRequest,
|
|
175
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
176
|
+
) -> ModelResponse:
|
|
177
|
+
"""Inject agent memory into the system prompt.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
request: The model request being processed.
|
|
181
|
+
handler: The handler function to call with the modified request.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The model response from the handler.
|
|
185
|
+
"""
|
|
186
|
+
# Get agent memory from state
|
|
187
|
+
agent_memory = request.state.get("agent_memory", "")
|
|
188
|
+
|
|
189
|
+
memory_section = self.system_prompt_template.format(agent_memory=agent_memory)
|
|
190
|
+
if request.system_prompt:
|
|
191
|
+
request.system_prompt = memory_section + "\n\n" + request.system_prompt
|
|
192
|
+
else:
|
|
193
|
+
request.system_prompt = memory_section
|
|
194
|
+
request.system_prompt = request.system_prompt + "\n\n" + LONGTERM_MEMORY_SYSTEM_PROMPT.format(memory_path=self.memory_path)
|
|
195
|
+
|
|
196
|
+
return handler(request)
|
|
197
|
+
|
|
198
|
+
async def awrap_model_call(
|
|
199
|
+
self,
|
|
200
|
+
request: ModelRequest,
|
|
201
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
202
|
+
) -> ModelResponse:
|
|
203
|
+
"""(async) Inject agent memory into the system prompt.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
request: The model request being processed.
|
|
207
|
+
handler: The handler function to call with the modified request.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The model response from the handler.
|
|
211
|
+
"""
|
|
212
|
+
# Get agent memory from state
|
|
213
|
+
agent_memory = request.state.get("agent_memory", "")
|
|
214
|
+
|
|
215
|
+
memory_section = self.system_prompt_template.format(agent_memory=agent_memory)
|
|
216
|
+
if request.system_prompt:
|
|
217
|
+
request.system_prompt = memory_section + "\n\n" + request.system_prompt
|
|
218
|
+
else:
|
|
219
|
+
request.system_prompt = memory_section
|
|
220
|
+
request.system_prompt = request.system_prompt + "\n\n" + LONGTERM_MEMORY_SYSTEM_PROMPT.format(memory_path=self.memory_path)
|
|
221
|
+
|
|
222
|
+
return await handler(request)
|
|
@@ -35,7 +35,7 @@ EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
|
35
35
|
MAX_LINE_LENGTH = 2000
|
|
36
36
|
LINE_NUMBER_WIDTH = 6
|
|
37
37
|
DEFAULT_READ_OFFSET = 0
|
|
38
|
-
DEFAULT_READ_LIMIT =
|
|
38
|
+
DEFAULT_READ_LIMIT = 500
|
|
39
39
|
BACKEND_TYPES = (
|
|
40
40
|
BackendProtocol
|
|
41
41
|
| BackendFactory
|
|
@@ -155,8 +155,12 @@ Assume this tool is able to read all files on the machine. If the User provides
|
|
|
155
155
|
|
|
156
156
|
Usage:
|
|
157
157
|
- The file_path parameter must be an absolute path, not a relative path
|
|
158
|
-
- By default, it reads up to
|
|
159
|
-
-
|
|
158
|
+
- By default, it reads up to 500 lines starting from the beginning of the file
|
|
159
|
+
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
160
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
161
|
+
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
162
|
+
- Only omit limit (read full file) when necessary for editing
|
|
163
|
+
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
160
164
|
- Any lines longer than 2000 characters will be truncated
|
|
161
165
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
162
166
|
- 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.
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Shell tool middleware that survives HITL pauses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable, cast
|
|
6
|
+
|
|
7
|
+
from langchain.agents.middleware.shell_tool import (
|
|
8
|
+
ShellToolMiddleware,
|
|
9
|
+
_PersistentShellTool,
|
|
10
|
+
_SessionResources,
|
|
11
|
+
ShellToolState,
|
|
12
|
+
)
|
|
13
|
+
from langchain.agents.middleware.types import AgentState
|
|
14
|
+
from langchain_core.messages import ToolMessage
|
|
15
|
+
from langchain.tools.tool_node import ToolCallRequest
|
|
16
|
+
from langgraph.types import Command
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResumableShellToolMiddleware(ShellToolMiddleware):
|
|
20
|
+
"""Shell middleware that recreates session resources after human interrupts.
|
|
21
|
+
|
|
22
|
+
``ShellToolMiddleware`` stores its session handle in middleware state using an
|
|
23
|
+
``UntrackedValue``. When a run pauses for human approval, that attribute is not
|
|
24
|
+
checkpointed. Upon resuming, LangGraph restores the state without the shell
|
|
25
|
+
resources, so the next tool execution fails with
|
|
26
|
+
``Shell session resources are unavailable``.
|
|
27
|
+
|
|
28
|
+
This subclass lazily recreates the shell session the first time a resumed run
|
|
29
|
+
touches the shell tool again and only performs shutdown when a session is
|
|
30
|
+
actually active. This keeps behaviour identical for uninterrupted runs while
|
|
31
|
+
allowing HITL pauses to succeed.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def wrap_tool_call(
|
|
35
|
+
self,
|
|
36
|
+
request: ToolCallRequest,
|
|
37
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command],
|
|
38
|
+
) -> ToolMessage | Command:
|
|
39
|
+
if isinstance(request.tool, _PersistentShellTool):
|
|
40
|
+
resources = self._get_or_create_resources(request.state)
|
|
41
|
+
return self._run_shell_tool(
|
|
42
|
+
resources,
|
|
43
|
+
request.tool_call["args"],
|
|
44
|
+
tool_call_id=request.tool_call.get("id"),
|
|
45
|
+
)
|
|
46
|
+
return super().wrap_tool_call(request, handler)
|
|
47
|
+
|
|
48
|
+
async def awrap_tool_call(
|
|
49
|
+
self,
|
|
50
|
+
request: ToolCallRequest,
|
|
51
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
52
|
+
) -> ToolMessage | Command:
|
|
53
|
+
if isinstance(request.tool, _PersistentShellTool):
|
|
54
|
+
resources = self._get_or_create_resources(request.state)
|
|
55
|
+
return self._run_shell_tool(
|
|
56
|
+
resources,
|
|
57
|
+
request.tool_call["args"],
|
|
58
|
+
tool_call_id=request.tool_call.get("id"),
|
|
59
|
+
)
|
|
60
|
+
return await super().awrap_tool_call(request, handler)
|
|
61
|
+
|
|
62
|
+
def after_agent(self, state: ShellToolState, runtime) -> None: # type: ignore[override]
|
|
63
|
+
if self._has_resources(state):
|
|
64
|
+
super().after_agent(state, runtime)
|
|
65
|
+
|
|
66
|
+
async def aafter_agent(self, state: ShellToolState, runtime) -> None: # type: ignore[override]
|
|
67
|
+
if self._has_resources(state):
|
|
68
|
+
await super().aafter_agent(state, runtime)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _has_resources(state: AgentState) -> bool:
|
|
72
|
+
resources = state.get("shell_session_resources")
|
|
73
|
+
return isinstance(resources, _SessionResources)
|
|
74
|
+
|
|
75
|
+
def _get_or_create_resources(self, state: AgentState) -> _SessionResources:
|
|
76
|
+
resources = state.get("shell_session_resources")
|
|
77
|
+
if isinstance(resources, _SessionResources):
|
|
78
|
+
return resources
|
|
79
|
+
|
|
80
|
+
new_resources = self._create_resources()
|
|
81
|
+
cast(dict[str, Any], state)["shell_session_resources"] = new_resources
|
|
82
|
+
return new_resources
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["ResumableShellToolMiddleware"]
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
src/deepagents/__init__.py
|
|
5
|
+
src/deepagents/default_agent_prompt.md
|
|
5
6
|
src/deepagents/graph.py
|
|
6
7
|
src/deepagents.egg-info/PKG-INFO
|
|
7
8
|
src/deepagents.egg-info/SOURCES.txt
|
|
@@ -16,7 +17,9 @@ src/deepagents/backends/state.py
|
|
|
16
17
|
src/deepagents/backends/store.py
|
|
17
18
|
src/deepagents/backends/utils.py
|
|
18
19
|
src/deepagents/middleware/__init__.py
|
|
20
|
+
src/deepagents/middleware/agent_memory.py
|
|
19
21
|
src/deepagents/middleware/filesystem.py
|
|
20
22
|
src/deepagents/middleware/patch_tool_calls.py
|
|
23
|
+
src/deepagents/middleware/resumable_shell.py
|
|
21
24
|
src/deepagents/middleware/subagents.py
|
|
22
25
|
tests/test_middleware.py
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
"""Middleware for the DeepAgent."""
|
|
2
|
-
|
|
3
|
-
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
|
-
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
5
|
-
|
|
6
|
-
__all__ = ["CompiledSubAgent", "FilesystemMiddleware", "SubAgent", "SubAgentMiddleware"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|