fix(cli): only prefix voice-transcribed messages with the voice-input instruction (#65827)

Typed messages sent while voice mode was active were also getting the
'[Voice input — respond concisely...]' API-local prefix, because the gate
checked only self._voice_mode. Route STT transcripts through a
_VoiceInputMessage sentinel in _pending_input (both the PTT/continuous
transcription path and the barge-in utterance path), unwrap it in
process_loop, and thread voice_input= through chat() so the prefix applies
only to genuinely voice-transcribed messages.

Re-cut of PR #65961 (@webtecnica) — the original diff had the sentinel
class embedded inside __init__'s docstring. Credit also to the earliest
route-by-origin attempt in PR #11744 (@KeroZelvin).

Fixes #65827
Closes #65961
Closes #11744
This commit is contained in:
webtecnica 2026-07-28 09:27:40 -07:00 committed by Teknium
parent 3524b20728
commit 0062107094
1 changed files with 31 additions and 6 deletions

37
cli.py
View File

@ -4075,6 +4075,23 @@ def _normalize_moa_model(model: Optional[str]) -> tuple[Optional[str], Optional[
return None, model
class _VoiceInputMessage:
"""Sentinel wrapper for voice-transcribed messages in ``_pending_input``.
Distinguishes STT output from manually typed text while voice mode is
active, so the concise-voice-response prefix is applied only to messages
that actually came from the microphone (#65827).
"""
__slots__ = ("text",)
def __init__(self, text: str):
self.text = text
def __str__(self) -> str:
return self.text
class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
"""
Interactive CLI for the Hermes Agent.
@ -11928,7 +11945,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
self._attached_images.clear()
if hasattr(self, '_app') and self._app:
self._app.invalidate()
self._pending_input.put(transcript)
self._pending_input.put(_VoiceInputMessage(transcript))
submitted = True
elif result.get("success"):
_cprint(f"{_DIM}No speech detected.{_RST}")
@ -12116,7 +12133,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
_cprint(f"\n{_DIM}Stop phrase detected — ending voice chat.{_RST}")
self._disable_voice_mode()
return
self._pending_input.put(transcript)
self._pending_input.put(_VoiceInputMessage(transcript))
submitted = True
elif not result.get("success"):
_cprint(f"\n{_DIM}Transcription failed: {result.get('error', 'Unknown error')}{_RST}")
@ -12820,7 +12837,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
except Exception:
pass
def chat(self, message, images: list = None) -> Optional[str]:
def chat(self, message, images: list = None, voice_input: bool = False) -> Optional[str]:
"""
Send a message to the agent and get a response.
@ -12835,6 +12852,8 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
Args:
message: The user's message (str or multimodal content list)
images: Optional list of Path objects for attached images
voice_input: True when the message came from voice transcription
(gates the concise voice-response prefix, #65827)
Returns:
The agent's response, or None on error
@ -13062,7 +13081,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
# model responds concisely. The prefix is API-call-local only —
# run_conversation persists the original clean user message.
_voice_prefix = ""
if self._voice_mode and isinstance(message, str):
if voice_input and isinstance(message, str):
_voice_prefix = (
"[Voice input — respond concisely and conversationally, "
"2-3 sentences max. No code blocks or markdown.] "
@ -16226,7 +16245,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
except Exception:
pass
continue
# Voice-transcribed messages arrive wrapped in a sentinel
# so only genuine STT output gets the voice prefix (#65827).
is_voice_input = isinstance(user_input, _VoiceInputMessage)
if is_voice_input:
user_input = user_input.text
if not user_input:
continue
@ -16322,7 +16347,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
app.invalidate() # Refresh status line
try:
self.chat(user_input, images=submit_images or None)
self.chat(user_input, images=submit_images or None, voice_input=is_voice_input)
finally:
self._agent_running = False
self._spinner_text = ""