Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ in the same way you would any LangGraph agent.

**Context Management**

File system tools (`ls`, `read_file`, `write_file`, `edit_file`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.
File system tools (`ls`, `read_file`, `write_file`, `edit_file`, `delete_file`) allow agents to offload large context to memory, preventing context window overflow and enabling work with variable-length tool results.

**Subagent Spawning**

Expand Down Expand Up @@ -375,11 +375,12 @@ agent = create_agent(
### FilesystemMiddleware

Context engineering is one of the main challenges in building effective agents. This can be particularly hard when using tools that can return variable length results (ex. web_search, rag), as long ToolResults can quickly fill up your context window.
**FilesystemMiddleware** provides four tools to your agent to interact with both short-term and long-term memory.
**FilesystemMiddleware** provides five tools to your agent to interact with both short-term and long-term memory.
- **ls**: List the files in your filesystem
- **read_file**: Read an entire file, or a certain number of lines from a file
- **write_file**: Write a new file to your filesystem
- **edit_file**: Edit an existing file in your filesystem
- **delete_file**: Delete a file from your filesystem

```python
from langchain.agents import create_agent
Expand Down Expand Up @@ -509,4 +510,4 @@ async def main():
chunk["messages"][-1].pretty_print()

asyncio.run(main())
```
```
97 changes: 92 additions & 5 deletions src/deepagents/middleware/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -411,15 +411,27 @@ class FilesystemState(AgentState):
f"\n- file_paths prefixed with the {MEMORIES_PREFIX} path will be written to the longterm filesystem."
)

FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`
DELETE_FILE_TOOL_DESCRIPTION = """Deletes a file from the filesystem.

Usage:
- The file_path parameter must be an absolute path, not a relative path
- The delete_file tool will permanently remove the file.
- Use this tool when you need to clean up temporary files or remove files that are no longer needed.
- The file must exist in the filesystem to be deleted."""
DELETE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT = (
f"\n- file_paths prefixed with the {MEMORIES_PREFIX} path will be deleted from the longterm filesystem."
)

FILESYSTEM_SYSTEM_PROMPT = """## Filesystem Tools `ls`, `read_file`, `write_file`, `edit_file`, `delete_file`

You have access to a filesystem which you can interact with using these tools.
All file paths must start with a /.

- ls: list all files in the filesystem
- read_file: read a file from the filesystem
- write_file: write to a file in the filesystem
- edit_file: edit a file in the filesystem"""
- edit_file: edit a file in the filesystem
- delete_file: delete a file from the filesystem"""
FILESYSTEM_SYSTEM_PROMPT_LONGTERM_SUPPLEMENT = f"""

You also have access to a longterm filesystem in which you can store files that you want to keep around for longer than the current conversation.
Expand Down Expand Up @@ -888,11 +900,86 @@ def edit_file(
return edit_file


def _delete_file_tool_generator(custom_description: str | None = None, *, long_term_memory: bool) -> BaseTool:
"""Generate the delete_file tool.

Args:
custom_description: Optional custom description for the tool.
long_term_memory: Whether to enable longterm memory support.

Returns:
Configured delete_file tool that deletes files from state or longterm store.
"""
tool_description = DELETE_FILE_TOOL_DESCRIPTION
if custom_description:
tool_description = custom_description
elif long_term_memory:
tool_description += DELETE_FILE_TOOL_DESCRIPTION_LONGTERM_SUPPLEMENT

def _delete_file_from_state(state: FilesystemState, tool_call_id: str, file_path: str) -> Command | str:
"""Delete a file from the filesystem state.

Args:
state: The current filesystem state.
tool_call_id: ID of the tool call for generating ToolMessage.
file_path: The path of the file to delete.

Returns:
Command to update state with file deletion, or error string if file doesn't exist.
"""
mock_filesystem = state.get("files", {})
if file_path not in mock_filesystem:
return f"Error: File '{file_path}' not found"
return Command(
update={
"files": {file_path: None}, # None triggers deletion via reducer
"messages": [ToolMessage(f"Deleted file {file_path}", tool_call_id=tool_call_id)],
}
)

if long_term_memory:

@tool(description=tool_description)
def delete_file(
file_path: str,
runtime: ToolRuntime[None, FilesystemState],
) -> Command | str:
file_path = _validate_path(file_path)
if not runtime.tool_call_id:
value_error_msg = "Tool call ID is required for delete_file invocation"
raise ValueError(value_error_msg)
if _has_memories_prefix(file_path):
stripped_file_path = _strip_memories_prefix(file_path)
store = _get_store(runtime)
namespace = _get_namespace()
if store.get(namespace, stripped_file_path) is None:
return f"Error: File '{file_path}' not found"
store.delete(namespace, stripped_file_path)
return f"Deleted longterm memories file {file_path}"
return _delete_file_from_state(runtime.state, runtime.tool_call_id, file_path)

else:

@tool(description=tool_description)
def delete_file(
file_path: str,
runtime: ToolRuntime[None, FilesystemState],
) -> Command | str:
file_path = _validate_path(file_path)
if not runtime.tool_call_id:
value_error_msg = "Tool call ID is required for delete_file invocation"
raise ValueError(value_error_msg)
return _delete_file_from_state(runtime.state, runtime.tool_call_id, file_path)

return delete_file


TOOL_GENERATORS = {
"ls": _ls_tool_generator,
"read_file": _read_file_tool_generator,
"write_file": _write_file_tool_generator,
"edit_file": _edit_file_tool_generator,
"delete_file": _delete_file_tool_generator,
}


Expand All @@ -904,7 +991,7 @@ def _get_filesystem_tools(custom_tool_descriptions: dict[str, str] | None = None
long_term_memory: Whether to enable longterm memory support.

Returns:
List of configured filesystem tools (ls, read_file, write_file, edit_file).
List of configured filesystem tools (ls, read_file, write_file, edit_file, delete_file).
"""
if custom_tool_descriptions is None:
custom_tool_descriptions = {}
Expand All @@ -928,8 +1015,8 @@ def _get_filesystem_tools(custom_tool_descriptions: dict[str, str] | None = None
class FilesystemMiddleware(AgentMiddleware):
"""Middleware for providing filesystem tools to an agent.

This middleware adds four filesystem tools to the agent: ls, read_file, write_file,
and edit_file. Files can be stored in two locations:
This middleware adds five filesystem tools to the agent: ls, read_file, write_file,
edit_file, and delete_file. Files can be stored in two locations:
- Short-term: In the agent's state (ephemeral, lasts only for the conversation)
- Long-term: In a persistent store (persists across conversations when enabled)

Expand Down
142 changes: 142 additions & 0 deletions tests/integration_tests/test_filesystem_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,126 @@ def test_command_with_tool_call_existing_state(self):
assert "/test.txt" in response["files"].keys()
assert "research" in response

def test_delete_file_shortterm(self):
checkpointer = MemorySaver()
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
middleware=[
FilesystemMiddleware(
long_term_memory=False,
)
],
checkpointer=checkpointer,
)
config = {"configurable": {"thread_id": uuid.uuid4()}}
# First create a file
response = agent.invoke(
{
"messages": [HumanMessage(content="Write a haiku about Pikachu to /pikachu.txt")],
"files": {},
},
config=config,
)
assert "/pikachu.txt" in response["files"]

# Now delete the file
response = agent.invoke(
{"messages": [HumanMessage(content="Delete the file /pikachu.txt")]},
config=config,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
assert "Deleted file /pikachu.txt" in delete_file_message.content
assert "/pikachu.txt" not in response["files"]

def test_delete_file_longterm(self):
checkpointer = MemorySaver()
store = InMemoryStore()
store.put(
("filesystem",),
"/pikachu.txt",
{
"content": ["Thunder shock!"],
"created_at": "2021-01-01",
"modified_at": "2021-01-01",
},
)
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
middleware=[
FilesystemMiddleware(
long_term_memory=True,
)
],
checkpointer=checkpointer,
store=store,
)
config = {"configurable": {"thread_id": uuid.uuid4()}}
response = agent.invoke(
{
"messages": [HumanMessage(content="Delete the file /pikachu.txt from longterm memory")],
"files": {},
},
config=config,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
assert "Deleted longterm memories file /memories/pikachu.txt" in delete_file_message.content
# Verify the file was actually deleted from store
assert store.get(("filesystem",), "/pikachu.txt") is None

def test_delete_file_not_found_shortterm(self):
checkpointer = MemorySaver()
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
middleware=[
FilesystemMiddleware(
long_term_memory=False,
)
],
checkpointer=checkpointer,
)
config = {"configurable": {"thread_id": uuid.uuid4()}}
response = agent.invoke(
{
"messages": [HumanMessage(content="Delete the file /nonexistent.txt")],
"files": {},
},
config=config,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
assert "Error: File '/nonexistent.txt' not found" in delete_file_message.content

def test_delete_file_not_found_longterm(self):
checkpointer = MemorySaver()
store = InMemoryStore()
agent = create_agent(
model=ChatAnthropic(model="claude-sonnet-4-20250514"),
middleware=[
FilesystemMiddleware(
long_term_memory=True,
)
],
checkpointer=checkpointer,
store=store,
)
config = {"configurable": {"thread_id": uuid.uuid4()}}
response = agent.invoke(
{
"messages": [HumanMessage(content="Delete the file /nonexistent.txt from longterm memory")],
"files": {},
},
config=config,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
assert "Error: File '/memories/nonexistent.txt' not found" in delete_file_message.content


# Take actions on multiple threads to test longterm memory
def assert_longterm_mem_tools(agent, store):
Expand Down Expand Up @@ -640,6 +760,18 @@ def assert_longterm_mem_tools(agent, store):
read_file_message = next(message for message in messages if message.type == "tool" and message.name == "read_file")
assert "ember" in read_file_message.content or "Ember" in read_file_message.content

# Delete the longterm memory file
config6 = {"configurable": {"thread_id": uuid.uuid4()}}
response = agent.invoke(
{"messages": [HumanMessage(content="Delete the haiku about Charmander from longterm memory at /charmander.txt")]},
config=config6,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
file_item = store.get(("filesystem",), "/charmander.txt")
assert file_item is None


def assert_shortterm_mem_tools(agent):
# Write a shortterm memory file
Expand Down Expand Up @@ -686,3 +818,13 @@ def assert_shortterm_mem_tools(agent):
messages = response["messages"]
read_file_message = next(message for message in reversed(messages) if message.type == "tool" and message.name == "read_file")
assert "ember" in read_file_message.content or "Ember" in read_file_message.content

# Delete the shortterm memory file
response = agent.invoke(
{"messages": [HumanMessage(content="Delete the haiku about Charmander at /charmander.txt")]},
config=config,
)
messages = response["messages"]
delete_file_message = next(message for message in messages if message.type == "tool" and message.name == "delete_file")
assert delete_file_message is not None
assert "/charmander.txt" not in response["files"]