refactor(memory): single shared trivial-prompt classifier + gate tests

Rebase fold on the salvaged gate:
- is_trivial_prompt/TRIVIAL_PROMPT_RE move to agent/memory_provider (the
  ABC both the core gate and providers already import) — one source of
  truth; honcho's _TRIVIAL_PROMPT_RE now aliases it, turn_context and the
  queue_prefetch_all warm path (a sibling site main grew after the PR's
  base) both use it
- tests: gate tests at the prefetch call site (mutation-checked), shared
  classifier tests incl. prefix-collision guards (k8s/yolo/note/supper),
  and honcho dialectic-machinery tests re-driven with a substantive prompt
  ("hello" became trivial by design — those tests exercise thread cadence,
  not the classifier)
This commit is contained in:
kshitijk4poor
2026-08-03 17:42:02 +05:30
committed by kshitij
parent 46073d7b1c
commit c093492b06
7 changed files with 110 additions and 49 deletions
+38
View File
@@ -34,12 +34,50 @@ Optional hooks (override to opt in):
from __future__ import annotations
import logging
import re
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Optional
logger = logging.getLogger(__name__)
# Prompts that carry no semantic signal — trivial acknowledgements, greetings,
# slash commands, empty input. Single source of truth shared by the core
# per-turn prefetch gate (agent/turn_context.py, run_agent.py) and provider-
# side classifiers (plugins/memory/honcho) so the two can never drift apart.
# The alternation is anchored and may only be followed by whitespace or
# punctuation, so words that merely START with a trivial word ("k8s", "yolo",
# "note", "hindsight") do NOT match, while trailing-punctuation variants
# ("hi!", "hey.", "thanks :)", "done???") do.
TRIVIAL_PROMPT_RE = re.compile(
r'^(yes|no|ok|okay|sure|thanks|thank you|y|n|yep|nope|yeah|nah|'
r'hi|hey|hello|yo|sup|'
r'continue|go ahead|do it|proceed|got it|cool|nice|great|done|next|lgtm|k)'
r'[\s!?.:;,"' + "'" + r'~\u2018\u2019\u201c\u201d\u2014\u2013\u2026()\[\]{}<>*&^%$#@!+=`\u00a0]*$',
re.IGNORECASE,
)
def is_trivial_prompt(text: Optional[str]) -> bool:
"""Return True if a user prompt is too trivial to warrant memory recall.
Empty/whitespace-only input, slash commands, and bare greetings or
acknowledgements (with optional trailing punctuation) all count as
trivial. Callers use this to skip memory-provider prefetch/injection
on turns that carry no semantic signal — saving a blocking network
round-trip and preventing stale user-model context from derailing
one-word replies.
"""
if not text:
return True
stripped = text.strip()
if not stripped:
return True
if stripped.startswith("/"):
return True
return bool(TRIVIAL_PROMPT_RE.match(stripped))
class MemoryProvider(ABC):
"""Abstract base class for memory providers."""
+2 -33
View File
@@ -25,7 +25,6 @@ move-and-name refactor with no semantic change.
from __future__ import annotations
import logging
import re
import threading
import time
import uuid
@@ -42,6 +41,7 @@ from agent.conversation_compression import (
from agent.context_engine import automatic_compaction_status_message
from agent.iteration_budget import IterationBudget
from agent.memory_manager import build_memory_context_block
from agent.memory_provider import is_trivial_prompt
from agent.model_metadata import (
estimate_messages_tokens_rough,
estimate_request_tokens_rough,
@@ -50,37 +50,6 @@ from agent.model_metadata import (
logger = logging.getLogger(__name__)
# Trivial user-input pattern — used to gate the per-turn memory-provider
# prefetch on short, semantically-empty queries. Covers greetings,
# acknowledgements, and trailing-punctuation variants (e.g. "hi!", "hey.",
# "thanks :)").
_RE_TRIVIAL_USER_QUERY = re.compile(
r'^(yes|no|ok|okay|sure|thanks|thank you|y|n|yep|nope|yeah|nah|'
r'hi|hey|hello|yo|sup|'
r'continue|go ahead|do it|proceed|got it|cool|nice|great|done|next|lgtm|k)'
r'[\s!?.:;,"' + "'" + r'~\u2018\u2019\u201c\u201d\u2014\u2013\u2026()\[\]{}<>*&^%$#@!+=`\u00a0]*$',
re.IGNORECASE,
)
def _is_trivial_user_query(query: str) -> bool:
"""Return True if the query is a greeting or too trivial to warrant prefetch.
Strips leading/trailing whitespace first, then checks against
_RE_TRIVIAL_USER_QUERY which permits common trailing punctuation
(exclamation, question mark, emoji, etc.) so variants like "hi!",
"hey.", and "thanks :)" register as trivial.
"""
if not query:
return True
stripped = query.strip()
if not stripped:
return True
if stripped.startswith("/"):
return True
return bool(_RE_TRIVIAL_USER_QUERY.match(stripped))
def compose_user_api_content(
content: Any,
ext_prefetch_cache: str,
@@ -1191,7 +1160,7 @@ def build_turn_context(
if agent._memory_manager:
try:
_query = original_user_message if isinstance(original_user_message, str) else ""
if not _is_trivial_user_query(_query):
if not is_trivial_prompt(_query):
ext_prefetch_cache = agent._memory_manager.prefetch_all(_query) or ""
except Exception:
pass
+5 -9
View File
@@ -23,7 +23,7 @@ import time
from typing import Any, Callable, Dict, List, Optional
from agent.memory_manager import sanitize_context
from agent.memory_provider import MemoryProvider
from agent.memory_provider import TRIVIAL_PROMPT_RE, MemoryProvider
from tools.registry import tool_error
logger = logging.getLogger(__name__)
@@ -1198,14 +1198,10 @@ class HonchoMemoryProvider(MemoryProvider):
# Prompts that carry no semantic signal — trivial acknowledgements, greetings,
# slash commands, empty input. Skipping injection here saves tokens and prevents
# stale user-model context from derailing one-word replies.
_TRIVIAL_PROMPT_RE = re.compile(
r'^(yes|no|ok|okay|sure|thanks|thank you|y|n|yep|nope|yeah|nah|'
r'hi|hey|hello|yo|sup|'
r'continue|go ahead|do it|proceed|got it|cool|nice|great|done|next|lgtm|k)'
r'[\s!?.:;,~]*$',
re.IGNORECASE,
)
# stale user-model context from derailing one-word replies. The pattern is
# shared with the core prefetch gate (agent/memory_provider.TRIVIAL_PROMPT_RE)
# so the provider-side classifier and the core gate can never drift apart.
_TRIVIAL_PROMPT_RE = TRIVIAL_PROMPT_RE
@classmethod
def _is_trivial_prompt(cls, text: str) -> bool:
+10 -4
View File
@@ -148,6 +148,7 @@ from tools.browser_tool import cleanup_browser
# Agent internals extracted to agent/ package for modularity
from agent.memory_manager import sanitize_context
from agent.memory_provider import is_trivial_prompt
from agent.error_classifier import FailoverReason
from agent.redact import redact_sensitive_text
from agent.message_content import flatten_message_text
@@ -4106,10 +4107,15 @@ class AIAgent:
response_text,
**sync_kwargs,
)
self._memory_manager.queue_prefetch_all(
user_text,
session_id=self.session_id or "",
)
# Sibling of the build_turn_context() prefetch gate: warming the
# next turn's recall with a trivial prompt ("hi", "thanks") keys
# provider searches on zero-signal text — skip it. The sync above
# still runs so the turn itself is persisted.
if not is_trivial_prompt(user_text):
self._memory_manager.queue_prefetch_all(
user_text,
session_id=self.session_id or "",
)
except Exception:
pass
+20
View File
@@ -1138,3 +1138,23 @@ class TestMemoryInjectionRejectsMalformedSchema:
names = {t["function"]["name"] for t in agent.tools}
assert names == {"good_tool"}
assert agent.valid_tool_names == {"good_tool"}
class TestTrivialPromptClassifier:
"""is_trivial_prompt — the shared gate for core prefetch + provider injection."""
def test_trivial_variants(self):
from agent.memory_provider import is_trivial_prompt
for t in ("hi", "HI!", "hey.", "hello", "yo", "sup~", "thanks :)",
"done???", "ok", "yes.", "k", "", " ", "/help", "lgtm"):
assert is_trivial_prompt(t), f"expected trivial: {t!r}"
def test_substantive_and_prefix_collisions_pass_through(self):
from agent.memory_provider import is_trivial_prompt
# Words that merely START with a trivial word must not match.
for t in ("k8s", "yolo", "hive", "note", "supper", "hind",
"hello world", "ok so what's next", "what's my name",
"hey can you check the logs", "continue the migration plan"):
assert not is_trivial_prompt(t), f"expected non-trivial: {t!r}"
+32
View File
@@ -208,6 +208,38 @@ def test_returns_turn_context_with_user_message_appended():
assert ctx.active_system_prompt == "SYSTEM"
# ── Trivial-prompt prefetch gate (PR #25350 salvage) ─────────────────────────
#
# The prologue is the ONLY place the per-turn synchronous
# memory_manager.prefetch_all() fires; a bare greeting must not block the
# turn on provider network round-trips, while a substantive question must
# still prefetch. These assert the gate at the call site (the classifier
# itself is covered in tests/agent/test_memory_provider.py).
def _agent_with_memory_manager():
agent = _FakeAgent()
mm = MagicMock()
mm.prefetch_all.return_value = "REMEMBERED CONTEXT"
agent._memory_manager = mm
return agent, mm
def test_prefetch_skipped_for_trivial_user_message():
agent, mm = _agent_with_memory_manager()
ctx = _build(agent, user_message="hi!")
mm.prefetch_all.assert_not_called()
assert ctx.ext_prefetch_cache == ""
def test_prefetch_runs_for_substantive_user_message():
agent, mm = _agent_with_memory_manager()
query = "what did we decide about the deploy pipeline?"
ctx = _build(agent, user_message=query)
mm.prefetch_all.assert_called_once_with(query)
assert ctx.ext_prefetch_cache == "REMEMBERED CONTEXT"
def test_turn_start_replaces_stale_parent_history_with_compression_child():
agent = _FakeAgent()
stale_history = [{"role": "user", "content": "stale parent"}]
+3 -3
View File
@@ -884,7 +884,7 @@ class TestDialecticCadenceAdvancesOnSuccess:
provider._turn_count = 5
provider._last_dialectic_turn = 0
provider.queue_prefetch("hello")
provider.queue_prefetch("what changed in the repo today")
if provider._prefetch_thread:
provider._prefetch_thread.join(timeout=2.0)
@@ -909,7 +909,7 @@ class TestDialecticCadenceAdvancesOnSuccess:
provider._prefetch_thread = fresh
provider._prefetch_thread_started_at = _time.monotonic() # fresh start
provider.queue_prefetch("hello")
provider.queue_prefetch("what changed in the repo today")
# Should have short-circuited — no new dialectic call
assert provider._manager.dialectic_query.call_count == 0
hold.set()
@@ -1015,7 +1015,7 @@ class TestDialecticLiveness:
# timeout=2.0, multiplier=2.0, so anything older than 4s is stale
p._prefetch_thread_started_at = 0.0 # very old (1970 monotonic baseline)
p.queue_prefetch("hello")
p.queue_prefetch("what changed in the repo today")
# New thread should have been spawned since stuck one is stale
assert p._prefetch_thread is not stuck, "stale thread must be recycled"
if p._prefetch_thread: