fix(sdk): strip HTML comments from memory content before system prompt injection (#3462)

`MemoryMiddleware` injected memory file contents verbatim into the
system prompt, including HTML comment markers used internally by
`dcode`'s onboarding flow (`<!-- deepagents:onboarding-name:start/end
-->`). These are metadata, not instructions, and shouldn't be visible to
the model.

## Changes

- `_format_agent_memory` now strips HTML comments (single- and
multi-line) from each source's content before injection, via a compiled
`re.DOTALL` regex. Sources that are empty after stripping are excluded
entirely rather than producing a phantom path-header section.
- Module docstring updated to document that HTML comments are stripped
and can be used for authoring notes or machine-managed markers.
This commit is contained in:
Mason Daugherty
2026-05-18 22:09:08 -07:00
committed by GitHub
parent da43b7f9d9
commit bfbb8bc557
2 changed files with 107 additions and 1 deletions
@@ -46,11 +46,16 @@ Common sections include:
- Build/test commands
- Code style guidelines
- Architecture notes
HTML comments (`<!-- ... -->`) are stripped before content is injected into the
system prompt. They can be used for authoring notes or machine-managed markers
without exposing them to the model.
"""
from __future__ import annotations
import logging
import re
from typing import TYPE_CHECKING, Annotated, NotRequired, TypedDict
if TYPE_CHECKING:
@@ -164,6 +169,13 @@ MEMORY_SYSTEM_PROMPT = """<agent_memory>
"""
_HTML_COMMENT_RE = re.compile(r"<!--.*?-->", re.DOTALL)
def _strip_html_comments(text: str) -> str:
return _HTML_COMMENT_RE.sub("", text)
class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]):
"""Middleware for loading agent memory from `AGENTS.md` files.
@@ -270,7 +282,16 @@ class MemoryMiddleware(AgentMiddleware[MemoryState, ContextT, ResponseT]):
if not contents:
return template.format(agent_memory="(No memory loaded)")
sections = [f"{path}\n\n{contents[path].rstrip()}" for path in self.sources if contents.get(path)]
sections = []
for path in self.sources:
raw = contents.get(path)
if not raw:
continue
stripped = _strip_html_comments(raw).rstrip()
if not stripped:
logger.debug("Memory source %s was empty after stripping HTML comments", path)
continue
sections.append(f"{path}\n\n{stripped}")
if not sections:
return template.format(agent_memory="(No memory loaded)")
@@ -1111,3 +1111,88 @@ def test_modify_request_cache_control_runs_with_system_prompt_none() -> None:
assert blocks[-1].get("cache_control") == {"type": "ephemeral"}
# No memory fragment was appended on top of `base`.
assert "agent_memory" not in blocks[-1].get("text", "")
def test_html_comments_stripped_from_memory_before_injection(tmp_path: Path) -> None:
"""HTML comment markers in memory files must not appear in the system prompt."""
source = str(tmp_path / "AGENTS.md")
(tmp_path / "AGENTS.md").write_text(
"<!-- deepagents:onboarding-name:start -->\n"
'- The user\'s preferred name is "Alice".\n'
"<!-- deepagents:onboarding-name:end -->\n"
"\nSome other memory.\n",
encoding="utf-8",
)
middleware = MemoryMiddleware(
backend=FilesystemBackend(),
sources=[source],
system_prompt="{agent_memory}",
)
request = ModelRequest(
model=_fake_anthropic(),
messages=[HumanMessage(content="hi")],
system_message=SystemMessage(content="base"),
state={"messages": [], "memory_contents": {source: (tmp_path / "AGENTS.md").read_text(encoding="utf-8")}}, # type: ignore[typeddict-unknown-key]
)
result = middleware.modify_request(request)
injected = _system_blocks(result.system_message)[-1].get("text", "")
assert "<!--" not in injected
assert "-->" not in injected
assert "Alice" in injected
assert "Some other memory." in injected
def test_comment_only_memory_file_produces_no_phantom_section(tmp_path: Path) -> None:
"""A memory file that is entirely HTML comments must not produce a ghost section header."""
source = str(tmp_path / "AGENTS.md")
(tmp_path / "AGENTS.md").write_text(
"<!-- deepagents:onboarding-name:start -->\n<!-- deepagents:onboarding-name:end -->\n",
encoding="utf-8",
)
middleware = MemoryMiddleware(
backend=FilesystemBackend(),
sources=[source],
system_prompt="{agent_memory}",
)
request = ModelRequest(
model=_fake_anthropic(),
messages=[HumanMessage(content="hi")],
system_message=SystemMessage(content="base"),
state={"messages": [], "memory_contents": {source: (tmp_path / "AGENTS.md").read_text(encoding="utf-8")}}, # type: ignore[typeddict-unknown-key]
)
result = middleware.modify_request(request)
injected = _system_blocks(result.system_message)[-1].get("text", "")
assert source not in injected
assert "(No memory loaded)" in injected
def test_multiline_html_comment_stripped_from_memory(tmp_path: Path) -> None:
"""Multi-line HTML comments require re.DOTALL; this test catches its accidental removal."""
source = str(tmp_path / "AGENTS.md")
(tmp_path / "AGENTS.md").write_text(
"preamble\n<!--\n line one of comment\n line two of comment\n-->\npostamble\n",
encoding="utf-8",
)
middleware = MemoryMiddleware(
backend=FilesystemBackend(),
sources=[source],
system_prompt="{agent_memory}",
)
request = ModelRequest(
model=_fake_anthropic(),
messages=[HumanMessage(content="hi")],
system_message=SystemMessage(content="base"),
state={"messages": [], "memory_contents": {source: (tmp_path / "AGENTS.md").read_text(encoding="utf-8")}}, # type: ignore[typeddict-unknown-key]
)
result = middleware.modify_request(request)
injected = _system_blocks(result.system_message)[-1].get("text", "")
assert "line one of comment" not in injected
assert "line two of comment" not in injected
assert "preamble" in injected
assert "postamble" in injected