deepagents 0.2.1rc1__tar.gz → 0.2.2__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.
- {deepagents-0.2.1rc1/src/deepagents.egg-info → deepagents-0.2.2}/PKG-INFO +2 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/pyproject.toml +7 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/utils.py +9 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/graph.py +2 -2
- deepagents-0.2.2/src/deepagents/middleware/__init__.py +13 -0
- deepagents-0.2.2/src/deepagents/middleware/agent_memory.py +222 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/middleware/filesystem.py +91 -49
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/middleware/patch_tool_calls.py +3 -3
- deepagents-0.2.2/src/deepagents/middleware/resumable_shell.py +85 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2/src/deepagents.egg-info}/PKG-INFO +2 -2
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents.egg-info/SOURCES.txt +2 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents.egg-info/requires.txt +1 -1
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/tests/test_middleware.py +176 -60
- deepagents-0.2.1rc1/src/deepagents/middleware/__init__.py +0 -6
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/LICENSE +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/README.md +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/setup.cfg +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/__init__.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/__init__.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/composite.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/filesystem.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/protocol.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/state.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/backends/store.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents/middleware/subagents.py +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/src/deepagents.egg-info/dependency_links.txt +0 -0
- {deepagents-0.2.1rc1 → deepagents-0.2.2}/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.
|
|
3
|
+
Version: 0.2.2
|
|
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.
|
|
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.
|
|
3
|
+
version = "0.2.2"
|
|
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.
|
|
10
|
+
"langchain>=1.0.2,<2.0.0",
|
|
11
11
|
"langchain-core>=1.0.0,<2.0.0",
|
|
12
12
|
"wcmatch"
|
|
13
13
|
]
|
|
@@ -93,3 +93,8 @@ enable_error_code = ["deprecated"]
|
|
|
93
93
|
# Optional: reduce strictness if needed
|
|
94
94
|
disallow_any_generics = false
|
|
95
95
|
warn_return_any = false
|
|
96
|
+
|
|
97
|
+
[tool.uv.workspace]
|
|
98
|
+
members = [
|
|
99
|
+
"libs/deepagents-cli",
|
|
100
|
+
]
|
|
@@ -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,
|
|
@@ -123,10 +123,10 @@ def create_deep_agent(
|
|
|
123
123
|
AnthropicPromptCachingMiddleware(unsupported_model_behavior="ignore"),
|
|
124
124
|
PatchToolCallsMiddleware(),
|
|
125
125
|
]
|
|
126
|
+
if middleware:
|
|
127
|
+
deepagent_middleware.extend(middleware)
|
|
126
128
|
if interrupt_on is not None:
|
|
127
129
|
deepagent_middleware.append(HumanInTheLoopMiddleware(interrupt_on=interrupt_on))
|
|
128
|
-
if middleware is not None:
|
|
129
|
-
deepagent_middleware.extend(middleware)
|
|
130
130
|
|
|
131
131
|
return create_agent(
|
|
132
132
|
model,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Middleware for the DeepAgent."""
|
|
2
|
+
|
|
3
|
+
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
|
+
from deepagents.middleware.resumable_shell import ResumableShellToolMiddleware
|
|
5
|
+
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"CompiledSubAgent",
|
|
9
|
+
"FilesystemMiddleware",
|
|
10
|
+
"ResumableShellToolMiddleware",
|
|
11
|
+
"SubAgent",
|
|
12
|
+
"SubAgentMiddleware",
|
|
13
|
+
]
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
"""Middleware for loading agent-specific long-term memory into the system prompt."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from langgraph.runtime import Runtime
|
|
8
|
+
|
|
9
|
+
from langchain.agents.middleware.types import (
|
|
10
|
+
AgentMiddleware,
|
|
11
|
+
AgentState,
|
|
12
|
+
ModelRequest,
|
|
13
|
+
ModelResponse,
|
|
14
|
+
)
|
|
15
|
+
from typing_extensions import NotRequired, TypedDict
|
|
16
|
+
|
|
17
|
+
from deepagents.backends.protocol import BackendProtocol
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class AgentMemoryState(AgentState):
|
|
21
|
+
"""State for the agent memory middleware."""
|
|
22
|
+
|
|
23
|
+
agent_memory: NotRequired[str | None]
|
|
24
|
+
"""Long-term memory content for the agent."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
AGENT_MEMORY_FILE_PATH = "/agent.md"
|
|
28
|
+
|
|
29
|
+
# Long-term Memory Documentation
|
|
30
|
+
LONGTERM_MEMORY_SYSTEM_PROMPT = """
|
|
31
|
+
|
|
32
|
+
## Long-term Memory
|
|
33
|
+
|
|
34
|
+
You have access to a long-term memory system using the {memory_path} path prefix.
|
|
35
|
+
Files stored in {memory_path} persist across sessions and conversations.
|
|
36
|
+
|
|
37
|
+
Your system prompt is loaded from {memory_path}agent.md at startup. You can update your own instructions by editing this file.
|
|
38
|
+
|
|
39
|
+
**When to CHECK/READ memories (CRITICAL - do this FIRST):**
|
|
40
|
+
- **At the start of ANY new session**: Run `ls {memory_path}` to see what you know
|
|
41
|
+
- **BEFORE answering questions**: If asked "what do you know about X?" or "how do I do Y?", check `ls {memory_path}` for relevant files FIRST
|
|
42
|
+
- **When user asks you to do something**: Check if you have guides, examples, or patterns in {memory_path} before proceeding
|
|
43
|
+
- **When user references past work or conversations**: Search {memory_path} for related content
|
|
44
|
+
- **If you're unsure**: Check your memories rather than guessing or using only general knowledge
|
|
45
|
+
|
|
46
|
+
**Memory-first response pattern:**
|
|
47
|
+
1. User asks a question → Run `ls {memory_path}` to check for relevant files
|
|
48
|
+
2. If relevant files exist → Read them with `read_file {memory_path}[filename]`
|
|
49
|
+
3. Base your answer on saved knowledge (from memories) supplemented by general knowledge
|
|
50
|
+
4. If no relevant memories exist → Use general knowledge, then consider if this is worth saving
|
|
51
|
+
|
|
52
|
+
**When to update memories:**
|
|
53
|
+
- **IMMEDIATELY when the user describes your role or how you should behave** (e.g., "you are a web researcher", "you are an expert in X")
|
|
54
|
+
- **IMMEDIATELY when the user gives feedback on your work** - Before continuing, update memories to capture what was wrong and how to do it better
|
|
55
|
+
- When the user explicitly asks you to remember something
|
|
56
|
+
- When patterns or preferences emerge (coding styles, conventions, workflows)
|
|
57
|
+
- After significant work where context would help in future sessions
|
|
58
|
+
|
|
59
|
+
**Learning from feedback:**
|
|
60
|
+
- When user says something is better/worse, capture WHY and encode it as a pattern
|
|
61
|
+
- Each correction is a chance to improve permanently - don't just fix the immediate issue, update your instructions
|
|
62
|
+
- When user says "you should remember X" or "be careful about Y", treat this as HIGH PRIORITY - update memories IMMEDIATELY
|
|
63
|
+
- Look for the underlying principle behind corrections, not just the specific mistake
|
|
64
|
+
- If it's something you "should have remembered", identify where that instruction should live permanently
|
|
65
|
+
|
|
66
|
+
**What to store where:**
|
|
67
|
+
- **{memory_path}agent.md**: Update this to modify your core instructions and behavioral patterns
|
|
68
|
+
- **Other {memory_path} files**: Use for project-specific context, reference information, or structured notes
|
|
69
|
+
- If you create additional memory files, add references to them in {memory_path}agent.md so you remember to consult them
|
|
70
|
+
|
|
71
|
+
The portion of your system prompt that comes from {memory_path}agent.md is marked with `<agent_memory>` tags so you can identify what instructions come from your persistent memory.
|
|
72
|
+
|
|
73
|
+
Example: `ls {memory_path}` to see what memories you have
|
|
74
|
+
Example: `read_file '{memory_path}deep-agents-guide.md'` to recall saved knowledge
|
|
75
|
+
Example: `edit_file('{memory_path}agent.md', ...)` to update your instructions
|
|
76
|
+
Example: `write_file('{memory_path}project_context.md', ...)` for project-specific notes, then reference it in agent.md
|
|
77
|
+
|
|
78
|
+
Remember: To interact with the longterm filesystem, you must prefix the filename with the {memory_path} path."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
DEFAULT_MEMORY_SNIPPET = """<agent_memory>
|
|
82
|
+
{agent_memory}
|
|
83
|
+
</agent_memory>
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
class AgentMemoryMiddleware(AgentMiddleware):
|
|
87
|
+
"""Middleware for loading agent-specific long-term memory.
|
|
88
|
+
|
|
89
|
+
This middleware loads the agent's long-term memory from a file (agent.md)
|
|
90
|
+
and injects it into the system prompt. The memory is loaded once at the
|
|
91
|
+
start of the conversation and stored in state.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
backend: Backend to use for loading the agent memory file.
|
|
95
|
+
system_prompt_template: Optional custom template for how to inject
|
|
96
|
+
the agent memory into the system prompt. Use {agent_memory} as
|
|
97
|
+
a placeholder. Defaults to a simple section header.
|
|
98
|
+
|
|
99
|
+
Example:
|
|
100
|
+
```python
|
|
101
|
+
from deepagents.middleware.agent_memory import AgentMemoryMiddleware
|
|
102
|
+
from deepagents.memory.backends import FilesystemBackend
|
|
103
|
+
from pathlib import Path
|
|
104
|
+
|
|
105
|
+
# Set up backend pointing to agent's directory
|
|
106
|
+
agent_dir = Path.home() / ".deepagents" / "my-agent"
|
|
107
|
+
backend = FilesystemBackend(root_dir=agent_dir)
|
|
108
|
+
|
|
109
|
+
# Create middleware
|
|
110
|
+
middleware = AgentMemoryMiddleware(backend=backend)
|
|
111
|
+
```
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
state_schema = AgentMemoryState
|
|
115
|
+
|
|
116
|
+
def __init__(
|
|
117
|
+
self,
|
|
118
|
+
*,
|
|
119
|
+
backend: BackendProtocol,
|
|
120
|
+
memory_path: str,
|
|
121
|
+
system_prompt_template: str | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Initialize the agent memory middleware.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
backend: Backend to use for loading the agent memory file.
|
|
127
|
+
system_prompt_template: Optional custom template for injecting
|
|
128
|
+
agent memory into system prompt.
|
|
129
|
+
"""
|
|
130
|
+
self.backend = backend
|
|
131
|
+
self.memory_path = memory_path
|
|
132
|
+
self.system_prompt_template = system_prompt_template or DEFAULT_MEMORY_SNIPPET
|
|
133
|
+
|
|
134
|
+
def before_agent(
|
|
135
|
+
self,
|
|
136
|
+
state: AgentMemoryState,
|
|
137
|
+
runtime,
|
|
138
|
+
) -> AgentMemoryState:
|
|
139
|
+
"""Load agent memory from file before agent execution.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
state: Current agent state.
|
|
143
|
+
handler: Handler function to call after loading memory.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Updated state with agent_memory populated.
|
|
147
|
+
"""
|
|
148
|
+
# Only load memory if it hasn't been loaded yet
|
|
149
|
+
if "agent_memory" not in state or state.get("agent_memory") is None:
|
|
150
|
+
file_data = self.backend.read(AGENT_MEMORY_FILE_PATH)
|
|
151
|
+
return {"agent_memory": file_data}
|
|
152
|
+
|
|
153
|
+
async def abefore_agent(
|
|
154
|
+
self,
|
|
155
|
+
state: AgentMemoryState,
|
|
156
|
+
runtime,
|
|
157
|
+
) -> AgentMemoryState:
|
|
158
|
+
"""(async) Load agent memory from file before agent execution.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
state: Current agent state.
|
|
162
|
+
handler: Handler function to call after loading memory.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
Updated state with agent_memory populated.
|
|
166
|
+
"""
|
|
167
|
+
# Only load memory if it hasn't been loaded yet
|
|
168
|
+
if "agent_memory" not in state or state.get("agent_memory") is None:
|
|
169
|
+
file_data = self.backend.read(AGENT_MEMORY_FILE_PATH)
|
|
170
|
+
return {"agent_memory": file_data}
|
|
171
|
+
|
|
172
|
+
def wrap_model_call(
|
|
173
|
+
self,
|
|
174
|
+
request: ModelRequest,
|
|
175
|
+
handler: Callable[[ModelRequest], ModelResponse],
|
|
176
|
+
) -> ModelResponse:
|
|
177
|
+
"""Inject agent memory into the system prompt.
|
|
178
|
+
|
|
179
|
+
Args:
|
|
180
|
+
request: The model request being processed.
|
|
181
|
+
handler: The handler function to call with the modified request.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The model response from the handler.
|
|
185
|
+
"""
|
|
186
|
+
# Get agent memory from state
|
|
187
|
+
agent_memory = request.state.get("agent_memory", "")
|
|
188
|
+
|
|
189
|
+
memory_section = self.system_prompt_template.format(agent_memory=agent_memory)
|
|
190
|
+
if request.system_prompt:
|
|
191
|
+
request.system_prompt = memory_section + "\n\n" + request.system_prompt
|
|
192
|
+
else:
|
|
193
|
+
request.system_prompt = memory_section
|
|
194
|
+
request.system_prompt = request.system_prompt + "\n\n" + LONGTERM_MEMORY_SYSTEM_PROMPT.format(memory_path=self.memory_path)
|
|
195
|
+
|
|
196
|
+
return handler(request)
|
|
197
|
+
|
|
198
|
+
async def awrap_model_call(
|
|
199
|
+
self,
|
|
200
|
+
request: ModelRequest,
|
|
201
|
+
handler: Callable[[ModelRequest], Awaitable[ModelResponse]],
|
|
202
|
+
) -> ModelResponse:
|
|
203
|
+
"""(async) Inject agent memory into the system prompt.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
request: The model request being processed.
|
|
207
|
+
handler: The handler function to call with the modified request.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
The model response from the handler.
|
|
211
|
+
"""
|
|
212
|
+
# Get agent memory from state
|
|
213
|
+
agent_memory = request.state.get("agent_memory", "")
|
|
214
|
+
|
|
215
|
+
memory_section = self.system_prompt_template.format(agent_memory=agent_memory)
|
|
216
|
+
if request.system_prompt:
|
|
217
|
+
request.system_prompt = memory_section + "\n\n" + request.system_prompt
|
|
218
|
+
else:
|
|
219
|
+
request.system_prompt = memory_section
|
|
220
|
+
request.system_prompt = request.system_prompt + "\n\n" + LONGTERM_MEMORY_SYSTEM_PROMPT.format(memory_path=self.memory_path)
|
|
221
|
+
|
|
222
|
+
return await handler(request)
|
|
@@ -24,18 +24,18 @@ 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"
|
|
35
35
|
MAX_LINE_LENGTH = 2000
|
|
36
36
|
LINE_NUMBER_WIDTH = 6
|
|
37
37
|
DEFAULT_READ_OFFSET = 0
|
|
38
|
-
DEFAULT_READ_LIMIT =
|
|
38
|
+
DEFAULT_READ_LIMIT = 500
|
|
39
39
|
BACKEND_TYPES = (
|
|
40
40
|
BackendProtocol
|
|
41
41
|
| BackendFactory
|
|
@@ -155,8 +155,12 @@ Assume this tool is able to read all files on the machine. If the User provides
|
|
|
155
155
|
|
|
156
156
|
Usage:
|
|
157
157
|
- The file_path parameter must be an absolute path, not a relative path
|
|
158
|
-
- By default, it reads up to
|
|
159
|
-
-
|
|
158
|
+
- By default, it reads up to 500 lines starting from the beginning of the file
|
|
159
|
+
- **IMPORTANT for large files and codebase exploration**: Use pagination with offset and limit parameters to avoid context overflow
|
|
160
|
+
- First scan: read_file(path, limit=100) to see file structure
|
|
161
|
+
- Read more sections: read_file(path, offset=100, limit=200) for next 200 lines
|
|
162
|
+
- Only omit limit (read full file) when necessary for editing
|
|
163
|
+
- Specify offset and limit: read_file(path, offset=0, limit=100) reads first 100 lines
|
|
160
164
|
- Any lines longer than 2000 characters will be truncated
|
|
161
165
|
- Results are returned using cat -n format, with line numbers starting at 1
|
|
162
166
|
- You have the capability to call multiple tools in a single response. It is always better to speculatively read multiple files as a batch that are potentially useful.
|
|
@@ -227,6 +231,15 @@ All file paths must start with a /.
|
|
|
227
231
|
|
|
228
232
|
|
|
229
233
|
def _get_backend(backend: BACKEND_TYPES, runtime: ToolRuntime) -> BackendProtocol:
|
|
234
|
+
"""Get the resolved backend instance from backend or factory.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
backend: Backend instance or factory function.
|
|
238
|
+
runtime: The tool runtime context.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Resolved backend instance.
|
|
242
|
+
"""
|
|
230
243
|
if callable(backend):
|
|
231
244
|
return backend(runtime)
|
|
232
245
|
return backend
|
|
@@ -532,6 +545,19 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
532
545
|
|
|
533
546
|
self.tools = _get_filesystem_tools(self.backend, custom_tool_descriptions)
|
|
534
547
|
|
|
548
|
+
def _get_backend(self, runtime: ToolRuntime) -> BackendProtocol:
|
|
549
|
+
"""Get the resolved backend instance from backend or factory.
|
|
550
|
+
|
|
551
|
+
Args:
|
|
552
|
+
runtime: The tool runtime context.
|
|
553
|
+
|
|
554
|
+
Returns:
|
|
555
|
+
Resolved backend instance.
|
|
556
|
+
"""
|
|
557
|
+
if callable(self.backend):
|
|
558
|
+
return self.backend(runtime)
|
|
559
|
+
return self.backend
|
|
560
|
+
|
|
535
561
|
def wrap_model_call(
|
|
536
562
|
self,
|
|
537
563
|
request: ModelRequest,
|
|
@@ -568,54 +594,70 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
568
594
|
request.system_prompt = request.system_prompt + "\n\n" + self.system_prompt if request.system_prompt else self.system_prompt
|
|
569
595
|
return await handler(request)
|
|
570
596
|
|
|
571
|
-
def
|
|
597
|
+
def _process_large_message(
|
|
598
|
+
self,
|
|
599
|
+
message: ToolMessage,
|
|
600
|
+
resolved_backend: BackendProtocol,
|
|
601
|
+
) -> tuple[ToolMessage, dict[str, FileData] | None]:
|
|
602
|
+
content = message.content
|
|
603
|
+
if not isinstance(content, str) or len(content) <= 4 * self.tool_token_limit_before_evict:
|
|
604
|
+
return message, None
|
|
605
|
+
|
|
606
|
+
sanitized_id = sanitize_tool_call_id(message.tool_call_id)
|
|
607
|
+
file_path = f"/large_tool_results/{sanitized_id}"
|
|
608
|
+
result = resolved_backend.write(file_path, content)
|
|
609
|
+
if result.error:
|
|
610
|
+
return message, None
|
|
611
|
+
content_sample = format_content_with_line_numbers(content.splitlines()[:10], start_line=1)
|
|
612
|
+
processed_message = ToolMessage(
|
|
613
|
+
TOO_LARGE_TOOL_MSG.format(
|
|
614
|
+
tool_call_id=message.tool_call_id,
|
|
615
|
+
file_path=file_path,
|
|
616
|
+
content_sample=content_sample,
|
|
617
|
+
),
|
|
618
|
+
tool_call_id=message.tool_call_id,
|
|
619
|
+
)
|
|
620
|
+
return processed_message, result.files_update
|
|
621
|
+
|
|
622
|
+
def _intercept_large_tool_result(self, tool_result: ToolMessage | Command, runtime: ToolRuntime) -> ToolMessage | Command:
|
|
572
623
|
if isinstance(tool_result, ToolMessage) and isinstance(tool_result.content, str):
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
)
|
|
587
|
-
],
|
|
588
|
-
"files": {file_path: file_data},
|
|
589
|
-
}
|
|
590
|
-
return Command(update=state_update)
|
|
624
|
+
if not (self.tool_token_limit_before_evict and
|
|
625
|
+
len(tool_result.content) > 4 * self.tool_token_limit_before_evict):
|
|
626
|
+
return tool_result
|
|
627
|
+
resolved_backend = self._get_backend(runtime)
|
|
628
|
+
processed_message, files_update = self._process_large_message(
|
|
629
|
+
tool_result,
|
|
630
|
+
resolved_backend,
|
|
631
|
+
)
|
|
632
|
+
return (Command(update={
|
|
633
|
+
"files": files_update,
|
|
634
|
+
"messages": [processed_message],
|
|
635
|
+
}) if files_update is not None else processed_message)
|
|
636
|
+
|
|
591
637
|
elif isinstance(tool_result, Command):
|
|
592
638
|
update = tool_result.update
|
|
593
639
|
if update is None:
|
|
594
640
|
return tool_result
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
for message in
|
|
600
|
-
if self.tool_token_limit_before_evict and
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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})
|
|
641
|
+
command_messages = update.get("messages", [])
|
|
642
|
+
accumulated_file_updates = dict(update.get("files", {}))
|
|
643
|
+
resolved_backend = self._get_backend(runtime)
|
|
644
|
+
processed_messages = []
|
|
645
|
+
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):
|
|
650
|
+
processed_messages.append(message)
|
|
651
|
+
continue
|
|
652
|
+
processed_message, files_update = self._process_large_message(
|
|
653
|
+
message,
|
|
654
|
+
resolved_backend,
|
|
655
|
+
)
|
|
656
|
+
processed_messages.append(processed_message)
|
|
657
|
+
if files_update is not None:
|
|
658
|
+
accumulated_file_updates.update(files_update)
|
|
659
|
+
return Command(update={**update, "messages": processed_messages, "files": accumulated_file_updates})
|
|
660
|
+
|
|
619
661
|
return tool_result
|
|
620
662
|
|
|
621
663
|
def wrap_tool_call(
|
|
@@ -636,7 +678,7 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
636
678
|
return handler(request)
|
|
637
679
|
|
|
638
680
|
tool_result = handler(request)
|
|
639
|
-
return self._intercept_large_tool_result(tool_result)
|
|
681
|
+
return self._intercept_large_tool_result(tool_result, request.runtime)
|
|
640
682
|
|
|
641
683
|
async def awrap_tool_call(
|
|
642
684
|
self,
|
|
@@ -656,4 +698,4 @@ class FilesystemMiddleware(AgentMiddleware):
|
|
|
656
698
|
return await handler(request)
|
|
657
699
|
|
|
658
700
|
tool_result = await handler(request)
|
|
659
|
-
return self._intercept_large_tool_result(tool_result)
|
|
701
|
+
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
|
|
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":
|
|
44
|
+
return {"messages": Overwrite(patched_messages)}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Shell tool middleware that survives HITL pauses."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Awaitable, Callable, cast
|
|
6
|
+
|
|
7
|
+
from langchain.agents.middleware.shell_tool import (
|
|
8
|
+
ShellToolMiddleware,
|
|
9
|
+
_PersistentShellTool,
|
|
10
|
+
_SessionResources,
|
|
11
|
+
ShellToolState,
|
|
12
|
+
)
|
|
13
|
+
from langchain.agents.middleware.types import AgentState
|
|
14
|
+
from langchain_core.messages import ToolMessage
|
|
15
|
+
from langchain.tools.tool_node import ToolCallRequest
|
|
16
|
+
from langgraph.types import Command
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class ResumableShellToolMiddleware(ShellToolMiddleware):
|
|
20
|
+
"""Shell middleware that recreates session resources after human interrupts.
|
|
21
|
+
|
|
22
|
+
``ShellToolMiddleware`` stores its session handle in middleware state using an
|
|
23
|
+
``UntrackedValue``. When a run pauses for human approval, that attribute is not
|
|
24
|
+
checkpointed. Upon resuming, LangGraph restores the state without the shell
|
|
25
|
+
resources, so the next tool execution fails with
|
|
26
|
+
``Shell session resources are unavailable``.
|
|
27
|
+
|
|
28
|
+
This subclass lazily recreates the shell session the first time a resumed run
|
|
29
|
+
touches the shell tool again and only performs shutdown when a session is
|
|
30
|
+
actually active. This keeps behaviour identical for uninterrupted runs while
|
|
31
|
+
allowing HITL pauses to succeed.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def wrap_tool_call(
|
|
35
|
+
self,
|
|
36
|
+
request: ToolCallRequest,
|
|
37
|
+
handler: Callable[[ToolCallRequest], ToolMessage | Command],
|
|
38
|
+
) -> ToolMessage | Command:
|
|
39
|
+
if isinstance(request.tool, _PersistentShellTool):
|
|
40
|
+
resources = self._get_or_create_resources(request.state)
|
|
41
|
+
return self._run_shell_tool(
|
|
42
|
+
resources,
|
|
43
|
+
request.tool_call["args"],
|
|
44
|
+
tool_call_id=request.tool_call.get("id"),
|
|
45
|
+
)
|
|
46
|
+
return super().wrap_tool_call(request, handler)
|
|
47
|
+
|
|
48
|
+
async def awrap_tool_call(
|
|
49
|
+
self,
|
|
50
|
+
request: ToolCallRequest,
|
|
51
|
+
handler: Callable[[ToolCallRequest], Awaitable[ToolMessage | Command]],
|
|
52
|
+
) -> ToolMessage | Command:
|
|
53
|
+
if isinstance(request.tool, _PersistentShellTool):
|
|
54
|
+
resources = self._get_or_create_resources(request.state)
|
|
55
|
+
return self._run_shell_tool(
|
|
56
|
+
resources,
|
|
57
|
+
request.tool_call["args"],
|
|
58
|
+
tool_call_id=request.tool_call.get("id"),
|
|
59
|
+
)
|
|
60
|
+
return await super().awrap_tool_call(request, handler)
|
|
61
|
+
|
|
62
|
+
def after_agent(self, state: ShellToolState, runtime) -> None: # type: ignore[override]
|
|
63
|
+
if self._has_resources(state):
|
|
64
|
+
super().after_agent(state, runtime)
|
|
65
|
+
|
|
66
|
+
async def aafter_agent(self, state: ShellToolState, runtime) -> None: # type: ignore[override]
|
|
67
|
+
if self._has_resources(state):
|
|
68
|
+
await super().aafter_agent(state, runtime)
|
|
69
|
+
|
|
70
|
+
@staticmethod
|
|
71
|
+
def _has_resources(state: AgentState) -> bool:
|
|
72
|
+
resources = state.get("shell_session_resources")
|
|
73
|
+
return isinstance(resources, _SessionResources)
|
|
74
|
+
|
|
75
|
+
def _get_or_create_resources(self, state: AgentState) -> _SessionResources:
|
|
76
|
+
resources = state.get("shell_session_resources")
|
|
77
|
+
if isinstance(resources, _SessionResources):
|
|
78
|
+
return resources
|
|
79
|
+
|
|
80
|
+
new_resources = self._create_resources()
|
|
81
|
+
cast(dict[str, Any], state)["shell_session_resources"] = new_resources
|
|
82
|
+
return new_resources
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
__all__ = ["ResumableShellToolMiddleware"]
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: deepagents
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
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.
|
|
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
|
|
@@ -16,7 +16,9 @@ src/deepagents/backends/state.py
|
|
|
16
16
|
src/deepagents/backends/store.py
|
|
17
17
|
src/deepagents/backends/utils.py
|
|
18
18
|
src/deepagents/middleware/__init__.py
|
|
19
|
+
src/deepagents/middleware/agent_memory.py
|
|
19
20
|
src/deepagents/middleware/filesystem.py
|
|
20
21
|
src/deepagents/middleware/patch_tool_calls.py
|
|
22
|
+
src/deepagents/middleware/resumable_shell.py
|
|
21
23
|
src/deepagents/middleware/subagents.py
|
|
22
24
|
tests/test_middleware.py
|
|
@@ -8,7 +8,7 @@ from langchain_core.messages import (
|
|
|
8
8
|
ToolCall,
|
|
9
9
|
ToolMessage,
|
|
10
10
|
)
|
|
11
|
-
from langgraph.
|
|
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
|
|
871
|
-
|
|
872
|
-
assert
|
|
873
|
-
assert
|
|
874
|
-
assert
|
|
875
|
-
assert
|
|
876
|
-
assert
|
|
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
|
|
893
|
-
|
|
894
|
-
assert
|
|
895
|
-
assert
|
|
896
|
-
assert
|
|
897
|
-
assert
|
|
898
|
-
assert
|
|
899
|
-
assert
|
|
900
|
-
assert
|
|
901
|
-
|
|
902
|
-
assert
|
|
903
|
-
assert
|
|
904
|
-
assert
|
|
905
|
-
assert
|
|
906
|
-
assert
|
|
907
|
-
assert
|
|
908
|
-
assert
|
|
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
|
|
927
|
-
|
|
928
|
-
assert
|
|
929
|
-
|
|
930
|
-
assert
|
|
931
|
-
assert
|
|
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
|
|
954
|
-
|
|
955
|
-
assert
|
|
956
|
-
assert
|
|
957
|
-
assert
|
|
958
|
-
assert
|
|
959
|
-
assert
|
|
960
|
-
assert
|
|
961
|
-
assert
|
|
962
|
-
assert
|
|
963
|
-
assert
|
|
964
|
-
assert
|
|
965
|
-
assert
|
|
966
|
-
assert
|
|
967
|
-
|
|
968
|
-
assert
|
|
969
|
-
assert
|
|
970
|
-
assert
|
|
971
|
-
assert
|
|
972
|
-
assert
|
|
973
|
-
assert
|
|
974
|
-
assert
|
|
975
|
-
assert
|
|
976
|
-
assert
|
|
977
|
-
assert
|
|
978
|
-
assert
|
|
979
|
-
assert
|
|
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:
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
"""Middleware for the DeepAgent."""
|
|
2
|
-
|
|
3
|
-
from deepagents.middleware.filesystem import FilesystemMiddleware
|
|
4
|
-
from deepagents.middleware.subagents import CompiledSubAgent, SubAgent, SubAgentMiddleware
|
|
5
|
-
|
|
6
|
-
__all__ = ["CompiledSubAgent", "FilesystemMiddleware", "SubAgent", "SubAgentMiddleware"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|