deepagents 0.1.4__tar.gz → 0.1.5rc2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {deepagents-0.1.4/src/deepagents.egg-info → deepagents-0.1.5rc2}/PKG-INFO +31 -11
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/README.md +30 -11
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/pyproject.toml +3 -2
- deepagents-0.1.5rc2/src/deepagents/backends/__init__.py +16 -0
- deepagents-0.1.5rc2/src/deepagents/backends/composite.py +235 -0
- deepagents-0.1.5rc2/src/deepagents/backends/filesystem.py +452 -0
- deepagents-0.1.5rc2/src/deepagents/backends/protocol.py +122 -0
- deepagents-0.1.5rc2/src/deepagents/backends/state.py +161 -0
- deepagents-0.1.5rc2/src/deepagents/backends/store.py +350 -0
- deepagents-0.1.5rc2/src/deepagents/backends/utils.py +424 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents/graph.py +10 -13
- deepagents-0.1.5rc2/src/deepagents/middleware/filesystem.py +666 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents/middleware/subagents.py +1 -1
- {deepagents-0.1.4 → deepagents-0.1.5rc2/src/deepagents.egg-info}/PKG-INFO +31 -11
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents.egg-info/SOURCES.txt +7 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents.egg-info/requires.txt +1 -0
- deepagents-0.1.5rc2/tests/test_middleware.py +870 -0
- deepagents-0.1.4/src/deepagents/middleware/filesystem.py +0 -1129
- deepagents-0.1.4/tests/test_middleware.py +0 -341
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/LICENSE +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/setup.cfg +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents/__init__.py +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents/middleware/__init__.py +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents/middleware/patch_tool_calls.py +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.1.4 → deepagents-0.1.5rc2}/src/deepagents.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.5rc2
|
|
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
|
|
@@ -9,6 +9,7 @@ License-File: LICENSE
|
|
|
9
9
|
Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
|
|
10
10
|
Requires-Dist: langchain<2.0.0,>=1.0.0
|
|
11
11
|
Requires-Dist: langchain-core<2.0.0,>=1.0.0
|
|
12
|
+
Requires-Dist: wcmatch
|
|
12
13
|
Provides-Extra: dev
|
|
13
14
|
Requires-Dist: pytest; extra == "dev"
|
|
14
15
|
Requires-Dist: pytest-cov; extra == "dev"
|
|
@@ -128,9 +129,7 @@ By default, `deepagents` uses `"claude-sonnet-4-5-20250929"`. You can customize
|
|
|
128
129
|
from langchain.chat_models import init_chat_model
|
|
129
130
|
from deepagents import create_deep_agent
|
|
130
131
|
|
|
131
|
-
model = init_chat_model(
|
|
132
|
-
model="openai:gpt-5",
|
|
133
|
-
)
|
|
132
|
+
model = init_chat_model("openai:gpt-4o")
|
|
134
133
|
agent = create_deep_agent(
|
|
135
134
|
model=model,
|
|
136
135
|
)
|
|
@@ -315,19 +314,30 @@ agent = create_deep_agent(
|
|
|
315
314
|
)
|
|
316
315
|
```
|
|
317
316
|
|
|
318
|
-
### `
|
|
319
|
-
Deep agents come with a local filesystem to offload memory to.
|
|
317
|
+
### `backend`
|
|
318
|
+
Deep agents come with a local filesystem to offload memory to. By default, this filesystem is stored in state (ephemeral, transient to a single thread).
|
|
320
319
|
|
|
321
|
-
You can
|
|
320
|
+
You can configure persistent long-term memory using a composite backend that routes a path prefix (for example, `/memories/`) to a persistent store.
|
|
322
321
|
|
|
323
322
|
```python
|
|
324
323
|
from deepagents import create_deep_agent
|
|
324
|
+
from deepagents.backends import build_composite_state_backend, StoreBackend
|
|
325
325
|
from langgraph.store.memory import InMemoryStore
|
|
326
326
|
|
|
327
|
-
store = InMemoryStore() # Or any other Store
|
|
327
|
+
store = InMemoryStore() # Or any other Store implementation
|
|
328
|
+
|
|
329
|
+
# Provide a backend factory to the agent/middleware.
|
|
330
|
+
# This builds a state-backed composite at runtime and routes /memories/ to StoreBackend.
|
|
331
|
+
backend_factory = lambda rt: build_composite_state_backend(
|
|
332
|
+
rt,
|
|
333
|
+
routes={
|
|
334
|
+
"/memories/": (lambda r: StoreBackend(r)),
|
|
335
|
+
},
|
|
336
|
+
)
|
|
337
|
+
|
|
328
338
|
agent = create_deep_agent(
|
|
339
|
+
backend=backend_factory,
|
|
329
340
|
store=store,
|
|
330
|
-
use_longterm_memory=True
|
|
331
341
|
)
|
|
332
342
|
```
|
|
333
343
|
|
|
@@ -403,6 +413,11 @@ Context engineering is one of the main challenges in building effective agents.
|
|
|
403
413
|
```python
|
|
404
414
|
from langchain.agents import create_agent
|
|
405
415
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
416
|
+
from deepagents.backends import (
|
|
417
|
+
StateBackend,
|
|
418
|
+
CompositeBackend,
|
|
419
|
+
StoreBackend,
|
|
420
|
+
)
|
|
406
421
|
|
|
407
422
|
# FilesystemMiddleware is included by default in create_deep_agent
|
|
408
423
|
# You can customize it if building a custom agent
|
|
@@ -410,8 +425,13 @@ agent = create_agent(
|
|
|
410
425
|
model="anthropic:claude-sonnet-4-20250514",
|
|
411
426
|
middleware=[
|
|
412
427
|
FilesystemMiddleware(
|
|
413
|
-
|
|
414
|
-
|
|
428
|
+
backend=(lambda rt: StateBackend(rt)), # Optional: customize storage backend (defaults to lambda rt: )
|
|
429
|
+
# For persistent memory, use CompositeBackend:
|
|
430
|
+
# backend=CompositeBackend(
|
|
431
|
+
# default=lambda rt: StateBackend(rt)
|
|
432
|
+
# routes={"/memories/": lambda rt: StoreBackend(rt)}
|
|
433
|
+
# )
|
|
434
|
+
system_prompt="Write to the filesystem when...", # Optional custom system prompt override
|
|
415
435
|
custom_tool_descriptions={
|
|
416
436
|
"ls": "Use the ls tool when...",
|
|
417
437
|
"read_file": "Use the read_file tool to..."
|
|
@@ -109,9 +109,7 @@ By default, `deepagents` uses `"claude-sonnet-4-5-20250929"`. You can customize
|
|
|
109
109
|
from langchain.chat_models import init_chat_model
|
|
110
110
|
from deepagents import create_deep_agent
|
|
111
111
|
|
|
112
|
-
model = init_chat_model(
|
|
113
|
-
model="openai:gpt-5",
|
|
114
|
-
)
|
|
112
|
+
model = init_chat_model("openai:gpt-4o")
|
|
115
113
|
agent = create_deep_agent(
|
|
116
114
|
model=model,
|
|
117
115
|
)
|
|
@@ -296,19 +294,30 @@ agent = create_deep_agent(
|
|
|
296
294
|
)
|
|
297
295
|
```
|
|
298
296
|
|
|
299
|
-
### `
|
|
300
|
-
Deep agents come with a local filesystem to offload memory to.
|
|
297
|
+
### `backend`
|
|
298
|
+
Deep agents come with a local filesystem to offload memory to. By default, this filesystem is stored in state (ephemeral, transient to a single thread).
|
|
301
299
|
|
|
302
|
-
You can
|
|
300
|
+
You can configure persistent long-term memory using a composite backend that routes a path prefix (for example, `/memories/`) to a persistent store.
|
|
303
301
|
|
|
304
302
|
```python
|
|
305
303
|
from deepagents import create_deep_agent
|
|
304
|
+
from deepagents.backends import build_composite_state_backend, StoreBackend
|
|
306
305
|
from langgraph.store.memory import InMemoryStore
|
|
307
306
|
|
|
308
|
-
store = InMemoryStore() # Or any other Store
|
|
307
|
+
store = InMemoryStore() # Or any other Store implementation
|
|
308
|
+
|
|
309
|
+
# Provide a backend factory to the agent/middleware.
|
|
310
|
+
# This builds a state-backed composite at runtime and routes /memories/ to StoreBackend.
|
|
311
|
+
backend_factory = lambda rt: build_composite_state_backend(
|
|
312
|
+
rt,
|
|
313
|
+
routes={
|
|
314
|
+
"/memories/": (lambda r: StoreBackend(r)),
|
|
315
|
+
},
|
|
316
|
+
)
|
|
317
|
+
|
|
309
318
|
agent = create_deep_agent(
|
|
319
|
+
backend=backend_factory,
|
|
310
320
|
store=store,
|
|
311
|
-
use_longterm_memory=True
|
|
312
321
|
)
|
|
313
322
|
```
|
|
314
323
|
|
|
@@ -384,6 +393,11 @@ Context engineering is one of the main challenges in building effective agents.
|
|
|
384
393
|
```python
|
|
385
394
|
from langchain.agents import create_agent
|
|
386
395
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
396
|
+
from deepagents.backends import (
|
|
397
|
+
StateBackend,
|
|
398
|
+
CompositeBackend,
|
|
399
|
+
StoreBackend,
|
|
400
|
+
)
|
|
387
401
|
|
|
388
402
|
# FilesystemMiddleware is included by default in create_deep_agent
|
|
389
403
|
# You can customize it if building a custom agent
|
|
@@ -391,8 +405,13 @@ agent = create_agent(
|
|
|
391
405
|
model="anthropic:claude-sonnet-4-20250514",
|
|
392
406
|
middleware=[
|
|
393
407
|
FilesystemMiddleware(
|
|
394
|
-
|
|
395
|
-
|
|
408
|
+
backend=(lambda rt: StateBackend(rt)), # Optional: customize storage backend (defaults to lambda rt: )
|
|
409
|
+
# For persistent memory, use CompositeBackend:
|
|
410
|
+
# backend=CompositeBackend(
|
|
411
|
+
# default=lambda rt: StateBackend(rt)
|
|
412
|
+
# routes={"/memories/": lambda rt: StoreBackend(rt)}
|
|
413
|
+
# )
|
|
414
|
+
system_prompt="Write to the filesystem when...", # Optional custom system prompt override
|
|
396
415
|
custom_tool_descriptions={
|
|
397
416
|
"ls": "Use the ls tool when...",
|
|
398
417
|
"read_file": "Use the read_file tool to..."
|
|
@@ -509,4 +528,4 @@ async def main():
|
|
|
509
528
|
chunk["messages"][-1].pretty_print()
|
|
510
529
|
|
|
511
530
|
asyncio.run(main())
|
|
512
|
-
```
|
|
531
|
+
```
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "deepagents"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.5rc2"
|
|
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" }
|
|
@@ -9,6 +9,7 @@ dependencies = [
|
|
|
9
9
|
"langchain-anthropic>=1.0.0,<2.0.0",
|
|
10
10
|
"langchain>=1.0.0,<2.0.0",
|
|
11
11
|
"langchain-core>=1.0.0,<2.0.0",
|
|
12
|
+
"wcmatch"
|
|
12
13
|
]
|
|
13
14
|
|
|
14
15
|
[project.optional-dependencies]
|
|
@@ -17,7 +18,7 @@ dev = [
|
|
|
17
18
|
"pytest-cov",
|
|
18
19
|
"build",
|
|
19
20
|
"twine",
|
|
20
|
-
"langchain-openai"
|
|
21
|
+
"langchain-openai",
|
|
21
22
|
]
|
|
22
23
|
|
|
23
24
|
[dependency-groups]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Memory backends for pluggable file storage."""
|
|
2
|
+
|
|
3
|
+
from deepagents.backends.composite import CompositeBackend, build_composite_state_backend
|
|
4
|
+
from deepagents.backends.filesystem import FilesystemBackend
|
|
5
|
+
from deepagents.backends.state import StateBackend
|
|
6
|
+
from deepagents.backends.store import StoreBackend
|
|
7
|
+
from deepagents.backends.protocol import BackendProtocol
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BackendProtocol",
|
|
11
|
+
"CompositeBackend",
|
|
12
|
+
"build_composite_state_backend",
|
|
13
|
+
"FilesystemBackend",
|
|
14
|
+
"StateBackend",
|
|
15
|
+
"StoreBackend",
|
|
16
|
+
]
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""CompositeBackend: Route operations to different backends based on path prefix."""
|
|
2
|
+
|
|
3
|
+
from typing import Any, Literal, Optional, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from langchain.tools import ToolRuntime
|
|
6
|
+
|
|
7
|
+
from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
|
|
8
|
+
from deepagents.backends.state import StateBackend
|
|
9
|
+
from deepagents.backends.utils import FileInfo, GrepMatch
|
|
10
|
+
from deepagents.backends.protocol import BackendFactory
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class CompositeBackend:
|
|
14
|
+
|
|
15
|
+
def __init__(
|
|
16
|
+
self,
|
|
17
|
+
default: BackendProtocol | StateBackend,
|
|
18
|
+
routes: dict[str, BackendProtocol],
|
|
19
|
+
) -> None:
|
|
20
|
+
# Default backend
|
|
21
|
+
self.default = default
|
|
22
|
+
|
|
23
|
+
# Virtual routes
|
|
24
|
+
self.routes = routes
|
|
25
|
+
|
|
26
|
+
# Sort routes by length (longest first) for correct prefix matching
|
|
27
|
+
self.sorted_routes = sorted(routes.items(), key=lambda x: len(x[0]), reverse=True)
|
|
28
|
+
|
|
29
|
+
def _get_backend_and_key(self, key: str) -> tuple[BackendProtocol, str]:
|
|
30
|
+
"""Determine which backend handles this key and strip prefix.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
key: Original file path
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
Tuple of (backend, stripped_key) where stripped_key has the route
|
|
37
|
+
prefix removed (but keeps leading slash).
|
|
38
|
+
"""
|
|
39
|
+
# Check routes in order of length (longest first)
|
|
40
|
+
for prefix, backend in self.sorted_routes:
|
|
41
|
+
if key.startswith(prefix):
|
|
42
|
+
# Strip full prefix and ensure a leading slash remains
|
|
43
|
+
# e.g., "/memories/notes.txt" → "/notes.txt"; "/memories/" → "/"
|
|
44
|
+
suffix = key[len(prefix):]
|
|
45
|
+
stripped_key = f"/{suffix}" if suffix else "/"
|
|
46
|
+
return backend, stripped_key
|
|
47
|
+
|
|
48
|
+
return self.default, key
|
|
49
|
+
|
|
50
|
+
def ls_info(self, path: str) -> list[FileInfo]:
|
|
51
|
+
"""List files from backends, with appropriate prefixes.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
path: Absolute path to directory.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of FileInfo-like dicts with route prefixes added.
|
|
58
|
+
"""
|
|
59
|
+
# Check if path matches a specific route
|
|
60
|
+
for route_prefix, backend in self.sorted_routes:
|
|
61
|
+
if path.startswith(route_prefix.rstrip("/")):
|
|
62
|
+
# Query only the matching routed backend
|
|
63
|
+
suffix = path[len(route_prefix):]
|
|
64
|
+
search_path = f"/{suffix}" if suffix else "/"
|
|
65
|
+
infos = backend.ls_info(search_path)
|
|
66
|
+
prefixed: list[FileInfo] = []
|
|
67
|
+
for fi in infos:
|
|
68
|
+
fi = dict(fi)
|
|
69
|
+
fi["path"] = f"{route_prefix[:-1]}{fi['path']}"
|
|
70
|
+
prefixed.append(fi)
|
|
71
|
+
return prefixed
|
|
72
|
+
|
|
73
|
+
# At root, aggregate default and all routed backends
|
|
74
|
+
if path == "/":
|
|
75
|
+
results: list[FileInfo] = []
|
|
76
|
+
results.extend(self.default.ls_info(path))
|
|
77
|
+
for route_prefix, backend in self.sorted_routes:
|
|
78
|
+
infos = backend.ls_info("/")
|
|
79
|
+
for fi in infos:
|
|
80
|
+
fi = dict(fi)
|
|
81
|
+
fi["path"] = f"{route_prefix[:-1]}{fi['path']}"
|
|
82
|
+
results.append(fi)
|
|
83
|
+
results.sort(key=lambda x: x.get("path", ""))
|
|
84
|
+
return results
|
|
85
|
+
|
|
86
|
+
# Path doesn't match a route: query only default backend
|
|
87
|
+
return self.default.ls_info(path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def read(
|
|
91
|
+
self,
|
|
92
|
+
file_path: str,
|
|
93
|
+
offset: int = 0,
|
|
94
|
+
limit: int = 2000,
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Read file content, routing to appropriate backend.
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
file_path: Absolute file path
|
|
100
|
+
offset: Line offset to start reading from (0-indexed)
|
|
101
|
+
limit: Maximum number of lines to readReturns:
|
|
102
|
+
Formatted file content with line numbers, or error message.
|
|
103
|
+
"""
|
|
104
|
+
backend, stripped_key = self._get_backend_and_key(file_path)
|
|
105
|
+
return backend.read(stripped_key, offset=offset, limit=limit)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def grep_raw(
|
|
109
|
+
self,
|
|
110
|
+
pattern: str,
|
|
111
|
+
path: Optional[str] = None,
|
|
112
|
+
glob: Optional[str] = None,
|
|
113
|
+
) -> list[GrepMatch] | str:
|
|
114
|
+
# If path targets a specific route, search only that backend
|
|
115
|
+
for route_prefix, backend in self.sorted_routes:
|
|
116
|
+
if path is not None and path.startswith(route_prefix.rstrip("/")):
|
|
117
|
+
search_path = path[len(route_prefix) - 1:]
|
|
118
|
+
raw = backend.grep_raw(pattern, search_path if search_path else "/", glob)
|
|
119
|
+
if isinstance(raw, str):
|
|
120
|
+
return raw
|
|
121
|
+
return [{**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw]
|
|
122
|
+
|
|
123
|
+
# Otherwise, search default and all routed backends and merge
|
|
124
|
+
all_matches: list[GrepMatch] = []
|
|
125
|
+
raw_default = self.default.grep_raw(pattern, path, glob) # type: ignore[attr-defined]
|
|
126
|
+
if isinstance(raw_default, str):
|
|
127
|
+
# This happens if error occurs
|
|
128
|
+
return raw_default
|
|
129
|
+
all_matches.extend(raw_default)
|
|
130
|
+
|
|
131
|
+
for route_prefix, backend in self.routes.items():
|
|
132
|
+
raw = backend.grep_raw(pattern, "/", glob)
|
|
133
|
+
if isinstance(raw, str):
|
|
134
|
+
# This happens if error occurs
|
|
135
|
+
return raw
|
|
136
|
+
all_matches.extend({**m, "path": f"{route_prefix[:-1]}{m['path']}"} for m in raw)
|
|
137
|
+
|
|
138
|
+
return all_matches
|
|
139
|
+
|
|
140
|
+
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
|
141
|
+
results: list[FileInfo] = []
|
|
142
|
+
|
|
143
|
+
# Route based on path, not pattern
|
|
144
|
+
for route_prefix, backend in self.sorted_routes:
|
|
145
|
+
if path.startswith(route_prefix.rstrip("/")):
|
|
146
|
+
search_path = path[len(route_prefix) - 1:]
|
|
147
|
+
infos = backend.glob_info(pattern, search_path if search_path else "/")
|
|
148
|
+
return [
|
|
149
|
+
{**fi, "path": f"{route_prefix[:-1]}{fi['path']}"}
|
|
150
|
+
for fi in infos
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
# Path doesn't match any specific route - search default backend AND all routed backends
|
|
154
|
+
results.extend(self.default.glob_info(pattern, path))
|
|
155
|
+
|
|
156
|
+
for route_prefix, backend in self.routes.items():
|
|
157
|
+
infos = backend.glob_info(pattern, "/")
|
|
158
|
+
results.extend({**fi, "path": f"{route_prefix[:-1]}{fi['path']}"} for fi in infos)
|
|
159
|
+
|
|
160
|
+
# Deterministic ordering
|
|
161
|
+
results.sort(key=lambda x: x.get("path", ""))
|
|
162
|
+
return results
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def write(
|
|
166
|
+
self,
|
|
167
|
+
file_path: str,
|
|
168
|
+
content: str,
|
|
169
|
+
) -> WriteResult:
|
|
170
|
+
"""Create a new file, routing to appropriate backend.
|
|
171
|
+
|
|
172
|
+
Args:
|
|
173
|
+
file_path: Absolute file path
|
|
174
|
+
content: File content as a stringReturns:
|
|
175
|
+
Success message or Command object, or error if file already exists.
|
|
176
|
+
"""
|
|
177
|
+
backend, stripped_key = self._get_backend_and_key(file_path)
|
|
178
|
+
res = backend.write(stripped_key, content)
|
|
179
|
+
# If this is a state-backed update and default has state, merge so listings reflect changes
|
|
180
|
+
if res.files_update:
|
|
181
|
+
try:
|
|
182
|
+
runtime = getattr(self.default, "runtime", None)
|
|
183
|
+
if runtime is not None:
|
|
184
|
+
state = runtime.state
|
|
185
|
+
files = state.get("files", {})
|
|
186
|
+
files.update(res.files_update)
|
|
187
|
+
state["files"] = files
|
|
188
|
+
except Exception:
|
|
189
|
+
pass
|
|
190
|
+
return res
|
|
191
|
+
|
|
192
|
+
def edit(
|
|
193
|
+
self,
|
|
194
|
+
file_path: str,
|
|
195
|
+
old_string: str,
|
|
196
|
+
new_string: str,
|
|
197
|
+
replace_all: bool = False,
|
|
198
|
+
) -> EditResult:
|
|
199
|
+
"""Edit a file, routing to appropriate backend.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
file_path: Absolute file path
|
|
203
|
+
old_string: String to find and replace
|
|
204
|
+
new_string: Replacement string
|
|
205
|
+
replace_all: If True, replace all occurrencesReturns:
|
|
206
|
+
Success message or Command object, or error message on failure.
|
|
207
|
+
"""
|
|
208
|
+
backend, stripped_key = self._get_backend_and_key(file_path)
|
|
209
|
+
res = backend.edit(stripped_key, old_string, new_string, replace_all=replace_all)
|
|
210
|
+
if res.files_update:
|
|
211
|
+
try:
|
|
212
|
+
runtime = getattr(self.default, "runtime", None)
|
|
213
|
+
if runtime is not None:
|
|
214
|
+
state = runtime.state
|
|
215
|
+
files = state.get("files", {})
|
|
216
|
+
files.update(res.files_update)
|
|
217
|
+
state["files"] = files
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
return res
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def build_composite_state_backend(
|
|
224
|
+
runtime: ToolRuntime,
|
|
225
|
+
*,
|
|
226
|
+
routes: dict[str, BackendProtocol | BackendFactory],
|
|
227
|
+
) -> BackendProtocol:
|
|
228
|
+
built_routes: dict[str, BackendProtocol] = {}
|
|
229
|
+
for k, v in routes.items():
|
|
230
|
+
if isinstance(v, BackendProtocol):
|
|
231
|
+
built_routes[k] = v
|
|
232
|
+
else:
|
|
233
|
+
built_routes[k] = v(runtime)
|
|
234
|
+
default_state = StateBackend(runtime)
|
|
235
|
+
return CompositeBackend(default=default_state, routes=built_routes)
|