Skip to content

Feature: Implicit Signal Detection & Behavioral Nudges — Detect User State from Text and Adjust Agent Behavior (inspired by NeuroSkill) #692

@teknium1

Description

@teknium1

Overview

When a user says "this stupid thing still won't compile, I've been at this for hours", the agent should pick up on the frustration and fatigue — not because the user asked for empathy, but because the signals are right there in the text. Today, Hermes ignores these implicit signals entirely and responds the same way whether the user is calm, frustrated, confused, or in a rush.

NeuroSkill (MIT Media Lab, March 2026) uses 36 regex-based "signal detectors" that scan every user message for implicit state indicators. When signals fire, the agent receives behavioral guidance that changes how the LLM responds. The detection itself is trivial — the behavioral nudges that result from detection are what actually matter.

This feature adds a lightweight signal detector that scans each user message and prepends a behavioral nudge to the user message itself before the LLM sees it. The nudge is invisible to the user but visible to the model, steering it to respond more appropriately. No system prompt changes, no cache invalidation, no external dependencies.

Replaces the relevant portion of #500 (closed as over-scoped).


Critical Constraint: Never Modify the System Prompt Per-Turn

The system prompt is frozen at session start (run_agent.py line 1359) and only rebuilt after context compression. Changing it per-turn would break prompt caching and waste tokens.

Nudges must be injected into the user message, not the system prompt. This is the only per-turn content that changes anyway. The nudge is prepended to the user's text as a brief instruction block:

[Context: user appears frustrated and fatigued. Acknowledge briefly, pivot to alternative approach, don't repeat what they've tried.]

this stupid thing still won't compile, I've been at this for hours

The model sees the nudge as part of the user turn. The actual user never sees it (it's added server-side before the API call). The system prompt, conversation history, and cache remain untouched.


How It Works

1. Signal Detection (~10 categories)

A new signal_detector.py module with regex-based pattern matching on the user's message text. Each signal is a set of patterns with word boundaries:

SIGNALS = {
    "frustration": [
        r"\b(stupid|annoying|ugh|wtf|damn|hate this|sick of)\b",
        r"\b(still (not|won't|doesn't|can't))\b",
        r"\b(for hours|all day|keeps? (failing|breaking|crashing))\b",
        r"\b(tried everything|nothing works|give up)\b",
    ],
    "confusion": [
        r"\b(confused|don't (understand|get it)|makes? no sense)\b",
        r"\b(lost|stuck|no idea|bewildered)\b",
    ],
    "urgency": [
        r"\b(asap|urgent|deadline|due (today|tomorrow|soon))\b",
        r"\b(quickly|fast|hurry|right now|immediately)\b",
    ],
    "fatigue": [
        r"\b(exhausted|tired|burned? out|drained|sleepy)\b",
        r"\b(been (at|doing|working on) this .{0,15}(hours|all day))\b",
        r"\b(can't think|brain.{0,5}(fried|dead|mush))\b",
    ],
    "learning": [
        r"\b(learning|studying|new to|beginner|first time)\b",
        r"\b(explain|teach|walk me through|eli5)\b",
    ],
    "exploration": [
        r"\b(curious|wondering|what if|explore|experiment)\b",
        r"\b(brainstorm|ideas?|possibilities)\b",
    ],
    "celebration": [
        r"\b(it works|finally|hell yeah|awesome|nailed it)\b",
        r"\b(fixed|solved|figured (it )?out|got it)\b",
    ],
    "anxiety": [
        r"\b(worried|nervous|anxious|scared|afraid)\b",
        r"\b(might (break|fail|crash)|what if .{0,20}(goes wrong|breaks))\b",
    ],
    "overwhelm": [
        r"\b(overwhelm|too (much|many)|where do I (start|begin))\b",
        r"\b(information overload|drowning in)\b",
    ],
    "deep_work": [
        r"\b(deep (dive|work)|focus|concentrate|in the zone)\b",
        r"\b(don't (interrupt|distract)|flow state)\b",
    ],
}

Returns dict[str, bool] of which signals fired:

def detect_signals(message: str) -> dict[str, bool]:
    lowered = message.lower()
    return {name: any(re.search(p, lowered) for p in patterns)
            for name, patterns in SIGNALS.items()}

2. Behavioral Nudges (the actual behavior change)

Each signal maps to a short nudge string:

NUDGES = {
    "frustration": "User sounds frustrated. Acknowledge briefly, pivot to a concrete alternative. Don't repeat what they've tried.",
    "confusion": "User seems confused. Use simpler language, break into clear steps, lead with a concrete example.",
    "urgency": "User is in a hurry. Lead with the answer, skip preamble, most direct solution first.",
    "fatigue": "User sounds tired/burned out. Keep response focused, avoid overload. Offer to handle more autonomously.",
    "learning": "User is learning. Be patient, explain concepts before implementation, point out beginner pitfalls.",
    "exploration": "User is exploring. Be creative, suggest multiple approaches, encourage experimentation.",
    "celebration": "User just succeeded. Match their energy briefly, then suggest next steps.",
    "anxiety": "User is anxious about risk. Reassure with specifics, suggest reversible approaches, offer dry runs.",
    "overwhelm": "User is overwhelmed. Give ONE clear next step, offer to break the problem down.",
    "deep_work": "User is in deep focus. Be precise and efficient, no small talk.",
}

3. Injection Point: Prepend to User Message

def build_nudge_prefix(message: str) -> str:
    """Detect signals and return a nudge prefix to prepend to the user message.
    Returns empty string if no signals detected."""
    signals = detect_signals(message)
    active = [NUDGES[name] for name, fired in signals.items() if fired]
    if not active:
        return ""
    nudge_text = " ".join(active)
    return f"[Context: {nudge_text}]\n\n"

In the agent run loop, before sending the user message to the LLM:

nudge = build_nudge_prefix(user_message)
if nudge:
    api_message_content = nudge + user_message
else:
    api_message_content = user_message

This changes ZERO cached content. The system prompt stays frozen. The conversation history stays untouched. Only the new user message (which is never cached) gets the prefix.


Current State in Hermes Agent

Component Status
System prompt ✅ Frozen at session start, rebuilt only on compression. Must not be modified per-turn.
Prompt caching prompt_caching.py caches system + last 3 messages. Nudges in user message won't affect cache.
User message processing ✅ Agent receives user message before API call — integration point for prepending nudge
Signal detection ❌ Nothing like this exists today

Implementation

Files to Create

  • agent/signal_detector.pydetect_signals(), SIGNALS patterns, NUDGES mapping, build_nudge_prefix() (~100-120 lines)

Files to Modify

  • Agent run loop (where user message is prepared for API call) — Prepend nudge prefix (~5-8 lines)

Files NOT Modified

  • agent/prompt_builder.pyNo changes. System prompt is untouched.
  • agent/prompt_caching.pyNo changes. Cache strategy is unaffected.

Tests

  • tests/agent/test_signal_detector.py — Each signal fires on expected inputs, doesn't fire on neutral text, nudge prefix format is correct, no signals = empty string (~80-100 lines)

Total Effort

~200 lines of new code + tests. Single PR, no cache impact.


Pros & Cons

Pros

  • Cache-safe — Only modifies the new user message, never touches system prompt or history
  • Immediate behavior improvement — Agent becomes noticeably more context-aware
  • Zero dependencies — Pure Python regex, no external services, no config
  • Invisible to user — Prefix is added server-side; user just notices the agent "gets" them
  • Cheap — Regex is microseconds; nudge prefix is ~20-40 tokens per message
  • Easy to extend — New signal = new regex list + nudge string

Cons

  • False positives — "I'm tired of hearing about React" triggers fatigue but user isn't tired. Mitigation: keep nudges subtle.
  • English-only — Regex patterns are English. Could add i18n later.
  • Token overhead — ~20-40 tokens per message when signals fire. Negligible but non-zero.
  • Nudge in user message — Some models might treat the [Context: ...] prefix as user instruction rather than system guidance. Need to test with different models.

Open Questions

  • Should the nudge prefix be wrapped in a specific tag format that models interpret as system-level? E.g., <system_note> or [INTERNAL:] vs plain [Context:]
  • Should nudges be logged anywhere for debugging/evaluation? Or completely invisible?
  • Should there be a config flag to disable signal detection entirely?
  • How to handle multi-message conversations — if the user was frustrated 3 messages ago but is now calm, should old nudges persist or fade?

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions