deepagents 0.2.5__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.
Files changed (33) hide show
  1. deepagents/backends/composite.py +37 -2
  2. deepagents/backends/protocol.py +48 -0
  3. deepagents/backends/sandbox.py +341 -0
  4. deepagents/backends/store.py +3 -11
  5. deepagents/graph.py +7 -3
  6. deepagents/middleware/filesystem.py +224 -21
  7. deepagents/middleware/subagents.py +7 -4
  8. {deepagents-0.2.5.dist-info → deepagents-0.2.6.dist-info}/METADATA +5 -4
  9. deepagents-0.2.6.dist-info/RECORD +19 -0
  10. deepagents-0.2.6.dist-info/top_level.txt +1 -0
  11. deepagents-0.2.5.dist-info/RECORD +0 -38
  12. deepagents-0.2.5.dist-info/licenses/LICENSE +0 -21
  13. deepagents-0.2.5.dist-info/top_level.txt +0 -2
  14. deepagents-cli/README.md +0 -3
  15. deepagents-cli/deepagents_cli/README.md +0 -196
  16. deepagents-cli/deepagents_cli/__init__.py +0 -5
  17. deepagents-cli/deepagents_cli/__main__.py +0 -6
  18. deepagents-cli/deepagents_cli/agent.py +0 -278
  19. deepagents-cli/deepagents_cli/agent_memory.py +0 -226
  20. deepagents-cli/deepagents_cli/commands.py +0 -89
  21. deepagents-cli/deepagents_cli/config.py +0 -118
  22. deepagents-cli/deepagents_cli/default_agent_prompt.md +0 -110
  23. deepagents-cli/deepagents_cli/execution.py +0 -636
  24. deepagents-cli/deepagents_cli/file_ops.py +0 -347
  25. deepagents-cli/deepagents_cli/input.py +0 -270
  26. deepagents-cli/deepagents_cli/main.py +0 -226
  27. deepagents-cli/deepagents_cli/py.typed +0 -0
  28. deepagents-cli/deepagents_cli/token_utils.py +0 -63
  29. deepagents-cli/deepagents_cli/tools.py +0 -140
  30. deepagents-cli/deepagents_cli/ui.py +0 -489
  31. deepagents-cli/tests/test_file_ops.py +0 -119
  32. deepagents-cli/tests/test_placeholder.py +0 -5
  33. {deepagents-0.2.5.dist-info → deepagents-0.2.6.dist-info}/WHEEL +0 -0
@@ -1,8 +1,15 @@
1
1
  """CompositeBackend: Route operations to different backends based on path prefix."""
2
2
 
3
- from deepagents.backends.protocol import BackendProtocol, EditResult, WriteResult
3
+ from deepagents.backends.protocol import (
4
+ BackendProtocol,
5
+ EditResult,
6
+ ExecuteResponse,
7
+ FileInfo,
8
+ GrepMatch,
9
+ SandboxBackendProtocol,
10
+ WriteResult,
11
+ )
4
12
  from deepagents.backends.state import StateBackend
5
- from deepagents.backends.utils import FileInfo, GrepMatch
6
13
 
7
14
 
8
15
  class CompositeBackend:
@@ -212,3 +219,31 @@ class CompositeBackend:
212
219
  except Exception:
213
220
  pass
214
221
  return res
222
+
223
+ def execute(
224
+ self,
225
+ command: str,
226
+ ) -> ExecuteResponse:
227
+ """Execute a command via the default backend.
228
+
229
+ Execution is not path-specific, so it always delegates to the default backend.
230
+ The default backend must implement SandboxBackendProtocol for this to work.
231
+
232
+ Args:
233
+ command: Full shell command string to execute.
234
+
235
+ Returns:
236
+ ExecuteResponse with combined output, exit code, and truncation flag.
237
+
238
+ Raises:
239
+ NotImplementedError: If default backend doesn't support execution.
240
+ """
241
+ if isinstance(self.default, SandboxBackendProtocol):
242
+ return self.default.execute(command)
243
+
244
+ # This shouldn't be reached if the runtime check in the execute tool works correctly,
245
+ # but we include it as a safety fallback.
246
+ raise NotImplementedError(
247
+ "Default backend doesn't support command execution (SandboxBackendProtocol). "
248
+ "To enable execution, provide a default backend that implements SandboxBackendProtocol."
249
+ )
@@ -145,4 +145,52 @@ class BackendProtocol(Protocol):
145
145
  ...
146
146
 
147
147
 
148
+ @dataclass
149
+ class ExecuteResponse:
150
+ """Result of code execution.
151
+
152
+ Simplified schema optimized for LLM consumption.
153
+ """
154
+
155
+ output: str
156
+ """Combined stdout and stderr output of the executed command."""
157
+
158
+ exit_code: int | None = None
159
+ """The process exit code. 0 indicates success, non-zero indicates failure."""
160
+
161
+ truncated: bool = False
162
+ """Whether the output was truncated due to backend limitations."""
163
+
164
+
165
+ @runtime_checkable
166
+ class SandboxBackendProtocol(BackendProtocol, Protocol):
167
+ """Protocol for sandboxed backends with isolated runtime.
168
+
169
+ Sandboxed backends run in isolated environments (e.g., separate processes,
170
+ containers) and communicate via defined interfaces.
171
+ """
172
+
173
+ def execute(
174
+ self,
175
+ command: str,
176
+ ) -> ExecuteResponse:
177
+ """Execute a command in the process.
178
+
179
+ Simplified interface optimized for LLM consumption.
180
+
181
+ Args:
182
+ command: Full shell command string to execute.
183
+
184
+ Returns:
185
+ ExecuteResponse with combined output, exit code, optional signal, and truncation flag.
186
+ """
187
+ ...
188
+
189
+ @property
190
+ def id(self) -> str:
191
+ """Unique identifier for the sandbox backend."""
192
+ ...
193
+
194
+
148
195
  BackendFactory: TypeAlias = Callable[[ToolRuntime], BackendProtocol]
196
+ BACKEND_TYPES = BackendProtocol | BackendFactory
@@ -0,0 +1,341 @@
1
+ """Base sandbox implementation with execute() as the only abstract method.
2
+
3
+ This module provides a base class that implements all SandboxBackendProtocol
4
+ methods using shell commands executed via execute(). Concrete implementations
5
+ only need to implement the execute() method.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import base64
11
+ import json
12
+ from abc import ABC, abstractmethod
13
+
14
+ from deepagents.backends.protocol import (
15
+ EditResult,
16
+ ExecuteResponse,
17
+ FileInfo,
18
+ GrepMatch,
19
+ SandboxBackendProtocol,
20
+ WriteResult,
21
+ )
22
+
23
+ _GLOB_COMMAND_TEMPLATE = """python3 -c "
24
+ import glob
25
+ import os
26
+ import json
27
+ import base64
28
+
29
+ # Decode base64-encoded parameters
30
+ path = base64.b64decode('{path_b64}').decode('utf-8')
31
+ pattern = base64.b64decode('{pattern_b64}').decode('utf-8')
32
+
33
+ os.chdir(path)
34
+ matches = sorted(glob.glob(pattern, recursive=True))
35
+ for m in matches:
36
+ stat = os.stat(m)
37
+ result = {{
38
+ 'path': m,
39
+ 'size': stat.st_size,
40
+ 'mtime': stat.st_mtime,
41
+ 'is_dir': os.path.isdir(m)
42
+ }}
43
+ print(json.dumps(result))
44
+ " 2>/dev/null"""
45
+
46
+ _WRITE_COMMAND_TEMPLATE = """python3 -c "
47
+ import os
48
+ import sys
49
+ import base64
50
+
51
+ file_path = '{file_path}'
52
+
53
+ # Check if file already exists (atomic with write)
54
+ if os.path.exists(file_path):
55
+ print(f'Error: File \\'{file_path}\\' already exists', file=sys.stderr)
56
+ sys.exit(1)
57
+
58
+ # Create parent directory if needed
59
+ parent_dir = os.path.dirname(file_path) or '.'
60
+ os.makedirs(parent_dir, exist_ok=True)
61
+
62
+ # Decode and write content
63
+ content = base64.b64decode('{content_b64}').decode('utf-8')
64
+ with open(file_path, 'w') as f:
65
+ f.write(content)
66
+ " 2>&1"""
67
+
68
+ _EDIT_COMMAND_TEMPLATE = """python3 -c "
69
+ import sys
70
+ import base64
71
+
72
+ # Read file content
73
+ with open('{file_path}', 'r') as f:
74
+ text = f.read()
75
+
76
+ # Decode base64-encoded strings
77
+ old = base64.b64decode('{old_b64}').decode('utf-8')
78
+ new = base64.b64decode('{new_b64}').decode('utf-8')
79
+
80
+ # Count occurrences
81
+ count = text.count(old)
82
+
83
+ # Exit with error codes if issues found
84
+ if count == 0:
85
+ sys.exit(1) # String not found
86
+ elif count > 1 and not {replace_all}:
87
+ sys.exit(2) # Multiple occurrences without replace_all
88
+
89
+ # Perform replacement
90
+ if {replace_all}:
91
+ result = text.replace(old, new)
92
+ else:
93
+ result = text.replace(old, new, 1)
94
+
95
+ # Write back to file
96
+ with open('{file_path}', 'w') as f:
97
+ f.write(result)
98
+
99
+ print(count)
100
+ " 2>&1"""
101
+
102
+ _READ_COMMAND_TEMPLATE = """python3 -c "
103
+ import os
104
+ import sys
105
+
106
+ file_path = '{file_path}'
107
+ offset = {offset}
108
+ limit = {limit}
109
+
110
+ # Check if file exists
111
+ if not os.path.isfile(file_path):
112
+ print('Error: File not found')
113
+ sys.exit(1)
114
+
115
+ # Check if file is empty
116
+ if os.path.getsize(file_path) == 0:
117
+ print('System reminder: File exists but has empty contents')
118
+ sys.exit(0)
119
+
120
+ # Read file with offset and limit
121
+ with open(file_path, 'r') as f:
122
+ lines = f.readlines()
123
+
124
+ # Apply offset and limit
125
+ start_idx = offset
126
+ end_idx = offset + limit
127
+ selected_lines = lines[start_idx:end_idx]
128
+
129
+ # Format with line numbers (1-indexed, starting from offset + 1)
130
+ for i, line in enumerate(selected_lines):
131
+ line_num = offset + i + 1
132
+ # Remove trailing newline for formatting, then add it back
133
+ line_content = line.rstrip('\\n')
134
+ print(f'{{line_num:6d}}\\t{{line_content}}')
135
+ " 2>&1"""
136
+
137
+
138
+ class BaseSandbox(SandboxBackendProtocol, ABC):
139
+ """Base sandbox implementation with execute() as abstract method.
140
+
141
+ This class provides default implementations for all protocol methods
142
+ using shell commands. Subclasses only need to implement execute().
143
+ """
144
+
145
+ @abstractmethod
146
+ def execute(
147
+ self,
148
+ command: str,
149
+ ) -> ExecuteResponse:
150
+ """Execute a command in the sandbox and return ExecuteResponse.
151
+
152
+ Args:
153
+ command: Full shell command string to execute.
154
+
155
+ Returns:
156
+ ExecuteResponse with combined output, exit code, optional signal, and truncation flag.
157
+ """
158
+ ...
159
+
160
+ def ls_info(self, path: str) -> list[FileInfo]:
161
+ """Structured listing with file metadata using os.scandir."""
162
+ cmd = f"""python3 -c "
163
+ import os
164
+ import json
165
+
166
+ path = '{path}'
167
+
168
+ try:
169
+ with os.scandir(path) as it:
170
+ for entry in it:
171
+ result = {{
172
+ 'path': entry.name,
173
+ 'is_dir': entry.is_dir(follow_symlinks=False)
174
+ }}
175
+ print(json.dumps(result))
176
+ except FileNotFoundError:
177
+ pass
178
+ except PermissionError:
179
+ pass
180
+ " 2>/dev/null"""
181
+
182
+ result = self.execute(cmd)
183
+
184
+ file_infos: list[FileInfo] = []
185
+ for line in result.output.strip().split("\n"):
186
+ if not line:
187
+ continue
188
+ try:
189
+ data = json.loads(line)
190
+ file_infos.append({"path": data["path"], "is_dir": data["is_dir"]})
191
+ except json.JSONDecodeError:
192
+ continue
193
+
194
+ return file_infos
195
+
196
+ def read(
197
+ self,
198
+ file_path: str,
199
+ offset: int = 0,
200
+ limit: int = 2000,
201
+ ) -> str:
202
+ """Read file content with line numbers using a single shell command."""
203
+ # Use template for reading file with offset and limit
204
+ cmd = _READ_COMMAND_TEMPLATE.format(file_path=file_path, offset=offset, limit=limit)
205
+ result = self.execute(cmd)
206
+
207
+ output = result.output.rstrip()
208
+ exit_code = result.exit_code
209
+
210
+ if exit_code != 0 or "Error: File not found" in output:
211
+ return f"Error: File '{file_path}' not found"
212
+
213
+ return output
214
+
215
+ def write(
216
+ self,
217
+ file_path: str,
218
+ content: str,
219
+ ) -> WriteResult:
220
+ """Create a new file. Returns WriteResult; error populated on failure."""
221
+ # Encode content as base64 to avoid any escaping issues
222
+ content_b64 = base64.b64encode(content.encode("utf-8")).decode("ascii")
223
+
224
+ # Single atomic check + write command
225
+ cmd = _WRITE_COMMAND_TEMPLATE.format(file_path=file_path, content_b64=content_b64)
226
+ result = self.execute(cmd)
227
+
228
+ # Check for errors (exit code or error message in output)
229
+ if result.exit_code != 0 or "Error:" in result.output:
230
+ error_msg = result.output.strip() or f"Failed to write file '{file_path}'"
231
+ return WriteResult(error=error_msg)
232
+
233
+ # External storage - no files_update needed
234
+ return WriteResult(path=file_path, files_update=None)
235
+
236
+ def edit(
237
+ self,
238
+ file_path: str,
239
+ old_string: str,
240
+ new_string: str,
241
+ replace_all: bool = False,
242
+ ) -> EditResult:
243
+ """Edit a file by replacing string occurrences. Returns EditResult."""
244
+ # Encode strings as base64 to avoid any escaping issues
245
+ old_b64 = base64.b64encode(old_string.encode("utf-8")).decode("ascii")
246
+ new_b64 = base64.b64encode(new_string.encode("utf-8")).decode("ascii")
247
+
248
+ # Use template for string replacement
249
+ cmd = _EDIT_COMMAND_TEMPLATE.format(file_path=file_path, old_b64=old_b64, new_b64=new_b64, replace_all=replace_all)
250
+ result = self.execute(cmd)
251
+
252
+ exit_code = result.exit_code
253
+ output = result.output.strip()
254
+
255
+ if exit_code == 1:
256
+ return EditResult(error=f"Error: String not found in file: '{old_string}'")
257
+ if exit_code == 2:
258
+ return EditResult(error=f"Error: String '{old_string}' appears multiple times. Use replace_all=True to replace all occurrences.")
259
+ if exit_code != 0:
260
+ return EditResult(error=f"Error: File '{file_path}' not found")
261
+
262
+ count = int(output)
263
+ # External storage - no files_update needed
264
+ return EditResult(path=file_path, files_update=None, occurrences=count)
265
+
266
+ def grep_raw(
267
+ self,
268
+ pattern: str,
269
+ path: str | None = None,
270
+ glob: str | None = None,
271
+ ) -> list[GrepMatch] | str:
272
+ """Structured search results or error string for invalid input."""
273
+ search_path = path or "."
274
+
275
+ # Build grep command to get structured output
276
+ grep_opts = "-rHn" # recursive, with filename, with line number
277
+
278
+ # Add glob pattern if specified
279
+ glob_pattern = ""
280
+ if glob:
281
+ glob_pattern = f"--include='{glob}'"
282
+
283
+ # Escape pattern for shell
284
+ pattern_escaped = pattern.replace("'", "'\\\\''")
285
+
286
+ cmd = f"grep {grep_opts} {glob_pattern} -e '{pattern_escaped}' '{search_path}' 2>/dev/null || true"
287
+ result = self.execute(cmd)
288
+
289
+ output = result.output.rstrip()
290
+ if not output:
291
+ return []
292
+
293
+ # Parse grep output into GrepMatch objects
294
+ matches: list[GrepMatch] = []
295
+ for line in output.split("\n"):
296
+ # Format is: path:line_number:text
297
+ parts = line.split(":", 2)
298
+ if len(parts) >= 3:
299
+ matches.append(
300
+ {
301
+ "path": parts[0],
302
+ "line": int(parts[1]),
303
+ "text": parts[2],
304
+ }
305
+ )
306
+
307
+ return matches
308
+
309
+ def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
310
+ """Structured glob matching returning FileInfo dicts."""
311
+ # Encode pattern and path as base64 to avoid escaping issues
312
+ pattern_b64 = base64.b64encode(pattern.encode("utf-8")).decode("ascii")
313
+ path_b64 = base64.b64encode(path.encode("utf-8")).decode("ascii")
314
+
315
+ cmd = _GLOB_COMMAND_TEMPLATE.format(path_b64=path_b64, pattern_b64=pattern_b64)
316
+ result = self.execute(cmd)
317
+
318
+ output = result.output.strip()
319
+ if not output:
320
+ return []
321
+
322
+ # Parse JSON output into FileInfo dicts
323
+ file_infos: list[FileInfo] = []
324
+ for line in output.split("\n"):
325
+ try:
326
+ data = json.loads(line)
327
+ file_infos.append(
328
+ {
329
+ "path": data["path"],
330
+ "is_dir": data["is_dir"],
331
+ }
332
+ )
333
+ except json.JSONDecodeError:
334
+ continue
335
+
336
+ return file_infos
337
+
338
+ @property
339
+ @abstractmethod
340
+ def id(self) -> str:
341
+ """Unique identifier for this backend instance."""
@@ -1,17 +1,12 @@
1
1
  """StoreBackend: Adapter for LangGraph's BaseStore (persistent, cross-thread)."""
2
2
 
3
- from typing import TYPE_CHECKING, Any
4
-
5
- if TYPE_CHECKING:
6
- from langchain.tools import ToolRuntime
3
+ from typing import Any
7
4
 
8
5
  from langgraph.config import get_config
9
6
  from langgraph.store.base import BaseStore, Item
10
7
 
11
- from deepagents.backends.protocol import EditResult, WriteResult
8
+ from deepagents.backends.protocol import BackendProtocol, EditResult, FileInfo, GrepMatch, WriteResult
12
9
  from deepagents.backends.utils import (
13
- FileInfo,
14
- GrepMatch,
15
10
  _glob_search_files,
16
11
  create_file_data,
17
12
  file_data_to_string,
@@ -22,7 +17,7 @@ from deepagents.backends.utils import (
22
17
  )
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.
@@ -381,6 +376,3 @@ class StoreBackend:
381
376
  }
382
377
  )
383
378
  return infos
384
-
385
-
386
- # Provider classes removed: prefer callables like `lambda rt: StoreBackend(rt)`
deepagents/graph.py CHANGED
@@ -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.