deepagents 0.2.1rc1__tar.gz → 0.2.1rc2__tar.gz

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 (24) hide show
  1. {deepagents-0.2.1rc1/src/deepagents.egg-info → deepagents-0.2.1rc2}/PKG-INFO +2 -2
  2. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/pyproject.toml +2 -2
  3. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/utils.py +9 -0
  4. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/filesystem.py +84 -46
  5. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/patch_tool_calls.py +3 -3
  6. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2/src/deepagents.egg-info}/PKG-INFO +2 -2
  7. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/requires.txt +1 -1
  8. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/tests/test_middleware.py +176 -60
  9. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/LICENSE +0 -0
  10. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/README.md +0 -0
  11. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/setup.cfg +0 -0
  12. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/__init__.py +0 -0
  13. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/__init__.py +0 -0
  14. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/composite.py +0 -0
  15. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/filesystem.py +0 -0
  16. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/protocol.py +0 -0
  17. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/state.py +0 -0
  18. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/backends/store.py +0 -0
  19. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/graph.py +0 -0
  20. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/__init__.py +0 -0
  21. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents/middleware/subagents.py +0 -0
  22. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/SOURCES.txt +0 -0
  23. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/dependency_links.txt +0 -0
  24. {deepagents-0.2.1rc1 → deepagents-0.2.1rc2}/src/deepagents.egg-info/top_level.txt +0 -0
@@ -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
@@ -1,13 +1,13 @@
1
1
  [project]
2
2
  name = "deepagents"
3
- version = "0.2.1rc1"
3
+ version = "0.2.1rc2"
4
4
  description = "General purpose 'deep agent' with sub-agent spawning, todo list capabilities, and mock file system. Built on LangGraph."
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
7
7
  requires-python = ">=3.11,<4.0"
8
8
  dependencies = [
9
9
  "langchain-anthropic>=1.0.0,<2.0.0",
10
- "langchain>=1.0.0,<2.0.0",
10
+ "langchain>=1.0.2,<2.0.0",
11
11
  "langchain-core>=1.0.0,<2.0.0",
12
12
  "wcmatch"
13
13
  ]
@@ -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
@@ -1,5 +1,5 @@
1
1
  langchain-anthropic<2.0.0,>=1.0.0
2
- langchain<2.0.0,>=1.0.0
2
+ langchain<2.0.0,>=1.0.2
3
3
  langchain-core<2.0.0,>=1.0.0
4
4
  wcmatch
5
5
 
@@ -8,7 +8,7 @@ from langchain_core.messages import (
8
8
  ToolCall,
9
9
  ToolMessage,
10
10
  )
11
- from langgraph.graph.message import add_messages
11
+ from langgraph.types import Overwrite
12
12
  from langgraph.store.memory import InMemoryStore
13
13
 
14
14
  from deepagents.middleware.filesystem import (
@@ -824,6 +824,113 @@ class TestFilesystemMiddleware:
824
824
  assert lines[1].count("m") == 2000
825
825
  assert " 4\tline4" in lines[2]
826
826
 
827
+ def test_intercept_short_toolmessage(self):
828
+ """Test that small ToolMessages pass through unchanged."""
829
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
830
+ state = FilesystemState(messages=[], files={})
831
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
832
+
833
+ small_content = "x" * 1000
834
+ tool_message = ToolMessage(content=small_content, tool_call_id="test_123")
835
+ result = middleware._intercept_large_tool_result(tool_message, runtime)
836
+
837
+ assert result == tool_message
838
+
839
+ def test_intercept_long_toolmessage(self):
840
+ """Test that large ToolMessages are intercepted and saved to filesystem."""
841
+ from langgraph.types import Command
842
+
843
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
844
+ state = FilesystemState(messages=[], files={})
845
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
846
+
847
+ large_content = "x" * 5000
848
+ tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
849
+ result = middleware._intercept_large_tool_result(tool_message, runtime)
850
+
851
+ assert isinstance(result, Command)
852
+ assert "/large_tool_results/test_123" in result.update["files"]
853
+ assert "Tool result too large" in result.update["messages"][0].content
854
+
855
+ def test_intercept_command_with_short_toolmessage(self):
856
+ """Test that Commands with small messages pass through unchanged."""
857
+ from langgraph.types import Command
858
+
859
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
860
+ state = FilesystemState(messages=[], files={})
861
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
862
+
863
+ small_content = "x" * 1000
864
+ tool_message = ToolMessage(content=small_content, tool_call_id="test_123")
865
+ command = Command(update={"messages": [tool_message], "files": {}})
866
+ result = middleware._intercept_large_tool_result(command, runtime)
867
+
868
+ assert isinstance(result, Command)
869
+ assert result.update["messages"][0].content == small_content
870
+
871
+ def test_intercept_command_with_long_toolmessage(self):
872
+ """Test that Commands with large messages are intercepted."""
873
+ from langgraph.types import Command
874
+
875
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
876
+ state = FilesystemState(messages=[], files={})
877
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
878
+
879
+ large_content = "y" * 5000
880
+ tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
881
+ command = Command(update={"messages": [tool_message], "files": {}})
882
+ result = middleware._intercept_large_tool_result(command, runtime)
883
+
884
+ assert isinstance(result, Command)
885
+ assert "/large_tool_results/test_123" in result.update["files"]
886
+ assert "Tool result too large" in result.update["messages"][0].content
887
+
888
+ def test_intercept_command_with_files_and_long_toolmessage(self):
889
+ """Test that file updates are properly merged with existing files and other keys preserved."""
890
+ from langgraph.types import Command
891
+
892
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
893
+ state = FilesystemState(messages=[], files={})
894
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
895
+
896
+ large_content = "z" * 5000
897
+ tool_message = ToolMessage(content=large_content, tool_call_id="test_123")
898
+ existing_file = FileData(content=["existing"], created_at="2021-01-01", modified_at="2021-01-01")
899
+ command = Command(update={
900
+ "messages": [tool_message],
901
+ "files": {"/existing.txt": existing_file},
902
+ "custom_key": "custom_value"
903
+ })
904
+ result = middleware._intercept_large_tool_result(command, runtime)
905
+
906
+ assert isinstance(result, Command)
907
+ assert "/existing.txt" in result.update["files"]
908
+ assert "/large_tool_results/test_123" in result.update["files"]
909
+ assert result.update["custom_key"] == "custom_value"
910
+
911
+ def test_sanitize_tool_call_id(self):
912
+ """Test that tool_call_id is sanitized to prevent path traversal."""
913
+ from deepagents.backends.utils import sanitize_tool_call_id
914
+
915
+ assert sanitize_tool_call_id("call_123") == "call_123"
916
+ assert sanitize_tool_call_id("call/123") == "call_123"
917
+ assert sanitize_tool_call_id("test.id") == "test_id"
918
+
919
+ def test_intercept_sanitizes_tool_call_id(self):
920
+ """Test that tool_call_id with dangerous characters is sanitized in file path."""
921
+ from langgraph.types import Command
922
+
923
+ middleware = FilesystemMiddleware(tool_token_limit_before_evict=1000)
924
+ state = FilesystemState(messages=[], files={})
925
+ runtime = ToolRuntime(state=state, context=None, tool_call_id="test_123", store=None, stream_writer=lambda _: None, config={})
926
+
927
+ large_content = "x" * 5000
928
+ tool_message = ToolMessage(content=large_content, tool_call_id="test/call.id")
929
+ result = middleware._intercept_large_tool_result(tool_message, runtime)
930
+
931
+ assert isinstance(result, Command)
932
+ assert "/large_tool_results/test_call_id" in result.update["files"]
933
+
827
934
 
828
935
  @pytest.mark.requires("langchain_openai")
829
936
  class TestSubagentMiddleware:
@@ -867,13 +974,14 @@ class TestPatchToolCallsMiddleware:
867
974
  middleware = PatchToolCallsMiddleware()
868
975
  state_update = middleware.before_agent({"messages": input_messages}, None)
869
976
  assert state_update is not None
870
- assert len(state_update["messages"]) == 3
871
- assert state_update["messages"][0].type == "remove"
872
- assert state_update["messages"][1].type == "system"
873
- assert state_update["messages"][1].content == "You are a helpful assistant."
874
- assert state_update["messages"][2].type == "human"
875
- assert state_update["messages"][2].content == "Hello, how are you?"
876
- assert state_update["messages"][2].id == "2"
977
+ assert isinstance(state_update["messages"], Overwrite)
978
+ patched_messages = state_update["messages"].value
979
+ assert len(patched_messages) == 2
980
+ assert patched_messages[0].type == "system"
981
+ assert patched_messages[0].content == "You are a helpful assistant."
982
+ assert patched_messages[1].type == "human"
983
+ assert patched_messages[1].content == "Hello, how are you?"
984
+ assert patched_messages[1].id == "2"
877
985
 
878
986
  def test_missing_tool_call(self) -> None:
879
987
  input_messages = [
@@ -889,24 +997,23 @@ class TestPatchToolCallsMiddleware:
889
997
  middleware = PatchToolCallsMiddleware()
890
998
  state_update = middleware.before_agent({"messages": input_messages}, None)
891
999
  assert state_update is not None
892
- assert len(state_update["messages"]) == 6
893
- assert state_update["messages"][0].type == "remove"
894
- assert state_update["messages"][1] == input_messages[0]
895
- assert state_update["messages"][2] == input_messages[1]
896
- assert state_update["messages"][3] == input_messages[2]
897
- assert state_update["messages"][4].type == "tool"
898
- assert state_update["messages"][4].tool_call_id == "123"
899
- assert state_update["messages"][4].name == "get_events_for_days"
900
- assert state_update["messages"][5] == input_messages[3]
901
- updated_messages = add_messages(input_messages, state_update["messages"])
902
- assert len(updated_messages) == 5
903
- assert updated_messages[0] == input_messages[0]
904
- assert updated_messages[1] == input_messages[1]
905
- assert updated_messages[2] == input_messages[2]
906
- assert updated_messages[3].type == "tool"
907
- assert updated_messages[3].tool_call_id == "123"
908
- assert updated_messages[3].name == "get_events_for_days"
909
- assert updated_messages[4] == input_messages[3]
1000
+ assert isinstance(state_update["messages"], Overwrite)
1001
+ patched_messages = state_update["messages"].value
1002
+ assert len(patched_messages) == 5
1003
+ assert patched_messages[0].type == "system"
1004
+ assert patched_messages[0].content == "You are a helpful assistant."
1005
+ assert patched_messages[1].type == "human"
1006
+ assert patched_messages[1].content == "Hello, how are you?"
1007
+ assert patched_messages[2].type == "ai"
1008
+ assert len(patched_messages[2].tool_calls) == 1
1009
+ assert patched_messages[2].tool_calls[0]["id"] == "123"
1010
+ assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
1011
+ assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
1012
+ assert patched_messages[3].type == "tool"
1013
+ assert patched_messages[3].name == "get_events_for_days"
1014
+ assert patched_messages[3].tool_call_id == "123"
1015
+ assert patched_messages[4].type == "human"
1016
+ assert patched_messages[4].content == "What is the weather in Tokyo?"
910
1017
 
911
1018
  def test_no_missing_tool_calls(self) -> None:
912
1019
  input_messages = [
@@ -923,12 +1030,22 @@ class TestPatchToolCallsMiddleware:
923
1030
  middleware = PatchToolCallsMiddleware()
924
1031
  state_update = middleware.before_agent({"messages": input_messages}, None)
925
1032
  assert state_update is not None
926
- assert len(state_update["messages"]) == 6
927
- assert state_update["messages"][0].type == "remove"
928
- assert state_update["messages"][1:] == input_messages
929
- updated_messages = add_messages(input_messages, state_update["messages"])
930
- assert len(updated_messages) == 5
931
- assert updated_messages == input_messages
1033
+ assert isinstance(state_update["messages"], Overwrite)
1034
+ patched_messages = state_update["messages"].value
1035
+ assert len(patched_messages) == 5
1036
+ assert patched_messages[0].type == "system"
1037
+ assert patched_messages[0].content == "You are a helpful assistant."
1038
+ assert patched_messages[1].type == "human"
1039
+ assert patched_messages[1].content == "Hello, how are you?"
1040
+ assert patched_messages[2].type == "ai"
1041
+ assert len(patched_messages[2].tool_calls) == 1
1042
+ assert patched_messages[2].tool_calls[0]["id"] == "123"
1043
+ assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
1044
+ assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
1045
+ assert patched_messages[3].type == "tool"
1046
+ assert patched_messages[3].tool_call_id == "123"
1047
+ assert patched_messages[4].type == "human"
1048
+ assert patched_messages[4].content == "What is the weather in Tokyo?"
932
1049
 
933
1050
  def test_two_missing_tool_calls(self) -> None:
934
1051
  input_messages = [
@@ -950,34 +1067,33 @@ class TestPatchToolCallsMiddleware:
950
1067
  middleware = PatchToolCallsMiddleware()
951
1068
  state_update = middleware.before_agent({"messages": input_messages}, None)
952
1069
  assert state_update is not None
953
- assert len(state_update["messages"]) == 9
954
- assert state_update["messages"][0].type == "remove"
955
- assert state_update["messages"][1] == input_messages[0]
956
- assert state_update["messages"][2] == input_messages[1]
957
- assert state_update["messages"][3] == input_messages[2]
958
- assert state_update["messages"][4].type == "tool"
959
- assert state_update["messages"][4].tool_call_id == "123"
960
- assert state_update["messages"][4].name == "get_events_for_days"
961
- assert state_update["messages"][5] == input_messages[3]
962
- assert state_update["messages"][6] == input_messages[4]
963
- assert state_update["messages"][7].type == "tool"
964
- assert state_update["messages"][7].tool_call_id == "456"
965
- assert state_update["messages"][7].name == "get_events_for_days"
966
- assert state_update["messages"][8] == input_messages[5]
967
- updated_messages = add_messages(input_messages, state_update["messages"])
968
- assert len(updated_messages) == 8
969
- assert updated_messages[0] == input_messages[0]
970
- assert updated_messages[1] == input_messages[1]
971
- assert updated_messages[2] == input_messages[2]
972
- assert updated_messages[3].type == "tool"
973
- assert updated_messages[3].tool_call_id == "123"
974
- assert updated_messages[3].name == "get_events_for_days"
975
- assert updated_messages[4] == input_messages[3]
976
- assert updated_messages[5] == input_messages[4]
977
- assert updated_messages[6].type == "tool"
978
- assert updated_messages[6].tool_call_id == "456"
979
- assert updated_messages[6].name == "get_events_for_days"
980
- assert updated_messages[7] == input_messages[5]
1070
+ assert isinstance(state_update["messages"], Overwrite)
1071
+ patched_messages = state_update["messages"].value
1072
+ assert len(patched_messages) == 8
1073
+ assert patched_messages[0].type == "system"
1074
+ assert patched_messages[0].content == "You are a helpful assistant."
1075
+ assert patched_messages[1].type == "human"
1076
+ assert patched_messages[1].content == "Hello, how are you?"
1077
+ assert patched_messages[2].type == "ai"
1078
+ assert len(patched_messages[2].tool_calls) == 1
1079
+ assert patched_messages[2].tool_calls[0]["id"] == "123"
1080
+ assert patched_messages[2].tool_calls[0]["name"] == "get_events_for_days"
1081
+ assert patched_messages[2].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
1082
+ assert patched_messages[3].type == "tool"
1083
+ assert patched_messages[3].name == "get_events_for_days"
1084
+ assert patched_messages[3].tool_call_id == "123"
1085
+ assert patched_messages[4].type == "human"
1086
+ assert patched_messages[4].content == "What is the weather in Tokyo?"
1087
+ assert patched_messages[5].type == "ai"
1088
+ assert len(patched_messages[5].tool_calls) == 1
1089
+ assert patched_messages[5].tool_calls[0]["id"] == "456"
1090
+ assert patched_messages[5].tool_calls[0]["name"] == "get_events_for_days"
1091
+ assert patched_messages[5].tool_calls[0]["args"] == {"date_str": "2025-01-01"}
1092
+ assert patched_messages[6].type == "tool"
1093
+ assert patched_messages[6].name == "get_events_for_days"
1094
+ assert patched_messages[6].tool_call_id == "456"
1095
+ assert patched_messages[7].type == "human"
1096
+ assert patched_messages[7].content == "What is the weather in Tokyo?"
981
1097
 
982
1098
 
983
1099
  class TestTruncation:
File without changes
File without changes
File without changes