mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-30 16:01:49 +08:00
feat(gateway): live-stream /update output + forward interactive prompts
Adds real-time output streaming and interactive prompt forwarding for the gateway /update command, so users on Telegram/Discord/etc see the full update progress and can respond to prompts (stash restore, config migration) without needing terminal access. Changes: hermes_cli/main.py: - Add --gateway flag to 'hermes update' argparse - Add _gateway_prompt() file-based IPC function that writes .update_prompt.json and polls for .update_response - Modify _restore_stashed_changes() to accept optional input_fn parameter for gateway mode prompt forwarding - cmd_update() uses _gateway_prompt when --gateway is set, enabling interactive stash restore and config migration prompts gateway/run.py: - _handle_update_command: spawn with --gateway flag and PYTHONUNBUFFERED=1 for real-time output flushing - Store session_key in .update_pending.json for cross-restart session matching - Add _update_prompt_pending dict to track sessions awaiting update prompt responses - Replace _watch_for_update_completion with _watch_update_progress: streams output chunks every ~4s, detects .update_prompt.json and forwards prompts to the user, handles completion/failure/timeout - Add update prompt interception in _handle_message: when a prompt is pending, the user's next message is written to .update_response instead of being processed normally - Preserve _send_update_notification as legacy fallback for post-restart cases where adapter isn't available yet File-based IPC protocol: - .update_prompt.json: written by update process with prompt text, default value, and unique ID - .update_response: written by gateway with user's answer - .update_output.txt: existing, now streamed in real-time - .update_exit_code: existing completion marker Tests: 16 new tests covering _gateway_prompt IPC, output streaming, prompt detection/forwarding, message interception, and cleanup.
This commit is contained in:
@@ -2554,6 +2554,57 @@ def _clear_bytecode_cache(root: Path) -> int:
|
||||
return removed
|
||||
|
||||
|
||||
def _gateway_prompt(prompt_text: str, default: str = "", timeout: float = 300.0) -> str:
|
||||
"""File-based IPC prompt for gateway mode.
|
||||
|
||||
Writes a prompt marker file so the gateway can forward the question to the
|
||||
user, then polls for a response file. Falls back to *default* on timeout.
|
||||
|
||||
Used by ``hermes update --gateway`` so interactive prompts (stash restore,
|
||||
config migration) are forwarded to the messenger instead of being silently
|
||||
skipped.
|
||||
"""
|
||||
import json as _json
|
||||
import uuid as _uuid
|
||||
from hermes_constants import get_hermes_home
|
||||
|
||||
home = get_hermes_home()
|
||||
prompt_path = home / ".update_prompt.json"
|
||||
response_path = home / ".update_response"
|
||||
|
||||
# Clean any stale response file
|
||||
response_path.unlink(missing_ok=True)
|
||||
|
||||
payload = {
|
||||
"prompt": prompt_text,
|
||||
"default": default,
|
||||
"id": str(_uuid.uuid4()),
|
||||
}
|
||||
tmp = prompt_path.with_suffix(".tmp")
|
||||
tmp.write_text(_json.dumps(payload))
|
||||
tmp.replace(prompt_path)
|
||||
|
||||
# Poll for response
|
||||
import time as _time
|
||||
deadline = _time.monotonic() + timeout
|
||||
while _time.monotonic() < deadline:
|
||||
if response_path.exists():
|
||||
try:
|
||||
answer = response_path.read_text().strip()
|
||||
response_path.unlink(missing_ok=True)
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
return answer if answer else default
|
||||
except (OSError, ValueError):
|
||||
pass
|
||||
_time.sleep(0.5)
|
||||
|
||||
# Timeout — clean up and use default
|
||||
prompt_path.unlink(missing_ok=True)
|
||||
response_path.unlink(missing_ok=True)
|
||||
print(f" (no response after {int(timeout)}s, using default: {default!r})")
|
||||
return default
|
||||
|
||||
|
||||
def _update_via_zip(args):
|
||||
"""Update Hermes Agent by downloading a ZIP archive.
|
||||
|
||||
@@ -2747,6 +2798,7 @@ def _restore_stashed_changes(
|
||||
cwd: Path,
|
||||
stash_ref: str,
|
||||
prompt_user: bool = False,
|
||||
input_fn=None,
|
||||
) -> bool:
|
||||
if prompt_user:
|
||||
print()
|
||||
@@ -2754,7 +2806,10 @@ def _restore_stashed_changes(
|
||||
print(" Restoring them may reapply local customizations onto the updated codebase.")
|
||||
print(" Review the result afterward if Hermes behaves unexpectedly.")
|
||||
print("Restore local changes now? [Y/n]")
|
||||
response = input().strip().lower()
|
||||
if input_fn is not None:
|
||||
response = input_fn("Restore local changes now? [Y/n]", "y")
|
||||
else:
|
||||
response = input().strip().lower()
|
||||
if response not in ("", "y", "yes"):
|
||||
print("Skipped restoring local changes.")
|
||||
print("Your changes are still preserved in git stash.")
|
||||
@@ -3185,6 +3240,10 @@ def cmd_update(args):
|
||||
if is_managed():
|
||||
managed_error("update Hermes Agent")
|
||||
return
|
||||
|
||||
gateway_mode = getattr(args, "gateway", False)
|
||||
# In gateway mode, use file-based IPC for prompts instead of stdin
|
||||
gw_input_fn = (lambda prompt, default="": _gateway_prompt(prompt, default)) if gateway_mode else None
|
||||
|
||||
print("⚕ Updating Hermes Agent...")
|
||||
print()
|
||||
@@ -3281,7 +3340,9 @@ def cmd_update(args):
|
||||
else:
|
||||
auto_stash_ref = _stash_local_changes_if_needed(git_cmd, PROJECT_ROOT)
|
||||
|
||||
prompt_for_restore = auto_stash_ref is not None and sys.stdin.isatty() and sys.stdout.isatty()
|
||||
prompt_for_restore = auto_stash_ref is not None and (
|
||||
gateway_mode or (sys.stdin.isatty() and sys.stdout.isatty())
|
||||
)
|
||||
|
||||
# Check if there are updates
|
||||
result = subprocess.run(
|
||||
@@ -3300,6 +3361,7 @@ def cmd_update(args):
|
||||
_restore_stashed_changes(
|
||||
git_cmd, PROJECT_ROOT, auto_stash_ref,
|
||||
prompt_user=prompt_for_restore,
|
||||
input_fn=gw_input_fn,
|
||||
)
|
||||
if current_branch not in ("main", "HEAD"):
|
||||
subprocess.run(
|
||||
@@ -3351,6 +3413,7 @@ def cmd_update(args):
|
||||
PROJECT_ROOT,
|
||||
auto_stash_ref,
|
||||
prompt_user=prompt_for_restore,
|
||||
input_fn=gw_input_fn,
|
||||
)
|
||||
|
||||
_invalidate_update_cache()
|
||||
@@ -3490,7 +3553,11 @@ def cmd_update(args):
|
||||
print(f" ℹ️ {len(missing_config)} new config option(s) available")
|
||||
|
||||
print()
|
||||
if not (sys.stdin.isatty() and sys.stdout.isatty()):
|
||||
if gateway_mode:
|
||||
response = _gateway_prompt(
|
||||
"Would you like to configure new options now? [Y/n]", "n"
|
||||
).strip().lower()
|
||||
elif not (sys.stdin.isatty() and sys.stdout.isatty()):
|
||||
print(" ℹ Non-interactive session — skipping config migration prompt.")
|
||||
print(" Run 'hermes config migrate' later to apply any new config/env options.")
|
||||
response = "n"
|
||||
@@ -3502,11 +3569,15 @@ def cmd_update(args):
|
||||
|
||||
if response in ('', 'y', 'yes'):
|
||||
print()
|
||||
results = migrate_config(interactive=True, quiet=False)
|
||||
# In gateway mode, run auto-migrations only (no input() prompts
|
||||
# for API keys which would hang the detached process).
|
||||
results = migrate_config(interactive=not gateway_mode, quiet=False)
|
||||
|
||||
if results["env_added"] or results["config_added"]:
|
||||
print()
|
||||
print("✓ Configuration updated!")
|
||||
if gateway_mode and missing_env:
|
||||
print(" ℹ API keys require manual entry: hermes config migrate")
|
||||
else:
|
||||
print()
|
||||
print("Skipped. Run 'hermes config migrate' later to configure.")
|
||||
@@ -5247,6 +5318,10 @@ For more help on a command:
|
||||
help="Update Hermes Agent to the latest version",
|
||||
description="Pull the latest changes from git and reinstall dependencies"
|
||||
)
|
||||
update_parser.add_argument(
|
||||
"--gateway", action="store_true", default=False,
|
||||
help="Gateway mode: use file-based IPC for prompts instead of stdin (used internally by /update)"
|
||||
)
|
||||
update_parser.set_defaults(func=cmd_update)
|
||||
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user