fix(agent): hint-preview truncation log names the real remedy; invariant tests
Follow-up to the two salvaged commits (#111777, #111781 by @KoNit-K): - agent/prompt_builder.py::_truncate_content — with queue_warning=False the logged line no longer tells the operator to "pin a larger context_file_max_chars, or use a larger-context model": the subdirectory hint cap is a constant neither knob raises. It now points at the read_file recovery the marker already discloses. - tests/gateway/test_startup_environment_probe.py — replace the call-detection test with the behavioural invariant: an oversized SOUL.md in HERMES_HOME and a warm-up leave the truncation-warning queue empty for the next default-executor task (the api_server turn path runs on that executor without copy_context, which is how the boot warning reached a foreign session). - tests/agent/test_subdirectory_hints.py — fold the new drain assertion into the existing oversized-hint test (same fixture) and pin that the log carries no context_file_max_chars advice. - agent/AGENTS.md, website/docs/.../context-files.md — the hint cap is 32,000 (docs said 8,000) and is fixed; document that it is logged, not surfaced as a chat warning.
This commit is contained in:
@@ -57,7 +57,8 @@ Adding one: register in that table (no `if name == ...` chain); `tools/todo_tool
|
||||
a conversation; the ONLY context mutation is compression. Anything that must inject content
|
||||
mid-conversation rides a **user message or tool result**, never the system prompt: skill slash
|
||||
commands (`agent/skill_commands.py`) inject as a user message; subdirectory `AGENTS.md` hints
|
||||
(`agent/subdirectory_hints.py`) append to the tool result (head+tail truncated past `_MAX_HINT_CHARS = 32_000`, with a warning).
|
||||
(`agent/subdirectory_hints.py`) append to the tool result (head+tail truncated past `_MAX_HINT_CHARS = 32_000`;
|
||||
the truncation is logged, never queued as a chat status warning — `context_file_max_chars` does not raise that cap).
|
||||
- **Strict role alternation.** Never two same-role messages in a row; never a synthetic user
|
||||
message injected mid-loop. The one exception is `/steer`, delivered as a standalone user row
|
||||
after a tool result (`assistant(tool_calls) → tool → user` is legal on every provider path) —
|
||||
|
||||
@@ -1425,16 +1425,18 @@ def _truncate_content(
|
||||
read_path: Optional[str] = None, queue_warning: bool = True,
|
||||
) -> str:
|
||||
"""Head/tail truncation with a marker in the middle; ``read_path`` (default ``filename``) is what the
|
||||
agent is told to ``read_file`` to recover the full content. ``queue_warning`` controls whether startup
|
||||
context-file callers surface the truncation through the chat warning queue."""
|
||||
agent is told to ``read_file`` to recover the full content. ``queue_warning=False`` is for bounded
|
||||
previews (subdirectory hints) whose fixed cap no config key or model raises: the truncation is logged
|
||||
with the marker as the only disclosure, never queued for the chat status line."""
|
||||
if max_chars is None:
|
||||
max_chars = _get_context_file_max_chars(context_length)
|
||||
if len(content) <= max_chars:
|
||||
return content
|
||||
msg = (
|
||||
f"⚠️ Context file {filename} TRUNCATED: {len(content)} chars exceeds limit of {max_chars} — "
|
||||
f"trim the file, pin a larger context_file_max_chars, or use a larger-context model!"
|
||||
remedy = (
|
||||
"trim the file, pin a larger context_file_max_chars, or use a larger-context model!" if queue_warning
|
||||
else f"the full file stays readable with read_file: {read_path or filename}"
|
||||
)
|
||||
msg = f"⚠️ Context file {filename} TRUNCATED: {len(content)} chars exceeds limit of {max_chars} — {remedy}"
|
||||
logger.warning(msg)
|
||||
if queue_warning:
|
||||
if (warnings := _truncation_warnings.get()) is None:
|
||||
|
||||
@@ -128,6 +128,7 @@ class TestSubdirectoryHintTracker:
|
||||
(sub / "AGENTS.md").write_text(body, encoding="utf-8")
|
||||
|
||||
tracker = SubdirectoryHintTracker(working_dir=str(tmp_path))
|
||||
drain_truncation_warnings()
|
||||
with caplog.at_level(logging.WARNING, logger="agent.prompt_builder"):
|
||||
result = tracker.check_tool_call("read_file", {"path": str(sub / "file.py")})
|
||||
assert result is not None
|
||||
@@ -135,22 +136,10 @@ class TestSubdirectoryHintTracker:
|
||||
assert "truncated AGENTS.md" in result and "bigdir/AGENTS.md" in result
|
||||
assert len(result) < len(body)
|
||||
assert any("TRUNCATED" in r.message and "AGENTS.md" in r.message for r in caplog.records)
|
||||
|
||||
def test_truncation_of_large_hints_does_not_queue_context_file_warning(self, tmp_path):
|
||||
"""Hint previews retain their marker without surfacing a startup-context warning in chat (#111772)."""
|
||||
from agent import subdirectory_hints as sh
|
||||
|
||||
drain_truncation_warnings()
|
||||
sub = tmp_path / "bigdir"
|
||||
sub.mkdir()
|
||||
(sub / "AGENTS.md").write_text("x" * (sh._MAX_HINT_CHARS + 1), encoding="utf-8")
|
||||
|
||||
tracker = SubdirectoryHintTracker(working_dir=str(tmp_path))
|
||||
result = tracker.check_tool_call("read_file", {"path": str(sub / "file.py")})
|
||||
|
||||
assert result is not None
|
||||
assert "truncated AGENTS.md" in result
|
||||
# A preview capped by a constant is not a context_file_max_chars problem: no chat status warning is
|
||||
# queued and the log does not send the user to a knob that cannot raise the cap (#111772).
|
||||
assert drain_truncation_warnings() == []
|
||||
assert "context_file_max_chars" not in caplog.text
|
||||
|
||||
def test_area_file_under_ceiling_is_delivered_whole(self, tmp_path):
|
||||
"""An area AGENTS.md sized like ours (well under the ceiling) arrives intact — no marker."""
|
||||
|
||||
@@ -38,15 +38,17 @@ def test_warmup_leaves_probe_cached_for_first_prompt(tmp_path, monkeypatch):
|
||||
assert calls == [1] # single worker; the first turn reuses the cache
|
||||
|
||||
|
||||
def test_warmup_does_not_build_context_files_without_turn_context(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
prompt_builder,
|
||||
"build_context_files_prompt",
|
||||
lambda: pytest.fail("gateway warm-up must not build context files"),
|
||||
)
|
||||
def test_warmup_queues_no_context_file_warning_for_later_turns(tmp_path, monkeypatch):
|
||||
"""Boot has no agent and no model, so the warm-up must not read context files: an identity file over
|
||||
the model-less default cap would otherwise queue a truncation warning into the executor thread's context,
|
||||
where the next default-executor turn drains it as its own (#111773)."""
|
||||
(tmp_path / "SOUL.md").write_text("x" * (prompt_builder.CONTEXT_FILE_MAX_CHARS + 1), encoding="utf-8")
|
||||
prompt_builder.drain_truncation_warnings()
|
||||
|
||||
_warm(tmp_path, monkeypatch, {}, "local")
|
||||
|
||||
assert prompt_builder.drain_truncation_warnings() == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("agent_section,backend", [({"environment_probe": False}, "local"), ({}, "ssh")])
|
||||
def test_warmup_skips_probe_when_disabled_or_remote(tmp_path, monkeypatch, agent_section, backend):
|
||||
|
||||
@@ -139,7 +139,7 @@ Context files are loaded by `build_context_files_prompt()` in `agent/prompt_buil
|
||||
2. **Ancestor walk** — the directory and up to 5 parent directories are checked (stopping at already-visited directories)
|
||||
3. **Hint loading** — if an `AGENTS.md`, `CLAUDE.md`, or `.cursorrules` is found, it's loaded (first match per directory)
|
||||
4. **Security scan** — same prompt injection scan as startup files
|
||||
5. **Truncation** — capped at 8,000 characters per file
|
||||
5. **Truncation** — capped at 32,000 characters per file (a fixed preview cap; `context_file_max_chars` and the model's context window do not change it). An oversized hint keeps its head/tail marker pointing at the full file and is logged, but does not raise the chat truncation warning that startup context files do
|
||||
6. **Injection** — appended to the tool result, so the model sees it in context naturally
|
||||
|
||||
The final prompt section looks roughly like:
|
||||
|
||||
Reference in New Issue
Block a user