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,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 typing import Literal, Optional
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(update={
331
- "files": res.files_update,
332
- "messages": [
333
- ToolMessage(
334
- content=f"Updated file {res.path}",
335
- tool_call_id=runtime.tool_call_id,
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(update={
375
- "files": res.files_update,
376
- "messages": [
377
- ToolMessage(
378
- content=f"Successfully replaced {res.occurrences} instance(s) of the string in '{res.path}'",
379
- tool_call_id=runtime.tool_call_id,
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: Optional[str] = None,
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 filesystem tools (ls, read_file, write_file, edit_file, glob, grep).
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 six filesystem tools to the agent: ls, read_file, write_file,
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.memory.backends import StateBackend, StoreBackend, CompositeBackend
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
- default=StateBackend(),
514
- routes={"/memories/": StoreBackend()}
515
- )
516
- agent = create_agent(middleware=[FilesystemMiddleware(memory_backend=backend)])
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. Defaults to StateBackend if not provided.
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.system_prompt = system_prompt if system_prompt is not None else FILESYSTEM_SYSTEM_PROMPT
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 to include instructions on using the filesystem.
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 self.system_prompt is not None:
576
- request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt if request.system_prompt else self.system_prompt
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 to include instructions on using the filesystem.
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 self.system_prompt is not None:
594
- request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt if request.system_prompt else self.system_prompt
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 (Command(update={
633
- "files": files_update,
634
- "messages": [processed_message],
635
- }) if files_update is not None else processed_message)
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
- elif isinstance(tool_result, Command):
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 (self.tool_token_limit_before_evict and
647
- isinstance(message, ToolMessage) and
648
- isinstance(message.content, str) and
649
- len(message.content) > 4 * self.tool_token_limit_before_evict):
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 typing import Any, Awaitable, Callable, cast
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
- """Validate subagent type and prepare state for invocation."""
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.4
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
- Provides-Extra: dev
20
- Requires-Dist: pytest; extra == "dev"
21
- Requires-Dist: pytest-cov; extra == "dev"
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 **PlanningMiddleware**, **FilesystemMiddleware** and **SubAgentMiddleware** to your agent.
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,,