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.
- deepagents/backends/__init__.py +1 -1
- deepagents/backends/composite.py +66 -41
- deepagents/backends/filesystem.py +92 -86
- deepagents/backends/protocol.py +87 -13
- deepagents/backends/sandbox.py +341 -0
- deepagents/backends/state.py +59 -58
- deepagents/backends/store.py +73 -74
- deepagents/backends/utils.py +7 -21
- deepagents/graph.py +8 -4
- deepagents/middleware/filesystem.py +271 -66
- deepagents/middleware/resumable_shell.py +5 -4
- deepagents/middleware/subagents.py +8 -6
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/METADATA +5 -10
- deepagents-0.2.6.dist-info/RECORD +19 -0
- deepagents-0.2.4.dist-info/RECORD +0 -19
- deepagents-0.2.4.dist-info/licenses/LICENSE +0 -21
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/WHEEL +0 -0
- {deepagents-0.2.4.dist-info → deepagents-0.2.6.dist-info}/top_level.txt +0 -0
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
"""Middleware for providing filesystem tools to an agent."""
|
|
2
2
|
# ruff: noqa: E501
|
|
3
3
|
|
|
4
|
-
from collections.abc import Awaitable, Callable, Sequence
|
|
5
|
-
from typing import Annotated
|
|
6
|
-
from typing_extensions import NotRequired
|
|
7
|
-
|
|
8
4
|
import os
|
|
9
|
-
from
|
|
5
|
+
from collections.abc import Awaitable, Callable, Sequence
|
|
6
|
+
from typing import Annotated, Literal, NotRequired
|
|
10
7
|
|
|
11
8
|
from langchain.agents.middleware.types import (
|
|
12
9
|
AgentMiddleware,
|
|
@@ -21,14 +18,21 @@ from langchain_core.tools import BaseTool, tool
|
|
|
21
18
|
from langgraph.types import Command
|
|
22
19
|
from typing_extensions import TypedDict
|
|
23
20
|
|
|
24
|
-
from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
|
|
25
21
|
from deepagents.backends import StateBackend
|
|
22
|
+
|
|
23
|
+
# Re-export type here for backwards compatibility
|
|
24
|
+
from deepagents.backends.protocol import BACKEND_TYPES as BACKEND_TYPES
|
|
25
|
+
from deepagents.backends.protocol import (
|
|
26
|
+
BackendProtocol,
|
|
27
|
+
EditResult,
|
|
28
|
+
SandboxBackendProtocol,
|
|
29
|
+
WriteResult,
|
|
30
|
+
)
|
|
26
31
|
from deepagents.backends.utils import (
|
|
27
|
-
update_file_data,
|
|
28
32
|
format_content_with_line_numbers,
|
|
29
33
|
format_grep_matches,
|
|
30
|
-
truncate_if_too_long,
|
|
31
34
|
sanitize_tool_call_id,
|
|
35
|
+
truncate_if_too_long,
|
|
32
36
|
)
|
|
33
37
|
|
|
34
38
|
EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
|
|
@@ -36,10 +40,6 @@ MAX_LINE_LENGTH = 2000
|
|
|
36
40
|
LINE_NUMBER_WIDTH = 6
|
|
37
41
|
DEFAULT_READ_OFFSET = 0
|
|
38
42
|
DEFAULT_READ_LIMIT = 500
|
|
39
|
-
BACKEND_TYPES = (
|
|
40
|
-
BackendProtocol
|
|
41
|
-
| BackendFactory
|
|
42
|
-
)
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class FileData(TypedDict):
|
|
@@ -135,6 +135,7 @@ def _validate_path(path: str, *, allowed_prefixes: Sequence[str] | None = None)
|
|
|
135
135
|
|
|
136
136
|
return normalized
|
|
137
137
|
|
|
138
|
+
|
|
138
139
|
class FilesystemState(AgentState):
|
|
139
140
|
"""State for the filesystem middleware."""
|
|
140
141
|
|
|
@@ -217,6 +218,50 @@ Examples:
|
|
|
217
218
|
- Search Python files only: `grep(pattern="import", glob="*.py")`
|
|
218
219
|
- Show matching lines: `grep(pattern="error", output_mode="content")`"""
|
|
219
220
|
|
|
221
|
+
EXECUTE_TOOL_DESCRIPTION = """Executes a given command in the sandbox environment with proper handling and security measures.
|
|
222
|
+
|
|
223
|
+
Before executing the command, please follow these steps:
|
|
224
|
+
|
|
225
|
+
1. Directory Verification:
|
|
226
|
+
- If the command will create new directories or files, first use the ls tool to verify the parent directory exists and is the correct location
|
|
227
|
+
- For example, before running "mkdir foo/bar", first use ls to check that "foo" exists and is the intended parent directory
|
|
228
|
+
|
|
229
|
+
2. Command Execution:
|
|
230
|
+
- Always quote file paths that contain spaces with double quotes (e.g., cd "path with spaces/file.txt")
|
|
231
|
+
- Examples of proper quoting:
|
|
232
|
+
- cd "/Users/name/My Documents" (correct)
|
|
233
|
+
- cd /Users/name/My Documents (incorrect - will fail)
|
|
234
|
+
- python "/path/with spaces/script.py" (correct)
|
|
235
|
+
- python /path/with spaces/script.py (incorrect - will fail)
|
|
236
|
+
- After ensuring proper quoting, execute the command
|
|
237
|
+
- Capture the output of the command
|
|
238
|
+
|
|
239
|
+
Usage notes:
|
|
240
|
+
- The command parameter is required
|
|
241
|
+
- Commands run in an isolated sandbox environment
|
|
242
|
+
- Returns combined stdout/stderr output with exit code
|
|
243
|
+
- If the output is very large, it may be truncated
|
|
244
|
+
- VERY IMPORTANT: You MUST avoid using search commands like find and grep. Instead use the grep, glob tools to search. You MUST avoid read tools like cat, head, tail, and use read_file to read files.
|
|
245
|
+
- When issuing multiple commands, use the ';' or '&&' operator to separate them. DO NOT use newlines (newlines are ok in quoted strings)
|
|
246
|
+
- Use '&&' when commands depend on each other (e.g., "mkdir dir && cd dir")
|
|
247
|
+
- Use ';' only when you need to run commands sequentially but don't care if earlier commands fail
|
|
248
|
+
- Try to maintain your current working directory throughout the session by using absolute paths and avoiding usage of cd
|
|
249
|
+
|
|
250
|
+
Examples:
|
|
251
|
+
Good examples:
|
|
252
|
+
- execute(command="pytest /foo/bar/tests")
|
|
253
|
+
- execute(command="python /path/to/script.py")
|
|
254
|
+
- execute(command="npm install && npm test")
|
|
255
|
+
|
|
256
|
+
Bad examples (avoid these):
|
|
257
|
+
- execute(command="cd /foo/bar && pytest tests") # Use absolute path instead
|
|
258
|
+
- execute(command="cat file.txt") # Use read_file tool instead
|
|
259
|
+
- execute(command="find . -name '*.py'") # Use glob tool instead
|
|
260
|
+
- execute(command="grep -r 'pattern' .") # Use grep tool instead
|
|
261
|
+
|
|
262
|
+
Note: This tool is only available if the backend supports execution (SandboxBackendProtocol).
|
|
263
|
+
If execution is not supported, the tool will return an error message."""
|
|
264
|
+
|
|
220
265
|
FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `glob`, `grep`
|
|
221
266
|
|
|
222
267
|
You have access to a filesystem which you can interact with using these tools.
|
|
@@ -229,6 +274,13 @@ All file paths must start with a /.
|
|
|
229
274
|
- glob: find files matching a pattern (e.g., "**/*.py")
|
|
230
275
|
- grep: search for text within files"""
|
|
231
276
|
|
|
277
|
+
EXECUTION_SYSTEM_PROMPT = """## Execute Tool `execute`
|
|
278
|
+
|
|
279
|
+
You have access to an `execute` tool for running shell commands in a sandboxed environment.
|
|
280
|
+
Use this tool to run commands, scripts, tests, builds, and other shell operations.
|
|
281
|
+
|
|
282
|
+
- execute: run a shell command in the sandbox (returns output and exit code)"""
|
|
283
|
+
|
|
232
284
|
|
|
233
285
|
def _get_backend(backend: BACKEND_TYPES, runtime: ToolRuntime) -> BackendProtocol:
|
|
234
286
|
"""Get the resolved backend instance from backend or factory.
|
|
@@ -327,15 +379,17 @@ def _write_file_tool_generator(
|
|
|
327
379
|
return res.error
|
|
328
380
|
# If backend returns state update, wrap into Command with ToolMessage
|
|
329
381
|
if res.files_update is not None:
|
|
330
|
-
return Command(
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
382
|
+
return Command(
|
|
383
|
+
update={
|
|
384
|
+
"files": res.files_update,
|
|
385
|
+
"messages": [
|
|
386
|
+
ToolMessage(
|
|
387
|
+
content=f"Updated file {res.path}",
|
|
388
|
+
tool_call_id=runtime.tool_call_id,
|
|
389
|
+
)
|
|
390
|
+
],
|
|
391
|
+
}
|
|
392
|
+
)
|
|
339
393
|
return f"Updated file {res.path}"
|
|
340
394
|
|
|
341
395
|
return write_file
|
|
@@ -371,15 +425,17 @@ def _edit_file_tool_generator(
|
|
|
371
425
|
if res.error:
|
|
372
426
|
return res.error
|
|
373
427
|
if res.files_update is not None:
|
|
374
|
-
return Command(
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
428
|
+
return Command(
|
|
429
|
+
update={
|
|
430
|
+
"files": res.files_update,
|
|
431
|
+
"messages": [
|
|
432
|
+
ToolMessage(
|
|
433
|
+
content=f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'",
|
|
434
|
+
tool_call_id=runtime.tool_call_id,
|
|
435
|
+
)
|
|
436
|
+
],
|
|
437
|
+
}
|
|
438
|
+
)
|
|
383
439
|
return f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'"
|
|
384
440
|
|
|
385
441
|
return edit_file
|
|
@@ -428,7 +484,7 @@ def _grep_tool_generator(
|
|
|
428
484
|
def grep(
|
|
429
485
|
pattern: str,
|
|
430
486
|
runtime: ToolRuntime[None, FilesystemState],
|
|
431
|
-
path:
|
|
487
|
+
path: str | None = None,
|
|
432
488
|
glob: str | None = None,
|
|
433
489
|
output_mode: Literal["files_with_matches", "content", "count"] = "files_with_matches",
|
|
434
490
|
) -> str:
|
|
@@ -442,6 +498,80 @@ def _grep_tool_generator(
|
|
|
442
498
|
return grep
|
|
443
499
|
|
|
444
500
|
|
|
501
|
+
def _supports_execution(backend: BackendProtocol) -> bool:
|
|
502
|
+
"""Check if a backend supports command execution.
|
|
503
|
+
|
|
504
|
+
For CompositeBackend, checks if the default backend supports execution.
|
|
505
|
+
For other backends, checks if they implement SandboxBackendProtocol.
|
|
506
|
+
|
|
507
|
+
Args:
|
|
508
|
+
backend: The backend to check.
|
|
509
|
+
|
|
510
|
+
Returns:
|
|
511
|
+
True if the backend supports execution, False otherwise.
|
|
512
|
+
"""
|
|
513
|
+
# Import here to avoid circular dependency
|
|
514
|
+
from deepagents.backends.composite import CompositeBackend
|
|
515
|
+
|
|
516
|
+
# For CompositeBackend, check the default backend
|
|
517
|
+
if isinstance(backend, CompositeBackend):
|
|
518
|
+
return isinstance(backend.default, SandboxBackendProtocol)
|
|
519
|
+
|
|
520
|
+
# For other backends, use isinstance check
|
|
521
|
+
return isinstance(backend, SandboxBackendProtocol)
|
|
522
|
+
|
|
523
|
+
|
|
524
|
+
def _execute_tool_generator(
|
|
525
|
+
backend: BackendProtocol | Callable[[ToolRuntime], BackendProtocol],
|
|
526
|
+
custom_description: str | None = None,
|
|
527
|
+
) -> BaseTool:
|
|
528
|
+
"""Generate the execute tool for sandbox command execution.
|
|
529
|
+
|
|
530
|
+
Args:
|
|
531
|
+
backend: Backend to use for execution, or a factory function that takes runtime and returns a backend.
|
|
532
|
+
custom_description: Optional custom description for the tool.
|
|
533
|
+
|
|
534
|
+
Returns:
|
|
535
|
+
Configured execute tool that runs commands if backend supports SandboxBackendProtocol.
|
|
536
|
+
"""
|
|
537
|
+
tool_description = custom_description or EXECUTE_TOOL_DESCRIPTION
|
|
538
|
+
|
|
539
|
+
@tool(description=tool_description)
|
|
540
|
+
def execute(
|
|
541
|
+
command: str,
|
|
542
|
+
runtime: ToolRuntime[None, FilesystemState],
|
|
543
|
+
) -> str:
|
|
544
|
+
resolved_backend = _get_backend(backend, runtime)
|
|
545
|
+
|
|
546
|
+
# Runtime check - fail gracefully if not supported
|
|
547
|
+
if not _supports_execution(resolved_backend):
|
|
548
|
+
return (
|
|
549
|
+
"Error: Execution not available. This agent's backend "
|
|
550
|
+
"does not support command execution (SandboxBackendProtocol). "
|
|
551
|
+
"To use the execute tool, provide a backend that implements SandboxBackendProtocol."
|
|
552
|
+
)
|
|
553
|
+
|
|
554
|
+
try:
|
|
555
|
+
result = resolved_backend.execute(command)
|
|
556
|
+
except NotImplementedError as e:
|
|
557
|
+
# Handle case where execute() exists but raises NotImplementedError
|
|
558
|
+
return f"Error: Execution not available. {e}"
|
|
559
|
+
|
|
560
|
+
# Format output for LLM consumption
|
|
561
|
+
parts = [result.output]
|
|
562
|
+
|
|
563
|
+
if result.exit_code is not None:
|
|
564
|
+
status = "succeeded" if result.exit_code == 0 else "failed"
|
|
565
|
+
parts.append(f"\n[Command {status} with exit code {result.exit_code}]")
|
|
566
|
+
|
|
567
|
+
if result.truncated:
|
|
568
|
+
parts.append("\n[Output was truncated due to size limits]")
|
|
569
|
+
|
|
570
|
+
return "".join(parts)
|
|
571
|
+
|
|
572
|
+
return execute
|
|
573
|
+
|
|
574
|
+
|
|
445
575
|
TOOL_GENERATORS = {
|
|
446
576
|
"ls": _ls_tool_generator,
|
|
447
577
|
"read_file": _read_file_tool_generator,
|
|
@@ -449,6 +579,7 @@ TOOL_GENERATORS = {
|
|
|
449
579
|
"edit_file": _edit_file_tool_generator,
|
|
450
580
|
"glob": _glob_tool_generator,
|
|
451
581
|
"grep": _grep_tool_generator,
|
|
582
|
+
"execute": _execute_tool_generator,
|
|
452
583
|
}
|
|
453
584
|
|
|
454
585
|
|
|
@@ -456,18 +587,19 @@ def _get_filesystem_tools(
|
|
|
456
587
|
backend: BackendProtocol,
|
|
457
588
|
custom_tool_descriptions: dict[str, str] | None = None,
|
|
458
589
|
) -> list[BaseTool]:
|
|
459
|
-
"""Get filesystem tools.
|
|
590
|
+
"""Get filesystem and execution tools.
|
|
460
591
|
|
|
461
592
|
Args:
|
|
462
|
-
backend: Backend to use for file storage, or a factory function that takes runtime and returns a backend.
|
|
593
|
+
backend: Backend to use for file storage and optional execution, or a factory function that takes runtime and returns a backend.
|
|
463
594
|
custom_tool_descriptions: Optional custom descriptions for tools.
|
|
464
595
|
|
|
465
596
|
Returns:
|
|
466
|
-
List of configured
|
|
597
|
+
List of configured tools: ls, read_file, write_file, edit_file, glob, grep, execute.
|
|
467
598
|
"""
|
|
468
599
|
if custom_tool_descriptions is None:
|
|
469
600
|
custom_tool_descriptions = {}
|
|
470
601
|
tools = []
|
|
602
|
+
|
|
471
603
|
for tool_name, tool_generator in TOOL_GENERATORS.items():
|
|
472
604
|
tool = tool_generator(backend, custom_tool_descriptions.get(tool_name))
|
|
473
605
|
tools.append(tool)
|
|
@@ -485,16 +617,20 @@ Here are the first 10 lines of the result:
|
|
|
485
617
|
|
|
486
618
|
|
|
487
619
|
class FilesystemMiddleware(AgentMiddleware):
|
|
488
|
-
"""Middleware for providing filesystem tools to an agent.
|
|
620
|
+
"""Middleware for providing filesystem and optional execution tools to an agent.
|
|
489
621
|
|
|
490
|
-
This middleware adds
|
|
622
|
+
This middleware adds filesystem tools to the agent: ls, read_file, write_file,
|
|
491
623
|
edit_file, glob, and grep. Files can be stored using any backend that implements
|
|
492
624
|
the BackendProtocol.
|
|
493
625
|
|
|
626
|
+
If the backend implements SandboxBackendProtocol, an execute tool is also added
|
|
627
|
+
for running shell commands.
|
|
628
|
+
|
|
494
629
|
Args:
|
|
495
|
-
backend: Backend for file storage. If not provided, defaults to StateBackend
|
|
630
|
+
backend: Backend for file storage and optional execution. If not provided, defaults to StateBackend
|
|
496
631
|
(ephemeral storage in agent state). For persistent storage or hybrid setups,
|
|
497
|
-
use CompositeBackend with custom routes.
|
|
632
|
+
use CompositeBackend with custom routes. For execution support, use a backend
|
|
633
|
+
that implements SandboxBackendProtocol.
|
|
498
634
|
system_prompt: Optional custom system prompt override.
|
|
499
635
|
custom_tool_descriptions: Optional custom tool descriptions override.
|
|
500
636
|
tool_token_limit_before_evict: Optional token limit before evicting a tool result to the filesystem.
|
|
@@ -502,18 +638,21 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
502
638
|
Example:
|
|
503
639
|
```python
|
|
504
640
|
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
505
|
-
from deepagents.
|
|
641
|
+
from deepagents.backends import StateBackend, StoreBackend, CompositeBackend
|
|
506
642
|
from langchain.agents import create_agent
|
|
507
643
|
|
|
508
|
-
# Ephemeral storage only (default)
|
|
644
|
+
# Ephemeral storage only (default, no execution)
|
|
509
645
|
agent = create_agent(middleware=[FilesystemMiddleware()])
|
|
510
646
|
|
|
511
647
|
# With hybrid storage (ephemeral + persistent /memories/)
|
|
512
|
-
backend = CompositeBackend(
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
)
|
|
516
|
-
|
|
648
|
+
backend = CompositeBackend(default=StateBackend(), routes={"/memories/": StoreBackend()})
|
|
649
|
+
agent = create_agent(middleware=[FilesystemMiddleware(backend=backend)])
|
|
650
|
+
|
|
651
|
+
# With sandbox backend (supports execution)
|
|
652
|
+
from my_sandbox import DockerSandboxBackend
|
|
653
|
+
|
|
654
|
+
sandbox = DockerSandboxBackend(container_id="my-container")
|
|
655
|
+
agent = create_agent(middleware=[FilesystemMiddleware(backend=sandbox)])
|
|
517
656
|
```
|
|
518
657
|
"""
|
|
519
658
|
|
|
@@ -530,7 +669,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
530
669
|
"""Initialize the filesystem middleware.
|
|
531
670
|
|
|
532
671
|
Args:
|
|
533
|
-
backend: Backend for file storage, or a factory callable.
|
|
672
|
+
backend: Backend for file storage and optional execution, or a factory callable.
|
|
673
|
+
Defaults to StateBackend if not provided.
|
|
534
674
|
system_prompt: Optional custom system prompt override.
|
|
535
675
|
custom_tool_descriptions: Optional custom tool descriptions override.
|
|
536
676
|
tool_token_limit_before_evict: Optional token limit before evicting a tool result to the filesystem.
|
|
@@ -540,8 +680,8 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
540
680
|
# Use provided backend or default to StateBackend factory
|
|
541
681
|
self.backend = backend if backend is not None else (lambda rt: StateBackend(rt))
|
|
542
682
|
|
|
543
|
-
# Set system prompt (allow full override)
|
|
544
|
-
self.
|
|
683
|
+
# Set system prompt (allow full override or None to generate dynamically)
|
|
684
|
+
self._custom_system_prompt = system_prompt
|
|
545
685
|
|
|
546
686
|
self.tools = _get_filesystem_tools(self.backend, custom_tool_descriptions)
|
|
547
687
|
|
|
@@ -563,7 +703,7 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
563
703
|
request: ModelRequest,
|
|
564
704
|
handler: Callable[[ModelRequest], ModelResponse],
|
|
565
705
|
) -> ModelResponse:
|
|
566
|
-
"""Update the system prompt
|
|
706
|
+
"""Update the system prompt and filter tools based on backend capabilities.
|
|
567
707
|
|
|
568
708
|
Args:
|
|
569
709
|
request: The model request being processed.
|
|
@@ -572,8 +712,37 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
572
712
|
Returns:
|
|
573
713
|
The model response from the handler.
|
|
574
714
|
"""
|
|
575
|
-
if
|
|
576
|
-
|
|
715
|
+
# Check if execute tool is present and if backend supports it
|
|
716
|
+
has_execute_tool = any((tool.name if hasattr(tool, "name") else tool.get("name")) == "execute" for tool in request.tools)
|
|
717
|
+
|
|
718
|
+
backend_supports_execution = False
|
|
719
|
+
if has_execute_tool:
|
|
720
|
+
# Resolve backend to check execution support
|
|
721
|
+
backend = self._get_backend(request.runtime)
|
|
722
|
+
backend_supports_execution = _supports_execution(backend)
|
|
723
|
+
|
|
724
|
+
# If execute tool exists but backend doesn't support it, filter it out
|
|
725
|
+
if not backend_supports_execution:
|
|
726
|
+
filtered_tools = [tool for tool in request.tools if (tool.name if hasattr(tool, "name") else tool.get("name")) != "execute"]
|
|
727
|
+
request = request.override(tools=filtered_tools)
|
|
728
|
+
has_execute_tool = False
|
|
729
|
+
|
|
730
|
+
# Use custom system prompt if provided, otherwise generate dynamically
|
|
731
|
+
if self._custom_system_prompt is not None:
|
|
732
|
+
system_prompt = self._custom_system_prompt
|
|
733
|
+
else:
|
|
734
|
+
# Build dynamic system prompt based on available tools
|
|
735
|
+
prompt_parts = [FILESYSTEM_SYSTEM_PROMPT]
|
|
736
|
+
|
|
737
|
+
# Add execution instructions if execute tool is available
|
|
738
|
+
if has_execute_tool and backend_supports_execution:
|
|
739
|
+
prompt_parts.append(EXECUTION_SYSTEM_PROMPT)
|
|
740
|
+
|
|
741
|
+
system_prompt = "\n\n".join(prompt_parts)
|
|
742
|
+
|
|
743
|
+
if system_prompt:
|
|
744
|
+
request = request.override(system_prompt=request.system_prompt + "\n\n" + system_prompt if request.system_prompt else system_prompt)
|
|
745
|
+
|
|
577
746
|
return handler(request)
|
|
578
747
|
|
|
579
748
|
async def awrap_model_call(
|
|
@@ -581,7 +750,7 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
581
750
|
request: ModelRequest,
|
|
582
751
|
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
583
752
|
) -> ModelResponse:
|
|
584
|
-
"""(async) Update the system prompt
|
|
753
|
+
"""(async) Update the system prompt and filter tools based on backend capabilities.
|
|
585
754
|
|
|
586
755
|
Args:
|
|
587
756
|
request: The model request being processed.
|
|
@@ -590,8 +759,37 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
590
759
|
Returns:
|
|
591
760
|
The model response from the handler.
|
|
592
761
|
"""
|
|
593
|
-
if
|
|
594
|
-
|
|
762
|
+
# Check if execute tool is present and if backend supports it
|
|
763
|
+
has_execute_tool = any((tool.name if hasattr(tool, "name") else tool.get("name")) == "execute" for tool in request.tools)
|
|
764
|
+
|
|
765
|
+
backend_supports_execution = False
|
|
766
|
+
if has_execute_tool:
|
|
767
|
+
# Resolve backend to check execution support
|
|
768
|
+
backend = self._get_backend(request.runtime)
|
|
769
|
+
backend_supports_execution = _supports_execution(backend)
|
|
770
|
+
|
|
771
|
+
# If execute tool exists but backend doesn't support it, filter it out
|
|
772
|
+
if not backend_supports_execution:
|
|
773
|
+
filtered_tools = [tool for tool in request.tools if (tool.name if hasattr(tool, "name") else tool.get("name")) != "execute"]
|
|
774
|
+
request = request.override(tools=filtered_tools)
|
|
775
|
+
has_execute_tool = False
|
|
776
|
+
|
|
777
|
+
# Use custom system prompt if provided, otherwise generate dynamically
|
|
778
|
+
if self._custom_system_prompt is not None:
|
|
779
|
+
system_prompt = self._custom_system_prompt
|
|
780
|
+
else:
|
|
781
|
+
# Build dynamic system prompt based on available tools
|
|
782
|
+
prompt_parts = [FILESYSTEM_SYSTEM_PROMPT]
|
|
783
|
+
|
|
784
|
+
# Add execution instructions if execute tool is available
|
|
785
|
+
if has_execute_tool and backend_supports_execution:
|
|
786
|
+
prompt_parts.append(EXECUTION_SYSTEM_PROMPT)
|
|
787
|
+
|
|
788
|
+
system_prompt = "\n\n".join(prompt_parts)
|
|
789
|
+
|
|
790
|
+
if system_prompt:
|
|
791
|
+
request = request.override(system_prompt=request.system_prompt + "\n\n" + system_prompt if request.system_prompt else system_prompt)
|
|
792
|
+
|
|
595
793
|
return await handler(request)
|
|
596
794
|
|
|
597
795
|
def _process_large_message(
|
|
@@ -608,7 +806,7 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
608
806
|
result = resolved_backend.write(file_path, content)
|
|
609
807
|
if result.error:
|
|
610
808
|
return message, None
|
|
611
|
-
content_sample = format_content_with_line_numbers(content.splitlines()[:10], start_line=1)
|
|
809
|
+
content_sample = format_content_with_line_numbers([line[:1000] for line in content.splitlines()[:10]], start_line=1)
|
|
612
810
|
processed_message = ToolMessage(
|
|
613
811
|
TOO_LARGE_TOOL_MSG.format(
|
|
614
812
|
tool_call_id=message.tool_call_id,
|
|
@@ -621,20 +819,25 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
621
819
|
|
|
622
820
|
def _intercept_large_tool_result(self, tool_result: ToolMessage | Command, runtime: ToolRuntime) -> ToolMessage | Command:
|
|
623
821
|
if isinstance(tool_result, ToolMessage) and isinstance(tool_result.content, str):
|
|
624
|
-
if not (self.tool_token_limit_before_evict and
|
|
625
|
-
len(tool_result.content) > 4 * self.tool_token_limit_before_evict):
|
|
822
|
+
if not (self.tool_token_limit_before_evict and len(tool_result.content) > 4 * self.tool_token_limit_before_evict):
|
|
626
823
|
return tool_result
|
|
627
824
|
resolved_backend = self._get_backend(runtime)
|
|
628
825
|
processed_message, files_update = self._process_large_message(
|
|
629
826
|
tool_result,
|
|
630
827
|
resolved_backend,
|
|
631
828
|
)
|
|
632
|
-
return (
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
829
|
+
return (
|
|
830
|
+
Command(
|
|
831
|
+
update={
|
|
832
|
+
"files": files_update,
|
|
833
|
+
"messages": [processed_message],
|
|
834
|
+
}
|
|
835
|
+
)
|
|
836
|
+
if files_update is not None
|
|
837
|
+
else processed_message
|
|
838
|
+
)
|
|
636
839
|
|
|
637
|
-
|
|
840
|
+
if isinstance(tool_result, Command):
|
|
638
841
|
update = tool_result.update
|
|
639
842
|
if update is None:
|
|
640
843
|
return tool_result
|
|
@@ -643,10 +846,12 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
643
846
|
resolved_backend = self._get_backend(runtime)
|
|
644
847
|
processed_messages = []
|
|
645
848
|
for message in command_messages:
|
|
646
|
-
if not (
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
849
|
+
if not (
|
|
850
|
+
self.tool_token_limit_before_evict
|
|
851
|
+
and isinstance(message, ToolMessage)
|
|
852
|
+
and isinstance(message.content, str)
|
|
853
|
+
and len(message.content) > 4 * self.tool_token_limit_before_evict
|
|
854
|
+
):
|
|
650
855
|
processed_messages.append(message)
|
|
651
856
|
continue
|
|
652
857
|
processed_message, files_update = self._process_large_message(
|
|
@@ -2,17 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
-
from
|
|
5
|
+
from collections.abc import Awaitable, Callable
|
|
6
|
+
from typing import Any, cast
|
|
6
7
|
|
|
7
8
|
from langchain.agents.middleware.shell_tool import (
|
|
8
9
|
ShellToolMiddleware,
|
|
10
|
+
ShellToolState,
|
|
9
11
|
_PersistentShellTool,
|
|
10
12
|
_SessionResources,
|
|
11
|
-
ShellToolState,
|
|
12
13
|
)
|
|
13
14
|
from langchain.agents.middleware.types import AgentState
|
|
14
|
-
from langchain_core.messages import ToolMessage
|
|
15
15
|
from langchain.tools.tool_node import ToolCallRequest
|
|
16
|
+
from langchain_core.messages import ToolMessage
|
|
16
17
|
from langgraph.types import Command
|
|
17
18
|
|
|
18
19
|
|
|
@@ -78,7 +79,7 @@ class ResumableShellToolMiddleware(ShellToolMiddleware):
|
|
|
78
79
|
return resources
|
|
79
80
|
|
|
80
81
|
new_resources = self._create_resources()
|
|
81
|
-
cast(dict[str, Any], state)["shell_session_resources"] = new_resources
|
|
82
|
+
cast("dict[str, Any]", state)["shell_session_resources"] = new_resources
|
|
82
83
|
return new_resources
|
|
83
84
|
|
|
84
85
|
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
"""Middleware for providing subagents to an agent via a `task` tool."""
|
|
2
2
|
|
|
3
3
|
from collections.abc import Awaitable, Callable, Sequence
|
|
4
|
-
from typing import Any, TypedDict, cast
|
|
5
|
-
from typing_extensions import NotRequired
|
|
4
|
+
from typing import Any, NotRequired, TypedDict, cast
|
|
6
5
|
|
|
7
6
|
from langchain.agents import create_agent
|
|
8
7
|
from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig
|
|
@@ -323,10 +322,7 @@ def _create_task_tool(
|
|
|
323
322
|
)
|
|
324
323
|
|
|
325
324
|
def _validate_and_prepare_state(subagent_type: str, description: str, runtime: ToolRuntime) -> tuple[Runnable, dict]:
|
|
326
|
-
"""
|
|
327
|
-
if subagent_type not in subagent_graphs:
|
|
328
|
-
msg = f"Error: invoked agent of type {subagent_type}, the only allowed types are {[f'`{k}`' for k in subagent_graphs]}"
|
|
329
|
-
raise ValueError(msg)
|
|
325
|
+
"""Prepare state for invocation."""
|
|
330
326
|
subagent = subagent_graphs[subagent_type]
|
|
331
327
|
# Create a new state dict to avoid mutating the original
|
|
332
328
|
subagent_state = {k: v for k, v in runtime.state.items() if k not in _EXCLUDED_STATE_KEYS}
|
|
@@ -345,6 +341,9 @@ def _create_task_tool(
|
|
|
345
341
|
subagent_type: str,
|
|
346
342
|
runtime: ToolRuntime,
|
|
347
343
|
) -> str | Command:
|
|
344
|
+
if subagent_type not in subagent_graphs:
|
|
345
|
+
allowed_types = ", ".join([f"`{k}`" for k in subagent_graphs])
|
|
346
|
+
return f"We cannot invoke subagent {subagent_type} because it does not exist, the only allowed types are {allowed_types}"
|
|
348
347
|
subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
|
|
349
348
|
result = subagent.invoke(subagent_state)
|
|
350
349
|
if not runtime.tool_call_id:
|
|
@@ -357,6 +356,9 @@ def _create_task_tool(
|
|
|
357
356
|
subagent_type: str,
|
|
358
357
|
runtime: ToolRuntime,
|
|
359
358
|
) -> str | Command:
|
|
359
|
+
if subagent_type not in subagent_graphs:
|
|
360
|
+
allowed_types = ", ".join([f"`{k}`" for k in subagent_graphs])
|
|
361
|
+
return f"We cannot invoke subagent {subagent_type} because it does not exist, the only allowed types are {allowed_types}"
|
|
360
362
|
subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
|
|
361
363
|
result = await subagent.ainvoke(subagent_state)
|
|
362
364
|
if not runtime.tool_call_id:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.6
|
|
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
|
Project-URL: Homepage, https://docs.langchain.com/oss/python/deepagents/overview
|
|
@@ -11,18 +11,13 @@ Project-URL: Slack, https://www.langchain.com/join-community
|
|
|
11
11
|
Project-URL: Reddit, https://www.reddit.com/r/LangChain/
|
|
12
12
|
Requires-Python: <4.0,>=3.11
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
|
-
License-File: LICENSE
|
|
15
14
|
Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
|
|
16
15
|
Requires-Dist: langchain<2.0.0,>=1.0.2
|
|
17
16
|
Requires-Dist: langchain-core<2.0.0,>=1.0.0
|
|
18
17
|
Requires-Dist: wcmatch
|
|
19
|
-
|
|
20
|
-
Requires-Dist:
|
|
21
|
-
Requires-Dist:
|
|
22
|
-
Requires-Dist: build; extra == "dev"
|
|
23
|
-
Requires-Dist: twine; extra == "dev"
|
|
24
|
-
Requires-Dist: langchain-openai; extra == "dev"
|
|
25
|
-
Dynamic: license-file
|
|
18
|
+
Requires-Dist: daytona>=0.113.0
|
|
19
|
+
Requires-Dist: runloop-api-client>=0.66.1
|
|
20
|
+
Requires-Dist: tavily>=1.1.0
|
|
26
21
|
|
|
27
22
|
# 🧠🤖Deep Agents
|
|
28
23
|
|
|
@@ -353,7 +348,7 @@ Deep Agents are built with a modular middleware architecture. As a reminder, Dee
|
|
|
353
348
|
- A filesystem for storing context and long-term memories
|
|
354
349
|
- The ability to spawn subagents
|
|
355
350
|
|
|
356
|
-
Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **
|
|
351
|
+
Each of these features is implemented as separate middleware. When you create a deep agent with `create_deep_agent`, we automatically attach **TodoListMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
|
|
357
352
|
|
|
358
353
|
Middleware is a composable concept, and you can choose to add as many or as few middleware to an agent depending on your use case. That means that you can also use any of the aforementioned middleware independently!
|
|
359
354
|
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
deepagents/__init__.py,sha256=9BVNn4lfF5N8l2KY8Ttxi82zO609I-fGqoSIF7DAxiU,342
|
|
2
|
+
deepagents/graph.py,sha256=c6ggWJIPaFOK2OCWxZdGEPDfuDvjdDuqHY06-bUiqPg,6379
|
|
3
|
+
deepagents/backends/__init__.py,sha256=BOKu2cQ1OdMyO_l2rLqZQiXppYFmQbx7OIQb7WYwvZc,457
|
|
4
|
+
deepagents/backends/composite.py,sha256=2BSyAurAt1FXV7iFyajzVaRZvjGkUPBybg7J8E6kRNE,9548
|
|
5
|
+
deepagents/backends/filesystem.py,sha256=SsVDx__j_AARIwRzaDuokbxbkquJ_Lw3Qi7dLWaqRUs,18674
|
|
6
|
+
deepagents/backends/protocol.py,sha256=Hi6u3MWIfMUGFWwnIFwvmwJbHYsx8IxeU2aaoP_9OMk,5831
|
|
7
|
+
deepagents/backends/sandbox.py,sha256=JueMe_2cZcA359JIqIi8kDUkmdtevC4VbKHw-fBPOWs,10125
|
|
8
|
+
deepagents/backends/state.py,sha256=ST_tUExPxArJaA3U8vc1dyzxuYl2BQH-HU7P0eu_Ty8,6518
|
|
9
|
+
deepagents/backends/store.py,sha256=f2LVSl65Dg-BZl-cY3pl3RqrUJCBMBm2kuAzZEODwsE,13098
|
|
10
|
+
deepagents/backends/utils.py,sha256=Iyk2jW-gfoLvMnz-W_2FRCoJW_j3r1zoumU9iww-jd0,13973
|
|
11
|
+
deepagents/middleware/__init__.py,sha256=x7UHqGcrKlhzORNdChPvnUwa_PIJCbFUHY6zTKVfloI,418
|
|
12
|
+
deepagents/middleware/filesystem.py,sha256=3PAetXqWy0i9bE6moM0FDZAEmjMm_M48B4AWWYl4Luk,37271
|
|
13
|
+
deepagents/middleware/patch_tool_calls.py,sha256=PdNhxPaQqwnFkhEAZEE2kEzadTNAOO3_iJRA30WqpGE,1981
|
|
14
|
+
deepagents/middleware/resumable_shell.py,sha256=KjhafjKu28Nf_8pDmSk_aWRK7pgkXZoubvWQljIEv3w,3382
|
|
15
|
+
deepagents/middleware/subagents.py,sha256=RbNpWLXC0Bhr0nUIs40whybNnzNkhxG9Fie7QKsICRk,23748
|
|
16
|
+
deepagents-0.2.6.dist-info/METADATA,sha256=gcNhcchWORoY_wyqYv8xU1lvvYccmOhuVDZIYTubNYI,18887
|
|
17
|
+
deepagents-0.2.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
18
|
+
deepagents-0.2.6.dist-info/top_level.txt,sha256=drAzchOzPNePwpb3_pbPuvLuayXkN7SNqeIKMBWJoAo,11
|
|
19
|
+
deepagents-0.2.6.dist-info/RECORD,,
|