deepagents 0.1.5rc2__tar.gz → 0.2.1rc1__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.5rc2/src/deepagents.egg-info → deepagents-0.2.1rc1}/PKG-INFO +5 -41
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/README.md +4 -40
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/pyproject.toml +1 -1
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/__init__.py +1 -2
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/composite.py +16 -27
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/filesystem.py +54 -29
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/state.py +34 -5
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/store.py +35 -6
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/utils.py +35 -17
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/middleware/filesystem.py +0 -7
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/middleware/subagents.py +0 -1
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1/src/deepagents.egg-info}/PKG-INFO +5 -41
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/tests/test_middleware.py +167 -19
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/LICENSE +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/setup.cfg +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/__init__.py +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/backends/protocol.py +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/graph.py +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/middleware/__init__.py +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents/middleware/patch_tool_calls.py +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents.egg-info/SOURCES.txt +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/src/deepagents.egg-info/requires.txt +0 -0
- {deepagents-0.1.5rc2 → deepagents-0.2.1rc1}/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.
|
|
3
|
+
Version: 0.2.1rc1
|
|
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
|
|
@@ -28,7 +28,7 @@ a **planning tool**, **sub agents**, access to a **file system**, and a **detail
|
|
|
28
28
|
|
|
29
29
|
<img src="deep_agents.png" alt="deep agent" width="600"/>
|
|
30
30
|
|
|
31
|
-
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application.
|
|
31
|
+
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application. For a full overview and quickstart of `deepagents`, the best resource is our [docs](https://docs.langchain.com/oss/python/deepagents/overview).
|
|
32
32
|
|
|
33
33
|
**Acknowledgements: This project was primarily inspired by Claude Code, and initially was largely an attempt to see what made Claude Code general purpose, and make it even more so.**
|
|
34
34
|
|
|
@@ -107,7 +107,7 @@ in the same way you would any LangGraph agent.
|
|
|
107
107
|
|
|
108
108
|
**Context Management**
|
|
109
109
|
|
|
110
|
-
File system tools (`ls`, `read_file`, `write_file`, `edit_file`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
110
|
+
File system tools (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
111
111
|
|
|
112
112
|
**Subagent Spawning**
|
|
113
113
|
|
|
@@ -314,33 +314,6 @@ agent = create_deep_agent(
|
|
|
314
314
|
)
|
|
315
315
|
```
|
|
316
316
|
|
|
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).
|
|
319
|
-
|
|
320
|
-
You can configure persistent long-term memory using a composite backend that routes a path prefix (for example, `/memories/`) to a persistent store.
|
|
321
|
-
|
|
322
|
-
```python
|
|
323
|
-
from deepagents import create_deep_agent
|
|
324
|
-
from deepagents.backends import build_composite_state_backend, StoreBackend
|
|
325
|
-
from langgraph.store.memory import InMemoryStore
|
|
326
|
-
|
|
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
|
-
|
|
338
|
-
agent = create_deep_agent(
|
|
339
|
-
backend=backend_factory,
|
|
340
|
-
store=store,
|
|
341
|
-
)
|
|
342
|
-
```
|
|
343
|
-
|
|
344
317
|
### `interrupt_on`
|
|
345
318
|
A common reality for agents is that some tool operations may be sensitive and require human approval before execution. Deep Agents supports human-in-the-loop workflows through LangGraph’s interrupt capabilities. You can configure which tools require approval using a checkpointer.
|
|
346
319
|
|
|
@@ -413,11 +386,7 @@ Context engineering is one of the main challenges in building effective agents.
|
|
|
413
386
|
```python
|
|
414
387
|
from langchain.agents import create_agent
|
|
415
388
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
416
|
-
|
|
417
|
-
StateBackend,
|
|
418
|
-
CompositeBackend,
|
|
419
|
-
StoreBackend,
|
|
420
|
-
)
|
|
389
|
+
|
|
421
390
|
|
|
422
391
|
# FilesystemMiddleware is included by default in create_deep_agent
|
|
423
392
|
# You can customize it if building a custom agent
|
|
@@ -425,12 +394,7 @@ agent = create_agent(
|
|
|
425
394
|
model="anthropic:claude-sonnet-4-20250514",
|
|
426
395
|
middleware=[
|
|
427
396
|
FilesystemMiddleware(
|
|
428
|
-
backend
|
|
429
|
-
# For persistent memory, use CompositeBackend:
|
|
430
|
-
# backend=CompositeBackend(
|
|
431
|
-
# default=lambda rt: StateBackend(rt)
|
|
432
|
-
# routes={"/memories/": lambda rt: StoreBackend(rt)}
|
|
433
|
-
# )
|
|
397
|
+
backend=..., # Optional: customize storage backend
|
|
434
398
|
system_prompt="Write to the filesystem when...", # Optional custom system prompt override
|
|
435
399
|
custom_tool_descriptions={
|
|
436
400
|
"ls": "Use the ls tool when...",
|
|
@@ -8,7 +8,7 @@ a **planning tool**, **sub agents**, access to a **file system**, and a **detail
|
|
|
8
8
|
|
|
9
9
|
<img src="deep_agents.png" alt="deep agent" width="600"/>
|
|
10
10
|
|
|
11
|
-
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application.
|
|
11
|
+
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application. For a full overview and quickstart of `deepagents`, the best resource is our [docs](https://docs.langchain.com/oss/python/deepagents/overview).
|
|
12
12
|
|
|
13
13
|
**Acknowledgements: This project was primarily inspired by Claude Code, and initially was largely an attempt to see what made Claude Code general purpose, and make it even more so.**
|
|
14
14
|
|
|
@@ -87,7 +87,7 @@ in the same way you would any LangGraph agent.
|
|
|
87
87
|
|
|
88
88
|
**Context Management**
|
|
89
89
|
|
|
90
|
-
File system tools (`ls`, `read_file`, `write_file`, `edit_file`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
90
|
+
File system tools (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
91
91
|
|
|
92
92
|
**Subagent Spawning**
|
|
93
93
|
|
|
@@ -294,33 +294,6 @@ agent = create_deep_agent(
|
|
|
294
294
|
)
|
|
295
295
|
```
|
|
296
296
|
|
|
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).
|
|
299
|
-
|
|
300
|
-
You can configure persistent long-term memory using a composite backend that routes a path prefix (for example, `/memories/`) to a persistent store.
|
|
301
|
-
|
|
302
|
-
```python
|
|
303
|
-
from deepagents import create_deep_agent
|
|
304
|
-
from deepagents.backends import build_composite_state_backend, StoreBackend
|
|
305
|
-
from langgraph.store.memory import InMemoryStore
|
|
306
|
-
|
|
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
|
-
|
|
318
|
-
agent = create_deep_agent(
|
|
319
|
-
backend=backend_factory,
|
|
320
|
-
store=store,
|
|
321
|
-
)
|
|
322
|
-
```
|
|
323
|
-
|
|
324
297
|
### `interrupt_on`
|
|
325
298
|
A common reality for agents is that some tool operations may be sensitive and require human approval before execution. Deep Agents supports human-in-the-loop workflows through LangGraph’s interrupt capabilities. You can configure which tools require approval using a checkpointer.
|
|
326
299
|
|
|
@@ -393,11 +366,7 @@ Context engineering is one of the main challenges in building effective agents.
|
|
|
393
366
|
```python
|
|
394
367
|
from langchain.agents import create_agent
|
|
395
368
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
396
|
-
|
|
397
|
-
StateBackend,
|
|
398
|
-
CompositeBackend,
|
|
399
|
-
StoreBackend,
|
|
400
|
-
)
|
|
369
|
+
|
|
401
370
|
|
|
402
371
|
# FilesystemMiddleware is included by default in create_deep_agent
|
|
403
372
|
# You can customize it if building a custom agent
|
|
@@ -405,12 +374,7 @@ agent = create_agent(
|
|
|
405
374
|
model="anthropic:claude-sonnet-4-20250514",
|
|
406
375
|
middleware=[
|
|
407
376
|
FilesystemMiddleware(
|
|
408
|
-
backend
|
|
409
|
-
# For persistent memory, use CompositeBackend:
|
|
410
|
-
# backend=CompositeBackend(
|
|
411
|
-
# default=lambda rt: StateBackend(rt)
|
|
412
|
-
# routes={"/memories/": lambda rt: StoreBackend(rt)}
|
|
413
|
-
# )
|
|
377
|
+
backend=..., # Optional: customize storage backend
|
|
414
378
|
system_prompt="Write to the filesystem when...", # Optional custom system prompt override
|
|
415
379
|
custom_tool_descriptions={
|
|
416
380
|
"ls": "Use the ls tool when...",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Memory backends for pluggable file storage."""
|
|
2
2
|
|
|
3
|
-
from deepagents.backends.composite import CompositeBackend
|
|
3
|
+
from deepagents.backends.composite import CompositeBackend
|
|
4
4
|
from deepagents.backends.filesystem import FilesystemBackend
|
|
5
5
|
from deepagents.backends.state import StateBackend
|
|
6
6
|
from deepagents.backends.store import StoreBackend
|
|
@@ -9,7 +9,6 @@ from deepagents.backends.protocol import BackendProtocol
|
|
|
9
9
|
__all__ = [
|
|
10
10
|
"BackendProtocol",
|
|
11
11
|
"CompositeBackend",
|
|
12
|
-
"build_composite_state_backend",
|
|
13
12
|
"FilesystemBackend",
|
|
14
13
|
"StateBackend",
|
|
15
14
|
"StoreBackend",
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
"""CompositeBackend: Route operations to different backends based on path prefix."""
|
|
2
2
|
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Optional
|
|
4
4
|
|
|
5
|
-
from
|
|
6
|
-
|
|
7
|
-
from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
|
|
5
|
+
from deepagents.backends.protocol import BackendProtocol, WriteResult, EditResult
|
|
8
6
|
from deepagents.backends.state import StateBackend
|
|
9
7
|
from deepagents.backends.utils import FileInfo, GrepMatch
|
|
10
|
-
from deepagents.backends.protocol import BackendFactory
|
|
11
8
|
|
|
12
9
|
|
|
13
10
|
class CompositeBackend:
|
|
@@ -48,13 +45,14 @@ class CompositeBackend:
|
|
|
48
45
|
return self.default, key
|
|
49
46
|
|
|
50
47
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
51
|
-
"""List files
|
|
52
|
-
|
|
48
|
+
"""List files and directories in the specified directory (non-recursive).
|
|
49
|
+
|
|
53
50
|
Args:
|
|
54
51
|
path: Absolute path to directory.
|
|
55
|
-
|
|
52
|
+
|
|
56
53
|
Returns:
|
|
57
|
-
List of FileInfo-like dicts with route prefixes added.
|
|
54
|
+
List of FileInfo-like dicts with route prefixes added, for files and directories directly in the directory.
|
|
55
|
+
Directories have a trailing / in their path and is_dir=True.
|
|
58
56
|
"""
|
|
59
57
|
# Check if path matches a specific route
|
|
60
58
|
for route_prefix, backend in self.sorted_routes:
|
|
@@ -75,11 +73,14 @@ class CompositeBackend:
|
|
|
75
73
|
results: list[FileInfo] = []
|
|
76
74
|
results.extend(self.default.ls_info(path))
|
|
77
75
|
for route_prefix, backend in self.sorted_routes:
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
76
|
+
# 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
|
+
})
|
|
83
|
+
|
|
83
84
|
results.sort(key=lambda x: x.get("path", ""))
|
|
84
85
|
return results
|
|
85
86
|
|
|
@@ -220,16 +221,4 @@ class CompositeBackend:
|
|
|
220
221
|
return res
|
|
221
222
|
|
|
222
223
|
|
|
223
|
-
|
|
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)
|
|
224
|
+
|
|
@@ -13,16 +13,12 @@ import json
|
|
|
13
13
|
import subprocess
|
|
14
14
|
from datetime import datetime
|
|
15
15
|
from pathlib import Path
|
|
16
|
-
from typing import
|
|
17
|
-
|
|
18
|
-
if TYPE_CHECKING:
|
|
19
|
-
from langchain.tools import ToolRuntime
|
|
16
|
+
from typing import Optional
|
|
20
17
|
|
|
21
18
|
from .utils import (
|
|
22
19
|
check_empty_content,
|
|
23
20
|
format_content_with_line_numbers,
|
|
24
21
|
perform_string_replacement,
|
|
25
|
-
truncate_if_too_long,
|
|
26
22
|
)
|
|
27
23
|
import wcmatch.glob as wcglob
|
|
28
24
|
from deepagents.backends.utils import FileInfo, GrepMatch
|
|
@@ -51,7 +47,7 @@ class FilesystemBackend:
|
|
|
51
47
|
all file paths will be resolved relative to this directory.
|
|
52
48
|
If not provided, uses the current working directory.
|
|
53
49
|
"""
|
|
54
|
-
self.cwd = Path(root_dir) if root_dir else Path.cwd()
|
|
50
|
+
self.cwd = Path(root_dir).resolve() if root_dir else Path.cwd()
|
|
55
51
|
self.virtual_mode = virtual_mode
|
|
56
52
|
self.max_file_size_bytes = max_file_size_mb * 1024 * 1024
|
|
57
53
|
|
|
@@ -77,7 +73,7 @@ class FilesystemBackend:
|
|
|
77
73
|
try:
|
|
78
74
|
full.relative_to(self.cwd)
|
|
79
75
|
except ValueError:
|
|
80
|
-
raise ValueError(f"Path outside root directory: {
|
|
76
|
+
raise ValueError(f"Path:{full} outside root directory: {self.cwd}") from None
|
|
81
77
|
return full
|
|
82
78
|
|
|
83
79
|
path = Path(key)
|
|
@@ -86,13 +82,14 @@ class FilesystemBackend:
|
|
|
86
82
|
return (self.cwd / path).resolve()
|
|
87
83
|
|
|
88
84
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
89
|
-
"""List files
|
|
85
|
+
"""List files and directories in the specified directory (non-recursive).
|
|
90
86
|
|
|
91
87
|
Args:
|
|
92
88
|
path: Absolute directory path to list files from.
|
|
93
|
-
|
|
89
|
+
|
|
94
90
|
Returns:
|
|
95
|
-
List of FileInfo-like dicts.
|
|
91
|
+
List of FileInfo-like dicts for files and directories directly in the directory.
|
|
92
|
+
Directories have a trailing / in their path and is_dir=True.
|
|
96
93
|
"""
|
|
97
94
|
dir_path = self._resolve_path(path)
|
|
98
95
|
if not dir_path.exists() or not dir_path.is_dir():
|
|
@@ -105,18 +102,22 @@ class FilesystemBackend:
|
|
|
105
102
|
if not cwd_str.endswith("/"):
|
|
106
103
|
cwd_str += "/"
|
|
107
104
|
|
|
108
|
-
#
|
|
105
|
+
# List only direct children (non-recursive)
|
|
109
106
|
try:
|
|
110
|
-
for
|
|
107
|
+
for child_path in dir_path.iterdir():
|
|
111
108
|
try:
|
|
112
|
-
is_file =
|
|
109
|
+
is_file = child_path.is_file()
|
|
110
|
+
is_dir = child_path.is_dir()
|
|
113
111
|
except OSError:
|
|
114
112
|
continue
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
|
|
114
|
+
abs_path = str(child_path)
|
|
115
|
+
|
|
116
|
+
if not self.virtual_mode:
|
|
117
|
+
# Non-virtual mode: use absolute paths
|
|
118
|
+
if is_file:
|
|
118
119
|
try:
|
|
119
|
-
st =
|
|
120
|
+
st = child_path.stat()
|
|
120
121
|
results.append({
|
|
121
122
|
"path": abs_path,
|
|
122
123
|
"is_dir": False,
|
|
@@ -125,8 +126,19 @@ class FilesystemBackend:
|
|
|
125
126
|
})
|
|
126
127
|
except OSError:
|
|
127
128
|
results.append({"path": abs_path, "is_dir": False})
|
|
128
|
-
|
|
129
|
-
|
|
129
|
+
elif is_dir:
|
|
130
|
+
try:
|
|
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
|
+
})
|
|
138
|
+
except OSError:
|
|
139
|
+
results.append({"path": abs_path + "/", "is_dir": True})
|
|
140
|
+
else:
|
|
141
|
+
# Virtual mode: strip cwd prefix
|
|
130
142
|
if abs_path.startswith(cwd_str):
|
|
131
143
|
relative_path = abs_path[len(cwd_str):]
|
|
132
144
|
elif abs_path.startswith(str(self.cwd)):
|
|
@@ -137,16 +149,29 @@ class FilesystemBackend:
|
|
|
137
149
|
relative_path = abs_path
|
|
138
150
|
|
|
139
151
|
virt_path = "/" + relative_path
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
152
|
+
|
|
153
|
+
if is_file:
|
|
154
|
+
try:
|
|
155
|
+
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
|
+
})
|
|
162
|
+
except OSError:
|
|
163
|
+
results.append({"path": virt_path, "is_dir": False})
|
|
164
|
+
elif is_dir:
|
|
165
|
+
try:
|
|
166
|
+
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
|
+
})
|
|
173
|
+
except OSError:
|
|
174
|
+
results.append({"path": virt_path + "/", "is_dir": True})
|
|
150
175
|
except (OSError, PermissionError):
|
|
151
176
|
pass
|
|
152
177
|
|
|
@@ -40,19 +40,38 @@ class StateBackend:
|
|
|
40
40
|
self.runtime = runtime
|
|
41
41
|
|
|
42
42
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
43
|
-
"""List files
|
|
44
|
-
|
|
43
|
+
"""List files and directories in the specified directory (non-recursive).
|
|
44
|
+
|
|
45
45
|
Args:
|
|
46
46
|
path: Absolute path to directory.
|
|
47
|
-
|
|
47
|
+
|
|
48
48
|
Returns:
|
|
49
|
-
List of FileInfo-like dicts.
|
|
49
|
+
List of FileInfo-like dicts for files and directories directly in the directory.
|
|
50
|
+
Directories have a trailing / in their path and is_dir=True.
|
|
50
51
|
"""
|
|
51
52
|
files = self.runtime.state.get("files", {})
|
|
52
53
|
infos: list[FileInfo] = []
|
|
54
|
+
subdirs: set[str] = set()
|
|
55
|
+
|
|
56
|
+
# Normalize path to have trailing slash for proper prefix matching
|
|
57
|
+
normalized_path = path if path.endswith("/") else path + "/"
|
|
58
|
+
|
|
53
59
|
for k, fd in files.items():
|
|
54
|
-
if
|
|
60
|
+
# Check if file is in the specified directory or a subdirectory
|
|
61
|
+
if not k.startswith(normalized_path):
|
|
62
|
+
continue
|
|
63
|
+
|
|
64
|
+
# Get the relative path after the directory
|
|
65
|
+
relative = k[len(normalized_path):]
|
|
66
|
+
|
|
67
|
+
# If relative path contains '/', it's in a subdirectory
|
|
68
|
+
if "/" in relative:
|
|
69
|
+
# Extract the immediate subdirectory name
|
|
70
|
+
subdir_name = relative.split("/")[0]
|
|
71
|
+
subdirs.add(normalized_path + subdir_name + "/")
|
|
55
72
|
continue
|
|
73
|
+
|
|
74
|
+
# This is a file directly in the current directory
|
|
56
75
|
size = len("\n".join(fd.get("content", [])))
|
|
57
76
|
infos.append({
|
|
58
77
|
"path": k,
|
|
@@ -60,6 +79,16 @@ class StateBackend:
|
|
|
60
79
|
"size": int(size),
|
|
61
80
|
"modified_at": fd.get("modified_at", ""),
|
|
62
81
|
})
|
|
82
|
+
|
|
83
|
+
# Add directories to the results
|
|
84
|
+
for subdir in sorted(subdirs):
|
|
85
|
+
infos.append({
|
|
86
|
+
"path": subdir,
|
|
87
|
+
"is_dir": True,
|
|
88
|
+
"size": 0,
|
|
89
|
+
"modified_at": "",
|
|
90
|
+
})
|
|
91
|
+
|
|
63
92
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
64
93
|
return infos
|
|
65
94
|
|
|
@@ -179,24 +179,43 @@ class StoreBackend:
|
|
|
179
179
|
return all_items
|
|
180
180
|
|
|
181
181
|
def ls_info(self, path: str) -> list[FileInfo]:
|
|
182
|
-
"""List files
|
|
183
|
-
|
|
182
|
+
"""List files and directories in the specified directory (non-recursive).
|
|
183
|
+
|
|
184
184
|
Args:
|
|
185
185
|
path: Absolute path to directory.
|
|
186
|
-
|
|
186
|
+
|
|
187
187
|
Returns:
|
|
188
|
-
List of FileInfo-like dicts.
|
|
188
|
+
List of FileInfo-like dicts for files and directories directly in the directory.
|
|
189
|
+
Directories have a trailing / in their path and is_dir=True.
|
|
189
190
|
"""
|
|
190
191
|
store = self._get_store()
|
|
191
192
|
namespace = self._get_namespace()
|
|
192
|
-
|
|
193
|
+
|
|
193
194
|
# Retrieve all items and filter by path prefix locally to avoid
|
|
194
195
|
# coupling to store-specific filter semantics
|
|
195
196
|
items = self._search_store_paginated(store, namespace)
|
|
196
197
|
infos: list[FileInfo] = []
|
|
198
|
+
subdirs: set[str] = set()
|
|
199
|
+
|
|
200
|
+
# Normalize path to have trailing slash for proper prefix matching
|
|
201
|
+
normalized_path = path if path.endswith("/") else path + "/"
|
|
202
|
+
|
|
197
203
|
for item in items:
|
|
198
|
-
if
|
|
204
|
+
# Check if file is in the specified directory or a subdirectory
|
|
205
|
+
if not str(item.key).startswith(normalized_path):
|
|
199
206
|
continue
|
|
207
|
+
|
|
208
|
+
# Get the relative path after the directory
|
|
209
|
+
relative = str(item.key)[len(normalized_path):]
|
|
210
|
+
|
|
211
|
+
# If relative path contains '/', it's in a subdirectory
|
|
212
|
+
if "/" in relative:
|
|
213
|
+
# Extract the immediate subdirectory name
|
|
214
|
+
subdir_name = relative.split("/")[0]
|
|
215
|
+
subdirs.add(normalized_path + subdir_name + "/")
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# This is a file directly in the current directory
|
|
200
219
|
try:
|
|
201
220
|
fd = self._convert_store_item_to_file_data(item)
|
|
202
221
|
except ValueError:
|
|
@@ -208,6 +227,16 @@ class StoreBackend:
|
|
|
208
227
|
"size": int(size),
|
|
209
228
|
"modified_at": fd.get("modified_at", ""),
|
|
210
229
|
})
|
|
230
|
+
|
|
231
|
+
# Add directories to the results
|
|
232
|
+
for subdir in sorted(subdirs):
|
|
233
|
+
infos.append({
|
|
234
|
+
"path": subdir,
|
|
235
|
+
"is_dir": True,
|
|
236
|
+
"size": 0,
|
|
237
|
+
"modified_at": "",
|
|
238
|
+
})
|
|
239
|
+
|
|
211
240
|
infos.sort(key=lambda x: x.get("path", ""))
|
|
212
241
|
return infos
|
|
213
242
|
|
|
@@ -12,7 +12,7 @@ from pathlib import Path
|
|
|
12
12
|
from typing import Any, Literal, TypedDict, List, Dict
|
|
13
13
|
|
|
14
14
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
15
|
-
MAX_LINE_LENGTH =
|
|
15
|
+
MAX_LINE_LENGTH = 10000
|
|
16
16
|
LINE_NUMBER_WIDTH = 6
|
|
17
17
|
TOOL_RESULT_TOKEN_LIMIT = 20000 # Same threshold as eviction
|
|
18
18
|
TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]"
|
|
@@ -42,13 +42,15 @@ def format_content_with_line_numbers(
|
|
|
42
42
|
start_line: int = 1,
|
|
43
43
|
) -> str:
|
|
44
44
|
"""Format file content with line numbers (cat -n style).
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
Chunks lines longer than MAX_LINE_LENGTH with continuation markers (e.g., 5.1, 5.2).
|
|
47
|
+
|
|
46
48
|
Args:
|
|
47
49
|
content: File content as string or list of lines
|
|
48
50
|
start_line: Starting line number (default: 1)
|
|
49
|
-
|
|
51
|
+
|
|
50
52
|
Returns:
|
|
51
|
-
Formatted content with line numbers
|
|
53
|
+
Formatted content with line numbers and continuation markers
|
|
52
54
|
"""
|
|
53
55
|
if isinstance(content, str):
|
|
54
56
|
lines = content.split("\n")
|
|
@@ -56,11 +58,29 @@ def format_content_with_line_numbers(
|
|
|
56
58
|
lines = lines[:-1]
|
|
57
59
|
else:
|
|
58
60
|
lines = content
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
|
|
62
|
+
result_lines = []
|
|
63
|
+
for i, line in enumerate(lines):
|
|
64
|
+
line_num = i + start_line
|
|
65
|
+
|
|
66
|
+
if len(line) <= MAX_LINE_LENGTH:
|
|
67
|
+
result_lines.append(f"{line_num:{LINE_NUMBER_WIDTH}d}\t{line}")
|
|
68
|
+
else:
|
|
69
|
+
# Split long line into chunks with continuation markers
|
|
70
|
+
num_chunks = (len(line) + MAX_LINE_LENGTH - 1) // MAX_LINE_LENGTH
|
|
71
|
+
for chunk_idx in range(num_chunks):
|
|
72
|
+
start = chunk_idx * MAX_LINE_LENGTH
|
|
73
|
+
end = min(start + MAX_LINE_LENGTH, len(line))
|
|
74
|
+
chunk = line[start:end]
|
|
75
|
+
if chunk_idx == 0:
|
|
76
|
+
# First chunk: use normal line number
|
|
77
|
+
result_lines.append(f"{line_num:{LINE_NUMBER_WIDTH}d}\t{chunk}")
|
|
78
|
+
else:
|
|
79
|
+
# Continuation chunks: use decimal notation (e.g., 5.1, 5.2)
|
|
80
|
+
continuation_marker = f"{line_num}.{chunk_idx}"
|
|
81
|
+
result_lines.append(f"{continuation_marker:>{LINE_NUMBER_WIDTH}}\t{chunk}")
|
|
82
|
+
|
|
83
|
+
return "\n".join(result_lines)
|
|
64
84
|
|
|
65
85
|
|
|
66
86
|
def check_empty_content(content: str) -> str | None:
|
|
@@ -91,18 +111,17 @@ def file_data_to_string(file_data: dict[str, Any]) -> str:
|
|
|
91
111
|
|
|
92
112
|
def create_file_data(content: str, created_at: str | None = None) -> dict[str, Any]:
|
|
93
113
|
"""Create a FileData object with timestamps.
|
|
94
|
-
|
|
114
|
+
|
|
95
115
|
Args:
|
|
96
116
|
content: File content as string
|
|
97
117
|
created_at: Optional creation timestamp (ISO format)
|
|
98
|
-
|
|
118
|
+
|
|
99
119
|
Returns:
|
|
100
120
|
FileData dict with content and timestamps
|
|
101
121
|
"""
|
|
102
122
|
lines = content.split("\n") if isinstance(content, str) else content
|
|
103
|
-
lines = [line[i:i+MAX_LINE_LENGTH] for line in lines for i in range(0, len(line) or 1, MAX_LINE_LENGTH)]
|
|
104
123
|
now = datetime.now(UTC).isoformat()
|
|
105
|
-
|
|
124
|
+
|
|
106
125
|
return {
|
|
107
126
|
"content": lines,
|
|
108
127
|
"created_at": created_at or now,
|
|
@@ -112,18 +131,17 @@ def create_file_data(content: str, created_at: str | None = None) -> dict[str, A
|
|
|
112
131
|
|
|
113
132
|
def update_file_data(file_data: dict[str, Any], content: str) -> dict[str, Any]:
|
|
114
133
|
"""Update FileData with new content, preserving creation timestamp.
|
|
115
|
-
|
|
134
|
+
|
|
116
135
|
Args:
|
|
117
136
|
file_data: Existing FileData dict
|
|
118
137
|
content: New content as string
|
|
119
|
-
|
|
138
|
+
|
|
120
139
|
Returns:
|
|
121
140
|
Updated FileData dict
|
|
122
141
|
"""
|
|
123
142
|
lines = content.split("\n") if isinstance(content, str) else content
|
|
124
|
-
lines = [line[i:i+MAX_LINE_LENGTH] for line in lines for i in range(0, len(line) or 1, MAX_LINE_LENGTH)]
|
|
125
143
|
now = datetime.now(UTC).isoformat()
|
|
126
|
-
|
|
144
|
+
|
|
127
145
|
return {
|
|
128
146
|
"content": lines,
|
|
129
147
|
"created_at": file_data["created_at"],
|
|
@@ -657,10 +657,3 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
657
657
|
|
|
658
658
|
tool_result = await handler(request)
|
|
659
659
|
return self._intercept_large_tool_result(tool_result)
|
|
660
|
-
|
|
661
|
-
# Back-compat aliases expected by some tests
|
|
662
|
-
def _create_file_data(content: str):
|
|
663
|
-
return create_file_data(content)
|
|
664
|
-
|
|
665
|
-
def _update_file_data(file_data: dict, content: str):
|
|
666
|
-
return update_file_data(file_data, content)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.1rc1
|
|
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
|
|
@@ -28,7 +28,7 @@ a **planning tool**, **sub agents**, access to a **file system**, and a **detail
|
|
|
28
28
|
|
|
29
29
|
<img src="deep_agents.png" alt="deep agent" width="600"/>
|
|
30
30
|
|
|
31
|
-
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application.
|
|
31
|
+
`deepagents` is a Python package that implements these in a general purpose way so that you can easily create a Deep Agent for your application. For a full overview and quickstart of `deepagents`, the best resource is our [docs](https://docs.langchain.com/oss/python/deepagents/overview).
|
|
32
32
|
|
|
33
33
|
**Acknowledgements: This project was primarily inspired by Claude Code, and initially was largely an attempt to see what made Claude Code general purpose, and make it even more so.**
|
|
34
34
|
|
|
@@ -107,7 +107,7 @@ in the same way you would any LangGraph agent.
|
|
|
107
107
|
|
|
108
108
|
**Context Management**
|
|
109
109
|
|
|
110
|
-
File system tools (`ls`, `read_file`, `write_file`, `edit_file`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
110
|
+
File system tools (`ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
|
|
111
111
|
|
|
112
112
|
**Subagent Spawning**
|
|
113
113
|
|
|
@@ -314,33 +314,6 @@ agent = create_deep_agent(
|
|
|
314
314
|
)
|
|
315
315
|
```
|
|
316
316
|
|
|
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).
|
|
319
|
-
|
|
320
|
-
You can configure persistent long-term memory using a composite backend that routes a path prefix (for example, `/memories/`) to a persistent store.
|
|
321
|
-
|
|
322
|
-
```python
|
|
323
|
-
from deepagents import create_deep_agent
|
|
324
|
-
from deepagents.backends import build_composite_state_backend, StoreBackend
|
|
325
|
-
from langgraph.store.memory import InMemoryStore
|
|
326
|
-
|
|
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
|
-
|
|
338
|
-
agent = create_deep_agent(
|
|
339
|
-
backend=backend_factory,
|
|
340
|
-
store=store,
|
|
341
|
-
)
|
|
342
|
-
```
|
|
343
|
-
|
|
344
317
|
### `interrupt_on`
|
|
345
318
|
A common reality for agents is that some tool operations may be sensitive and require human approval before execution. Deep Agents supports human-in-the-loop workflows through LangGraph’s interrupt capabilities. You can configure which tools require approval using a checkpointer.
|
|
346
319
|
|
|
@@ -413,11 +386,7 @@ Context engineering is one of the main challenges in building effective agents.
|
|
|
413
386
|
```python
|
|
414
387
|
from langchain.agents import create_agent
|
|
415
388
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
416
|
-
|
|
417
|
-
StateBackend,
|
|
418
|
-
CompositeBackend,
|
|
419
|
-
StoreBackend,
|
|
420
|
-
)
|
|
389
|
+
|
|
421
390
|
|
|
422
391
|
# FilesystemMiddleware is included by default in create_deep_agent
|
|
423
392
|
# You can customize it if building a custom agent
|
|
@@ -425,12 +394,7 @@ agent = create_agent(
|
|
|
425
394
|
model="anthropic:claude-sonnet-4-20250514",
|
|
426
395
|
middleware=[
|
|
427
396
|
FilesystemMiddleware(
|
|
428
|
-
backend
|
|
429
|
-
# For persistent memory, use CompositeBackend:
|
|
430
|
-
# backend=CompositeBackend(
|
|
431
|
-
# default=lambda rt: StateBackend(rt)
|
|
432
|
-
# routes={"/memories/": lambda rt: StoreBackend(rt)}
|
|
433
|
-
# )
|
|
397
|
+
backend=..., # Optional: customize storage backend
|
|
434
398
|
system_prompt="Write to the filesystem when...", # Optional custom system prompt override
|
|
435
399
|
custom_tool_descriptions={
|
|
436
400
|
"ls": "Use the ls tool when...",
|
|
@@ -15,17 +15,25 @@ from deepagents.middleware.filesystem import (
|
|
|
15
15
|
FILESYSTEM_SYSTEM_PROMPT,
|
|
16
16
|
FileData,
|
|
17
17
|
FilesystemMiddleware,
|
|
18
|
-
FilesystemState
|
|
19
|
-
_create_file_data,
|
|
20
|
-
_update_file_data,
|
|
18
|
+
FilesystemState
|
|
21
19
|
)
|
|
22
|
-
from deepagents.backends import StoreBackend, CompositeBackend,
|
|
20
|
+
from deepagents.backends import StoreBackend, CompositeBackend, StateBackend
|
|
23
21
|
|
|
24
22
|
from deepagents.backends.utils import create_file_data, update_file_data
|
|
25
23
|
from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
|
|
26
24
|
from deepagents.middleware.subagents import DEFAULT_GENERAL_PURPOSE_DESCRIPTION, TASK_SYSTEM_PROMPT, TASK_TOOL_DESCRIPTION, SubAgentMiddleware
|
|
27
25
|
from deepagents.backends.utils import truncate_if_too_long
|
|
28
26
|
|
|
27
|
+
def build_composite_state_backend(runtime: ToolRuntime, *, routes):
|
|
28
|
+
built_routes = {}
|
|
29
|
+
for prefix, backend_or_factory in routes.items():
|
|
30
|
+
if callable(backend_or_factory):
|
|
31
|
+
built_routes[prefix] = backend_or_factory(runtime)
|
|
32
|
+
else:
|
|
33
|
+
built_routes[prefix] = backend_or_factory
|
|
34
|
+
default_state = StateBackend(runtime)
|
|
35
|
+
return CompositeBackend(default=default_state, routes=built_routes)
|
|
36
|
+
|
|
29
37
|
class TestAddMiddleware:
|
|
30
38
|
def test_filesystem_middleware(self):
|
|
31
39
|
middleware = [FilesystemMiddleware()]
|
|
@@ -160,8 +168,55 @@ class TestFilesystemMiddleware:
|
|
|
160
168
|
"runtime": ToolRuntime(state=state, context=None, tool_call_id="", store=None, stream_writer=lambda _: None, config={}),
|
|
161
169
|
}
|
|
162
170
|
)
|
|
171
|
+
# ls should only return files directly in /pokemon/, not in subdirectories
|
|
163
172
|
assert "/pokemon/test2.txt" in result
|
|
164
173
|
assert "/pokemon/charmander.txt" in result
|
|
174
|
+
assert "/pokemon/water/squirtle.txt" not in result # In subdirectory, should NOT be listed
|
|
175
|
+
# ls should also list subdirectories with trailing /
|
|
176
|
+
assert "/pokemon/water/" in result
|
|
177
|
+
|
|
178
|
+
def test_ls_shortterm_lists_directories(self):
|
|
179
|
+
"""Test that ls lists directories with trailing / for traversal."""
|
|
180
|
+
state = FilesystemState(
|
|
181
|
+
messages=[],
|
|
182
|
+
files={
|
|
183
|
+
"/test.txt": FileData(
|
|
184
|
+
content=["Hello world"],
|
|
185
|
+
modified_at="2021-01-01",
|
|
186
|
+
created_at="2021-01-01",
|
|
187
|
+
),
|
|
188
|
+
"/pokemon/charmander.txt": FileData(
|
|
189
|
+
content=["Ember"],
|
|
190
|
+
modified_at="2021-01-01",
|
|
191
|
+
created_at="2021-01-01",
|
|
192
|
+
),
|
|
193
|
+
"/pokemon/water/squirtle.txt": FileData(
|
|
194
|
+
content=["Water"],
|
|
195
|
+
modified_at="2021-01-01",
|
|
196
|
+
created_at="2021-01-01",
|
|
197
|
+
),
|
|
198
|
+
"/docs/readme.md": FileData(
|
|
199
|
+
content=["Documentation"],
|
|
200
|
+
modified_at="2021-01-01",
|
|
201
|
+
created_at="2021-01-01",
|
|
202
|
+
),
|
|
203
|
+
},
|
|
204
|
+
)
|
|
205
|
+
middleware = FilesystemMiddleware()
|
|
206
|
+
ls_tool = next(tool for tool in middleware.tools if tool.name == "ls")
|
|
207
|
+
result = ls_tool.invoke(
|
|
208
|
+
{
|
|
209
|
+
"path": "/",
|
|
210
|
+
"runtime": ToolRuntime(state=state, context=None, tool_call_id="", store=None, stream_writer=lambda _: None, config={}),
|
|
211
|
+
}
|
|
212
|
+
)
|
|
213
|
+
# ls should list both files and directories at root level
|
|
214
|
+
assert "/test.txt" in result
|
|
215
|
+
assert "/pokemon/" in result
|
|
216
|
+
assert "/docs/" in result
|
|
217
|
+
# But NOT subdirectory files
|
|
218
|
+
assert "/pokemon/charmander.txt" not in result
|
|
219
|
+
assert "/pokemon/water/squirtle.txt" not in result
|
|
165
220
|
|
|
166
221
|
def test_glob_search_shortterm_simple_pattern(self):
|
|
167
222
|
state = FilesystemState(
|
|
@@ -641,22 +696,21 @@ class TestFilesystemMiddleware:
|
|
|
641
696
|
keys = {item.key for item in result}
|
|
642
697
|
assert keys == {f"/file{i}.txt" for i in range(55)}
|
|
643
698
|
|
|
644
|
-
def
|
|
699
|
+
def test_create_file_data_preserves_long_lines(self):
|
|
700
|
+
"""Test that create_file_data stores long lines as-is without splitting."""
|
|
645
701
|
long_line = "a" * 3500
|
|
646
702
|
short_line = "short line"
|
|
647
703
|
content = f"{short_line}\n{long_line}"
|
|
648
704
|
|
|
649
705
|
file_data = create_file_data(content)
|
|
650
706
|
|
|
651
|
-
|
|
652
|
-
assert len(line) <= 2000
|
|
653
|
-
|
|
654
|
-
assert len(file_data["content"]) == 3
|
|
707
|
+
assert len(file_data["content"]) == 2
|
|
655
708
|
assert file_data["content"][0] == short_line
|
|
656
|
-
assert file_data["content"][1] ==
|
|
657
|
-
assert file_data["content"][
|
|
709
|
+
assert file_data["content"][1] == long_line
|
|
710
|
+
assert len(file_data["content"][1]) == 3500
|
|
658
711
|
|
|
659
|
-
def
|
|
712
|
+
def test_update_file_data_preserves_long_lines(self):
|
|
713
|
+
"""Test that update_file_data stores long lines as-is without splitting."""
|
|
660
714
|
initial_file_data = create_file_data("initial content")
|
|
661
715
|
|
|
662
716
|
long_line = "b" * 5000
|
|
@@ -665,17 +719,111 @@ class TestFilesystemMiddleware:
|
|
|
665
719
|
|
|
666
720
|
updated_file_data = update_file_data(initial_file_data, new_content)
|
|
667
721
|
|
|
668
|
-
|
|
669
|
-
assert len(line) <= 2000
|
|
670
|
-
|
|
671
|
-
assert len(updated_file_data["content"]) == 4
|
|
722
|
+
assert len(updated_file_data["content"]) == 2
|
|
672
723
|
assert updated_file_data["content"][0] == short_line
|
|
673
|
-
assert updated_file_data["content"][1] ==
|
|
674
|
-
assert updated_file_data["content"][
|
|
675
|
-
assert updated_file_data["content"][3] == "b" * 1000
|
|
724
|
+
assert updated_file_data["content"][1] == long_line
|
|
725
|
+
assert len(updated_file_data["content"][1]) == 5000
|
|
676
726
|
|
|
677
727
|
assert updated_file_data["created_at"] == initial_file_data["created_at"]
|
|
678
728
|
|
|
729
|
+
def test_format_content_with_line_numbers_short_lines(self):
|
|
730
|
+
"""Test that short lines (<=10000 chars) are displayed normally."""
|
|
731
|
+
from deepagents.backends.utils import format_content_with_line_numbers
|
|
732
|
+
|
|
733
|
+
content = ["short line 1", "short line 2", "short line 3"]
|
|
734
|
+
result = format_content_with_line_numbers(content, start_line=1)
|
|
735
|
+
|
|
736
|
+
lines = result.split("\n")
|
|
737
|
+
assert len(lines) == 3
|
|
738
|
+
assert " 1\tshort line 1" in lines[0]
|
|
739
|
+
assert " 2\tshort line 2" in lines[1]
|
|
740
|
+
assert " 3\tshort line 3" in lines[2]
|
|
741
|
+
|
|
742
|
+
def test_format_content_with_line_numbers_long_line_with_continuation(self):
|
|
743
|
+
"""Test that long lines (>10000 chars) are split with continuation markers."""
|
|
744
|
+
from deepagents.backends.utils import format_content_with_line_numbers
|
|
745
|
+
|
|
746
|
+
long_line = "a" * 25000
|
|
747
|
+
content = ["short line", long_line, "another short line"]
|
|
748
|
+
result = format_content_with_line_numbers(content, start_line=1)
|
|
749
|
+
|
|
750
|
+
lines = result.split("\n")
|
|
751
|
+
assert len(lines) == 5
|
|
752
|
+
assert " 1\tshort line" in lines[0]
|
|
753
|
+
assert " 2\t" in lines[1]
|
|
754
|
+
assert lines[1].count("a") == 10000
|
|
755
|
+
assert " 2.1\t" in lines[2]
|
|
756
|
+
assert lines[2].count("a") == 10000
|
|
757
|
+
assert " 2.2\t" in lines[3]
|
|
758
|
+
assert lines[3].count("a") == 5000
|
|
759
|
+
assert " 3\tanother short line" in lines[4]
|
|
760
|
+
|
|
761
|
+
def test_format_content_with_line_numbers_multiple_long_lines(self):
|
|
762
|
+
"""Test multiple long lines in sequence with proper line numbering."""
|
|
763
|
+
from deepagents.backends.utils import format_content_with_line_numbers
|
|
764
|
+
|
|
765
|
+
long_line_1 = "x" * 15000
|
|
766
|
+
long_line_2 = "y" * 15000
|
|
767
|
+
content = [long_line_1, "middle", long_line_2]
|
|
768
|
+
result = format_content_with_line_numbers(content, start_line=5)
|
|
769
|
+
lines = result.split("\n")
|
|
770
|
+
assert len(lines) == 5
|
|
771
|
+
assert " 5\t" in lines[0]
|
|
772
|
+
assert lines[0].count("x") == 10000
|
|
773
|
+
assert " 5.1\t" in lines[1]
|
|
774
|
+
assert lines[1].count("x") == 5000
|
|
775
|
+
assert " 6\tmiddle" in lines[2]
|
|
776
|
+
assert " 7\t" in lines[3]
|
|
777
|
+
assert lines[3].count("y") == 10000
|
|
778
|
+
assert " 7.1\t" in lines[4]
|
|
779
|
+
assert lines[4].count("y") == 5000
|
|
780
|
+
|
|
781
|
+
def test_format_content_with_line_numbers_exact_limit(self):
|
|
782
|
+
"""Test that a line exactly at the 10000 char limit is not split."""
|
|
783
|
+
from deepagents.backends.utils import format_content_with_line_numbers
|
|
784
|
+
|
|
785
|
+
exact_line = "b" * 10000
|
|
786
|
+
content = [exact_line]
|
|
787
|
+
result = format_content_with_line_numbers(content, start_line=1)
|
|
788
|
+
|
|
789
|
+
lines = result.split("\n")
|
|
790
|
+
assert len(lines) == 1
|
|
791
|
+
assert " 1\t" in lines[0]
|
|
792
|
+
assert lines[0].count("b") == 10000
|
|
793
|
+
|
|
794
|
+
def test_read_file_with_long_lines_shows_continuation_markers(self):
|
|
795
|
+
"""Test that read_file displays long lines with continuation markers."""
|
|
796
|
+
from deepagents.backends.utils import format_read_response, create_file_data
|
|
797
|
+
|
|
798
|
+
long_line = "z" * 15000
|
|
799
|
+
content = f"first line\n{long_line}\nthird line"
|
|
800
|
+
file_data = create_file_data(content)
|
|
801
|
+
result = format_read_response(file_data, offset=0, limit=100)
|
|
802
|
+
lines = result.split("\n")
|
|
803
|
+
assert len(lines) == 4
|
|
804
|
+
assert " 1\tfirst line" in lines[0]
|
|
805
|
+
assert " 2\t" in lines[1]
|
|
806
|
+
assert lines[1].count("z") == 10000
|
|
807
|
+
assert " 2.1\t" in lines[2]
|
|
808
|
+
assert lines[2].count("z") == 5000
|
|
809
|
+
assert " 3\tthird line" in lines[3]
|
|
810
|
+
|
|
811
|
+
def test_read_file_with_offset_and_long_lines(self):
|
|
812
|
+
"""Test that read_file with offset handles long lines correctly."""
|
|
813
|
+
from deepagents.backends.utils import format_read_response, create_file_data
|
|
814
|
+
|
|
815
|
+
long_line = "m" * 12000
|
|
816
|
+
content = f"line1\nline2\n{long_line}\nline4"
|
|
817
|
+
file_data = create_file_data(content)
|
|
818
|
+
result = format_read_response(file_data, offset=2, limit=10)
|
|
819
|
+
lines = result.split("\n")
|
|
820
|
+
assert len(lines) == 3
|
|
821
|
+
assert " 3\t" in lines[0]
|
|
822
|
+
assert lines[0].count("m") == 10000
|
|
823
|
+
assert " 3.1\t" in lines[1]
|
|
824
|
+
assert lines[1].count("m") == 2000
|
|
825
|
+
assert " 4\tline4" in lines[2]
|
|
826
|
+
|
|
679
827
|
|
|
680
828
|
@pytest.mark.requires("langchain_openai")
|
|
681
829
|
class TestSubagentMiddleware:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|