deepagents 0.2.1rc1__py3-none-any.whl → 0.2.1rc2__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.
@@ -37,6 +37,15 @@ class GrepMatch(TypedDict):
37
37
  text: str
38
38
 
39
39
 
40
+ def sanitize_tool_call_id(tool_call_id: str) -> str:
41
+ """Sanitize tool_call_id to prevent path traversal and separator issues.
42
+
43
+ Replaces dangerous characters (., /, \) with underscores.
44
+ """
45
+ sanitized = tool_call_id.replace(".", "_").replace("/", "_").replace("\\", "_")
46
+ return sanitized
47
+
48
+
40
49
  def format_content_with_line_numbers(
41
50
  content: str | list[str],
42
51
  start_line: int = 1,
@@ -24,11 +24,11 @@ from typing_extensions import TypedDict
24
24
  from deepagents.backends.protocol import BackendProtocol, BackendFactory, WriteResult, EditResult
25
25
  from deepagents.backends import StateBackend
26
26
  from deepagents.backends.utils import (
27
- create_file_data,
28
27
  update_file_data,
29
28
  format_content_with_line_numbers,
30
29
  format_grep_matches,
31
30
  truncate_if_too_long,
31
+ sanitize_tool_call_id,
32
32
  )
33
33
 
34
34
  EMPTY_CONTENT_WARNING = "System reminder: File exists but has empty contents"
@@ -227,6 +227,15 @@ All file paths must start with a /.
227
227
 
228
228
 
229
229
  def _get_backend(backend: BACKEND_TYPES, runtime: ToolRuntime) -> BackendProtocol:
230
+ """Get the resolved backend instance from backend or factory.
231
+
232
+ Args:
233
+ backend: Backend instance or factory function.
234
+ runtime: The tool runtime context.
235
+
236
+ Returns:
237
+ Resolved backend instance.
238
+ """
230
239
  if callable(backend):
231
240
  return backend(runtime)
232
241
  return backend
@@ -532,6 +541,19 @@ class FilesystemMiddleware(AgentMiddleware):
532
541
 
533
542
  self.tools = _get_filesystem_tools(self.backend, custom_tool_descriptions)
534
543
 
544
+ def _get_backend(self, runtime: ToolRuntime) -> BackendProtocol:
545
+ """Get the resolved backend instance from backend or factory.
546
+
547
+ Args:
548
+ runtime: The tool runtime context.
549
+
550
+ Returns:
551
+ Resolved backend instance.
552
+ """
553
+ if callable(self.backend):
554
+ return self.backend(runtime)
555
+ return self.backend
556
+
535
557
  def wrap_model_call(
536
558
  self,
537
559
  request: ModelRequest,
@@ -568,54 +590,70 @@ class FilesystemMiddleware(AgentMiddleware):
568
590
  request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt if request.system_prompt else self.system_prompt
569
591
  return await handler(request)
570
592
 
571
- def _intercept_large_tool_result(self, tool_result: ToolMessage | Command) -> ToolMessage | Command:
593
+ def _process_large_message(
594
+ self,
595
+ message: ToolMessage,
596
+ resolved_backend: BackendProtocol,
597
+ ) -> tuple[ToolMessage, dict[str, FileData] | None]:
598
+ content = message.content
599
+ if not isinstance(content, str) or len(content) <= 4 * self.tool_token_limit_before_evict:
600
+ return message, None
601
+
602
+ sanitized_id = sanitize_tool_call_id(message.tool_call_id)
603
+ file_path = f"/large_tool_results/{sanitized_id}"
604
+ result = resolved_backend.write(file_path, content)
605
+ if result.error:
606
+ return message, None
607
+ content_sample = format_content_with_line_numbers(content.splitlines()[:10], start_line=1)
608
+ processed_message = ToolMessage(
609
+ TOO_LARGE_TOOL_MSG.format(
610
+ tool_call_id=message.tool_call_id,
611
+ file_path=file_path,
612
+ content_sample=content_sample,
613
+ ),
614
+ tool_call_id=message.tool_call_id,
615
+ )
616
+ return processed_message, result.files_update
617
+
618
+ def _intercept_large_tool_result(self, tool_result: ToolMessage | Command, runtime: ToolRuntime) -> ToolMessage | Command:
572
619
  if isinstance(tool_result, ToolMessage) and isinstance(tool_result.content, str):
573
- content = tool_result.content
574
- if self.tool_token_limit_before_evict and len(content) > 4 * self.tool_token_limit_before_evict:
575
- file_path = f"/large_tool_results/{tool_result.tool_call_id}"
576
- file_data = create_file_data(content)
577
- state_update = {
578
- "messages": [
579
- ToolMessage(
580
- TOO_LARGE_TOOL_MSG.format(
581
- tool_call_id=tool_result.tool_call_id,
582
- file_path=file_path,
583
- content_sample=format_content_with_line_numbers(file_data["content"][:10], start_line=1),
584
- ),
585
- tool_call_id=tool_result.tool_call_id,
586
- )
587
- ],
588
- "files": {file_path: file_data},
589
- }
590
- return Command(update=state_update)
620
+ if not (self.tool_token_limit_before_evict and
621
+ len(tool_result.content) > 4 * self.tool_token_limit_before_evict):
622
+ return tool_result
623
+ resolved_backend = self._get_backend(runtime)
624
+ processed_message, files_update = self._process_large_message(
625
+ tool_result,
626
+ resolved_backend,
627
+ )
628
+ return (Command(update={
629
+ "files": files_update,
630
+ "messages": [processed_message],
631
+ }) if files_update is not None else processed_message)
632
+
591
633
  elif isinstance(tool_result, Command):
592
634
  update = tool_result.update
593
635
  if update is None:
594
636
  return tool_result
595
- message_updates = update.get("messages", [])
596
- file_updates = update.get("files", {})
597
-
598
- edited_message_updates = []
599
- for message in message_updates:
600
- if self.tool_token_limit_before_evict and isinstance(message, ToolMessage) and isinstance(message.content, str):
601
- content = message.content
602
- if len(content) > 4 * self.tool_token_limit_before_evict:
603
- file_path = f"/large_tool_results/{message.tool_call_id}"
604
- file_data = create_file_data(content)
605
- edited_message_updates.append(
606
- ToolMessage(
607
- TOO_LARGE_TOOL_MSG.format(
608
- tool_call_id=message.tool_call_id,
609
- file_path=file_path,
610
- content_sample=format_content_with_line_numbers(file_data["content"][:10], start_line=1),
611
- ),
612
- tool_call_id=message.tool_call_id,
613
- )
614
- )
615
- file_updates[file_path] = file_data
616
- continue
617
- edited_message_updates.append(message)
618
- return Command(update={**update, "messages": edited_message_updates, "files": file_updates})
637
+ command_messages = update.get("messages", [])
638
+ accumulated_file_updates = dict(update.get("files", {}))
639
+ resolved_backend = self._get_backend(runtime)
640
+ processed_messages = []
641
+ for message in command_messages:
642
+ if not (self.tool_token_limit_before_evict and
643
+ isinstance(message, ToolMessage) and
644
+ isinstance(message.content, str) and
645
+ len(message.content) > 4 * self.tool_token_limit_before_evict):
646
+ processed_messages.append(message)
647
+ continue
648
+ processed_message, files_update = self._process_large_message(
649
+ message,
650
+ resolved_backend,
651
+ )
652
+ processed_messages.append(processed_message)
653
+ if files_update is not None:
654
+ accumulated_file_updates.update(files_update)
655
+ return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
656
+
619
657
  return tool_result
620
658
 
621
659
  def wrap_tool_call(
@@ -636,7 +674,7 @@ class FilesystemMiddleware(AgentMiddleware):
636
674
  return handler(request)
637
675
 
638
676
  tool_result = handler(request)
639
- return self._intercept_large_tool_result(tool_result)
677
+ return self._intercept_large_tool_result(tool_result, request.runtime)
640
678
 
641
679
  async def awrap_tool_call(
642
680
  self,
@@ -656,4 +694,4 @@ class FilesystemMiddleware(AgentMiddleware):
656
694
  return await handler(request)
657
695
 
658
696
  tool_result = await handler(request)
659
- return self._intercept_large_tool_result(tool_result)
697
+ return self._intercept_large_tool_result(tool_result, request.runtime)
@@ -3,9 +3,9 @@
3
3
  from typing import Any
4
4
 
5
5
  from langchain.agents.middleware import AgentMiddleware, AgentState
6
- from langchain_core.messages import RemoveMessage, ToolMessage
7
- from langgraph.graph.message import REMOVE_ALL_MESSAGES
6
+ from langchain_core.messages import ToolMessage
8
7
  from langgraph.runtime import Runtime
8
+ from langgraph.types import Overwrite
9
9
 
10
10
 
11
11
  class PatchToolCallsMiddleware(AgentMiddleware):
@@ -41,4 +41,4 @@ class PatchToolCallsMiddleware(AgentMiddleware):
41
41
  )
42
42
  )
43
43
 
44
- return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *patched_messages]}
44
+ return {"messages": Overwrite(patched_messages)}
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.2.1rc1
3
+ Version: 0.2.1rc2
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
  Requires-Python: <4.0,>=3.11
7
7
  Description-Content-Type: text/markdown
8
8
  License-File: LICENSE
9
9
  Requires-Dist: langchain-anthropic<2.0.0,>=1.0.0
10
- Requires-Dist: langchain<2.0.0,>=1.0.0
10
+ Requires-Dist: langchain<2.0.0,>=1.0.2
11
11
  Requires-Dist: langchain-core<2.0.0,>=1.0.0
12
12
  Requires-Dist: wcmatch
13
13
  Provides-Extra: dev
@@ -6,13 +6,13 @@ deepagents/backends/filesystem.py,sha256=U9Tmf8BDTqKbw-gQjJkJOYG1nXQo9FhxyfzN97W
6
6
  deepagents/backends/protocol.py,sha256=fwqJa_Ec6F4BoNYz0bcPHL_fiKksxw2RoyA6x5wr7dc,4181
7
7
  deepagents/backends/state.py,sha256=BxMNm1kDpxtgzIzpuF78h1NuYh9VIpXqnUbbETGe4Y4,6584
8
8
  deepagents/backends/store.py,sha256=VsPSj6ayABPjkKiN6CcvOGm7YCWKuWP_ltJWvFJ1nF0,13358
9
- deepagents/backends/utils.py,sha256=CeGzpz1VaaqUjiodwdfDTfHuUR1OUGBYLgPbpp6pWAw,14119
9
+ deepagents/backends/utils.py,sha256=vQDMFMjf7pmfKqprpTlF7851FWmswZnMdLj-cezTsBk,14432
10
10
  deepagents/middleware/__init__.py,sha256=J7372TNGR27OU4C3uuQMryHHpXOBjFV_4aEZ_AoQ6n0,284
11
- deepagents/middleware/filesystem.py,sha256=IggpI5ENGy5ykB4HCtTq0l5mM618eUDaFiLYArTUi94,26897
12
- deepagents/middleware/patch_tool_calls.py,sha256=Cu8rUpt1GjrYgfMvZG6wOowvnmFeYTCauOJhlltNPmo,2045
11
+ deepagents/middleware/filesystem.py,sha256=jxtwma6xWE-EQeASs6rtnoiiLqb_HT4c-cxsLFquneE,27882
12
+ deepagents/middleware/patch_tool_calls.py,sha256=PdNhxPaQqwnFkhEAZEE2kEzadTNAOO3_iJRA30WqpGE,1981
13
13
  deepagents/middleware/subagents.py,sha256=JxXwZvi41pBKKMguKlyVqwjCoydnZboWEgJGkWOCIY8,23503
14
- deepagents-0.2.1rc1.dist-info/licenses/LICENSE,sha256=c__BaxUCK69leo2yEKynf8lWndu8iwYwge1CbyqAe-E,1071
15
- deepagents-0.2.1rc1.dist-info/METADATA,sha256=pyeoVgRcusjjJZq8uuZ50nLMt8yl1q1Vfl59kTmnz2s,18663
16
- deepagents-0.2.1rc1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
- deepagents-0.2.1rc1.dist-info/top_level.txt,sha256=drAzchOzPNePwpb3_pbPuvLuayXkN7SNqeIKMBWJoAo,11
18
- deepagents-0.2.1rc1.dist-info/RECORD,,
14
+ deepagents-0.2.1rc2.dist-info/licenses/LICENSE,sha256=c__BaxUCK69leo2yEKynf8lWndu8iwYwge1CbyqAe-E,1071
15
+ deepagents-0.2.1rc2.dist-info/METADATA,sha256=FeGBy6fP0dwLx4w-pFAn4A3_9TH50ROd5Q6LMY8LgxM,18663
16
+ deepagents-0.2.1rc2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
17
+ deepagents-0.2.1rc2.dist-info/top_level.txt,sha256=drAzchOzPNePwpb3_pbPuvLuayXkN7SNqeIKMBWJoAo,11
18
+ deepagents-0.2.1rc2.dist-info/RECORD,,