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.py — detect_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.py — No changes. System prompt is untouched.
agent/prompt_caching.py — No 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
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.pyline 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:
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.pymodule with regex-based pattern matching on the user's message text. Each signal is a set of patterns with word boundaries:Returns
dict[str, bool]of which signals fired:2. Behavioral Nudges (the actual behavior change)
Each signal maps to a short nudge string:
3. Injection Point: Prepend to User Message
In the agent run loop, before sending the user message to the LLM:
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
prompt_caching.pycaches system + last 3 messages. Nudges in user message won't affect cache.Implementation
Files to Create
agent/signal_detector.py—detect_signals(),SIGNALSpatterns,NUDGESmapping,build_nudge_prefix()(~100-120 lines)Files to Modify
Files NOT Modified
agent/prompt_builder.py— No changes. System prompt is untouched.agent/prompt_caching.py— No 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
Cons
[Context: ...]prefix as user instruction rather than system guidance. Need to test with different models.Open Questions
<system_note>or[INTERNAL:]vs plain[Context:]References
run_agent.pylines 1355-1420 — System prompt assembly (frozen, must not change per-turn)agent/prompt_caching.py— Cache strategy (system + last 3 messages)