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.
Files changed (28) hide show
  1. {deepagents-0.2.1rc2/src/deepagents.egg-info → deepagents-0.2.3}/PKG-INFO +1 -1
  2. {deepagents-0.2.1rc2 → deepagents-0.2.3}/pyproject.toml +7 -2
  3. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/utils.py +51 -52
  4. deepagents-0.2.3/src/deepagents/default_agent_prompt.md +110 -0
  5. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/graph.py +2 -2
  6. deepagents-0.2.3/src/deepagents/middleware/__init__.py +13 -0
  7. deepagents-0.2.3/src/deepagents/middleware/agent_memory.py +222 -0
  8. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/filesystem.py +7 -3
  9. deepagents-0.2.3/src/deepagents/middleware/resumable_shell.py +85 -0
  10. {deepagents-0.2.1rc2 → deepagents-0.2.3/src/deepagents.egg-info}/PKG-INFO +1 -1
  11. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/SOURCES.txt +3 -0
  12. deepagents-0.2.1rc2/src/deepagents/middleware/__init__.py +0 -6
  13. {deepagents-0.2.1rc2 → deepagents-0.2.3}/LICENSE +0 -0
  14. {deepagents-0.2.1rc2 → deepagents-0.2.3}/README.md +0 -0
  15. {deepagents-0.2.1rc2 → deepagents-0.2.3}/setup.cfg +0 -0
  16. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/__init__.py +0 -0
  17. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/__init__.py +0 -0
  18. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/composite.py +0 -0
  19. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/filesystem.py +0 -0
  20. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/protocol.py +0 -0
  21. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/state.py +0 -0
  22. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/backends/store.py +0 -0
  23. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/patch_tool_calls.py +0 -0
  24. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents/middleware/subagents.py +0 -0
  25. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/dependency_links.txt +0 -0
  26. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/requires.txt +0 -0
  27. {deepagents-0.2.1rc2 → deepagents-0.2.3}/src/deepagents.egg-info/top_level.txt +0 -0
  28. {deepagents-0.2.1rc2 → deepagents-0.2.3}/tests/test_middleware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.2.1rc2
3
+ Version: 0.2.3
4
4
  Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
5
  License: MIT
6
6
  Requires-Python: <4.0,>=3.11
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepagents"
3
- version = "0.2.1rc2"
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, List, Dict
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
- else: # string
229
- if len(result) > TOOL_RESULT_TOKEN_LIMIT * 4:
230
- return result[: TOOL_RESULT_TOKEN_LIMIT * 4] + "\n" + TRUNCATION_GUIDANCE
231
- return result
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
- elif output_mode == "count":
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
- else:
333
- lines = []
334
- for file_path in sorted(results.keys()):
335
- lines.append(f"{file_path}:")
336
- for line_num, line in results[file_path]:
337
- lines.append(f" {line_num}: {line}")
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: List[GrepMatch]) -> Dict[str, list[tuple[int, str]]]:
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: Dict[str, list[tuple[int, str]]] = {}
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: List[GrepMatch],
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 = 2000
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 2000 lines starting from the beginning of the file
159
- - 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
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"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.2.1rc2
3
+ Version: 0.2.3
4
4
  Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
5
  License: MIT
6
6
  Requires-Python: <4.0,>=3.11
@@ -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