fix: skip empty untrusted context blocks to prevent model hallucination
Some checks are pending
ci / docker publish / build (amd64) (push) Waiting to run
ci / docker publish / build (arm64) (push) Waiting to run
ci / docker publish / merge manifest + tag (push) Blocked by required conditions

untrusted_context_message() now returns None when content is empty or
whitespace-only. Previously an empty document would produce a blank
UNTRUSTED_SOURCE_DATA wrapper that confused Gemma into thinking the
user pasted a placeholder template.

Callers already use truthiness guards (if _doc_message:) for insertion,
so None propagates cleanly. Two _protected setter sites needed explicit
guards added (doc + email paths in agent_loop.py).
This commit is contained in:
Lukas Parsons 2026-07-07 21:38:15 -04:00
parent 6c9e04fa69
commit 60452712b2
2 changed files with 12 additions and 4 deletions

View file

@ -1504,7 +1504,8 @@ def _build_system_prompt(
"knowing the user's personal style."
)
_doc_message = untrusted_context_message("active editor document", doc_ctx)
_doc_message["_protected"] = True
if _doc_message:
_doc_message["_protected"] = True
# Auto-detect suggestion mode
_last_user_msg = ""
@ -1584,7 +1585,8 @@ def _build_system_prompt(
f"open email's sender.\n"
)
_email_message = untrusted_context_message("active email reader", email_ctx)
_email_message["_protected"] = True
if _email_message:
_email_message["_protected"] = True
# Inject writing style for any email writing path. This is deliberately
# broader than read/list: models may compose via send_email, reply_to_email,

View file

@ -61,7 +61,7 @@ def _sanitize_label(label: str) -> str:
return label
def untrusted_context_message(label: str, content: Any) -> Dict[str, Any]:
def untrusted_context_message(label: str, content: Any) -> Dict[str, Any] | None:
"""Return an LLM message that keeps retrieved/source text out of system role.
The template is structured so that *only* the hardcoded
@ -69,9 +69,15 @@ def untrusted_context_message(label: str, content: Any) -> Dict[str, Any]:
caller-derived text is placed in the pre-guard trusted framing zone.
The source label and the body content are both placed *inside* the
guarded block where the LLM treats them as untrusted data.
Returns None when content is empty or whitespace-only avoids
injecting blank wrapper blocks that confuse the model into thinking
the user pasted a placeholder template.
"""
text = "" if content is None else str(content).strip()
if not text:
return None
safe_label = _sanitize_label(label)
text = "" if content is None else str(content)
text = _escape_guard_markers(text)
return {
"role": "user",