diff --git a/agent/AGENTS.md b/agent/AGENTS.md index 77cd5f0bf2..364d6d8131 100644 --- a/agent/AGENTS.md +++ b/agent/AGENTS.md @@ -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) — diff --git a/agent/prompt_builder.py b/agent/prompt_builder.py index 771f946bf6..92cdf21d4b 100644 --- a/agent/prompt_builder.py +++ b/agent/prompt_builder.py @@ -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: diff --git a/tests/agent/test_subdirectory_hints.py b/tests/agent/test_subdirectory_hints.py index 3214b23236..eef49f3eee 100644 --- a/tests/agent/test_subdirectory_hints.py +++ b/tests/agent/test_subdirectory_hints.py @@ -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.""" diff --git a/tests/gateway/test_startup_environment_probe.py b/tests/gateway/test_startup_environment_probe.py index 60d395ba0d..b57903352b 100644 --- a/tests/gateway/test_startup_environment_probe.py +++ b/tests/gateway/test_startup_environment_probe.py @@ -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): diff --git a/website/docs/user-guide/features/context-files.md b/website/docs/user-guide/features/context-files.md index 2906c4f780..02fc7b4ec8 100644 --- a/website/docs/user-guide/features/context-files.md +++ b/website/docs/user-guide/features/context-files.md @@ -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: