deepagents 0.1.5rc2__tar.gz → 0.2.1__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 (24) hide show
  1. deepagents-0.1.5rc2/README.md → deepagents-0.2.1/PKG-INFO +24 -40
  2. deepagents-0.1.5rc2/PKG-INFO → deepagents-0.2.1/README.md +4 -60
  3. {deepagents-0.1.5rc2 → deepagents-0.2.1}/pyproject.toml +2 -2
  4. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/__init__.py +1 -2
  5. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/composite.py +16 -27
  6. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/filesystem.py +54 -29
  7. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/state.py +34 -5
  8. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/store.py +35 -6
  9. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/utils.py +44 -17
  10. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/middleware/filesystem.py +84 -53
  11. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/middleware/patch_tool_calls.py +3 -3
  12. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/middleware/subagents.py +0 -1
  13. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents.egg-info/PKG-INFO +6 -42
  14. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents.egg-info/requires.txt +1 -1
  15. {deepagents-0.1.5rc2 → deepagents-0.2.1}/tests/test_middleware.py +343 -79
  16. {deepagents-0.1.5rc2 → deepagents-0.2.1}/LICENSE +0 -0
  17. {deepagents-0.1.5rc2 → deepagents-0.2.1}/setup.cfg +0 -0
  18. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/__init__.py +0 -0
  19. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/backends/protocol.py +0 -0
  20. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/graph.py +0 -0
  21. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents/middleware/__init__.py +0 -0
  22. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents.egg-info/SOURCES.txt +0 -0
  23. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents.egg-info/dependency_links.txt +0 -0
  24. {deepagents-0.1.5rc2 → deepagents-0.2.1}/src/deepagents.egg-info/top_level.txt +0 -0
@@ -1,3 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: deepagents
3
+ Version: 0.2.1
4
+ Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
+ License: MIT
6
+ Requires-Python: <4.0,>=3.11
7
+ Description-Content-Type: text/markdown
8
+ License-File: LICENSE
9
+ Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
10
+ Requires-Dist: langchain<2.0.0,>=1.0.2
11
+ Requires-Dist: langchain-core<2.0.0,>=1.0.0
12
+ Requires-Dist: wcmatch
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest; extra == "dev"
15
+ Requires-Dist: pytest-cov; extra == "dev"
16
+ Requires-Dist: build; extra == "dev"
17
+ Requires-Dist: twine; extra == "dev"
18
+ Requires-Dist: langchain-openai; extra == "dev"
19
+ Dynamic: license-file
20
+
1
21
  # 🧠🤖Deep Agents
2
22
 
3
23
  Using an LLM to call tools in a loop is the simplest form of an agent.
@@ -8,7 +28,7 @@ a **planning tool**, **sub agents**, access to a **file system**, and a **detail
8
28
 
9
29
  <img src="deep_agents.png" alt="deep agent" width="600"/>
10
30
 
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.
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).
12
32
 
13
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.**
14
34
 
@@ -87,7 +107,7 @@ in the same way you would any LangGraph agent.
87
107
 
88
108
  **Context Management**
89
109
 
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.
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.
91
111
 
92
112
  **Subagent Spawning**
93
113
 
@@ -294,33 +314,6 @@ agent = create_deep_agent(
294
314
  )
295
315
  ```
296
316
 
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
317
  ### `interrupt_on`
325
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.
326
319
 
@@ -393,11 +386,7 @@ Context engineering is one of the main challenges in building effective agents.
393
386
  ```python
394
387
  from langchain.agents import create_agent
395
388
  from deepagents.middleware.filesystem import FilesystemMiddleware
396
- from deepagents.backends import (
397
- StateBackend,
398
- CompositeBackend,
399
- StoreBackend,
400
- )
389
+
401
390
 
402
391
  # FilesystemMiddleware is included by default in create_deep_agent
403
392
  # You can customize it if building a custom agent
@@ -405,12 +394,7 @@ agent = create_agent(
405
394
  model="anthropic:claude-sonnet-4-20250514",
406
395
  middleware=[
407
396
  FilesystemMiddleware(
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
- # )
397
+ backend=..., # Optional: customize storage backend
414
398
  system_prompt="Write to the filesystem when...", # Optional custom system prompt override
415
399
  custom_tool_descriptions={
416
400
  "ls": "Use the ls tool when...",
@@ -1,23 +1,3 @@
1
- Metadata-Version: 2.4
2
- Name: deepagents
3
- Version: 0.1.5rc2
4
- Summary: General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph.
5
- License: MIT
6
- Requires-Python: <4.0,>=3.11
7
- Description-Content-Type: text/markdown
8
- License-File: LICENSE
9
- Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
10
- Requires-Dist: langchain<2.0.0,>=1.0.0
11
- Requires-Dist: langchain-core<2.0.0,>=1.0.0
12
- Requires-Dist: wcmatch
13
- Provides-Extra: dev
14
- Requires-Dist: pytest; extra == "dev"
15
- Requires-Dist: pytest-cov; extra == "dev"
16
- Requires-Dist: build; extra == "dev"
17
- Requires-Dist: twine; extra == "dev"
18
- Requires-Dist: langchain-openai; extra == "dev"
19
- Dynamic: license-file
20
-
21
1
  # 🧠🤖Deep Agents
22
2
 
23
3
  Using an LLM to call tools in a loop is the simplest form of an agent.
@@ -28,7 +8,7 @@ a **planning tool**, **sub agents**, access to a **file system**, and a **detail
28
8
 
29
9
  <img src="deep_agents.png" alt="deep agent" width="600"/>
30
10
 
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.
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).
32
12
 
33
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.**
34
14
 
@@ -107,7 +87,7 @@ in the same way you would any LangGraph agent.
107
87
 
108
88
  **Context Management**
109
89
 
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.
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.
111
91
 
112
92
  **Subagent Spawning**
113
93
 
@@ -314,33 +294,6 @@ agent = create_deep_agent(
314
294
  )
315
295
  ```
316
296
 
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
297
  ### `interrupt_on`
345
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.
346
299
 
@@ -413,11 +366,7 @@ Context engineering is one of the main challenges in building effective agents.
413
366
  ```python
414
367
  from langchain.agents import create_agent
415
368
  from deepagents.middleware.filesystem import FilesystemMiddleware
416
- from deepagents.backends import (
417
- StateBackend,
418
- CompositeBackend,
419
- StoreBackend,
420
- )
369
+
421
370
 
422
371
  # FilesystemMiddleware is included by default in create_deep_agent
423
372
  # You can customize it if building a custom agent
@@ -425,12 +374,7 @@ agent = create_agent(
425
374
  model="anthropic:claude-sonnet-4-20250514",
426
375
  middleware=[
427
376
  FilesystemMiddleware(
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
- # )
377
+ backend=..., # Optional: customize storage backend
434
378
  system_prompt="Write to the filesystem when...", # Optional custom system prompt override
435
379
  custom_tool_descriptions={
436
380
  "ls": "Use the ls tool when...",
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "deepagents"
3
- version = "0.1.5rc2"
3
+ version = "0.2.1"
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" }
7
7
  requires-python = ">=3.11,<4.0"
8
8
  dependencies = [
9
9
  "langchain-anthropic>=1.0.0,<2.0.0",
10
- "langchain>=1.0.0,<2.0.0",
10
+ "langchain>=1.0.2,<2.0.0",
11
11
  "langchain-core>=1.0.0,<2.0.0",
12
12
  "wcmatch"
13
13
  ]
@@ -1,6 +1,6 @@
1
1
  """Memory backends for pluggable file storage."""
2
2
 
3
- from deepagents.backends.composite import CompositeBackend, build_composite_state_backend
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 Any, Literal, Optional, TYPE_CHECKING
3
+ from typing import Optional
4
4
 
5
- from langchain.tools import ToolRuntime
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 from backends, with appropriate prefixes.
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
- 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)
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
- 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)
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 Any, Optional, TYPE_CHECKING
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: {key}") from None
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 from filesystem.
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
- # Walk the directory tree
105
+ # List only direct children (non-recursive)
109
106
  try:
110
- for path in dir_path.rglob("*"):
107
+ for child_path in dir_path.iterdir():
111
108
  try:
112
- is_file = path.is_file()
109
+ is_file = child_path.is_file()
110
+ is_dir = child_path.is_dir()
113
111
  except OSError:
114
112
  continue
115
- if is_file:
116
- abs_path = str(path)
117
- if not self.virtual_mode:
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 = path.stat()
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
- continue
129
- # Strip the cwd prefix if present
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
- try:
141
- st = path.stat()
142
- results.append({
143
- "path": virt_path,
144
- "is_dir": False,
145
- "size": int(st.st_size),
146
- "modified_at": datetime.fromtimestamp(st.st_mtime).isoformat(),
147
- })
148
- except OSError:
149
- results.append({"path": virt_path, "is_dir": False})
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 from state.
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 not k.startswith(path):
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 from store.
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 not str(item.key).startswith(path):
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