11"""CompositeBackend: Route operations to different backends based on path prefix."""
22
3- from typing import Optional
43
5- from deepagents .backends .protocol import BackendProtocol , WriteResult , EditResult
4+ from deepagents .backends .protocol import BackendProtocol , EditResult , WriteResult
65from deepagents .backends .state import StateBackend
76from deepagents .backends .utils import FileInfo , GrepMatch
87
98
109class CompositeBackend :
11-
1210 def __init__ (
1311 self ,
1412 default : BackendProtocol | StateBackend ,
@@ -19,16 +17,16 @@ def __init__(
1917
2018 # Virtual routes
2119 self .routes = routes
22-
20+
2321 # Sort routes by length (longest first) for correct prefix matching
2422 self .sorted_routes = sorted (routes .items (), key = lambda x : len (x [0 ]), reverse = True )
2523
2624 def _get_backend_and_key (self , key : str ) -> tuple [BackendProtocol , str ]:
2725 """Determine which backend handles this key and strip prefix.
28-
26+
2927 Args:
3028 key: Original file path
31-
29+
3230 Returns:
3331 Tuple of (backend, stripped_key) where stripped_key has the route
3432 prefix removed (but keeps leading slash).
@@ -38,12 +36,12 @@ def _get_backend_and_key(self, key: str) -> tuple[BackendProtocol, str]:
3836 if key .startswith (prefix ):
3937 # Strip full prefix and ensure a leading slash remains
4038 # e.g., "/memories/notes.txt" → "/notes.txt"; "/memories/" → "/"
41- suffix = key [len (prefix ):]
39+ suffix = key [len (prefix ) :]
4240 stripped_key = f"/{ suffix } " if suffix else "/"
4341 return backend , stripped_key
44-
42+
4543 return self .default , key
46-
44+
4745 def ls_info (self , path : str ) -> list [FileInfo ]:
4846 """List files and directories in the specified directory (non-recursive).
4947
@@ -58,7 +56,7 @@ def ls_info(self, path: str) -> list[FileInfo]:
5856 for route_prefix , backend in self .sorted_routes :
5957 if path .startswith (route_prefix .rstrip ("/" )):
6058 # Query only the matching routed backend
61- suffix = path [len (route_prefix ):]
59+ suffix = path [len (route_prefix ) :]
6260 search_path = f"/{ suffix } " if suffix else "/"
6361 infos = backend .ls_info (search_path )
6462 prefixed : list [FileInfo ] = []
@@ -74,28 +72,29 @@ def ls_info(self, path: str) -> list[FileInfo]:
7472 results .extend (self .default .ls_info (path ))
7573 for route_prefix , backend in self .sorted_routes :
7674 # 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- })
75+ results .append (
76+ {
77+ "path" : route_prefix ,
78+ "is_dir" : True ,
79+ "size" : 0 ,
80+ "modified_at" : "" ,
81+ }
82+ )
8383
8484 results .sort (key = lambda x : x .get ("path" , "" ))
8585 return results
8686
8787 # Path doesn't match a route: query only default backend
8888 return self .default .ls_info (path )
8989
90-
9190 def read (
92- self ,
91+ self ,
9392 file_path : str ,
9493 offset : int = 0 ,
9594 limit : int = 2000 ,
9695 ) -> str :
9796 """Read file content, routing to appropriate backend.
98-
97+
9998 Args:
10099 file_path: Absolute file path
101100 offset: Line offset to start reading from (0-indexed)
@@ -105,17 +104,16 @@ def read(
105104 backend , stripped_key = self ._get_backend_and_key (file_path )
106105 return backend .read (stripped_key , offset = offset , limit = limit )
107106
108-
109107 def grep_raw (
110108 self ,
111109 pattern : str ,
112- path : Optional [ str ] = None ,
113- glob : Optional [ str ] = None ,
110+ path : str | None = None ,
111+ glob : str | None = None ,
114112 ) -> list [GrepMatch ] | str :
115113 # If path targets a specific route, search only that backend
116114 for route_prefix , backend in self .sorted_routes :
117115 if path is not None and path .startswith (route_prefix .rstrip ("/" )):
118- search_path = path [len (route_prefix ) - 1 :]
116+ search_path = path [len (route_prefix ) - 1 :]
119117 raw = backend .grep_raw (pattern , search_path if search_path else "/" , glob )
120118 if isinstance (raw , str ):
121119 return raw
@@ -137,19 +135,16 @@ def grep_raw(
137135 all_matches .extend ({** m , "path" : f"{ route_prefix [:- 1 ]} { m ['path' ]} " } for m in raw )
138136
139137 return all_matches
140-
138+
141139 def glob_info (self , pattern : str , path : str = "/" ) -> list [FileInfo ]:
142140 results : list [FileInfo ] = []
143141
144142 # Route based on path, not pattern
145143 for route_prefix , backend in self .sorted_routes :
146144 if path .startswith (route_prefix .rstrip ("/" )):
147- search_path = path [len (route_prefix ) - 1 :]
145+ search_path = path [len (route_prefix ) - 1 :]
148146 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- ]
147+ return [{** fi , "path" : f"{ route_prefix [:- 1 ]} { fi ['path' ]} " } for fi in infos ]
153148
154149 # Path doesn't match any specific route - search default backend AND all routed backends
155150 results .extend (self .default .glob_info (pattern , path ))
@@ -162,11 +157,10 @@ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
162157 results .sort (key = lambda x : x .get ("path" , "" ))
163158 return results
164159
165-
166160 def write (
167- self ,
168- file_path : str ,
169- content : str ,
161+ self ,
162+ file_path : str ,
163+ content : str ,
170164 ) -> WriteResult :
171165 """Create a new file, routing to appropriate backend.
172166
@@ -191,11 +185,11 @@ def write(
191185 return res
192186
193187 def edit (
194- self ,
195- file_path : str ,
196- old_string : str ,
197- new_string : str ,
198- replace_all : bool = False ,
188+ self ,
189+ file_path : str ,
190+ old_string : str ,
191+ new_string : str ,
192+ replace_all : bool = False ,
199193 ) -> EditResult :
200194 """Edit a file, routing to appropriate backend.
201195
@@ -219,6 +213,3 @@ def edit(
219213 except Exception :
220214 pass
221215 return res
222-
223-
224-
0 commit comments