mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-11-06 05:17:15 +00:00
Background processes now run in the gateway, so the CLI process's own tools.process_registry is empty: main's dock showed nothing, `⚙ N` never appeared and /stop printed "No running background processes." while a job ran. - hermes_cli/gateway_tools.py: GatewayProcessView answers the registry calls main's dock, status bar and /stop make (list_sessions / get / count_running / kill_process / kill_all) from process.list / process.kill / process.stop; polled at most once a second and only while a tool just finished or a listed process is live or recently exited; the status bar reads the cached count (no RPC on a repaint). Bound when the CLI builds its gateway-backed agent. - process.list rows gain started_epoch / exited_at / completion_reason (dock elapsed, exit age, killed/lost verdict) and a 32k output tail (main's dock tail view). - process.stop also interrupts the async delegations THIS session dispatched (main's /stop interrupted them), reported as `delegations`; the status bar's subagent count reads session.usage.active_subagents. - Main's dock / status-bar / /stop code is unchanged apart from where the registry comes from. LEDGER: W11 (IPS-078, IPS-079, IPS-081, IPS-084, IPS-085, SLASH-E1-020). (cherry picked from commit 4d6b1dd25793b51fa1e70eb4c563a4f36862bf72)
113 lines
4.4 KiB
Python
113 lines
4.4 KiB
Python
"""Background-process rows for the classic CLI live-work dock.
|
|
|
|
Sibling of ``cli_subagent_monitor``: the dock paints one "Processes" block under the
|
|
subagent rows from the same snapshot the ``process_manage`` tool reads, so a
|
|
``terminal(background=true)`` spawn is visible the moment it starts instead of only
|
|
when its completion notification lands.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import time
|
|
|
|
from agent.i18n import t
|
|
|
|
# A finished process stays on the dock long enough to read its exit line, then
|
|
# leaves; the completion notification in the transcript is the durable record.
|
|
RETAIN_SECONDS = 60
|
|
|
|
# Reader-thread exits report "exited"; `kill_process` / `kill_all` stamp "killed".
|
|
_STATUS_BY_REASON = {"killed": "killed", "lost": "lost", "failed_start": "failed"}
|
|
|
|
|
|
def _last_output_line(buffer: str) -> str:
|
|
for line in reversed((buffer or "").splitlines()):
|
|
text = " ".join(line.split())
|
|
if text:
|
|
return text
|
|
return ""
|
|
|
|
|
|
def process_status(entry: dict) -> str:
|
|
if entry.get("status") != "exited":
|
|
return "running"
|
|
reason = _STATUS_BY_REASON.get(entry.get("completion_reason") or "")
|
|
if reason:
|
|
return reason
|
|
return "done" if not entry.get("exit_code") else "failed"
|
|
|
|
|
|
def process_rows(now: float | None = None) -> list:
|
|
"""Running processes plus those that exited within ``RETAIN_SECONDS``, newest-running first.
|
|
|
|
The classic CLI is one conversation per OS process, so every registry entry (its own
|
|
spawns and any a child handed back) belongs to this dock; gateway-hosted surfaces scope
|
|
by session key in ``tui_gateway`` instead.
|
|
"""
|
|
from hermes_cli.gateway_tools import process_registry as _registry
|
|
process_registry = _registry() # the gateway session's processes (rewired CLI)
|
|
|
|
now = time.time() if now is None else now
|
|
rows = []
|
|
for entry in process_registry.list_sessions():
|
|
proc = process_registry.get(entry["session_id"])
|
|
if proc is None:
|
|
continue
|
|
status = process_status(entry)
|
|
exited_at = proc.exited_at if status != "running" else 0.0
|
|
if status != "running" and now - exited_at > RETAIN_SECONDS:
|
|
continue
|
|
end = exited_at or now
|
|
rows.append({
|
|
"kind": "process",
|
|
"key": proc.id,
|
|
"id": proc.id,
|
|
"command": " ".join((proc.command or "").split()) or t("cli.dock.process_goal_label"),
|
|
"status": status,
|
|
"exit_code": proc.exit_code,
|
|
"elapsed": max(0, int(end - proc.started_at)),
|
|
"since_exit": max(0, int(now - exited_at)) if exited_at else 0,
|
|
"detail": _last_output_line(proc.output_buffer),
|
|
})
|
|
rows.sort(key=lambda r: (r["status"] != "running", -r["elapsed"] if r["status"] == "running" else r["since_exit"]))
|
|
return rows
|
|
|
|
|
|
_GLYPH = {"running": "⚙", "done": "✔", "failed": "✘", "killed": "✘", "lost": "?"}
|
|
# Finished-status id → catalog key of the exit verdict (looked up at paint time).
|
|
_VERDICT_KEYS = {"done": "cli.dock.exit_code", "failed": "cli.dock.exit_code",
|
|
"killed": "cli.dock.verdict_killed", "lost": "cli.dock.verdict_lost"}
|
|
|
|
|
|
def process_activity(row: dict) -> str:
|
|
"""The trailing ``· …`` part of a dock line: elapsed + latest output while running, the exit
|
|
verdict + age once finished."""
|
|
if row["status"] == "running":
|
|
return f"{row['elapsed']}s · " + (
|
|
t("cli.dock.last_output", detail=row["detail"]) if row["detail"] else t("cli.dock.starting"))
|
|
verdict = _VERDICT_KEYS[row["status"]]
|
|
verdict = t(verdict, code=row["exit_code"])
|
|
return t("cli.dock.verdict_ago", verdict=verdict, seconds=row["since_exit"])
|
|
|
|
|
|
def process_glyph(row: dict) -> str:
|
|
return _GLYPH.get(row["status"], "?")
|
|
|
|
|
|
def process_tail(process_id: str) -> str:
|
|
from hermes_cli.gateway_tools import process_registry as _registry
|
|
process_registry = _registry() # the gateway session's processes (rewired CLI)
|
|
|
|
proc = process_registry.get(process_id)
|
|
if proc is None:
|
|
return t("cli.dock.process_gone")
|
|
text = (proc.output_buffer or "")[-32768:]
|
|
text = "".join(c for c in text if c.isprintable() or c in "\n\t")
|
|
return text or t("cli.dock.no_output")
|
|
|
|
|
|
def kill(process_id: str) -> dict:
|
|
from hermes_cli.gateway_tools import process_registry as _registry
|
|
process_registry = _registry() # the gateway session's processes (rewired CLI)
|
|
|
|
return process_registry.kill_process(process_id)
|