mirror of
https://github.com/langchain-ai/deepagents.git
synced 2026-07-21 17:25:26 -04:00
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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user