From 35f87869710df8ca0e50388991998fa5dc1b7ffb Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 4 Oct 2026 21:59:35 -0700 Subject: [PATCH] feat(agent): truncated context files name the sections that were dropped When a context file still overflows (small windows, other repositories), the marker listed only char counts, so the agent could not tell whether what it lost mattered. It now lists the markdown headings in the omitted middle (fenced # comments ignored). --- agent/prompt_builder.py | 22 ++++++++++++++++++++-- tests/agent/test_context_file_sources.py | 14 ++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 2569302882..6c975b0a6b 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -1576,14 +1576,32 @@ def _truncate_content( warnings.append(msg) head_chars = int(max_chars * CONTEXT_TRUNCATE_HEAD_RATIO) tail_chars = int(max_chars * CONTEXT_TRUNCATE_TAIL_RATIO) + omitted = _omitted_headings(content, head_chars, len(content) - tail_chars) + sections = f" Omitted sections: {'; '.join(omitted)}." if omitted else "" marker = ( f"\n\n[...truncated {filename}: kept {head_chars}+{tail_chars} of {len(content)} chars. The middle is " - f"omitted — if you need the full instructions, read the complete file with the read_file tool: " - f"{read_path or filename}]\n\n" + f"omitted.{sections} If you need the full instructions, read the complete file with the read_file " + f"tool: {read_path or filename}]\n\n" ) return content[:head_chars] + marker + content[-tail_chars:] +def _omitted_headings(content: str, start: int, end: int, limit: int = 15) -> list: + """Markdown headings whose line starts inside ``content[start:end]``, so a truncation marker tells + the agent what it lost; ``#`` lines inside fenced code blocks are comments, not headings.""" + import re + + headings, offset, fenced = [], 0, False + for line in content.splitlines(keepends=True): + stripped = line.strip() + if stripped.startswith(("```", "~~~")): + fenced = not fenced + elif not fenced and start <= offset < end and re.match(r"#{1,6} \S", stripped): + headings.append(stripped.lstrip("#").strip()) + offset += len(line) + return headings[:limit] + (["..."] if len(headings) > limit else []) + + def load_soul_md(context_length: Optional[int] = None, home_override: "Path | None" = None) -> Optional[str]: """SOUL.md from HERMES_HOME (identity slot #1), or None. diff --git a/tests/agent/test_context_file_sources.py b/tests/agent/test_context_file_sources.py index ef51e7cb97..24c4b4a22e 100644 --- a/tests/agent/test_context_file_sources.py +++ b/tests/agent/test_context_file_sources.py @@ -99,3 +99,17 @@ def test_truncated_and_suppressed_statuses_follow_the_builder(project, monkeypat assert "evil identity text" in prompt and "[BLOCKED: SOUL.md" not in prompt and "[BLOCKED: AGENTS.md" in prompt assert any("SOUL.md" in line and "review the file" in line for line in render_context_file_lines(list(entries.values()))) + + +def test_truncation_marker_names_the_omitted_sections_only(): + from agent.prompt_builder import _truncate_content + + pad = "word " * 400 + content = ( + f"# Kept head\n{pad}\n## Lost section\n{pad}\n```bash\n# a shell comment\n```\n" + f"### Lost subsection\n{pad}\n{pad}\n## Kept tail\nshort\n" + ) + out = _truncate_content(content, "AGENTS.md", max_chars=2500, queue_warning=False) + marker = out[out.index("[...truncated"):out.index("]", out.index("[...truncated"))] + assert "Lost section; Lost subsection" in marker + assert "Kept head" not in marker and "Kept tail" not in marker and "shell comment" not in marker