deepagents 0.2.4__tar.gz → 0.2.6__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 (27) hide show
  1. {deepagents-0.2.4/src/deepagents.egg-info → deepagents-0.2.6}/PKG-INFO +5 -10
  2. {deepagents-0.2.4 → deepagents-0.2.6}/README.md +1 -1
  3. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/composite.py +66 -41
  4. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/filesystem.py +92 -86
  5. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/protocol.py +87 -13
  6. deepagents-0.2.6/deepagents/backends/sandbox.py +341 -0
  7. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/state.py +59 -58
  8. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/store.py +73 -74
  9. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/utils.py +7 -21
  10. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/graph.py +8 -4
  11. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/middleware/filesystem.py +271 -66
  12. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/middleware/resumable_shell.py +5 -4
  13. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/middleware/subagents.py +8 -6
  14. {deepagents-0.2.4 → deepagents-0.2.6/deepagents.egg-info}/PKG-INFO +5 -10
  15. deepagents-0.2.6/deepagents.egg-info/SOURCES.txt +22 -0
  16. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents.egg-info/requires.txt +3 -7
  17. {deepagents-0.2.4 → deepagents-0.2.6}/pyproject.toml +16 -27
  18. deepagents-0.2.4/LICENSE +0 -21
  19. deepagents-0.2.4/src/deepagents.egg-info/SOURCES.txt +0 -23
  20. deepagents-0.2.4/tests/test_middleware.py +0 -1134
  21. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/__init__.py +0 -0
  22. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/backends/__init__.py +1 -1
  23. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/middleware/__init__.py +0 -0
  24. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents/middleware/patch_tool_calls.py +0 -0
  25. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents.egg-info/dependency_links.txt +0 -0
  26. {deepagents-0.2.4/src → deepagents-0.2.6}/deepagents.egg-info/top_level.txt +0 -0
  27. {deepagents-0.2.4 → deepagents-0.2.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.2.4
3
+ Version: 0.2.6
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
  Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
@@ -11,18 +11,13 @@ Project-URL: Slack, https://www.langchain.com/join-community
11
11
  Project-URL: Reddit, https://www.reddit.com/r/LangChain/
12
12
  Requires-Python: <4.0,>=3.11
13
13
  Description-Content-Type: text/markdown
14
- License-File: LICENSE
15
14
  Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
16
15
  Requires-Dist: langchain<2.0.0,>=1.0.2
17
16
  Requires-Dist: langchain-core<2.0.0,>=1.0.0
18
17
  Requires-Dist: wcmatch
19
- Provides-Extra: dev
20
- Requires-Dist: pytest; extra == "dev"
21
- Requires-Dist: pytest-cov; extra == "dev"
22
- Requires-Dist: build; extra == "dev"
23
- Requires-Dist: twine; extra == "dev"
24
- Requires-Dist: langchain-openai; extra == "dev"
25
- Dynamic: license-file
18
+ Requires-Dist: daytona>=0.113.0
19
+ Requires-Dist: runloop-api-client>=0.66.1
20
+ Requires-Dist: tavily>=1.1.0
26
21
 
27
22
  # 🧠🤖Deep Agents
28
23
 
@@ -353,7 +348,7 @@ Deep Agents are built with a modular middleware architecture. As a reminder, Dee
353
348
  - A filesystem for storing context and long-term memories
354
349
  - The ability to spawn subagents
355
350
 
356
- Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **PlanningMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
351
+ Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **TodoListMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
357
352
 
358
353
  Middleware is a composable concept, and you can choose to add as many or as few middleware to an agent depending on your use case. That means that you can also use any of the aforementioned middleware independently!
359
354
 
@@ -327,7 +327,7 @@ Deep Agents are built with a modular middleware architecture. As a reminder, Dee
327
327
  - A filesystem for storing context and long-term memories
328
328
  - The ability to spawn subagents
329
329
 
330
- Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **PlanningMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
330
+ Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **TodoListMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
331
331
 
332
332
  Middleware is a composable concept, and you can choose to add as many or as few middleware to an agent depending on your use case. That means that you can also use any of the aforementioned middleware independently!
333
333
 
@@ -1,14 +1,18 @@
1
1
  """CompositeBackend: Route operations to different backends based on path prefix."""
2
2
 
3
- from typing import Optional
4
-
5
- from deepagents.backends.protocol import BackendProtocol, WriteResult, EditResult
3
+ from deepagents.backends.protocol import (
4
+ BackendProtocol,
5
+ EditResult,
6
+ ExecuteResponse,
7
+ FileInfo,
8
+ GrepMatch,
9
+ SandboxBackendProtocol,
10
+ WriteResult,
11
+ )
6
12
  from deepagents.backends.state import StateBackend
7
- from deepagents.backends.utils import FileInfo, GrepMatch
8
13
 
9
14
 
10
15
  class CompositeBackend:
11
-
12
16
  def __init__(
13
17
  self,
14
18
  default: BackendProtocol | StateBackend,
@@ -19,16 +23,16 @@ class CompositeBackend:
19
23
 
20
24
  # Virtual routes
21
25
  self.routes = routes
22
-
26
+
23
27
  # Sort routes by length (longest first) for correct prefix matching
24
28
  self.sorted_routes = sorted(routes.items(), key=lambda x: len(x[0]), reverse=True)
25
29
 
26
30
  def _get_backend_and_key(self, key: str) -> tuple[BackendProtocol, str]:
27
31
  """Determine which backend handles this key and strip prefix.
28
-
32
+
29
33
  Args:
30
34
  key: Original file path
31
-
35
+
32
36
  Returns:
33
37
  Tuple of (backend, stripped_key) where stripped_key has the route
34
38
  prefix removed (but keeps leading slash).
@@ -38,12 +42,12 @@ class CompositeBackend:
38
42
  if key.startswith(prefix):
39
43
  # Strip full prefix and ensure a leading slash remains
40
44
  # e.g., "/memories/notes.txt" → "/notes.txt"; "/memories/" → "/"
41
- suffix = key[len(prefix):]
45
+ suffix = key[len(prefix) :]
42
46
  stripped_key = f"/{suffix}" if suffix else "/"
43
47
  return backend, stripped_key
44
-
48
+
45
49
  return self.default, key
46
-
50
+
47
51
  def ls_info(self, path: str) -> list[FileInfo]:
48
52
  """List files and directories in the specified directory (non-recursive).
49
53
 
@@ -58,7 +62,7 @@ class CompositeBackend:
58
62
  for route_prefix, backend in self.sorted_routes:
59
63
  if path.startswith(route_prefix.rstrip("/")):
60
64
  # Query only the matching routed backend
61
- suffix = path[len(route_prefix):]
65
+ suffix = path[len(route_prefix) :]
62
66
  search_path = f"/{suffix}" if suffix else "/"
63
67
  infos = backend.ls_info(search_path)
64
68
  prefixed: list[FileInfo] = []
@@ -74,12 +78,14 @@ class CompositeBackend:
74
78
  results.extend(self.default.ls_info(path))
75
79
  for route_prefix, backend in self.sorted_routes:
76
80
  # Add the route itself as a directory (e.g., /memories/)
77
- results.append({
78
- "path": route_prefix,
79
- "is_dir": True,
80
- "size": 0,
81
- "modified_at": "",
82
- })
81
+ results.append(
82
+ {
83
+ "path": route_prefix,
84
+ "is_dir": True,
85
+ "size": 0,
86
+ "modified_at": "",
87
+ }
88
+ )
83
89
 
84
90
  results.sort(key=lambda x: x.get("path", ""))
85
91
  return results
@@ -87,15 +93,14 @@ class CompositeBackend:
87
93
  # Path doesn't match a route: query only default backend
88
94
  return self.default.ls_info(path)
89
95
 
90
-
91
96
  def read(
92
- self,
97
+ self,
93
98
  file_path: str,
94
99
  offset: int = 0,
95
100
  limit: int = 2000,
96
101
  ) -> str:
97
102
  """Read file content, routing to appropriate backend.
98
-
103
+
99
104
  Args:
100
105
  file_path: Absolute file path
101
106
  offset: Line offset to start reading from (0-indexed)
@@ -105,17 +110,16 @@ class CompositeBackend:
105
110
  backend, stripped_key = self._get_backend_and_key(file_path)
106
111
  return backend.read(stripped_key, offset=offset, limit=limit)
107
112
 
108
-
109
113
  def grep_raw(
110
114
  self,
111
115
  pattern: str,
112
- path: Optional[str] = None,
113
- glob: Optional[str] = None,
116
+ path: str | None = None,
117
+ glob: str | None = None,
114
118
  ) -> list[GrepMatch] | str:
115
119
  # If path targets a specific route, search only that backend
116
120
  for route_prefix, backend in self.sorted_routes:
117
121
  if path is not None and path.startswith(route_prefix.rstrip("/")):
118
- search_path = path[len(route_prefix) - 1:]
122
+ search_path = path[len(route_prefix) - 1 :]
119
123
  raw = backend.grep_raw(pattern, search_path if search_path else "/", glob)
120
124
  if isinstance(raw, str):
121
125
  return raw
@@ -137,19 +141,16 @@ class CompositeBackend:
137
141
  all_matches.extend({**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw)
138
142
 
139
143
  return all_matches
140
-
144
+
141
145
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
142
146
  results: list[FileInfo] = []
143
147
 
144
148
  # Route based on path, not pattern
145
149
  for route_prefix, backend in self.sorted_routes:
146
150
  if path.startswith(route_prefix.rstrip("/")):
147
- search_path = path[len(route_prefix) - 1:]
151
+ search_path = path[len(route_prefix) - 1 :]
148
152
  infos = backend.glob_info(pattern, search_path if search_path else "/")
149
- return [
150
- {**fi, "path": f"{route_prefix[:-1]}{fi['path']}"}
151
- for fi in infos
152
- ]
153
+ return [{**fi, "path": f"{route_prefix[:-1]}{fi['path']}"} for fi in infos]
153
154
 
154
155
  # Path doesn't match any specific route - search default backend AND all routed backends
155
156
  results.extend(self.default.glob_info(pattern, path))
@@ -162,11 +163,10 @@ class CompositeBackend:
162
163
  results.sort(key=lambda x: x.get("path", ""))
163
164
  return results
164
165
 
165
-
166
166
  def write(
167
- self,
168
- file_path: str,
169
- content: str,
167
+ self,
168
+ file_path: str,
169
+ content: str,
170
170
  ) -> WriteResult:
171
171
  """Create a new file, routing to appropriate backend.
172
172
 
@@ -191,11 +191,11 @@ class CompositeBackend:
191
191
  return res
192
192
 
193
193
  def edit(
194
- self,
195
- file_path: str,
196
- old_string: str,
197
- new_string: str,
198
- replace_all: bool = False,
194
+ self,
195
+ file_path: str,
196
+ old_string: str,
197
+ new_string: str,
198
+ replace_all: bool = False,
199
199
  ) -> EditResult:
200
200
  """Edit a file, routing to appropriate backend.
201
201
 
@@ -220,5 +220,30 @@ class CompositeBackend:
220
220
  pass
221
221
  return res
222
222
 
223
+ def execute(
224
+ self,
225
+ command: str,
226
+ ) -> ExecuteResponse:
227
+ """Execute a command via the default backend.
228
+
229
+ Execution is not path-specific, so it always delegates to the default backend.
230
+ The default backend must implement SandboxBackendProtocol for this to work.
223
231
 
224
-
232
+ Args:
233
+ command: Full shell command string to execute.
234
+
235
+ Returns:
236
+ ExecuteResponse with combined output, exit code, and truncation flag.
237
+
238
+ Raises:
239
+ NotImplementedError: If default backend doesn't support execution.
240
+ """
241
+ if isinstance(self.default, SandboxBackendProtocol):
242
+ return self.default.execute(command)
243
+
244
+ # This shouldn't be reached if the runtime check in the execute tool works correctly,
245
+ # but we include it as a safety fallback.
246
+ raise NotImplementedError(
247
+ "Default backend doesn't support command execution (SandboxBackendProtocol). "
248
+ "To enable execution, provide a default backend that implements SandboxBackendProtocol."
249
+ )
@@ -7,26 +7,24 @@ Security and search upgrades:
7
7
  and optional glob include filtering, while preserving virtual path behavior
8
8
  """
9
9
 
10
+ import json
10
11
  import os
11
12
  import re
12
- import json
13
13
  import subprocess
14
14
  from datetime import datetime
15
15
  from pathlib import Path
16
- from typing import Optional
17
16
 
18
- from .utils import (
17
+ import wcmatch.glob as wcglob
18
+
19
+ from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
20
+ from deepagents.backends.utils import (
19
21
  check_empty_content,
20
22
  format_content_with_line_numbers,
21
23
  perform_string_replacement,
22
24
  )
23
- import wcmatch.glob as wcglob
24
- from deepagents.backends.utils import FileInfo, GrepMatch
25
- from deepagents.backends.protocol import WriteResult, EditResult
26
-
27
25
 
28
26
 
29
- class FilesystemBackend:
27
+ class FilesystemBackend(BackendProtocol):
30
28
  """Backend that reads and writes files directly from the filesystem.
31
29
 
32
30
  Files are accessed using their actual filesystem paths. Relative paths are
@@ -35,13 +33,13 @@ class FilesystemBackend:
35
33
  """
36
34
 
37
35
  def __init__(
38
- self,
39
- root_dir: Optional[str | Path] = None,
36
+ self,
37
+ root_dir: str | Path | None = None,
40
38
  virtual_mode: bool = False,
41
39
  max_file_size_mb: int = 10,
42
40
  ) -> None:
43
41
  """Initialize filesystem backend.
44
-
42
+
45
43
  Args:
46
44
  root_dir: Optional root directory for file operations. If provided,
47
45
  all file paths will be resolved relative to this directory.
@@ -118,32 +116,36 @@ class FilesystemBackend:
118
116
  if is_file:
119
117
  try:
120
118
  st = child_path.stat()
121
- results.append({
122
- "path": abs_path,
123
- "is_dir": False,
124
- "size": int(st.st_size),
125
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
126
- })
119
+ results.append(
120
+ {
121
+ "path": abs_path,
122
+ "is_dir": False,
123
+ "size": int(st.st_size),
124
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
125
+ }
126
+ )
127
127
  except OSError:
128
128
  results.append({"path": abs_path, "is_dir": False})
129
129
  elif is_dir:
130
130
  try:
131
131
  st = child_path.stat()
132
- results.append({
133
- "path": abs_path + "/",
134
- "is_dir": True,
135
- "size": 0,
136
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
137
- })
132
+ results.append(
133
+ {
134
+ "path": abs_path + "/",
135
+ "is_dir": True,
136
+ "size": 0,
137
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
138
+ }
139
+ )
138
140
  except OSError:
139
141
  results.append({"path": abs_path + "/", "is_dir": True})
140
142
  else:
141
143
  # Virtual mode: strip cwd prefix
142
144
  if abs_path.startswith(cwd_str):
143
- relative_path = abs_path[len(cwd_str):]
145
+ relative_path = abs_path[len(cwd_str) :]
144
146
  elif abs_path.startswith(str(self.cwd)):
145
147
  # Handle case where cwd doesn't end with /
146
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
148
+ relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
147
149
  else:
148
150
  # Path is outside cwd, return as-is or skip
149
151
  relative_path = abs_path
@@ -153,23 +155,27 @@ class FilesystemBackend:
153
155
  if is_file:
154
156
  try:
155
157
  st = child_path.stat()
156
- results.append({
157
- "path": virt_path,
158
- "is_dir": False,
159
- "size": int(st.st_size),
160
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
161
- })
158
+ results.append(
159
+ {
160
+ "path": virt_path,
161
+ "is_dir": False,
162
+ "size": int(st.st_size),
163
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
164
+ }
165
+ )
162
166
  except OSError:
163
167
  results.append({"path": virt_path, "is_dir": False})
164
168
  elif is_dir:
165
169
  try:
166
170
  st = child_path.stat()
167
- results.append({
168
- "path": virt_path + "/",
169
- "is_dir": True,
170
- "size": 0,
171
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
172
- })
171
+ results.append(
172
+ {
173
+ "path": virt_path + "/",
174
+ "is_dir": True,
175
+ "size": 0,
176
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
177
+ }
178
+ )
173
179
  except OSError:
174
180
  results.append({"path": virt_path + "/", "is_dir": True})
175
181
  except (OSError, PermissionError):
@@ -180,15 +186,15 @@ class FilesystemBackend:
180
186
  return results
181
187
 
182
188
  # Removed legacy ls() convenience to keep lean surface
183
-
189
+
184
190
  def read(
185
- self,
191
+ self,
186
192
  file_path: str,
187
193
  offset: int = 0,
188
194
  limit: int = 2000,
189
195
  ) -> str:
190
196
  """Read file content with line numbers.
191
-
197
+
192
198
  Args:
193
199
  file_path: Absolute or relative file path
194
200
  offset: Line offset to start reading from (0-indexed)
@@ -196,10 +202,10 @@ class FilesystemBackend:
196
202
  Formatted file content with line numbers, or error message.
197
203
  """
198
204
  resolved_path = self._resolve_path(file_path)
199
-
205
+
200
206
  if not resolved_path.exists() or not resolved_path.is_file():
201
207
  return f"Error: File '{file_path}' not found"
202
-
208
+
203
209
  try:
204
210
  # Open with O_NOFOLLOW where available to avoid symlink traversal
205
211
  try:
@@ -208,27 +214,27 @@ class FilesystemBackend:
208
214
  content = f.read()
209
215
  except OSError:
210
216
  # Fallback to normal open if O_NOFOLLOW unsupported or fails
211
- with open(resolved_path, "r", encoding="utf-8") as f:
217
+ with open(resolved_path, encoding="utf-8") as f:
212
218
  content = f.read()
213
-
219
+
214
220
  empty_msg = check_empty_content(content)
215
221
  if empty_msg:
216
222
  return empty_msg
217
-
223
+
218
224
  lines = content.splitlines()
219
225
  start_idx = offset
220
226
  end_idx = min(start_idx + limit, len(lines))
221
-
227
+
222
228
  if start_idx >= len(lines):
223
229
  return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
224
-
230
+
225
231
  selected_lines = lines[start_idx:end_idx]
226
232
  return format_content_with_line_numbers(selected_lines, start_line=start_idx + 1)
227
233
  except (OSError, UnicodeDecodeError) as e:
228
234
  return f"Error reading file '{file_path}': {e}"
229
-
235
+
230
236
  def write(
231
- self,
237
+ self,
232
238
  file_path: str,
233
239
  content: str,
234
240
  ) -> WriteResult:
@@ -236,10 +242,10 @@ class FilesystemBackend:
236
242
  Returns WriteResult. External storage sets files_update=None.
237
243
  """
238
244
  resolved_path = self._resolve_path(file_path)
239
-
245
+
240
246
  if resolved_path.exists():
241
247
  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.")
242
-
248
+
243
249
  try:
244
250
  # Create parent directories if needed
245
251
  resolved_path.parent.mkdir(parents=True, exist_ok=True)
@@ -251,13 +257,13 @@ class FilesystemBackend:
251
257
  fd = os.open(resolved_path, flags, 0o644)
252
258
  with os.fdopen(fd, "w", encoding="utf-8") as f:
253
259
  f.write(content)
254
-
260
+
255
261
  return WriteResult(path=file_path, files_update=None)
256
262
  except (OSError, UnicodeEncodeError) as e:
257
263
  return WriteResult(error=f"Error writing file '{file_path}': {e}")
258
-
264
+
259
265
  def edit(
260
- self,
266
+ self,
261
267
  file_path: str,
262
268
  old_string: str,
263
269
  new_string: str,
@@ -267,10 +273,10 @@ class FilesystemBackend:
267
273
  Returns EditResult. External storage sets files_update=None.
268
274
  """
269
275
  resolved_path = self._resolve_path(file_path)
270
-
276
+
271
277
  if not resolved_path.exists() or not resolved_path.is_file():
272
278
  return EditResult(error=f"Error: File '{file_path}' not found")
273
-
279
+
274
280
  try:
275
281
  # Read securely
276
282
  try:
@@ -278,16 +284,16 @@ class FilesystemBackend:
278
284
  with os.fdopen(fd, "r", encoding="utf-8") as f:
279
285
  content = f.read()
280
286
  except OSError:
281
- with open(resolved_path, "r", encoding="utf-8") as f:
287
+ with open(resolved_path, encoding="utf-8") as f:
282
288
  content = f.read()
283
-
289
+
284
290
  result = perform_string_replacement(content, old_string, new_string, replace_all)
285
-
291
+
286
292
  if isinstance(result, str):
287
293
  return EditResult(error=result)
288
-
294
+
289
295
  new_content, occurrences = result
290
-
296
+
291
297
  # Write securely
292
298
  flags = os.O_WRONLY | os.O_TRUNC
293
299
  if hasattr(os, "O_NOFOLLOW"):
@@ -295,18 +301,18 @@ class FilesystemBackend:
295
301
  fd = os.open(resolved_path, flags)
296
302
  with os.fdopen(fd, "w", encoding="utf-8") as f:
297
303
  f.write(new_content)
298
-
304
+
299
305
  return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
300
306
  except (OSError, UnicodeDecodeError, UnicodeEncodeError) as e:
301
307
  return EditResult(error=f"Error editing file '{file_path}': {e}")
302
-
308
+
303
309
  # Removed legacy grep() convenience to keep lean surface
304
310
 
305
311
  def grep_raw(
306
312
  self,
307
313
  pattern: str,
308
- path: Optional[str] = None,
309
- glob: Optional[str] = None,
314
+ path: str | None = None,
315
+ glob: str | None = None,
310
316
  ) -> list[GrepMatch] | str:
311
317
  # Validate regex
312
318
  try:
@@ -334,9 +340,7 @@ class FilesystemBackend:
334
340
  matches.append({"path": fpath, "line": int(line_num), "text": line_text})
335
341
  return matches
336
342
 
337
- def _ripgrep_search(
338
- self, pattern: str, base_full: Path, include_glob: Optional[str]
339
- ) -> Optional[dict[str, list[tuple[int, str]]]]:
343
+ def _ripgrep_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]] | None:
340
344
  cmd = ["rg", "--json"]
341
345
  if include_glob:
342
346
  cmd.extend(["--glob", include_glob])
@@ -381,9 +385,7 @@ class FilesystemBackend:
381
385
 
382
386
  return results
383
387
 
384
- def _python_search(
385
- self, pattern: str, base_full: Path, include_glob: Optional[str]
386
- ) -> dict[str, list[tuple[int, str]]]:
388
+ def _python_search(self, pattern: str, base_full: Path, include_glob: str | None) -> dict[str, list[tuple[int, str]]]:
387
389
  try:
388
390
  regex = re.compile(pattern)
389
391
  except re.error:
@@ -418,7 +420,7 @@ class FilesystemBackend:
418
420
  results.setdefault(virt_path, []).append((line_num, line))
419
421
 
420
422
  return results
421
-
423
+
422
424
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
423
425
  if pattern.startswith("/"):
424
426
  pattern = pattern.lstrip("/")
@@ -441,12 +443,14 @@ class FilesystemBackend:
441
443
  if not self.virtual_mode:
442
444
  try:
443
445
  st = matched_path.stat()
444
- results.append({
445
- "path": abs_path,
446
- "is_dir": False,
447
- "size": int(st.st_size),
448
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
449
- })
446
+ results.append(
447
+ {
448
+ "path": abs_path,
449
+ "is_dir": False,
450
+ "size": int(st.st_size),
451
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
452
+ }
453
+ )
450
454
  except OSError:
451
455
  results.append({"path": abs_path, "is_dir": False})
452
456
  else:
@@ -454,20 +458,22 @@ class FilesystemBackend:
454
458
  if not cwd_str.endswith("/"):
455
459
  cwd_str += "/"
456
460
  if abs_path.startswith(cwd_str):
457
- relative_path = abs_path[len(cwd_str):]
461
+ relative_path = abs_path[len(cwd_str) :]
458
462
  elif abs_path.startswith(str(self.cwd)):
459
- relative_path = abs_path[len(str(self.cwd)):].lstrip("/")
463
+ relative_path = abs_path[len(str(self.cwd)) :].lstrip("/")
460
464
  else:
461
465
  relative_path = abs_path
462
466
  virt = "/" + relative_path
463
467
  try:
464
468
  st = matched_path.stat()
465
- results.append({
466
- "path": virt,
467
- "is_dir": False,
468
- "size": int(st.st_size),
469
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
470
- })
469
+ results.append(
470
+ {
471
+ "path": virt,
472
+ "is_dir": False,
473
+ "size": int(st.st_size),
474
+ "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
475
+ }
476
+ )
471
477
  except OSError:
472
478
  results.append({"path": virt, "is_dir": False})
473
479
  except (OSError, ValueError):