feat(code): drop TodoListMiddleware (#4685)

dcode no longer exposes the `write_todos` tool / todo-list middleware to
the agent or its subagents when `DEEPAGENTS_CODE_EXPERIMENTAL` is set.

---

`dcode` builds its agent with `create_deep_agent`, which always injects
the SDK's `TodoListMiddleware` (and its `write_todos` tool) and exposes
no parameter to disable it. Because `create_deep_agent`'s
`_apply_custom_middleware` merge replaces a default middleware in place
when a caller middleware shares its `.name`, we add a tool-less no-op
middleware named `TodoListMiddleware` and thread it into the main-agent
and subagent middleware lists — so the real middleware (and
`write_todos`) is dropped without any SDK change.

This is gated behind the new `DEEPAGENTS_CODE_EXPERIMENTAL` env var and
is a no-op when unset, so default behavior is unchanged. Kept
intentionally narrow: the todo prompt guidance and TUI rendering are
left in place (dead only when the flag is on).

Made by [Open
SWE](https://openswe.vercel.app/agents/c5e51661-6737-cd07-a93a-30847caeef06)

## References
- Plan:
https://openswe.vercel.app/agents/c5e51661-6737-cd07-a93a-30847caeef06/plan

---------

Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
This commit is contained in:
Mason Daugherty
2026-07-13 17:18:06 -04:00
committed by GitHub
parent 96fe71a381
commit d3a3077e85
7 changed files with 325 additions and 13 deletions
+10
View File
@@ -136,6 +136,16 @@ When set, this replaces (takes precedence over) the
is never silently emptied.)
"""
EXPERIMENTAL = "DEEPAGENTS_CODE_EXPERIMENTAL"
"""Opt into experimental, unstable dcode behavior.
Off by default; parsed by `is_env_truthy` (see there for the accepted truthy
values). Currently gates dropping the SDK's `TodoListMiddleware` (and its
`write_todos` tool) from the agent and its subagents, along with the matching
todo-list prompt guidance. Behavior behind this flag may change or be removed
without notice.
"""
EXTERNAL_EVENT_SOCKET = "DEEPAGENTS_CODE_EXTERNAL_EVENT_SOCKET"
"""Enable the local Unix-socket external event listener.
+81 -1
View File
@@ -57,6 +57,7 @@ if TYPE_CHECKING:
from deepagents_code.mcp_tools import MCPServerInfo
from deepagents_code.output import OutputFormat
from langchain.agents.middleware import TodoListMiddleware
from langchain.agents.middleware.types import AgentMiddleware
from langchain.tools import (
ToolRuntime, # noqa: TC002 # LangChain inspects this annotation for runtime injection.
@@ -66,6 +67,7 @@ from langchain_core.tools import StructuredTool, tool
from deepagents_code import theme
from deepagents_code._cli_context import CLIContextSchema
from deepagents_code._constants import DEFAULT_AGENT_NAME
from deepagents_code._env_vars import EXPERIMENTAL, is_env_truthy
from deepagents_code.config import (
_INHERITED_PYTHONPATH_ENV,
_ShellAllowAll,
@@ -101,6 +103,73 @@ logger = logging.getLogger(__name__)
REQUIRE_COMPACT_TOOL_APPROVAL: bool = True
"""When `True`, `compact_conversation` requires HITL approval like other gated tools."""
class _NoTodoListMiddleware(AgentMiddleware):
"""No-op stand-in that drops the SDK's `TodoListMiddleware` by name.
`create_deep_agent` always injects `TodoListMiddleware` and exposes no
per-call parameter to disable it (only a globally registered
`HarnessProfile.excluded_middleware` can strip it, which dcode does not use
here). Its `_apply_custom_middleware` merge replaces a default middleware in
place when a caller-supplied middleware shares its `.name`, so threading
this tool-less stand-in into the agent and subagent middleware lists removes
the real middleware — and its `write_todos` tool — without touching the SDK.
Deriving `name` from `TodoListMiddleware.__name__` makes a *rename* or
removal of the SDK class fail loudly (`ImportError`) at the top-of-module
import. It does not, on its own, guard a `.name` *override* on an unrenamed
class: the merge keys on the instance `.name`, not `__name__`, so such an
override would slip past the import and silently turn this into a no-op.
That case is caught two ways — `_todo_list_middleware_override` re-checks the
match at build time and raises, and `test_agent.py` guards it in CI. Gated
behind `DEEPAGENTS_CODE_EXPERIMENTAL`; see `_todo_list_middleware_override`.
"""
name: str = TodoListMiddleware.__name__
tools: Sequence[BaseTool] = ()
"""No tools — replacing the real `TodoListMiddleware` drops its `write_todos`.
Declared explicitly (mirroring the base's `transformers = ()` default) so a
bare instance is self-contained rather than relying on the SDK's
`getattr(mw, "tools", [])` fallback.
"""
def _todo_list_middleware_override() -> list[AgentMiddleware]:
"""Return the middleware needed to strip `TodoListMiddleware`, if enabled.
Returns a single-element list with `_NoTodoListMiddleware` when the
experimental flag is set, else an empty list. Callers splice the result
into the middleware list they pass to `create_deep_agent` so the SDK's
name-based merge drops the real `TodoListMiddleware`.
Raises:
RuntimeError: If the stand-in's `.name` no longer matches the SDK
middleware's instance `.name`. The merge replaces by name, so a
mismatch would silently *append* the tool-less stand-in instead of
replacing the real middleware, leaving `write_todos` bound. Failing
fast here converts that silent no-op into a loud, actionable error
(only ever runs when the flag is on).
"""
if not is_env_truthy(EXPERIMENTAL):
return []
stand_in = _NoTodoListMiddleware()
sdk_name = TodoListMiddleware().name
if stand_in.name != sdk_name:
msg = (
f"{EXPERIMENTAL} is set but the TodoListMiddleware override would be "
f"a silent no-op: stand-in name {stand_in.name!r} no longer matches "
f"the SDK middleware's instance name {sdk_name!r}. The SDK likely "
f"overrode TodoListMiddleware.name; update _NoTodoListMiddleware."
)
raise RuntimeError(msg)
logger.info(
"%s set: dropping TodoListMiddleware / write_todos from this stack",
EXPERIMENTAL,
)
return [stand_in]
_RUBRIC_GRADER_READ_FILE_PREFIX = "/large_tool_results/"
_RUBRIC_GRADER_SYSTEM_PROMPT = (
GRADER_SYSTEM_PROMPT
@@ -825,7 +894,11 @@ def get_system_prompt(
... {CONDITIONAL SECTIONS} ...
```
"""
template = (Path(__file__).parent / "system_prompt.md").read_text()
prompt_dir = Path(__file__).parent
template = (prompt_dir / "system_prompt.md").read_text()
todo_list_section = ""
if not is_env_truthy(EXPERIMENTAL):
todo_list_section = (prompt_dir / "todo_list_prompt.md").read_text().rstrip()
skills_path = f"~/.deepagents/{assistant_id}/skills"
@@ -937,6 +1010,7 @@ def get_system_prompt(
template.replace("{mode_description}", mode_description)
.replace("{interactive_preamble}", interactive_preamble)
.replace("{ambiguity_guidance}", ambiguity_guidance)
.replace("{todo_list_section}", todo_list_section)
.replace("{todo_guidance}", todo_guidance)
.replace("{model_identity_section}", model_identity_section)
.replace("{working_dir_section}", working_dir_section)
@@ -1506,6 +1580,9 @@ def create_cli_agent(
def _subagent_cli_middleware(*, has_explicit_model: bool) -> list[AgentMiddleware]:
middleware: list[AgentMiddleware] = []
# Experimental: mirror the main agent and drop TodoListMiddleware /
# write_todos from subagent stacks too. No-op unless the flag is set.
middleware.extend(_todo_list_middleware_override())
if not has_explicit_model:
middleware.append(ConfigurableModelMiddleware(persist_model_state=False))
if restrictive_shell_allow_list is not None:
@@ -1567,6 +1644,9 @@ def create_cli_agent(
# Build middleware stack based on enabled features
agent_middleware: list[AgentMiddleware[Any, Any]] = [
ConfigurableModelMiddleware(),
# Experimental: drop the SDK's TodoListMiddleware / write_todos tool.
# No-op unless DEEPAGENTS_CODE_EXPERIMENTAL is truthy.
*_todo_list_middleware_override(),
]
# Resume state: declares private checkpoint channels used on resume.
@@ -1075,6 +1075,14 @@ _STATIC_OPTIONS: tuple[ConfigOption, ...] = (
default=True,
env_var=_env_vars.OLLAMA_DISCOVERY,
),
ConfigOption(
key="features.experimental",
group="Tools",
summary="Opt into experimental, unstable dcode behavior.",
kind=OptionKind.BOOL,
default=False,
env_var=_env_vars.EXPERIMENTAL,
),
ConfigOption(
key="events.external_socket",
group="Tools",
+1 -12
View File
@@ -201,15 +201,4 @@ When you use the web_search tool:
The user only sees your text responses - not tool results. Always provide a complete, natural language answer after using web_search.
### Todo List Management
When using the write_todos tool:
1. Use todos for any task with 2+ steps — they give the user visibility
2. Mark tasks `in_progress` before starting, `completed` immediately after
3. Don't batch completions — mark each item done as you finish it
4. If a task reveals sub-tasks, add them right away
5. For simple 1-step tasks, just do them directly
{todo_guidance}
The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.
{todo_list_section}
@@ -0,0 +1,12 @@
### Todo List Management
When using the write_todos tool:
1. Use todos for any task with 2+ steps — they give the user visibility
2. Mark tasks `in_progress` before starting, `completed` immediately after
3. Don't batch completions — mark each item done as you finish it
4. If a task reveals sub-tasks, add them right away
5. For simple 1-step tasks, just do them directly
{todo_guidance}
The todo list is a planning tool - use it judiciously to avoid overwhelming the user with excessive task tracking.
+192
View File
@@ -10,6 +10,7 @@ from typing import TYPE_CHECKING, Any, cast
from unittest.mock import Mock, patch
import pytest
from langchain.agents.middleware import TodoListMiddleware
from langchain_core.language_models.fake_chat_models import GenericFakeChatModel
from langchain_core.messages import AIMessage
@@ -20,6 +21,7 @@ if TYPE_CHECKING:
from langgraph.runtime import Runtime
from deepagents_code._cli_context import CLIContext, CLIContextSchema
from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.agent import (
DEFAULT_AGENT_NAME,
_add_interrupt_on,
@@ -916,6 +918,44 @@ class TestGetSystemPromptNonInteractive:
assert "do NOT ask the user to approve your plan" in prompt
assert "mark the first item `in_progress` immediately" in prompt
def test_experimental_prompt_omits_todo_section(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Experimental mode must not reference its removed todo tool."""
monkeypatch.setenv(EXPERIMENTAL, "1")
mock_settings = Mock()
mock_settings.model_name = None
with patch("deepagents_code.agent.settings", mock_settings):
prompt = get_system_prompt("test-agent")
assert "Todo List Management" not in prompt
assert "write_todos" not in prompt
# `{todo_guidance}` lives only inside the gated section, so dropping the
# section must not leave the placeholder unresolved.
assert "{todo_list_section}" not in prompt
assert "{todo_guidance}" not in prompt
def test_default_prompt_resolves_todo_placeholders(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
"""Default mode keeps the todo section with no unresolved placeholders.
Guards the `.replace` ordering in `get_system_prompt`: `{todo_guidance}`
is nested inside the todo section, so the section must be substituted
before the guidance placeholder is filled.
"""
monkeypatch.delenv(EXPERIMENTAL, raising=False)
mock_settings = Mock()
mock_settings.model_name = None
with patch("deepagents_code.agent.settings", mock_settings):
prompt = get_system_prompt("test-agent")
assert "Todo List Management" in prompt
assert "{todo_list_section}" not in prompt
assert "{todo_guidance}" not in prompt
class TestGetSystemPromptCwdOSError:
"""Tests for Path.cwd() OSError handling in get_system_prompt."""
@@ -3086,6 +3126,158 @@ class TestCreateCliAgentShellMiddlewareWiring:
)
class TestExperimentalTodoMiddlewareWiring:
"""`DEEPAGENTS_CODE_EXPERIMENTAL` drops TodoListMiddleware from every stack.
`collect_built_in_tools` (see `test_tool_catalog.py`) only inspects the main
agent's bound tools, so the subagent splice needs its own coverage: these
tests capture the `create_deep_agent` kwargs and assert the stand-in reaches
the main agent, custom subagents, and the general-purpose subagent that
dcode auto-adds.
"""
@staticmethod
def _build_mock_settings(tmp_path: Path) -> Mock:
agent_dir = tmp_path / "agent"
agent_dir.mkdir()
skills_dir = tmp_path / "skills"
skills_dir.mkdir()
mock_settings = Mock()
mock_settings.ensure_agent_dir.return_value = agent_dir
mock_settings.ensure_user_skills_dir.return_value = skills_dir
mock_settings.get_project_skills_dir.return_value = None
mock_settings.get_built_in_skills_dir.return_value = (
Settings.get_built_in_skills_dir()
)
mock_settings.get_user_agent_md_path.return_value = agent_dir / "AGENTS.md"
mock_settings.get_project_agent_md_path.return_value = []
mock_settings.get_user_agents_dir.return_value = tmp_path / "agents"
mock_settings.get_project_agents_dir.return_value = None
mock_settings.model_name = None
mock_settings.model_provider = None
mock_settings.model_unsupported_modalities = frozenset()
mock_settings.model_context_limit = None
mock_settings.project_root = None
mock_settings.shell_allow_list = ["ls", "cat"]
return mock_settings
def _capture_create_deep_agent_kwargs(
self, tmp_path: Path, *, subagent_model: str | None = None
) -> dict[str, Any]:
"""Build a default agent + custom subagent; capture `create_deep_agent` kwargs.
Returns the kwargs dcode forwards to `create_deep_agent` so callers can
assert on both the main `middleware` list and each `subagents` spec.
`subagent_model` sets the custom subagent's `model:` frontmatter, which
drives the `has_explicit_model` branch in `_subagent_cli_middleware`.
"""
mock_settings = self._build_mock_settings(tmp_path)
mock_agent = Mock()
mock_agent.with_config.return_value = mock_agent
fake_model = _make_fake_chat_model()
subagent_meta = {
"name": "researcher",
"description": "Researches things",
"system_prompt": "Investigate the task thoroughly.",
"model": subagent_model,
}
with (
patch("deepagents_code.agent.settings", mock_settings),
patch("deepagents_code.agent.SkillsMiddleware"),
patch("deepagents_code.agent.MemoryMiddleware"),
patch(
"deepagents_code.agent.list_subagents",
return_value=[subagent_meta],
),
patch(
"deepagents_code.agent.create_deep_agent",
return_value=mock_agent,
) as mock_create,
patch(
"deepagents._models.init_chat_model",
return_value=fake_model,
),
):
create_cli_agent(
model="fake-model",
assistant_id="test",
enable_memory=False,
enable_skills=False,
enable_shell=True,
)
_, kwargs = mock_create.call_args
return kwargs
@staticmethod
def _has_todo_standin(middleware: list[Any]) -> bool:
return any(
getattr(mw, "name", None) == TodoListMiddleware.__name__
for mw in middleware
)
def test_standin_name_matches_sdk_middleware(self) -> None:
"""The stand-in must impersonate the real middleware's `.name`.
The name-based merge keys on the instance `.name`, so this guards a
hypothetical SDK `.name` override that `__name__`-derivation would miss.
"""
from deepagents_code.agent import _NoTodoListMiddleware
assert _NoTodoListMiddleware().name == TodoListMiddleware().name
def test_dropped_from_main_and_subagents_when_experimental(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(EXPERIMENTAL, "1")
kwargs = self._capture_create_deep_agent_kwargs(tmp_path)
assert self._has_todo_standin(kwargs["middleware"])
subagents_by_name = {sa["name"]: sa for sa in kwargs["subagents"]}
assert {"researcher", "general-purpose"} <= set(subagents_by_name)
for name, spec in subagents_by_name.items():
assert self._has_todo_standin(spec.get("middleware", [])), (
f"Expected TodoListMiddleware stand-in on subagent {name!r}"
)
def test_dropped_from_explicit_model_subagent_when_experimental(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
"""The splice must survive the `has_explicit_model` branch.
`_subagent_cli_middleware` extends the stand-in in *before* the
`if not has_explicit_model:` model-middleware check, so a subagent with
an explicit `model:` in frontmatter must still receive it. The other
wiring cases only exercise the `model: None` branch, so without this a
regression that moved the splice inside that `if` would pass unnoticed.
"""
monkeypatch.setenv(EXPERIMENTAL, "1")
kwargs = self._capture_create_deep_agent_kwargs(
tmp_path, subagent_model="fake-model"
)
subagents_by_name = {sa["name"]: sa for sa in kwargs["subagents"]}
researcher = subagents_by_name["researcher"]
# Guards the premise: an explicit model must reach the spec, else the
# subagent would take the `model: None` path and the test proves nothing.
assert researcher.get("model"), "explicit subagent model was not forwarded"
assert self._has_todo_standin(researcher.get("middleware", []))
def test_absent_from_all_stacks_by_default(
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv(EXPERIMENTAL, raising=False)
kwargs = self._capture_create_deep_agent_kwargs(tmp_path)
assert not self._has_todo_standin(kwargs["middleware"])
for spec in kwargs["subagents"]:
assert not self._has_todo_standin(spec.get("middleware", []))
def _mock_agents_dir(agents_dir: Path) -> Mock:
mock_settings = Mock()
mock_settings.user_deepagents_dir = agents_dir
@@ -8,6 +8,7 @@ from unittest.mock import AsyncMock, PropertyMock, patch
import pytest
from deepagents_code._env_vars import EXPERIMENTAL
from deepagents_code.config import Settings
from deepagents_code.mcp_tools import MCPServerInfo, MCPToolInfo
from deepagents_code.tool_catalog import (
@@ -119,6 +120,26 @@ class TestCollectBuiltInTools:
collect_built_in_tools()
class TestExperimentalTodoRemoval:
"""`DEEPAGENTS_CODE_EXPERIMENTAL` drops the SDK `write_todos` tool."""
def test_write_todos_bound_by_default(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.delenv(EXPERIMENTAL, raising=False)
names = {tool.name for tool in collect_built_in_tools()}
assert "write_todos" in names
def test_write_todos_removed_when_experimental(
self, monkeypatch: pytest.MonkeyPatch
) -> None:
monkeypatch.setenv(EXPERIMENTAL, "1")
names = {tool.name for tool in collect_built_in_tools()}
assert "write_todos" not in names
# Only the todo tool is dropped; the rest of the core set stays bound.
assert names >= _CORE_BUILT_IN - {"write_todos"}
class TestCollectToolsFromAgent:
"""Tests for inspecting the tool node of an already-running local graph."""