Files
wehub-resource-sync 4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

145 lines
4.9 KiB
Python

"""Rich-table rendering for the ``/fleet`` slash-command dashboard.
Columns: ``agent``, ``pid``, ``uptime``, ``cpu%``, ``tokens/min``,
``$/hr``, ``status``. Every metric cell falls back to ``-`` when its
sampler accessor returns ``None``. ``0`` versus ``-`` is meaningful
in ``tokens/min``: ``0`` is observed-but-idle, ``-`` is unobservable
(no meter for this provider, or the JSONL is unreadable, or the
sampler task is not running — e.g. non-interactive
``opensre fleet list``).
This module lives outside ``tools/system/fleet_monitoring/`` so collectors don't pull
in Rich.
"""
from __future__ import annotations
from collections.abc import Iterable
from datetime import UTC, datetime, timedelta
from rich.console import Console, JustifyMethod
from rich.markup import escape
from rich.table import Table
from platform.terminal.theme import BOLD_BRAND
from surfaces.interactive_shell.ui.components.rendering import print_repl_table, repl_table
from tools.system.fleet_monitoring.registry import AgentRecord
from tools.system.fleet_monitoring.sampler import get_snapshot, get_tokens_per_min, get_usd_per_hour
from tools.system.fleet_monitoring.status import Status, compute_status
_UNFILLED = "-"
# Re-using Rich's own ``JustifyMethod`` so column-justify options
# stay in lockstep with the library.
_COLUMNS: tuple[tuple[str, JustifyMethod], ...] = (
("agent", "left"),
("pid", "right"),
("uptime", "right"),
("cpu%", "right"),
("tokens/min", "right"),
("$/hr", "right"),
("status", "left"),
)
_STATUS_COLORS: dict[Status, str] = {
Status.ACTIVE: "green",
Status.IDLE: "yellow",
Status.STUCK: "red",
}
def _format_uptime(delta: timedelta) -> str:
total_seconds = int(delta.total_seconds())
if total_seconds < 60:
return f"{total_seconds}s"
if total_seconds < 3600:
return f"{total_seconds // 60}m"
hours = total_seconds // 3600
if hours < 24:
minutes = (total_seconds % 3600) // 60
return f"{hours}h{minutes}m"
days = hours // 24
remaining_hours = hours % 24
return f"{days}d{remaining_hours}h"
def _format_tokens_per_min(value: float | None) -> str:
if value is None:
return _UNFILLED
# Round-then-compare so ``999.6`` doesn't render as 4-digit ``"1000"``
# next to its 3-digit neighbors.
rounded = int(round(value))
if rounded < 1000:
return f"{rounded}"
return f"{value / 1000:.1f}k"
def _format_usd_per_hour(value: float | None) -> str:
if value is None:
return _UNFILLED
return f"${value:.2f}"
def _format_status(status: Status, msg: str = "") -> str:
"""Return a Rich-markup-colorized status cell for the /fleet table."""
color = _STATUS_COLORS.get(status, "default")
label = f"{status.value} ({msg})" if msg else status.value
return f"[{color}]{label}[/{color}]"
def _build_agents_table(records: Iterable[AgentRecord]) -> Table:
"""Build and return the agents dashboard Table without printing it."""
materialized = list(records)
table = repl_table(
title="agents",
title_style=BOLD_BRAND,
caption="no agents discovered or registered yet" if not materialized else None,
)
for header, justify in _COLUMNS:
table.add_column(header, justify=justify)
now = datetime.now(UTC)
for record in materialized:
snapshot = get_snapshot(record.pid)
if snapshot is not None:
# Use output freshness when available; otherwise the status
# heuristic falls back to the process start time.
status = compute_status(
snapshot,
now,
last_output_at=snapshot.last_output_at,
idle_after_s=120,
stuck_after_s=480,
)
status_msg = ""
if status is Status.STUCK:
anchor = snapshot.last_output_at or snapshot.started_at
status_msg = f"{_format_uptime(now - anchor)} no progress"
uptime_cell = _format_uptime(now - snapshot.started_at)
cpu_cell = f"{snapshot.cpu_percent:.1f}"
status_cell = _format_status(status, status_msg)
else:
uptime_cell = _UNFILLED
cpu_cell = _UNFILLED
status_cell = _UNFILLED
tokens_cell = _format_tokens_per_min(get_tokens_per_min(record.pid))
hourly_cell = _format_usd_per_hour(get_usd_per_hour(record.pid))
table.add_row(
escape(record.name),
str(record.pid),
uptime_cell,
cpu_cell,
tokens_cell,
hourly_cell,
status_cell,
)
return table
def render_agents_table(console: Console, records: Iterable[AgentRecord]) -> None:
"""Print the agents dashboard table to the REPL console with TTY-safe width."""
print_repl_table(console, _build_agents_table(records))
__all__ = ["_build_agents_table", "render_agents_table"]