deepagents 0.3.5__py3-none-any.whl → 0.3.7__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.
@@ -1,11 +1,4 @@
1
- """FilesystemBackend: Read and write files directly from the filesystem.
2
-
3
- Security and search upgrades:
4
- - Secure path resolution with root containment when in virtual_mode (sandboxed to cwd)
5
- - Prevent symlink-following on file I/O using O_NOFOLLOW when available
6
- - Ripgrep-powered grep with JSON parsing, plus Python fallback with regex
7
- and optional glob include filtering, while preserving virtual path behavior
8
- """
1
+ """`FilesystemBackend`: Read and write files directly from the filesystem."""
9
2
 
10
3
  import json
11
4
  import os
@@ -38,6 +31,39 @@ class FilesystemBackend(BackendProtocol):
38
31
  Files are accessed using their actual filesystem paths. Relative paths are
39
32
  resolved relative to the current working directory. Content is read/written
40
33
  as plain text, and metadata (timestamps) are derived from filesystem stats.
34
+
35
+ !!! warning "Security Warning"
36
+
37
+ This backend grants agents direct filesystem read/write access. Use with
38
+ caution and only in appropriate environments.
39
+
40
+ **Appropriate use cases:**
41
+
42
+ - Local development CLIs (coding assistants, development tools)
43
+ - CI/CD pipelines (see security considerations below)
44
+
45
+ **Inappropriate use cases:**
46
+
47
+ - Web servers or HTTP APIs - use `StateBackend`, `StoreBackend`, or
48
+ `SandboxBackend` instead
49
+
50
+ **Security risks:**
51
+
52
+ - Agents can read any accessible file, including secrets (API keys,
53
+ credentials, `.env` files)
54
+ - Combined with network tools, secrets may be exfiltrated via SSRF attacks
55
+ - File modifications are permanent and irreversible
56
+
57
+ **Recommended safeguards:**
58
+
59
+ 1. Enable Human-in-the-Loop (HITL) middleware to review sensitive operations
60
+ 2. Exclude secrets from accessible filesystem paths (especially in CI/CD)
61
+ 3. Use `SandboxBackend` for production environments requiring filesystem
62
+ interaction
63
+ 4. **Always** use `virtual_mode=True` with `root_dir` to enable path-based
64
+ access restrictions (blocks `..`, `~`, and absolute paths outside root).
65
+ Note that the default (`virtual_mode=False`) provides no security even with
66
+ `root_dir` set.
41
67
  """
42
68
 
43
69
  def __init__(
@@ -49,9 +75,35 @@ class FilesystemBackend(BackendProtocol):
49
75
  """Initialize filesystem backend.
50
76
 
51
77
  Args:
52
- root_dir: Optional root directory for file operations. If provided,
53
- all file paths will be resolved relative to this directory.
54
- If not provided, uses the current working directory.
78
+ root_dir: Optional root directory for file operations.
79
+
80
+ - If not provided, defaults to the current working directory.
81
+ - When `virtual_mode=False` (default): Only affects relative path
82
+ resolution. Provides **no security** - agents can access any file
83
+ using absolute paths or `..` sequences.
84
+ - When `virtual_mode=True`: All paths are restricted to this
85
+ directory with traversal protection enabled.
86
+
87
+ virtual_mode: Enable path-based access restrictions.
88
+
89
+ When `True`, all paths are treated as virtual paths anchored to
90
+ `root_dir`. Path traversal (`..`, `~`) is blocked and all resolved paths
91
+ are verified to remain within `root_dir`.
92
+
93
+ When `False` (default), **no security is provided**:
94
+
95
+ - Absolute paths (e.g., `/etc/passwd`) bypass `root_dir` entirely
96
+ - Relative paths with `..` can escape `root_dir`
97
+ - Agents have unrestricted filesystem access
98
+
99
+ **Security note:** `virtual_mode=True` provides path-based access
100
+ control, not process isolation. It restricts which files can be
101
+ accessed via paths, but does not sandbox the Python process itself.
102
+
103
+ max_file_size_mb: Maximum file size in megabytes for operations like
104
+ grep's Python fallback search.
105
+
106
+ Files exceeding this limit are skipped during search. Defaults to 10 MB.
55
107
  """
56
108
  self.cwd = Path(root_dir).resolve() if root_dir else Path.cwd()
57
109
  self.virtual_mode = virtual_mode
@@ -60,16 +112,22 @@ class FilesystemBackend(BackendProtocol):
60
112
  def _resolve_path(self, key: str) -> Path:
61
113
  """Resolve a file path with security checks.
62
114
 
63
- When virtual_mode=True, treat incoming paths as virtual absolute paths under
64
- self.cwd, disallow traversal (.., ~) and ensure resolved path stays within root.
65
- When virtual_mode=False, preserve legacy behavior: absolute paths are allowed
115
+ When `virtual_mode=True`, treat incoming paths as virtual absolute paths under
116
+ `self.cwd`, disallow traversal (`..`, `~`) and ensure resolved path stays within
117
+ root.
118
+
119
+ When `virtual_mode=False`, preserve legacy behavior: absolute paths are allowed
66
120
  as-is; relative paths resolve under cwd.
67
121
 
68
122
  Args:
69
- key: File path (absolute, relative, or virtual when virtual_mode=True)
123
+ key: File path (absolute, relative, or virtual when `virtual_mode=True`).
70
124
 
71
125
  Returns:
72
- Resolved absolute Path object
126
+ Resolved absolute `Path` object.
127
+
128
+ Raises:
129
+ ValueError: If path traversal is attempted in `virtual_mode` or if the
130
+ resolved path escapes the root directory.
73
131
  """
74
132
  if self.virtual_mode:
75
133
  vpath = key if key.startswith("/") else "/" + key
@@ -94,8 +152,9 @@ class FilesystemBackend(BackendProtocol):
94
152
  path: Absolute directory path to list files from.
95
153
 
96
154
  Returns:
97
- List of FileInfo-like dicts for files and directories directly in the directory.
98
- Directories have a trailing / in their path and is_dir=True.
155
+ List of `FileInfo`-like dicts for files and directories directly in the
156
+ directory. Directories have a trailing `/` in their path and
157
+ `is_dir=True`.
99
158
  """
100
159
  dir_path = self._resolve_path(path)
101
160
  if not dir_path.exists() or not dir_path.is_dir():
@@ -242,7 +301,14 @@ class FilesystemBackend(BackendProtocol):
242
301
  content: str,
243
302
  ) -> WriteResult:
244
303
  """Create a new file with content.
245
- Returns WriteResult. External storage sets files_update=None.
304
+
305
+ Args:
306
+ file_path: Path where the new file will be created.
307
+ content: Text content to write to the file.
308
+
309
+ Returns:
310
+ `WriteResult` with path on success, or error message if the file
311
+ already exists or write fails. External storage sets `files_update=None`.
246
312
  """
247
313
  resolved_path = self._resolve_path(file_path)
248
314
 
@@ -273,7 +339,18 @@ class FilesystemBackend(BackendProtocol):
273
339
  replace_all: bool = False,
274
340
  ) -> EditResult:
275
341
  """Edit a file by replacing string occurrences.
276
- Returns EditResult. External storage sets files_update=None.
342
+
343
+ Args:
344
+ file_path: Path to the file to edit.
345
+ old_string: The text to search for and replace.
346
+ new_string: The replacement text.
347
+ replace_all: If `True`, replace all occurrences. If `False` (default),
348
+ replace only if exactly one occurrence exists.
349
+
350
+ Returns:
351
+ `EditResult` with path and occurrence count on success, or error
352
+ message if file not found or replacement fails. External storage sets
353
+ `files_update=None`.
277
354
  """
278
355
  resolved_path = self._resolve_path(file_path)
279
356
 
@@ -311,6 +388,19 @@ class FilesystemBackend(BackendProtocol):
311
388
  path: str | None = None,
312
389
  glob: str | None = None,
313
390
  ) -> list[GrepMatch] | str:
391
+ """Search for a regex pattern in files.
392
+
393
+ Uses ripgrep if available, falling back to Python regex search.
394
+
395
+ Args:
396
+ pattern: Regular expression pattern to search for.
397
+ path: Directory or file path to search in. Defaults to current directory.
398
+ glob: Optional glob pattern to filter which files to search.
399
+
400
+ Returns:
401
+ List of GrepMatch dicts containing path, line number, and matched text.
402
+ Returns an error string if the regex pattern is invalid.
403
+ """
314
404
  # Validate regex
315
405
  try:
316
406
  re.compile(pattern)
@@ -338,6 +428,17 @@ class FilesystemBackend(BackendProtocol):
338
428
  return matches
339
429
 
340
430
  def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]] | None:
431
+ """Search using ripgrep with JSON output parsing.
432
+
433
+ Args:
434
+ pattern: Regex pattern to search for.
435
+ base_full: Resolved base path to search in.
436
+ include_glob: Optional glob pattern to filter files.
437
+
438
+ Returns:
439
+ Dict mapping file paths to list of `(line_number, line_text)` tuples.
440
+ Returns `None` if ripgrep is unavailable or times out.
441
+ """
341
442
  cmd = ["rg", "--json"]
342
443
  if include_glob:
343
444
  cmd.extend(["--glob", include_glob])
@@ -383,6 +484,18 @@ class FilesystemBackend(BackendProtocol):
383
484
  return results
384
485
 
385
486
  def _python_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]]:
487
+ """Fallback search using Python regex when ripgrep is unavailable.
488
+
489
+ Recursively searches files, respecting `max_file_size_bytes` limit.
490
+
491
+ Args:
492
+ pattern: Regex pattern to search for.
493
+ base_full: Resolved base path to search in.
494
+ include_glob: Optional glob pattern to filter files by name.
495
+
496
+ Returns:
497
+ Dict mapping file paths to list of `(line_number, line_text)` tuples.
498
+ """
386
499
  try:
387
500
  regex = re.compile(pattern)
388
501
  except re.error:
@@ -392,7 +505,10 @@ class FilesystemBackend(BackendProtocol):
392
505
  root = base_full if base_full.is_dir() else base_full.parent
393
506
 
394
507
  for fp in root.rglob("*"):
395
- if not fp.is_file():
508
+ try:
509
+ if not fp.is_file():
510
+ continue
511
+ except (PermissionError, OSError):
396
512
  continue
397
513
  if include_glob and not wcglob.globmatch(fp.name, include_glob, flags=wcglob.BRACE):
398
514
  continue
@@ -419,6 +535,16 @@ class FilesystemBackend(BackendProtocol):
419
535
  return results
420
536
 
421
537
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
538
+ """Find files matching a glob pattern.
539
+
540
+ Args:
541
+ pattern: Glob pattern to match files against (e.g., `'*.py'`, `'**/*.txt'`).
542
+ path: Base directory to search from. Defaults to root (`/`).
543
+
544
+ Returns:
545
+ List of `FileInfo` dicts for matching files, sorted by path. Each dict
546
+ contains `path`, `is_dir`, `size`, and `modified_at` fields.
547
+ """
422
548
  if pattern.startswith("/"):
423
549
  pattern = pattern.lstrip("/")
424
550
 
@@ -432,7 +558,7 @@ class FilesystemBackend(BackendProtocol):
432
558
  for matched_path in search_path.rglob(pattern):
433
559
  try:
434
560
  is_file = matched_path.is_file()
435
- except OSError:
561
+ except (PermissionError, OSError):
436
562
  continue
437
563
  if not is_file:
438
564
  continue
@@ -172,7 +172,7 @@ try:
172
172
  with os.scandir(path) as it:
173
173
  for entry in it:
174
174
  result = {{
175
- 'path': entry.name,
175
+ 'path': os.path.join(path, entry.name),
176
176
  'is_dir': entry.is_dir(follow_symlinks=False)
177
177
  }}
178
178
  print(json.dumps(result))
@@ -279,6 +279,30 @@ class StoreBackend(BackendProtocol):
279
279
 
280
280
  return format_read_response(file_data, offset, limit)
281
281
 
282
+ async def aread(
283
+ self,
284
+ file_path: str,
285
+ offset: int = 0,
286
+ limit: int = 2000,
287
+ ) -> str:
288
+ """Async version of read using native store async methods.
289
+
290
+ This avoids sync calls in async context by using store.aget directly.
291
+ """
292
+ store = self._get_store()
293
+ namespace = self._get_namespace()
294
+ item: Item | None = await store.aget(namespace, file_path)
295
+
296
+ if item is None:
297
+ return f"Error: File '{file_path}' not found"
298
+
299
+ try:
300
+ file_data = self._convert_store_item_to_file_data(item)
301
+ except ValueError as e:
302
+ return f"Error: {e}"
303
+
304
+ return format_read_response(file_data, offset, limit)
305
+
282
306
  def write(
283
307
  self,
284
308
  file_path: str,
@@ -301,6 +325,29 @@ class StoreBackend(BackendProtocol):
301
325
  store.put(namespace, file_path, store_value)
302
326
  return WriteResult(path=file_path, files_update=None)
303
327
 
328
+ async def awrite(
329
+ self,
330
+ file_path: str,
331
+ content: str,
332
+ ) -> WriteResult:
333
+ """Async version of write using native store async methods.
334
+
335
+ This avoids sync calls in async context by using store.aget/aput directly.
336
+ """
337
+ store = self._get_store()
338
+ namespace = self._get_namespace()
339
+
340
+ # Check if file exists using async method
341
+ existing = await store.aget(namespace, file_path)
342
+ if existing is not None:
343
+ return WriteResult(error=f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path.")
344
+
345
+ # Create new file using async method
346
+ file_data = create_file_data(content)
347
+ store_value = self._convert_file_data_to_store_value(file_data)
348
+ await store.aput(namespace, file_path, store_value)
349
+ return WriteResult(path=file_path, files_update=None)
350
+
304
351
  def edit(
305
352
  self,
306
353
  file_path: str,
@@ -338,6 +385,44 @@ class StoreBackend(BackendProtocol):
338
385
  store.put(namespace, file_path, store_value)
339
386
  return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
340
387
 
388
+ async def aedit(
389
+ self,
390
+ file_path: str,
391
+ old_string: str,
392
+ new_string: str,
393
+ replace_all: bool = False,
394
+ ) -> EditResult:
395
+ """Async version of edit using native store async methods.
396
+
397
+ This avoids sync calls in async context by using store.aget/aput directly.
398
+ """
399
+ store = self._get_store()
400
+ namespace = self._get_namespace()
401
+
402
+ # Get existing file using async method
403
+ item = await store.aget(namespace, file_path)
404
+ if item is None:
405
+ return EditResult(error=f"Error: File '{file_path}' not found")
406
+
407
+ try:
408
+ file_data = self._convert_store_item_to_file_data(item)
409
+ except ValueError as e:
410
+ return EditResult(error=f"Error: {e}")
411
+
412
+ content = file_data_to_string(file_data)
413
+ result = perform_string_replacement(content, old_string, new_string, replace_all)
414
+
415
+ if isinstance(result, str):
416
+ return EditResult(error=result)
417
+
418
+ new_content, occurrences = result
419
+ new_file_data = update_file_data(file_data, new_content)
420
+
421
+ # Update file in store using async method
422
+ store_value = self._convert_file_data_to_store_value(new_file_data)
423
+ await store.aput(namespace, file_path, store_value)
424
+ return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
425
+
341
426
  # Removed legacy grep() convenience to keep lean surface
342
427
 
343
428
  def grep_raw(
@@ -12,11 +12,10 @@ from typing import Any, Literal
12
12
 
13
13
  import wcmatch.glob as wcglob
14
14
 
15
- from deepagents.backends.protocol import FileInfo as _FileInfo
16
- from deepagents.backends.protocol import GrepMatch as _GrepMatch
15
+ from deepagents.backends.protocol import FileInfo as _FileInfo, GrepMatch as _GrepMatch
17
16
 
18
17
  EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
19
- MAX_LINE_LENGTH = 10000
18
+ MAX_LINE_LENGTH = 5000
20
19
  LINE_NUMBER_WIDTH = 6
21
20
  TOOL_RESULT_TOKEN_LIMIT = 20000 # Same threshold as eviction
22
21
  TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]"
deepagents/graph.py CHANGED
@@ -5,13 +5,13 @@ from typing import Any
5
5
 
6
6
  from langchain.agents import create_agent
7
7
  from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig, TodoListMiddleware
8
- from langchain.agents.middleware.summarization import SummarizationMiddleware
9
8
  from langchain.agents.middleware.types import AgentMiddleware
10
9
  from langchain.agents.structured_output import ResponseFormat
11
10
  from langchain.chat_models import init_chat_model
12
11
  from langchain_anthropic import ChatAnthropic
13
12
  from langchain_anthropic.middleware import AnthropicPromptCachingMiddleware
14
13
  from langchain_core.language_models import BaseChatModel
14
+ from langchain_core.messages import SystemMessage
15
15
  from langchain_core.tools import BaseTool
16
16
  from langgraph.cache.base import BaseCache
17
17
  from langgraph.graph.state import CompiledStateGraph
@@ -25,6 +25,7 @@ from deepagents.middleware.memory import MemoryMiddleware
25
25
  from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
26
26
  from deepagents.middleware.skills import SkillsMiddleware
27
27
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
28
+ from deepagents.middleware.summarization import SummarizationMiddleware
28
29
 
29
30
  BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
30
31
 
@@ -37,7 +38,7 @@ def get_default_model() -> ChatAnthropic:
37
38
  """
38
39
  return ChatAnthropic(
39
40
  model_name="claude-sonnet-4-5-20250929",
40
- max_tokens=20000,
41
+ max_tokens=20000, # type: ignore[call-arg]
41
42
  )
42
43
 
43
44
 
@@ -45,7 +46,7 @@ def create_deep_agent(
45
46
  model: str | BaseChatModel | None = None,
46
47
  tools: Sequence[BaseTool | Callable | dict[str, Any]] | None = None,
47
48
  *,
48
- system_prompt: str | None = None,
49
+ system_prompt: str | SystemMessage | None = None,
49
50
  middleware: Sequence[AgentMiddleware] = (),
50
51
  subagents: list[SubAgent | CompiledSubAgent] | None = None,
51
52
  skills: list[str] | None = None,
@@ -62,19 +63,36 @@ def create_deep_agent(
62
63
  ) -> CompiledStateGraph:
63
64
  """Create a deep agent.
64
65
 
65
- This agent will by default have access to a tool to write todos (`write_todos`),
66
- seven file and execution tools: `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`, `execute`,
67
- and a tool to call subagents.
66
+ !!! warning "Deep agents require a LLM that supports tool calling!"
67
+
68
+ By default, this agent has access to the following tools:
69
+
70
+ - `write_todos`: manage a todo list
71
+ - `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`: file operations
72
+ - `execute`: run shell commands
73
+ - `task`: call subagents
68
74
 
69
75
  The `execute` tool allows running shell commands if the backend implements `SandboxBackendProtocol`.
70
76
  For non-sandbox backends, the `execute` tool will return an error message.
71
77
 
72
78
  Args:
73
- model: The model to use. Defaults to `claude-sonnet-4-5-20250929`.
79
+ model: The model to use.
80
+
81
+ Defaults to `claude-sonnet-4-5-20250929`.
82
+
83
+ Use the `provider:model` format (e.g., `openai:gpt-5`) to quickly switch between models.
74
84
  tools: The tools the agent should have access to.
75
- system_prompt: The additional instructions the agent should have. Will go in
76
- the system prompt.
77
- middleware: Additional middleware to apply after standard middleware.
85
+
86
+ In addition to custom tools you provide, deep agents include built-in tools for planning,
87
+ file management, and subagent spawning.
88
+ system_prompt: Custom system instructions to prepend before the base deep agent
89
+ prompt.
90
+
91
+ If a string, it's concatenated with the base prompt.
92
+ middleware: Additional middleware to apply after the standard middleware stack
93
+ (`TodoListMiddleware`, `FilesystemMiddleware`, `SubAgentMiddleware`,
94
+ `SummarizationMiddleware`, `AnthropicPromptCachingMiddleware`,
95
+ `PatchToolCallsMiddleware`).
78
96
  subagents: The subagents to use.
79
97
 
80
98
  Each subagent should be a `dict` with the following keys:
@@ -93,7 +111,10 @@ def create_deep_agent(
93
111
  to the backend's `root_dir`. Later sources override earlier ones for skills with the
94
112
  same name (last one wins).
95
113
  memory: Optional list of memory file paths (`AGENTS.md` files) to load
96
- (e.g., `["/memory/AGENTS.md"]`). Display names are automatically derived from paths.
114
+ (e.g., `["/memory/AGENTS.md"]`).
115
+
116
+ Display names are automatically derived from paths.
117
+
97
118
  Memory is loaded at agent startup and added into the system prompt.
98
119
  response_format: A structured output response format to use for the agent.
99
120
  context_schema: The schema of the deep agent.
@@ -104,6 +125,10 @@ def create_deep_agent(
104
125
  Pass either a `Backend` instance or a callable factory like `lambda rt: StateBackend(rt)`.
105
126
  For execution support, use a backend that implements `SandboxBackendProtocol`.
106
127
  interrupt_on: Mapping of tool names to interrupt configs.
128
+
129
+ Pass to pause agent execution at specified tool calls for human approval or modification.
130
+
131
+ Example: `interrupt_on={"edit_file": True}` pauses before every edit.
107
132
  debug: Whether to enable debug mode. Passed through to `create_agent`.
108
133
  name: The name of the agent. Passed through to `create_agent`.
109
134
  cache: The cache to use for the agent. Passed through to `create_agent`.
@@ -124,9 +149,17 @@ def create_deep_agent(
124
149
  ):
125
150
  trigger = ("fraction", 0.85)
126
151
  keep = ("fraction", 0.10)
152
+ truncate_args_settings = {
153
+ "trigger": ("fraction", 0.85),
154
+ "keep": ("fraction", 0.10),
155
+ }
127
156
  else:
128
157
  trigger = ("tokens", 170000)
129
158
  keep = ("messages", 6)
159
+ truncate_args_settings = {
160
+ "trigger": ("messages", 20),
161
+ "keep": ("messages", 20),
162
+ }
130
163
 
131
164
  # Build middleware stack for subagents (includes skills if provided)
132
165
  subagent_middleware: list[AgentMiddleware] = [
@@ -142,9 +175,11 @@ def create_deep_agent(
142
175
  FilesystemMiddleware(backend=backend),
143
176
  SummarizationMiddleware(
144
177
  model=model,
178
+ backend=backend,
145
179
  trigger=trigger,
146
180
  keep=keep,
147
181
  trim_tokens_to_summarize=None,
182
+ truncate_args_settings=truncate_args_settings,
148
183
  ),
149
184
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
150
185
  PatchToolCallsMiddleware(),
@@ -172,9 +207,11 @@ def create_deep_agent(
172
207
  ),
173
208
  SummarizationMiddleware(
174
209
  model=model,
210
+ backend=backend,
175
211
  trigger=trigger,
176
212
  keep=keep,
177
213
  trim_tokens_to_summarize=None,
214
+ truncate_args_settings=truncate_args_settings,
178
215
  ),
179
216
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
180
217
  PatchToolCallsMiddleware(),
@@ -185,9 +222,23 @@ def create_deep_agent(
185
222
  if interrupt_on is not None:
186
223
  deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
187
224
 
225
+ # Combine system_prompt with BASE_AGENT_PROMPT
226
+ if system_prompt is None:
227
+ final_system_prompt: str | SystemMessage = BASE_AGENT_PROMPT
228
+ elif isinstance(system_prompt, SystemMessage):
229
+ # SystemMessage: append BASE_AGENT_PROMPT to content_blocks
230
+ new_content = [
231
+ *system_prompt.content_blocks,
232
+ {"type": "text", "text": f"\n\n{BASE_AGENT_PROMPT}"},
233
+ ]
234
+ final_system_prompt = SystemMessage(content=new_content)
235
+ else:
236
+ # String: simple concatenation
237
+ final_system_prompt = system_prompt + "\n\n" + BASE_AGENT_PROMPT
238
+
188
239
  return create_agent(
189
240
  model,
190
- system_prompt=system_prompt + "\n\n" + BASE_AGENT_PROMPT if system_prompt else BASE_AGENT_PROMPT,
241
+ system_prompt=final_system_prompt,
191
242
  tools=tools,
192
243
  middleware=deepagent_middleware,
193
244
  response_format=response_format,
@@ -1,9 +1,10 @@
1
- """Middleware for the DeepAgent."""
1
+ """Middleware for the agent."""
2
2
 
3
3
  from deepagents.middleware.filesystem import FilesystemMiddleware
4
4
  from deepagents.middleware.memory import MemoryMiddleware
5
5
  from deepagents.middleware.skills import SkillsMiddleware
6
6
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
7
+ from deepagents.middleware.summarization import SummarizationMiddleware
7
8
 
8
9
  __all__ = [
9
10
  "CompiledSubAgent",
@@ -12,4 +13,5 @@ __all__ = [
12
13
  "SkillsMiddleware",
13
14
  "SubAgent",
14
15
  "SubAgentMiddleware",
16
+ "SummarizationMiddleware",
15
17
  ]
@@ -0,0 +1,23 @@
1
+ """Utility functions for middleware."""
2
+
3
+ from langchain_core.messages import SystemMessage
4
+
5
+
6
+ def append_to_system_message(
7
+ system_message: SystemMessage | None,
8
+ text: str,
9
+ ) -> SystemMessage:
10
+ """Append text to a system message.
11
+
12
+ Args:
13
+ system_message: Existing system message or None.
14
+ text: Text to add to the system message.
15
+
16
+ Returns:
17
+ New SystemMessage with the text appended.
18
+ """
19
+ new_content: list[str | dict[str, str]] = list(system_message.content_blocks) if system_message else []
20
+ if new_content:
21
+ text = f"\n\n{text}"
22
+ new_content.append({"type": "text", "text": text})
23
+ return SystemMessage(content=new_content)