deepagents 0.1.0__tar.gz → 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "deepagents"
3
- version = "0.1.0"
3
+ version = "0.1.1"
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" }
@@ -18,6 +18,7 @@ from langgraph.store.base import BaseStore
18
18
  from langgraph.types import Checkpointer
19
19
 
20
20
  from deepagents.middleware.filesystem import FilesystemMiddleware
21
+ from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
21
22
  from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
22
23
 
23
24
  BASE_AGENT_PROMPT = "In order to complete the objective that the user asks of you, you have access to a number of standard tools."
@@ -112,6 +113,7 @@ def create_deep_agent(
112
113
  messages_to_keep=6,
113
114
  ),
114
115
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
116
+ PatchToolCallsMiddleware(),
115
117
  ],
116
118
  default_interrupt_on=interrupt_on,
117
119
  general_purpose_agent=True,
@@ -122,6 +124,7 @@ def create_deep_agent(
122
124
  messages_to_keep=6,
123
125
  ),
124
126
  AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
127
+ PatchToolCallsMiddleware(),
125
128
  ]
126
129
  if interrupt_on is not None:
127
130
  deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
@@ -2,7 +2,8 @@
2
2
  # ruff: noqa: E501
3
3
 
4
4
  from collections.abc import Awaitable, Callable, Sequence
5
- from typing import TYPE_CHECKING, Annotated, Any, NotRequired
5
+ from typing import TYPE_CHECKING, Annotated, Any
6
+ from typing_extensions import NotRequired
6
7
 
7
8
  if TYPE_CHECKING:
8
9
  from langgraph.runtime import Runtime
@@ -442,7 +443,7 @@ def _get_namespace() -> tuple[str] | tuple[str, str]:
442
443
  return (assistant_id, "filesystem")
443
444
 
444
445
 
445
- def _get_store(runtime: Runtime[Any]) -> BaseStore:
446
+ def _get_store(runtime: ToolRuntime[None, FilesystemState]) -> BaseStore:
446
447
  """Get the store from the runtime, raising an error if unavailable.
447
448
 
448
449
  Args:
@@ -721,6 +722,9 @@ def _write_file_tool_generator(custom_description: str | None = None, *, long_te
721
722
  runtime: ToolRuntime[None, FilesystemState],
722
723
  ) -> Command | str:
723
724
  file_path = _validate_path(file_path)
725
+ if not runtime.tool_call_id:
726
+ value_error_msg = "Tool call ID is required for write_file invocation"
727
+ raise ValueError(value_error_msg)
724
728
  if _has_memories_prefix(file_path):
725
729
  stripped_file_path = _strip_memories_prefix(file_path)
726
730
  store = _get_store(runtime)
@@ -741,6 +745,9 @@ def _write_file_tool_generator(custom_description: str | None = None, *, long_te
741
745
  runtime: ToolRuntime[None, FilesystemState],
742
746
  ) -> Command | str:
743
747
  file_path = _validate_path(file_path)
748
+ if not runtime.tool_call_id:
749
+ value_error_msg = "Tool call ID is required for write_file invocation"
750
+ raise ValueError(value_error_msg)
744
751
  return _write_file_to_state(runtime.state, runtime.tool_call_id, file_path, content)
745
752
 
746
753
  return write_file
@@ -0,0 +1,44 @@
1
+ """Middleware to patch dangling tool calls in the messages history."""
2
+
3
+ from typing import Any
4
+
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
8
+ from langgraph.runtime import Runtime
9
+
10
+
11
+ class PatchToolCallsMiddleware(AgentMiddleware):
12
+ """Middleware to patch dangling tool calls in the messages history."""
13
+
14
+ def before_agent(self, state: AgentState, runtime: Runtime[Any]) -> dict[str, Any] | None: # noqa: ARG002
15
+ """Before the agent runs, handle dangling tool calls from any AIMessage."""
16
+ messages = state["messages"]
17
+ if not messages or len(messages) == 0:
18
+ return None
19
+
20
+ patched_messages = []
21
+ # Iterate over the messages and add any dangling tool calls
22
+ for i, msg in enumerate(messages):
23
+ patched_messages.append(msg)
24
+ if msg.type == "ai" and msg.tool_calls:
25
+ for tool_call in msg.tool_calls:
26
+ corresponding_tool_msg = next(
27
+ (msg for msg in messages[i:] if msg.type == "tool" and msg.tool_call_id == tool_call["id"]),
28
+ None,
29
+ )
30
+ if corresponding_tool_msg is None:
31
+ # We have a dangling tool call which needs a ToolMessage
32
+ tool_msg = (
33
+ f"Tool call {tool_call['name']} with id {tool_call['id']} was "
34
+ "cancelled - another message came in before it could be completed."
35
+ )
36
+ patched_messages.append(
37
+ ToolMessage(
38
+ content=tool_msg,
39
+ name=tool_call["name"],
40
+ tool_call_id=tool_call["id"],
41
+ )
42
+ )
43
+
44
+ return {"messages": [RemoveMessage(id=REMOVE_ALL_MESSAGES), *patched_messages]}
@@ -1,7 +1,8 @@
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, NotRequired, TypedDict, cast
4
+ from typing import Any, TypedDict, cast
5
+ from typing_extensions import NotRequired
5
6
 
6
7
  from langchain.agents import create_agent
7
8
  from langchain.agents.middleware import HumanInTheLoopMiddleware, InterruptOnConfig
@@ -13,11 +14,6 @@ from langchain_core.runnables import Runnable
13
14
  from langchain_core.tools import StructuredTool
14
15
  from langgraph.types import Command
15
16
 
16
- try:
17
- from langchain_anthropic.middleware.prompt_caching import AnthropicPromptCachingMiddleware
18
- except ImportError:
19
- AnthropicPromptCachingMiddleware = None
20
-
21
17
 
22
18
  class SubAgent(TypedDict):
23
19
  """Specification for an agent.
@@ -352,6 +348,9 @@ def _create_task_tool(
352
348
  ) -> str | Command:
353
349
  subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
354
350
  result = subagent.invoke(subagent_state)
351
+ if not runtime.tool_call_id:
352
+ value_error_msg = "Tool call ID is required for subagent invocation"
353
+ raise ValueError(value_error_msg)
355
354
  return _return_command_with_state_update(result, runtime.tool_call_id)
356
355
 
357
356
  async def atask(
@@ -361,6 +360,9 @@ def _create_task_tool(
361
360
  ) -> str | Command:
362
361
  subagent, subagent_state = _validate_and_prepare_state(subagent_type, description, runtime)
363
362
  result = await subagent.ainvoke(subagent_state)
363
+ if not runtime.tool_call_id:
364
+ value_error_msg = "Tool call ID is required for subagent invocation"
365
+ raise ValueError(value_error_msg)
364
366
  return _return_command_with_state_update(result, runtime.tool_call_id)
365
367
 
366
368
  return StructuredTool.from_function(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: deepagents
3
- Version: 0.1.0
3
+ Version: 0.1.1
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
@@ -10,5 +10,6 @@ src/deepagents.egg-info/requires.txt
10
10
  src/deepagents.egg-info/top_level.txt
11
11
  src/deepagents/middleware/__init__.py
12
12
  src/deepagents/middleware/filesystem.py
13
+ src/deepagents/middleware/patch_tool_calls.py
13
14
  src/deepagents/middleware/subagents.py
14
15
  tests/test_middleware.py
@@ -1,6 +1,14 @@
1
1
  import pytest
2
2
  from langchain.agents import create_agent
3
3
  from langchain.tools import ToolRuntime
4
+ from langchain_core.messages import (
5
+ AIMessage,
6
+ HumanMessage,
7
+ SystemMessage,
8
+ ToolCall,
9
+ ToolMessage,
10
+ )
11
+ from langgraph.graph.message import add_messages
4
12
 
5
13
  from deepagents.middleware.filesystem import (
6
14
  FILESYSTEM_SYSTEM_PROMPT,
@@ -9,6 +17,7 @@ from deepagents.middleware.filesystem import (
9
17
  FilesystemMiddleware,
10
18
  FilesystemState,
11
19
  )
20
+ from deepagents.middleware.patch_tool_calls import PatchToolCallsMiddleware
12
21
  from deepagents.middleware.subagents import DEFAULT_GENERAL_PURPOSE_DESCRIPTION, TASK_SYSTEM_PROMPT, TASK_TOOL_DESCRIPTION, SubAgentMiddleware
13
22
 
14
23
 
@@ -171,3 +180,125 @@ class TestSubagentMiddleware:
171
180
  )
172
181
  assert middleware is not None
173
182
  assert middleware.system_prompt == "Use the task tool to call a subagent."
183
+
184
+
185
+ class TestPatchToolCallsMiddleware:
186
+ def test_first_message(self) -> None:
187
+ input_messages = [
188
+ SystemMessage(content="You are a helpful assistant.", id="1"),
189
+ HumanMessage(content="Hello, how are you?", id="2"),
190
+ ]
191
+ middleware = PatchToolCallsMiddleware()
192
+ state_update = middleware.before_agent({"messages": input_messages}, None)
193
+ assert state_update is not None
194
+ assert len(state_update["messages"]) == 3
195
+ assert state_update["messages"][0].type == "remove"
196
+ assert state_update["messages"][1].type == "system"
197
+ assert state_update["messages"][1].content == "You are a helpful assistant."
198
+ assert state_update["messages"][2].type == "human"
199
+ assert state_update["messages"][2].content == "Hello, how are you?"
200
+ assert state_update["messages"][2].id == "2"
201
+
202
+ def test_missing_tool_call(self) -> None:
203
+ input_messages = [
204
+ SystemMessage(content="You are a helpful assistant.", id="1"),
205
+ HumanMessage(content="Hello, how are you?", id="2"),
206
+ AIMessage(
207
+ content="I'm doing well, thank you!",
208
+ tool_calls=[ToolCall(id="123", name="get_events_for_days", args={"date_str": "2025-01-01"})],
209
+ id="3",
210
+ ),
211
+ HumanMessage(content="What is the weather in Tokyo?", id="4"),
212
+ ]
213
+ middleware = PatchToolCallsMiddleware()
214
+ state_update = middleware.before_agent({"messages": input_messages}, None)
215
+ assert state_update is not None
216
+ assert len(state_update["messages"]) == 6
217
+ assert state_update["messages"][0].type == "remove"
218
+ assert state_update["messages"][1] == input_messages[0]
219
+ assert state_update["messages"][2] == input_messages[1]
220
+ assert state_update["messages"][3] == input_messages[2]
221
+ assert state_update["messages"][4].type == "tool"
222
+ assert state_update["messages"][4].tool_call_id == "123"
223
+ assert state_update["messages"][4].name == "get_events_for_days"
224
+ assert state_update["messages"][5] == input_messages[3]
225
+ updated_messages = add_messages(input_messages, state_update["messages"])
226
+ assert len(updated_messages) == 5
227
+ assert updated_messages[0] == input_messages[0]
228
+ assert updated_messages[1] == input_messages[1]
229
+ assert updated_messages[2] == input_messages[2]
230
+ assert updated_messages[3].type == "tool"
231
+ assert updated_messages[3].tool_call_id == "123"
232
+ assert updated_messages[3].name == "get_events_for_days"
233
+ assert updated_messages[4] == input_messages[3]
234
+
235
+ def test_no_missing_tool_calls(self) -> None:
236
+ input_messages = [
237
+ SystemMessage(content="You are a helpful assistant.", id="1"),
238
+ HumanMessage(content="Hello, how are you?", id="2"),
239
+ AIMessage(
240
+ content="I'm doing well, thank you!",
241
+ tool_calls=[ToolCall(id="123", name="get_events_for_days", args={"date_str": "2025-01-01"})],
242
+ id="3",
243
+ ),
244
+ ToolMessage(content="I have no events for that date.", tool_call_id="123", id="4"),
245
+ HumanMessage(content="What is the weather in Tokyo?", id="5"),
246
+ ]
247
+ middleware = PatchToolCallsMiddleware()
248
+ state_update = middleware.before_agent({"messages": input_messages}, None)
249
+ assert state_update is not None
250
+ assert len(state_update["messages"]) == 6
251
+ assert state_update["messages"][0].type == "remove"
252
+ assert state_update["messages"][1:] == input_messages
253
+ updated_messages = add_messages(input_messages, state_update["messages"])
254
+ assert len(updated_messages) == 5
255
+ assert updated_messages == input_messages
256
+
257
+ def test_two_missing_tool_calls(self) -> None:
258
+ input_messages = [
259
+ SystemMessage(content="You are a helpful assistant.", id="1"),
260
+ HumanMessage(content="Hello, how are you?", id="2"),
261
+ AIMessage(
262
+ content="I'm doing well, thank you!",
263
+ tool_calls=[ToolCall(id="123", name="get_events_for_days", args={"date_str": "2025-01-01"})],
264
+ id="3",
265
+ ),
266
+ HumanMessage(content="What is the weather in Tokyo?", id="4"),
267
+ AIMessage(
268
+ content="I'm doing well, thank you!",
269
+ tool_calls=[ToolCall(id="456", name="get_events_for_days", args={"date_str": "2025-01-01"})],
270
+ id="5",
271
+ ),
272
+ HumanMessage(content="What is the weather in Tokyo?", id="6"),
273
+ ]
274
+ middleware = PatchToolCallsMiddleware()
275
+ state_update = middleware.before_agent({"messages": input_messages}, None)
276
+ assert state_update is not None
277
+ assert len(state_update["messages"]) == 9
278
+ assert state_update["messages"][0].type == "remove"
279
+ assert state_update["messages"][1] == input_messages[0]
280
+ assert state_update["messages"][2] == input_messages[1]
281
+ assert state_update["messages"][3] == input_messages[2]
282
+ assert state_update["messages"][4].type == "tool"
283
+ assert state_update["messages"][4].tool_call_id == "123"
284
+ assert state_update["messages"][4].name == "get_events_for_days"
285
+ assert state_update["messages"][5] == input_messages[3]
286
+ assert state_update["messages"][6] == input_messages[4]
287
+ assert state_update["messages"][7].type == "tool"
288
+ assert state_update["messages"][7].tool_call_id == "456"
289
+ assert state_update["messages"][7].name == "get_events_for_days"
290
+ assert state_update["messages"][8] == input_messages[5]
291
+ updated_messages = add_messages(input_messages, state_update["messages"])
292
+ assert len(updated_messages) == 8
293
+ assert updated_messages[0] == input_messages[0]
294
+ assert updated_messages[1] == input_messages[1]
295
+ assert updated_messages[2] == input_messages[2]
296
+ assert updated_messages[3].type == "tool"
297
+ assert updated_messages[3].tool_call_id == "123"
298
+ assert updated_messages[3].name == "get_events_for_days"
299
+ assert updated_messages[4] == input_messages[3]
300
+ assert updated_messages[5] == input_messages[4]
301
+ assert updated_messages[6].type == "tool"
302
+ assert updated_messages[6].tool_call_id == "456"
303
+ assert updated_messages[6].name == "get_events_for_days"
304
+ assert updated_messages[7] == input_messages[5]
File without changes
File without changes
File without changes