Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
43 changes: 41 additions & 2 deletions libs/deepagents/deepagents/middleware/filesystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -1146,7 +1146,28 @@ def _build_fs_tools_section(visible: set[str]) -> tuple[str, str]:
return header, descriptions


_FILESYSTEM_SYSTEM_PROMPT_TEMPLATE = """## Following Conventions
def _large_tool_results_search_guidance(visible: set[str], prefix: str) -> str:
"""Build search guidance for offloaded tool results using visible tools.

Args:
visible: Names of the tools visible to the model.
prefix: Filesystem prefix containing offloaded tool results.

Returns:
A comma-prefixed search clause, or an empty string when no search tool is visible.
"""
if "grep" in visible:
return f", or use `grep` within `{prefix}/` if you need to search across offloaded tool results and do not know the exact file path"
if "execute" in visible:
return (
f", or use `execute` with `grep -r <pattern> {prefix}/` if you need to search "
"across offloaded tool results and do not know the exact file path"
Comment thread
open-swe[bot] marked this conversation as resolved.
Resolved
)
return ""


_FILESYSTEM_SYSTEM_PROMPT_TEMPLATE = (
"""## Following Conventions

- Read files before editing — understand existing content before making changes
- Mimic existing style, naming conventions, and patterns
Expand All @@ -1160,11 +1181,19 @@ def _build_fs_tools_section(visible: set[str]) -> tuple[str, str]:

## Large Tool Results

When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. In those cases, use `read_file` to inspect the saved result in chunks, or use `grep` within `{large_tool_results_prefix}/` if you need to search across offloaded tool results and do not know the exact file path. Offloaded tool results are stored under `{large_tool_results_prefix}/<tool_call_id>`."""
"""
"When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. "
"In those cases, use `read_file` to inspect the saved result in chunks{large_tool_search_guidance}. "
"Offloaded tool results are stored under `{large_tool_results_prefix}/<tool_call_id>`."
)

_default_tool_header, _default_tool_descriptions = _build_fs_tools_section(set(_FS_TOOL_ORDER))
FILESYSTEM_SYSTEM_PROMPT = _FILESYSTEM_SYSTEM_PROMPT_TEMPLATE.format(
large_tool_results_prefix="/large_tool_results",
large_tool_search_guidance=_large_tool_results_search_guidance(
set(_ALL_FS_TOOL_NAMES),
"/large_tool_results",
),
tool_header=_default_tool_header,
tool_descriptions=_default_tool_descriptions,
)
Expand Down Expand Up @@ -1579,10 +1608,16 @@ def _build_dynamic_system_prompt(self, *, include_execution: bool) -> str:
if cached is not None:
return cached
visible = set(self._enabled_tools) if self._enabled_tools is not None else set(_FS_TOOL_ORDER)
if not include_execution:
visible.discard("execute")
tool_header, tool_descriptions = _build_fs_tools_section(visible)
prompt_parts = [
_FILESYSTEM_SYSTEM_PROMPT_TEMPLATE.format(
large_tool_results_prefix=self._large_tool_results_prefix,
large_tool_search_guidance=_large_tool_results_search_guidance(
visible,
self._large_tool_results_prefix,
),
tool_header=tool_header,
tool_descriptions=tool_descriptions,
)
Expand Down Expand Up @@ -2910,6 +2945,10 @@ def _filter_unsupported_tools_and_apply_prompt(self, request: ModelRequest[Conte
prompt_parts = [
_FILESYSTEM_SYSTEM_PROMPT_TEMPLATE.format(
large_tool_results_prefix=self._large_tool_results_prefix,
large_tool_search_guidance=_large_tool_results_search_guidance(
visible_fs,
self._large_tool_results_prefix,
),
tool_header=tool_header,
tool_descriptions=tool_descriptions,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from langchain_core.messages import AIMessage, HumanMessage
from langgraph.store.memory import InMemoryStore

from deepagents.backends import CompositeBackend, StateBackend, StoreBackend
from deepagents.backends import CompositeBackend, LocalShellBackend, StateBackend, StoreBackend
from deepagents.middleware.filesystem import (
EXECUTION_SYSTEM_PROMPT,
WRITE_FILE_TOOL_DESCRIPTION,
Expand Down Expand Up @@ -67,6 +67,68 @@ async def handler(request: ModelRequest) -> ModelResponse:
assert captured[0].system_prompt == expected


class TestLargeToolResultsPrompt:
"""Search guidance reflects the filesystem tools visible to the model."""

def test_read_file_only_omits_search_guidance(self) -> None:
middleware = FilesystemMiddleware(backend=StateBackend(), tools=["read_file"])

prompt = middleware._build_dynamic_system_prompt(include_execution=False)

assert (
"In those cases, use `read_file` to inspect the saved result in chunks. "
"Offloaded tool results are stored under `/large_tool_results/<tool_call_id>`."
) in prompt
assert "`grep`" not in prompt
assert "`execute`" not in prompt

def test_execute_uses_shell_grep_guidance(self) -> None:
middleware = FilesystemMiddleware(
backend=LocalShellBackend(virtual_mode=True),
tools=["read_file", "execute"],
)

prompt = middleware._build_dynamic_system_prompt(include_execution=True)

assert (
"or use `execute` with `grep -r <pattern> /large_tool_results/` if you need to search "
"across offloaded tool results and do not know the exact file path"
) in prompt
assert "or use `grep` within" not in prompt

def test_backend_filtered_execute_omits_search_guidance(self) -> None:
middleware = FilesystemMiddleware(
backend=StateBackend(),
tools=["read_file", "execute"],
)

prompt = middleware._build_dynamic_system_prompt(include_execution=False)

assert "grep -r" not in prompt
assert "or use `grep` within" not in prompt

def test_grep_keeps_existing_search_guidance(self) -> None:
middleware = FilesystemMiddleware(
backend=StateBackend(),
tools=["read_file", "grep"],
)

prompt = middleware._build_dynamic_system_prompt(include_execution=False)

assert (
"or use `grep` within `/large_tool_results/` if you need to search across offloaded tool results and do not know the exact file path"
) in prompt

def test_default_tools_keep_existing_search_guidance(self) -> None:
middleware = FilesystemMiddleware(backend=StateBackend())

prompt = middleware._build_dynamic_system_prompt(include_execution=False)

assert (
"or use `grep` within `/large_tool_results/` if you need to search across offloaded tool results and do not know the exact file path"
) in prompt


class TestFilesystemMiddlewareInit:
"""Tests for FilesystemMiddleware initialization that don't require LLM invocation."""

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
You are a deep agent, an AI assistant that helps users accomplish tasks using tools. You respond with text and tool calls. The user can see your responses and tool outputs in real time.

## Core Behavior

- Be concise and direct. Don't over-explain unless asked.
- NEVER add unnecessary preamble ("Sure!", "Great question!", "I'll now...").
- Don't say "I'll now do X" — just do it.
- If the request is underspecified, ask only the minimum followup needed to take the next useful action.
- If asked how to approach something, explain first, then act.

## Professional Objectivity

- Prioritize accuracy over validating the user's beliefs
- Disagree respectfully when the user is incorrect
- Avoid unnecessary superlatives, praise, or emotional validation

## Doing Tasks

When the user asks you to do something:

1. **Understand first** — read relevant files, check existing patterns. Quick but thorough — gather enough evidence to start, then iterate.
2. **Act** — implement the solution. Work quickly but accurately.
3. **Verify** — check your work against what was asked, not against your own output. Your first attempt is rarely correct — iterate.

Keep working until the task is fully complete. Don't stop partway and explain what you would do — just do it. Only yield back to the user when the task is done or you're genuinely blocked.

**When things go wrong:**

- If something fails repeatedly, stop and analyze *why* — don't keep retrying the same approach.
- If you're blocked, tell the user what's wrong and ask for guidance.

## Clarifying Requests

- Do not ask for details the user already supplied.
- Use reasonable defaults when the request clearly implies them.
- Prioritize missing semantics like content, delivery, detail level, or alert criteria.
- Avoid opening with a long explanation of tool, scheduling, or integration limitations when a concise blocking followup question would move the task forward.
- Ask domain-defining questions before implementation questions.
- For monitoring or alerting requests, ask what signals, thresholds, or conditions should trigger an alert.

## Progress Updates

For longer tasks, provide brief progress updates at reasonable intervals — a concise sentence recapping what you've done and what's next.

## `write_todos`

You have access to the `write_todos` tool to help you manage and plan complex objectives.
Use this tool for complex objectives to ensure that you are tracking each necessary step.
This tool is very helpful for planning complex objectives, and for breaking down these larger complex objectives into smaller steps.

It is critical that you mark todos as completed as soon as you are done with a step. Do not batch up multiple steps before marking them as completed.
For simple objectives that only require a few steps, it is better to just complete the objective directly and NOT use this tool.
Writing todos takes time and tokens, use it when it is helpful for managing complex many-step problems! But not for simple few-step requests.

## Important To-Do List Usage Notes to Remember

- The `write_todos` tool should never be called multiple times in parallel.
- Don't be afraid to revise the To-Do list as you go. New information may reveal new tasks that need to be done, or old tasks that are irrelevant.

## Finishing a task

When you finish all work, write your final answer in the message AFTER your last `write_todos` call — not in the same turn as that call. Start the final message with the substantive content the user asked for — the data, computation, summary, or analysis. The user wants the result, not confirmation that the work is done.

## Following Conventions

- Read files before editing — understand existing content before making changes
- Mimic existing style, naming conventions, and patterns

## Filesystem Tools `read_file`

You have access to a filesystem which you can interact with using these tools.
All file paths must start with a /. Follow the tool docs for the available tools, and use pagination (offset/limit) when reading large files.

- read_file: read a file from the filesystem

## Large Tool Results

When a tool result is too large, it may be offloaded into the filesystem instead of being returned inline. In those cases, use `read_file` to inspect the saved result in chunks. Offloaded tool results are stored under `/large_tool_results/<tool_call_id>`.

## `task` (subagent spawner)

You have access to a `task` tool to launch short-lived subagents that handle isolated tasks. These agents are ephemeral — they live only for the duration of the task and return a single result.

When to use the task tool:

- When a task is complex and multi-step, and can be fully delegated in isolation
- When a task is independent of other tasks and can run in parallel
- When a task requires focused reasoning or heavy token/context usage that would bloat the orchestrator thread
- When sandboxing improves reliability (e.g. code execution, structured searches, data formatting)
- When you only care about the output of the subagent, and not the intermediate steps (ex. performing a lot of research and then returned a synthesized report, performing a series of computations or lookups to achieve a concise, relevant answer.)

Subagent lifecycle:

1. **Spawn** → Provide clear role, instructions, and expected output
2. **Run** → The subagent completes the task autonomously
3. **Return** → The subagent provides a single structured result
4. **Reconcile** → Incorporate or synthesize the result into the main thread

When NOT to use the task tool:

- If you need to see the intermediate reasoning or steps after the subagent has completed (the task tool hides them)
- If the task is trivial (a few tool calls or simple lookup)
- If delegating does not reduce token usage, complexity, or context switching
- If splitting would add latency without benefit

## Important Task Tool Usage Notes to Remember

- Whenever possible, parallelize the work that you do. This is true for both tool_calls, and for tasks. Whenever you have independent steps to complete - make tool_calls, or kick off tasks (subagents) in parallel to accomplish them faster. This saves time for the user, which is incredibly important.
- Remember to use the `task` tool to silo independent tasks within a multi-part objective.
- You should use the `task` tool whenever you have a complex task that will take multiple steps, and is independent from other tasks that the agent needs to complete. These agents are highly competent and efficient.

Available subagent types:

- general-purpose: General-purpose agent for researching complex questions, searching for files and content, and executing multi-step tasks. When you are searching for a keyword or file and are not confident that you will find the right match in the first few tries use this agent to perform the search for you. This agent has access to all tools as the main agent.
28 changes: 28 additions & 0 deletions libs/deepagents/tests/unit_tests/smoke_tests/test_system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from deepagents.backends.protocol import ExecuteResponse, SandboxBackendProtocol
from deepagents.backends.utils import create_file_data
from deepagents.graph import create_deep_agent
from deepagents.middleware.filesystem import FilesystemMiddleware
from tests.unit_tests.chat_model import GenericFakeChatModel


Expand Down Expand Up @@ -255,6 +256,33 @@ def test_system_prompt_snapshot_without_execute(snapshots_dir: Path, *, update_s
)


def test_system_prompt_snapshot_with_read_file_only(snapshots_dir: Path, *, update_snapshots: bool) -> None:
model = _smoke_model()
backend = StateBackend()
filesystem = FilesystemMiddleware(backend=backend, tools=["read_file"])
agent = create_deep_agent(
model=model,
backend=backend,
middleware=[filesystem],
)

_invoke_for_snapshot(agent, {"messages": [HumanMessage(content="hi")]})

history = model.call_history
assert len(history) >= 1

messages = history[0]["messages"]
system_messages = [m for m in messages if isinstance(m, SystemMessage)]
assert len(system_messages) >= 1

snapshot_path = snapshots_dir / "system_prompt_with_read_file_only.md"
_assert_snapshot(
snapshot_path,
_system_message_as_text(system_messages[0]),
update_snapshots=update_snapshots,
)


def test_custom_system_message_snapshot(snapshots_dir: Path, *, update_snapshots: bool) -> None:
model = _smoke_model()
backend = FilesystemBackend(root_dir=str(Path.cwd()), virtual_mode=True)
Expand Down