From 422d9da9bd6468881ed1fb7e1673141315218a66 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 10 Jul 2026 05:41:59 -0500 Subject: [PATCH] feat(agent): core affection reaction detector + reaction_callback MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a token-free, curated affection matcher (agent/reactions.py) — the single source of truth for detecting user "vibes" (ily / <3 / good bot / heart emoji). No model call, no tokens. Generalized to return a reaction *kind* so future reactions can ride the same signal. Wire an opt-in AIAgent.reaction_callback that fires from build_turn_context on the incoming user message. It never touches the conversation (cache-safe) and never fatal — a purely cosmetic side-beat each host can consume. --- agent/agent_init.py | 2 ++ agent/reactions.py | 56 +++++++++++++++++++++++++++++++++++ agent/turn_context.py | 14 +++++++++ run_agent.py | 2 ++ tests/agent/test_reactions.py | 56 +++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+) create mode 100644 agent/reactions.py create mode 100644 tests/agent/test_reactions.py diff --git a/agent/agent_init.py b/agent/agent_init.py index f1666a1aafb4..0f9376d6c795 100644 --- a/agent/agent_init.py +++ b/agent/agent_init.py @@ -302,6 +302,7 @@ def init_agent( notice_callback: callable = None, notice_clear_callback: callable = None, event_callback: Optional[Callable[[str, dict], None]] = None, + reaction_callback: Optional[Callable[[str], None]] = None, max_tokens: int = None, reasoning_config: Dict[str, Any] = None, service_tier: str = None, @@ -535,6 +536,7 @@ def init_agent( agent.notice_callback = notice_callback agent.notice_clear_callback = notice_clear_callback agent.event_callback = event_callback + agent.reaction_callback = reaction_callback agent.tool_gen_callback = tool_gen_callback diff --git a/agent/reactions.py b/agent/reactions.py new file mode 100644 index 000000000000..375366ff7099 --- /dev/null +++ b/agent/reactions.py @@ -0,0 +1,56 @@ +"""Token-free detection of user *reactions* to the agent. + +Currently the only reaction is ``vibe`` — an expression of affection or +gratitude toward the agent (``ily``, ``<3``, ``love you``, ``good bot``, a heart +emoji, …). Detection is a curated regex/lexicon: **no model call, no tokens**. + +This is the single source of truth shared by every surface — the CLI pet, the +TUI heart, and the desktop floating hearts all react off the same signal, +delivered via ``AIAgent.reaction_callback`` (wired per interactive host). + +Generalized on purpose: :func:`detect_reaction` returns a reaction *kind* +string, so new kinds (other emoji reactions, etc.) can be added here without +touching any caller. We match affection specifically — not general positive +sentiment — so "this is great" does NOT fire, but "good bot" / "❤️" do. +""" + +from __future__ import annotations + +import re + +#: The affection/gratitude reaction — the only kind today. +VIBE = "vibe" + +# Curated affection lexicon. Kept deliberately narrow: gratitude + love aimed at +# the agent, heart emoji, and ``<3`` (but not the broken heart `` str | None: + """Return the reaction kind for *text* (currently :data:`VIBE`), or ``None``. + + Pure, token-free, and safe to call on every user turn. + """ + if not text: + return None + + return VIBE if _VIBE_RE.search(text) else None diff --git a/agent/turn_context.py b/agent/turn_context.py index 4fd6ddff2c3e..a5e738588e48 100644 --- a/agent/turn_context.py +++ b/agent/turn_context.py @@ -319,6 +319,20 @@ def build_turn_context( current_turn_user_idx = len(messages) - 1 agent._persist_user_message_idx = current_turn_user_idx + # Cosmetic side-signal: detect an affection "reaction" (ily / <3 / good bot) + # and notify the host so it can play hearts. Token-free, never touches the + # conversation, and never fatal — a purely optional UI beat. + reaction_callback = getattr(agent, "reaction_callback", None) + if reaction_callback is not None: + try: + from agent.reactions import detect_reaction + + kind = detect_reaction(original_user_message) + if kind: + reaction_callback(kind) + except Exception: + pass + if not agent.quiet_mode: _print_preview = summarize_user_message_for_log(user_message) agent._safe_print( diff --git a/run_agent.py b/run_agent.py index 61e8657b9a87..89c746235b50 100644 --- a/run_agent.py +++ b/run_agent.py @@ -458,6 +458,7 @@ class AIAgent: notice_callback: callable = None, notice_clear_callback: callable = None, event_callback: Optional[Callable[[str, dict], None]] = None, + reaction_callback: Optional[Callable[[str], None]] = None, max_tokens: int = None, reasoning_config: Dict[str, Any] = None, service_tier: str = None, @@ -533,6 +534,7 @@ class AIAgent: notice_callback=notice_callback, notice_clear_callback=notice_clear_callback, event_callback=event_callback, + reaction_callback=reaction_callback, max_tokens=max_tokens, reasoning_config=reasoning_config, service_tier=service_tier, diff --git a/tests/agent/test_reactions.py b/tests/agent/test_reactions.py new file mode 100644 index 000000000000..3b29b1facf5e --- /dev/null +++ b/tests/agent/test_reactions.py @@ -0,0 +1,56 @@ +"""Behavior tests for the token-free reaction detector.""" + +import pytest + +from agent.reactions import VIBE, detect_reaction + + +@pytest.mark.parametrize( + "text", + [ + "good bot", + "Good Bot!", + "ily", + "ilysm", + "i love you", + "love you", + "love u", + "luv ya", + "thanks", + "thank you", + "thx", + "ty", + "tysm", + "you're the best <3", + "here you go <33", + "❤️", + "🥰 amazing", + "sending 💖", + "great job, thank you so much!", + ], +) +def test_affection_fires_vibe(text): + assert detect_reaction(text) == VIBE + + +@pytest.mark.parametrize( + "text", + [ + "", + None, + "run the tests", + "this is great", # positive sentiment, NOT affection — must not fire + "awesome work on the refactor", + "it's broken