deepagents 0.2.4__py3-none-any.whl → 0.2.6__py3-none-any.whl

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.
@@ -1,44 +1,38 @@
1
1
  """StateBackend: Store files in LangGraph agent state (ephemeral)."""
2
2
 
3
- import re
4
- from typing import Any, Literal, Optional, TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
5
4
 
6
- from langchain.tools import ToolRuntime
7
-
8
- from langchain_core.messages import ToolMessage
9
- from langgraph.types import Command
10
-
11
- from .utils import (
5
+ from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
6
+ from deepagents.backends.utils import (
7
+ _glob_search_files,
12
8
  create_file_data,
13
- update_file_data,
14
9
  file_data_to_string,
15
10
  format_read_response,
16
- perform_string_replacement,
17
- _glob_search_files,
18
11
  grep_matches_from_files,
12
+ perform_string_replacement,
13
+ update_file_data,
19
14
  )
20
- from deepagents.backends.utils import FileInfo, GrepMatch
21
- from deepagents.backends.protocol import WriteResult, EditResult
22
15
 
16
+ if TYPE_CHECKING:
17
+ from langchain.tools import ToolRuntime
23
18
 
24
- class StateBackend:
19
+
20
+ class StateBackend(BackendProtocol):
25
21
  """Backend that stores files in agent state (ephemeral).
26
-
22
+
27
23
  Uses LangGraph's state management and checkpointing. Files persist within
28
24
  a conversation thread but not across threads. State is automatically
29
25
  checkpointed after each agent step.
30
-
26
+
31
27
  Special handling: Since LangGraph state must be updated via Command objects
32
28
  (not direct mutation), operations return Command objects instead of None.
33
29
  This is indicated by the uses_state=True flag.
34
30
  """
35
-
31
+
36
32
  def __init__(self, runtime: "ToolRuntime"):
37
- """Initialize StateBackend with runtime.
38
-
39
- Args:"""
33
+ """Initialize StateBackend with runtime."""
40
34
  self.runtime = runtime
41
-
35
+
42
36
  def ls_info(self, path: str) -> list[FileInfo]:
43
37
  """List files and directories in the specified directory (non-recursive).
44
38
 
@@ -62,7 +56,7 @@ class StateBackend:
62
56
  continue
63
57
 
64
58
  # Get the relative path after the directory
65
- relative = k[len(normalized_path):]
59
+ relative = k[len(normalized_path) :]
66
60
 
67
61
  # If relative path contains '/', it's in a subdirectory
68
62
  if "/" in relative:
@@ -73,35 +67,39 @@ class StateBackend:
73
67
 
74
68
  # This is a file directly in the current directory
75
69
  size = len("\n".join(fd.get("content", [])))
76
- infos.append({
77
- "path": k,
78
- "is_dir": False,
79
- "size": int(size),
80
- "modified_at": fd.get("modified_at", ""),
81
- })
70
+ infos.append(
71
+ {
72
+ "path": k,
73
+ "is_dir": False,
74
+ "size": int(size),
75
+ "modified_at": fd.get("modified_at", ""),
76
+ }
77
+ )
82
78
 
83
79
  # Add directories to the results
84
80
  for subdir in sorted(subdirs):
85
- infos.append({
86
- "path": subdir,
87
- "is_dir": True,
88
- "size": 0,
89
- "modified_at": "",
90
- })
81
+ infos.append(
82
+ {
83
+ "path": subdir,
84
+ "is_dir": True,
85
+ "size": 0,
86
+ "modified_at": "",
87
+ }
88
+ )
91
89
 
92
90
  infos.sort(key=lambda x: x.get("path", ""))
93
91
  return infos
94
92
 
95
93
  # Removed legacy ls() convenience to keep lean surface
96
-
94
+
97
95
  def read(
98
- self,
96
+ self,
99
97
  file_path: str,
100
98
  offset: int = 0,
101
99
  limit: int = 2000,
102
100
  ) -> str:
103
101
  """Read file content with line numbers.
104
-
102
+
105
103
  Args:
106
104
  file_path: Absolute file path
107
105
  offset: Line offset to start reading from (0-indexed)
@@ -110,14 +108,14 @@ class StateBackend:
110
108
  """
111
109
  files = self.runtime.state.get("files", {})
112
110
  file_data = files.get(file_path)
113
-
111
+
114
112
  if file_data is None:
115
113
  return f"Error: File '{file_path}' not found"
116
-
114
+
117
115
  return format_read_response(file_data, offset, limit)
118
-
116
+
119
117
  def write(
120
- self,
118
+ self,
121
119
  file_path: str,
122
120
  content: str,
123
121
  ) -> WriteResult:
@@ -125,15 +123,15 @@ class StateBackend:
125
123
  Returns WriteResult with files_update to update LangGraph state.
126
124
  """
127
125
  files = self.runtime.state.get("files", {})
128
-
126
+
129
127
  if file_path in files:
130
128
  return WriteResult(error=f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path.")
131
-
129
+
132
130
  new_file_data = create_file_data(content)
133
131
  return WriteResult(path=file_path, files_update={file_path: new_file_data})
134
-
132
+
135
133
  def edit(
136
- self,
134
+ self,
137
135
  file_path: str,
138
136
  old_string: str,
139
137
  new_string: str,
@@ -144,31 +142,31 @@ class StateBackend:
144
142
  """
145
143
  files = self.runtime.state.get("files", {})
146
144
  file_data = files.get(file_path)
147
-
145
+
148
146
  if file_data is None:
149
147
  return EditResult(error=f"Error: File '{file_path}' not found")
150
-
148
+
151
149
  content = file_data_to_string(file_data)
152
150
  result = perform_string_replacement(content, old_string, new_string, replace_all)
153
-
151
+
154
152
  if isinstance(result, str):
155
153
  return EditResult(error=result)
156
-
154
+
157
155
  new_content, occurrences = result
158
156
  new_file_data = update_file_data(file_data, new_content)
159
157
  return EditResult(path=file_path, files_update={file_path: new_file_data}, occurrences=int(occurrences))
160
-
158
+
161
159
  # Removed legacy grep() convenience to keep lean surface
162
160
 
163
161
  def grep_raw(
164
162
  self,
165
163
  pattern: str,
166
164
  path: str = "/",
167
- glob: Optional[str] = None,
165
+ glob: str | None = None,
168
166
  ) -> list[GrepMatch] | str:
169
167
  files = self.runtime.state.get("files", {})
170
168
  return grep_matches_from_files(files, pattern, path, glob)
171
-
169
+
172
170
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
173
171
  files = self.runtime.state.get("files", {})
174
172
  result = _glob_search_files(files, pattern, path)
@@ -179,12 +177,15 @@ class StateBackend:
179
177
  for p in paths:
180
178
  fd = files.get(p)
181
179
  size = len("\n".join(fd.get("content", []))) if fd else 0
182
- infos.append({
183
- "path": p,
184
- "is_dir": False,
185
- "size": int(size),
186
- "modified_at": fd.get("modified_at", "") if fd else "",
187
- })
180
+ infos.append(
181
+ {
182
+ "path": p,
183
+ "is_dir": False,
184
+ "size": int(size),
185
+ "modified_at": fd.get("modified_at", "") if fd else "",
186
+ }
187
+ )
188
188
  return infos
189
189
 
190
+
190
191
  # Provider classes removed: prefer callables like `lambda rt: StateBackend(rt)`
@@ -1,48 +1,44 @@
1
1
  """StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread)."""
2
2
 
3
- import re
4
- from typing import Any, Optional, TYPE_CHECKING
5
-
6
- if TYPE_CHECKING:
7
- from langchain.tools import ToolRuntime
3
+ from typing import Any
8
4
 
9
5
  from langgraph.config import get_config
10
6
  from langgraph.store.base import BaseStore, Item
11
- from deepagents.backends.protocol import WriteResult, EditResult
12
7
 
8
+ from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
13
9
  from deepagents.backends.utils import (
10
+ _glob_search_files,
14
11
  create_file_data,
15
- update_file_data,
16
12
  file_data_to_string,
17
13
  format_read_response,
18
- perform_string_replacement,
19
- _glob_search_files,
20
14
  grep_matches_from_files,
15
+ perform_string_replacement,
16
+ update_file_data,
21
17
  )
22
- from deepagents.backends.utils import FileInfo, GrepMatch
23
18
 
24
19
 
25
- class StoreBackend:
20
+ class StoreBackend(BackendProtocol):
26
21
  """Backend that stores files in LangGraph's BaseStore (persistent).
27
-
22
+
28
23
  Uses LangGraph's Store for persistent, cross-conversation storage.
29
24
  Files are organized via namespaces and persist across all threads.
30
-
25
+
31
26
  The namespace can include an optional assistant_id for multi-agent isolation.
32
27
  """
28
+
33
29
  def __init__(self, runtime: "ToolRuntime"):
34
30
  """Initialize StoreBackend with runtime.
35
-
36
- Args:"""
37
- self.runtime = runtime
38
31
 
32
+ Args:
33
+ """
34
+ self.runtime = runtime
39
35
 
40
36
  def _get_store(self) -> BaseStore:
41
37
  """Get the store instance.
42
-
38
+
43
39
  Args:Returns:
44
40
  BaseStore instance
45
-
41
+
46
42
  Raises:
47
43
  ValueError: If no store is available or runtime not provided
48
44
  """
@@ -51,15 +47,15 @@ class StoreBackend:
51
47
  msg = "Store is required but not available in runtime"
52
48
  raise ValueError(msg)
53
49
  return store
54
-
50
+
55
51
  def _get_namespace(self) -> tuple[str, ...]:
56
52
  """Get the namespace for store operations.
57
-
53
+
58
54
  Preference order:
59
55
  1) Use `self.runtime.config` if present (tests pass this explicitly).
60
56
  2) Fallback to `langgraph.config.get_config()` if available.
61
57
  3) Default to ("filesystem",).
62
-
58
+
63
59
  If an assistant_id is available in the config metadata, return
64
60
  (assistant_id, "filesystem") to provide per-assistant isolation.
65
61
  """
@@ -88,16 +84,16 @@ class StoreBackend:
88
84
  if assistant_id:
89
85
  return (assistant_id, namespace)
90
86
  return (namespace,)
91
-
87
+
92
88
  def _convert_store_item_to_file_data(self, store_item: Item) -> dict[str, Any]:
93
89
  """Convert a store Item to FileData format.
94
-
90
+
95
91
  Args:
96
92
  store_item: The store Item containing file data.
97
-
93
+
98
94
  Returns:
99
95
  FileData dict with content, created_at, and modified_at fields.
100
-
96
+
101
97
  Raises:
102
98
  ValueError: If required fields are missing or have incorrect types.
103
99
  """
@@ -115,13 +111,13 @@ class StoreBackend:
115
111
  "created_at": store_item.value["created_at"],
116
112
  "modified_at": store_item.value["modified_at"],
117
113
  }
118
-
114
+
119
115
  def _convert_file_data_to_store_value(self, file_data: dict[str, Any]) -> dict[str, Any]:
120
116
  """Convert FileData to a dict suitable for store.put().
121
-
117
+
122
118
  Args:
123
119
  file_data: The FileData to convert.
124
-
120
+
125
121
  Returns:
126
122
  Dictionary with content, created_at, and modified_at fields.
127
123
  """
@@ -177,7 +173,7 @@ class StoreBackend:
177
173
  offset += page_size
178
174
 
179
175
  return all_items
180
-
176
+
181
177
  def ls_info(self, path: str) -> list[FileInfo]:
182
178
  """List files and directories in the specified directory (non-recursive).
183
179
 
@@ -206,7 +202,7 @@ class StoreBackend:
206
202
  continue
207
203
 
208
204
  # Get the relative path after the directory
209
- relative = str(item.key)[len(normalized_path):]
205
+ relative = str(item.key)[len(normalized_path) :]
210
206
 
211
207
  # If relative path contains '/', it's in a subdirectory
212
208
  if "/" in relative:
@@ -221,58 +217,62 @@ class StoreBackend:
221
217
  except ValueError:
222
218
  continue
223
219
  size = len("\n".join(fd.get("content", [])))
224
- infos.append({
225
- "path": item.key,
226
- "is_dir": False,
227
- "size": int(size),
228
- "modified_at": fd.get("modified_at", ""),
229
- })
220
+ infos.append(
221
+ {
222
+ "path": item.key,
223
+ "is_dir": False,
224
+ "size": int(size),
225
+ "modified_at": fd.get("modified_at", ""),
226
+ }
227
+ )
230
228
 
231
229
  # Add directories to the results
232
230
  for subdir in sorted(subdirs):
233
- infos.append({
234
- "path": subdir,
235
- "is_dir": True,
236
- "size": 0,
237
- "modified_at": "",
238
- })
231
+ infos.append(
232
+ {
233
+ "path": subdir,
234
+ "is_dir": True,
235
+ "size": 0,
236
+ "modified_at": "",
237
+ }
238
+ )
239
239
 
240
240
  infos.sort(key=lambda x: x.get("path", ""))
241
241
  return infos
242
242
 
243
243
  # Removed legacy ls() convenience to keep lean surface
244
-
244
+
245
245
  def read(
246
- self,
246
+ self,
247
247
  file_path: str,
248
248
  offset: int = 0,
249
249
  limit: int = 2000,
250
250
  ) -> str:
251
251
  """Read file content with line numbers.
252
-
252
+
253
253
  Args:
254
254
  file_path: Absolute file path
255
255
  offset: Line offset to start reading from (0-indexed)limit: Maximum number of lines to read
256
-
256
+
257
257
  Returns:
258
258
  Formatted file content with line numbers, or error message.
259
259
  """
260
260
  store = self._get_store()
261
261
  namespace = self._get_namespace()
262
- item: Optional[Item] = store.get(namespace, file_path)
263
-
262
+ item: Item | None = store.get(namespace, file_path)
263
+
264
264
  if item is None:
265
265
  return f"Error: File '{file_path}' not found"
266
-
266
+
267
267
  try:
268
268
  file_data = self._convert_store_item_to_file_data(item)
269
269
  except ValueError as e:
270
270
  return f"Error: {e}"
271
-
271
+
272
272
  return format_read_response(file_data, offset, limit)
273
-
273
+
274
274
  def write(
275
- self,
275
+ self,
276
276
  file_path: str,
277
277
  content: str,
278
278
  ) -> WriteResult:
@@ -281,20 +281,20 @@ class StoreBackend:
281
281
  """
282
282
  store = self._get_store()
283
283
  namespace = self._get_namespace()
284
-
284
+
285
285
  # Check if file exists
286
286
  existing = store.get(namespace, file_path)
287
287
  if existing is not None:
288
288
  return WriteResult(error=f"Cannot write to {file_path} because it already exists. Read and then make an edit, or write to a new path.")
289
-
289
+
290
290
  # Create new file
291
291
  file_data = create_file_data(content)
292
292
  store_value = self._convert_file_data_to_store_value(file_data)
293
293
  store.put(namespace, file_path, store_value)
294
294
  return WriteResult(path=file_path, files_update=None)
295
-
295
+
296
296
  def edit(
297
- self,
297
+ self,
298
298
  file_path: str,
299
299
  old_string: str,
300
300
  new_string: str,
@@ -305,38 +305,38 @@ class StoreBackend:
305
305
  """
306
306
  store = self._get_store()
307
307
  namespace = self._get_namespace()
308
-
308
+
309
309
  # Get existing file
310
310
  item = store.get(namespace, file_path)
311
311
  if item is None:
312
312
  return EditResult(error=f"Error: File '{file_path}' not found")
313
-
313
+
314
314
  try:
315
315
  file_data = self._convert_store_item_to_file_data(item)
316
316
  except ValueError as e:
317
317
  return EditResult(error=f"Error: {e}")
318
-
318
+
319
319
  content = file_data_to_string(file_data)
320
320
  result = perform_string_replacement(content, old_string, new_string, replace_all)
321
-
321
+
322
322
  if isinstance(result, str):
323
323
  return EditResult(error=result)
324
-
324
+
325
325
  new_content, occurrences = result
326
326
  new_file_data = update_file_data(file_data, new_content)
327
-
327
+
328
328
  # Update file in store
329
329
  store_value = self._convert_file_data_to_store_value(new_file_data)
330
330
  store.put(namespace, file_path, store_value)
331
331
  return EditResult(path=file_path, files_update=None, occurrences=int(occurrences))
332
-
332
+
333
333
  # Removed legacy grep() convenience to keep lean surface
334
334
 
335
335
  def grep_raw(
336
336
  self,
337
337
  pattern: str,
338
338
  path: str = "/",
339
- glob: Optional[str] = None,
339
+ glob: str | None = None,
340
340
  ) -> list[GrepMatch] | str:
341
341
  store = self._get_store()
342
342
  namespace = self._get_namespace()
@@ -348,7 +348,7 @@ class StoreBackend:
348
348
  except ValueError:
349
349
  continue
350
350
  return grep_matches_from_files(files, pattern, path, glob)
351
-
351
+
352
352
  def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
353
353
  store = self._get_store()
354
354
  namespace = self._get_namespace()
@@ -367,13 +367,12 @@ class StoreBackend:
367
367
  for p in paths:
368
368
  fd = files.get(p)
369
369
  size = len("\n".join(fd.get("content", []))) if fd else 0
370
- infos.append({
371
- "path": p,
372
- "is_dir": False,
373
- "size": int(size),
374
- "modified_at": fd.get("modified_at", "") if fd else "",
375
- })
370
+ infos.append(
371
+ {
372
+ "path": p,
373
+ "is_dir": False,
374
+ "size": int(size),
375
+ "modified_at": fd.get("modified_at", "") if fd else "",
376
+ }
377
+ )
376
378
  return infos
377
-
378
-
379
- # Provider classes removed: prefer callables like `lambda rt: StoreBackend(rt)`
@@ -8,36 +8,22 @@ enable composition without fragile string parsing.
8
8
  import re
9
9
  from datetime import UTC, datetime
10
10
  from pathlib import Path
11
- from typing import Any, Literal, TypedDict
11
+ from typing import Any, Literal
12
12
 
13
13
  import wcmatch.glob as wcglob
14
14
 
15
+ from deepagents.backends.protocol import FileInfo as _FileInfo
16
+ from deepagents.backends.protocol import GrepMatch as _GrepMatch
17
+
15
18
  EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
16
19
  MAX_LINE_LENGTH = 10000
17
20
  LINE_NUMBER_WIDTH = 6
18
21
  TOOL_RESULT_TOKEN_LIMIT = 20000 # Same threshold as eviction
19
22
  TRUNCATION_GUIDANCE = "... [results truncated, try being more specific with your parameters]"
20
23
 
21
-
22
- class FileInfo(TypedDict, total=False):
23
- """Structured file listing info.
24
-
25
- Minimal contract used across backends. Only "path" is required.
26
- Other fields are best-effort and may be absent depending on backend.
27
- """
28
-
29
- path: str
30
- is_dir: bool
31
- size: int # bytes (approx)
32
- modified_at: str # ISO timestamp if known
33
-
34
-
35
- class GrepMatch(TypedDict):
36
- """Structured grep match entry."""
37
-
38
- path: str
39
- line: int
40
- text: str
24
+ # Re-export protocol types for backwards compatibility
25
+ FileInfo = _FileInfo
26
+ GrepMatch = _GrepMatch
41
27
 
42
28
 
43
29
  def sanitize_tool_call_id(tool_call_id: str) -> str:
deepagents/graph.py CHANGED
@@ -17,7 +17,7 @@ from langgraph.graph.state import CompiledStateGraph
17
17
  from langgraph.store.base import BaseStore
18
18
  from langgraph.types import Checkpointer
19
19
 
20
- from deepagents.backends.protocol import BackendProtocol, BackendFactory
20
+ from deepagents.backends.protocol import BackendFactory, BackendProtocol
21
21
  from deepagents.middleware.filesystem import FilesystemMiddleware
22
22
  from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
23
23
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
@@ -57,9 +57,12 @@ def create_deep_agent(
57
57
  """Create a deep agent.
58
58
 
59
59
  This agent will by default have access to a tool to write todos (write_todos),
60
- six file editing tools: write_file, ls, read_file, edit_file, glob_search, grep_search,
60
+ seven file and execution tools: ls, read_file, write_file, edit_file, glob, grep, execute,
61
61
  and a tool to call subagents.
62
62
 
63
+ The execute tool allows running shell commands if the backend implements SandboxBackendProtocol.
64
+ For non-sandbox backends, the execute tool will return an error message.
65
+
63
66
  Args:
64
67
  model: The model to use. Defaults to Claude Sonnet 4.
65
68
  tools: The tools the agent should have access to.
@@ -80,8 +83,9 @@ def create_deep_agent(
80
83
  context_schema: The schema of the deep agent.
81
84
  checkpointer: Optional checkpointer for persisting agent state between runs.
82
85
  store: Optional store for persistent storage (required if backend uses StoreBackend).
83
- backend: Optional backend for file storage. Pass either a Backend instance or a
84
- callable factory like `lambda rt: StateBackend(rt)`.
86
+ backend: Optional backend for file storage and execution. Pass either a Backend instance
87
+ or a callable factory like `lambda rt: StateBackend(rt)`. For execution support,
88
+ use a backend that implements SandboxBackendProtocol.
85
89
  interrupt_on: Optional Dict[str, bool | InterruptOnConfig] mapping tool names to
86
90
  interrupt configs.
87
91
  debug: Whether to enable debug mode. Passed through to create_agent.