mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-24 05:41:40 +00:00
* feat(process-registry): add format_process_notification shared helper * feat(process-registry): add drain_notifications method * refactor(cli): use shared drain_notifications and format_process_notification * feat(tui): add background notification poller for completion_queue * feat(tui): wire notification poller into session init/finalize * refactor(tui): add post-turn drain using shared helper as safety net
This commit is contained in:
parent
db84a78e61
commit
d5416284f1
5 changed files with 486 additions and 55 deletions
|
|
@ -287,6 +287,9 @@ def _finalize_session(session: dict | None, end_reason: str = "tui_close") -> No
|
|||
if not session or session.get("_finalized"):
|
||||
return
|
||||
session["_finalized"] = True
|
||||
stop_event = session.get("_notif_stop")
|
||||
if stop_event is not None:
|
||||
stop_event.set()
|
||||
|
||||
agent = session.get("agent")
|
||||
lock = session.get("history_lock")
|
||||
|
|
@ -579,6 +582,7 @@ def _start_agent_build(sid: str, session: dict) -> None:
|
|||
pass
|
||||
|
||||
_wire_callbacks(sid)
|
||||
_sessions[sid]["_notif_stop"] = _start_notification_poller(sid, _sessions[sid])
|
||||
_notify_session_boundary("on_session_reset", key)
|
||||
|
||||
info = _session_info(agent)
|
||||
|
|
@ -1955,6 +1959,7 @@ def _init_session(sid: str, key: str, agent, history: list, cols: int = 80):
|
|||
# session startup resilient).
|
||||
pass
|
||||
_wire_callbacks(sid)
|
||||
_sessions[sid]["_notif_stop"] = _start_notification_poller(sid, _sessions[sid])
|
||||
_notify_session_boundary("on_session_reset", key)
|
||||
_emit("session.info", sid, _session_info(agent))
|
||||
|
||||
|
|
@ -3027,6 +3032,105 @@ def _(rid, params: dict) -> dict:
|
|||
return _ok(rid, {"status": "streaming"})
|
||||
|
||||
|
||||
def _notification_poller_loop(
|
||||
stop_event: threading.Event, sid: str, session: dict
|
||||
) -> None:
|
||||
"""Poll completion_queue and dispatch notifications autonomously.
|
||||
|
||||
Runs in a daemon thread started by _init_session(). Emits a
|
||||
status.update (kind=process) for user visibility, then chains an
|
||||
agent turn via _run_prompt_submit if the session is idle.
|
||||
|
||||
NOTE: The completion_queue is global (one per process). If multiple
|
||||
TUI sessions coexist, whichever poller wakes first grabs the event,
|
||||
even if the process was started by a different session. This matches
|
||||
CLI/gateway behavior (single session per process).
|
||||
"""
|
||||
from tools.process_registry import process_registry, format_process_notification
|
||||
|
||||
while not stop_event.is_set() and not session.get("_finalized"):
|
||||
try:
|
||||
evt = process_registry.completion_queue.get(timeout=0.5)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
_evt_sid = evt.get("session_id", "")
|
||||
if evt.get("type") == "completion" and process_registry.is_completion_consumed(_evt_sid):
|
||||
continue
|
||||
|
||||
text = format_process_notification(evt)
|
||||
if not text:
|
||||
continue
|
||||
|
||||
_emit("status.update", sid, {"kind": "process", "text": text})
|
||||
|
||||
with session["history_lock"]:
|
||||
if session.get("running"):
|
||||
process_registry.completion_queue.put(evt)
|
||||
continue
|
||||
session["running"] = True
|
||||
|
||||
rid = f"__notif__{int(time.time() * 1000)}"
|
||||
try:
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, text)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[tui_gateway] notification poller dispatch failed: "
|
||||
f"{type(exc).__name__}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
with session["history_lock"]:
|
||||
session["running"] = False
|
||||
|
||||
# Drain any remaining events after stop signal (process all pending
|
||||
# before exiting so nothing is lost on shutdown).
|
||||
while not process_registry.completion_queue.empty():
|
||||
try:
|
||||
evt = process_registry.completion_queue.get_nowait()
|
||||
except Exception:
|
||||
break
|
||||
_evt_sid = evt.get("session_id", "")
|
||||
if evt.get("type") == "completion" and process_registry.is_completion_consumed(_evt_sid):
|
||||
continue
|
||||
text = format_process_notification(evt)
|
||||
if not text:
|
||||
continue
|
||||
|
||||
_emit("status.update", sid, {"kind": "process", "text": text})
|
||||
|
||||
with session["history_lock"]:
|
||||
if session.get("running"):
|
||||
process_registry.completion_queue.put(evt)
|
||||
break
|
||||
session["running"] = True
|
||||
|
||||
rid = f"__notif__{int(time.time() * 1000)}"
|
||||
try:
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, text)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[tui_gateway] notification poller dispatch failed: "
|
||||
f"{type(exc).__name__}: {exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
with session["history_lock"]:
|
||||
session["running"] = False
|
||||
|
||||
|
||||
def _start_notification_poller(sid: str, session: dict) -> threading.Event:
|
||||
"""Start the background notification poller for a TUI session."""
|
||||
stop = threading.Event()
|
||||
t = threading.Thread(
|
||||
target=_notification_poller_loop,
|
||||
args=(stop, sid, session),
|
||||
daemon=True,
|
||||
)
|
||||
t.start()
|
||||
return stop
|
||||
|
||||
|
||||
def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
|
||||
with session["history_lock"]:
|
||||
history = list(session["history"])
|
||||
|
|
@ -3385,6 +3489,36 @@ def _run_prompt_submit(rid, sid: str, session: dict, text: Any) -> None:
|
|||
with session["history_lock"]:
|
||||
session["running"] = False
|
||||
|
||||
# Drain completion notifications that arrived during this turn.
|
||||
# The background poller handles between-turn delivery; this is
|
||||
# the safety net for events that arrived mid-turn.
|
||||
try:
|
||||
from tools.process_registry import process_registry
|
||||
|
||||
for _evt, synth in process_registry.drain_notifications():
|
||||
with session["history_lock"]:
|
||||
if session.get("running"):
|
||||
process_registry.completion_queue.put(_evt)
|
||||
break
|
||||
session["running"] = True
|
||||
try:
|
||||
_emit("message.start", sid)
|
||||
_run_prompt_submit(rid, sid, session, synth)
|
||||
except Exception as _n_exc:
|
||||
print(
|
||||
f"[tui_gateway] completion notification dispatch failed: "
|
||||
f"{type(_n_exc).__name__}: {_n_exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
with session["history_lock"]:
|
||||
session["running"] = False
|
||||
except Exception as _drain_exc:
|
||||
print(
|
||||
f"[tui_gateway] completion queue drain failed: "
|
||||
f"{type(_drain_exc).__name__}: {_drain_exc}",
|
||||
file=sys.stderr,
|
||||
)
|
||||
|
||||
threading.Thread(target=run, daemon=True).start()
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue