Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ A small web dashboard for **headless AI agents** running on a machine —
OpenClaw, Hermes, Claude Code, Ollama, vLLM, llama.cpp, or anything you name. It
shows which agents are alive and what they're costing you (CPU, memory, GPU), and
gives you a **kill button** per agent. Think `htop`, scoped to just your agents —
the [screenshot below](docs/dashboard.png) is a real run on a fleet node.
the [screenshot below](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/dashboard.png) is a real run on a fleet node.

No database, no auth framework (one static token file gates the kill switch),
four direct dependencies (FastAPI, uvicorn, psutil, PyYAML). Runs on macOS and Linux. It is a
per-node monitor and guarded local control panel: fleet schedulers may consume
its read-only telemetry, but should own their own scheduling and actuation.

![agent-usage-manager — live dashboard](docs/dashboard.png)
![agent-usage-manager — live dashboard](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/dashboard.png)

*A real run: ten agents grouped by process tree (`+N` = children rolled up),
per-agent CPU/memory/uptime, launchd-supervised jobs flagged, and a kill button
Expand Down Expand Up @@ -39,7 +39,10 @@ uvx agent-usage-manager # then open http://127.0.0.1:8765 (opens automatica
One command — no install, no virtualenv, no leftovers. Other install options,
config, and flags: [Install & run](#install--run).

![one command to a live dashboard](docs/demo.gif)
![one command to a live dashboard](https://raw.githubusercontent.com/minglong51/agent-usage-manager/main/docs/demo.gif)

*Field notes on running agents with discipline go out on the
[Agent Discipline](https://buttondown.com/minglong51) list — a few emails a month.*

## What it does

Expand Down
129 changes: 129 additions & 0 deletions agent_usage_manager/agents.default.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Default configuration shipped with the package. It is used only when you have
# no agents.yaml of your own: `--config` wins, then ./agents.yaml in the current
# directory, then this file. Copy it somewhere and edit rather than editing it
# in place — a package upgrade replaces this file.
#
# Which processes count as "agents". A process matches if the pattern hits its
# executable name + first few arguments (case-insensitive substring or regex) —
# NOT the entire command line, so a process that merely mentions an agent name
# deep in its args (e.g. inside a system prompt) is not misclassified.
# Only processes that match here can be listed AND killed — the kill endpoint
# re-checks the match before sending a signal, so the dashboard can never be
# used to kill an arbitrary unrelated PID.
#
# Matched processes are grouped by process tree: one row per agent (the root),
# with child processes rolled up. CPU/mem/GPU are tree totals, and "kill" stops
# the whole tree (root + children) so spawned helpers don't leak resources.
agents:
- label: openclaw
match: openclaw
- label: hermes
match: hermes
- label: claude-code
match: "claude(\\s|$|-code)"
regex: true
- label: kiro
match: "\\bkiro\\b" # word-anchored so it doesn't hit any kiro-named path
regex: true
- label: aider
match: "\\baider\\b" # anchored: bare 'aider' matches 'raider', 'spider'…
regex: true
- label: codex
match: "\\bcodex\\b"
regex: true
- label: cline
match: "\\bcline\\b" # anchored: bare 'cline' matches 'decline', 'incline'
regex: true
- label: ollama
match: ollama
- label: vllm
match: vllm
- label: llama.cpp
match: llama-server

# Patterns that must NEVER be killed even if they match an agent pattern above.
# Self (this monitor's own PID) and PID 1 are always protected in code.
protect:
- agent-usage-manager
- uvicorn

# Patterns that disqualify a process from being an agent at all — not listed,
# not killable. Use this for incidental processes that share a name or bundle
# path with a real agent. Checked against the same match target (executable
# basename + first args + macOS .app bundle name), case-insensitive substring.
#
# Why these: a GUI agent like Kiro.app launches its main process as "Electron",
# so agents are matched by the .app bundle name ("Kiro"). That also sweeps in the
# app's crash handler, its Squirrel auto-updater (ShipIt), and the editor's
# integrated-terminal shells (titled "kiro-cli-term") — none of which are the
# agent. Ignoring them leaves one clean row for the app (helpers roll up under
# it) instead of a cluster of un-killable, self-respawning fragments.
ignore:
- crashpad # chrome_crashpad_handler — the bundle's crash reporter
- shipit # Squirrel.framework auto-updater
- kiro-cli-term # Kiro's integrated-terminal shells (zsh), not the agent
- autoupdate # Sparkle's Autoupdate helper — Codex.app's ShipIt equivalent
- updater # Sparkle's Updater.app; argv[1] is the app path, so it matches
- for chrome # "Codex for Chrome" extension host, not the agent
- tmux attach # tmux clients: `attach -t <session>` carries the agent's name
- chatgpt codex # ChatGPT.app's embedded Codex helpers ("Codex (Renderer)",
# "Codex (Service)", Resources/codex app-server) — GUI-app
# plumbing, matched via the "ChatGPT" bundle prefix; not agents
- codex computer use # ~/.codex/computer-use desktop-automation helper app

# Optional: per-instance labels from launchd job labels — the supervised-fleet
# counterpart of tmux_labels below. Several LaunchAgents running the same
# binary all hit one agents: entry and land as indistinguishable rows, and tmux
# never sees them. When a matched root's own launchd job label matches this
# regex, the first capture group (the whole label if no group) becomes the row
# label — churn tracking, alert transitions, and /metrics all get per-instance
# identity. tmux_labels wins when both apply. Empty/absent = off.
#
# launchd_labels: "^com\\.example\\.worker-(.+)$" # → worker-1, worker-2

# Optional: per-instance labels from tmux session names. A fleet of identical
# agents (e.g. several claude-code bots, one per tmux session) all hit one
# agents: entry above and land as N indistinguishable rows — their cmdlines
# can't tell them apart, because matching deliberately sees only the executable
# + first args. The tmux session each one runs in IS its identity: when a
# matched root (or an ancestor) is a tmux pane whose session name matches this
# regex, the row is labeled with the first capture group (the whole session
# name if there is no group). Sessions that don't match keep their agents:
# label, so incidental tmux use never renames rows.
#
# tmux_labels: "^bot-(.+)$" # session bot-worker1 → row label worker1

# Labels whose idle state is NORMAL — agents that wait for work (bots parked on
# a chat poll, gateways waiting for requests). They get no "idle" badge: badging
# the whole waiting fleet wallpapers the dashboard and trains badge-blindness.
# Same reasoning as `idle` being opt-in for alerts below. Case-insensitive
# substrings of the row label (incl. tmux-derived ones), like ignore:.
#
# idle_ok:
# - gateway
# - worker

# GPU sampling: nvidia-smi is used automatically when present (Linux/NVIDIA).
# On Apple Silicon there is no per-process GPU API, so the GPU column is blank.

# Optional: run a command when a badge appears on an agent — a dashboard only
# helps while someone is looking at it. The command runs through the shell with
# details in env vars: $AUM_MSG (ready-made one-liner), $AUM_LABEL, $AUM_FLAG,
# $AUM_PID, $AUM_CPU, $AUM_MEM_MB, $AUM_RESTARTS, $AUM_HOST. Fires on a flag
# APPEARING (once per transition, with a per-agent-per-flag cooldown); server
# mode only — `list` never alerts. By default only hot/churn/leak alert: for a
# fleet of agents that wait for work, idle is the NORMAL state, and alerting on
# it floods the channel every time the server restarts and re-learns the fleet.
#
# Nothing is wired by default — uncomment and point it at your own channel, then
# prove it with `agent-usage-manager test-alert` before a real badge depends on
# it. Two things worth knowing before you turn `hot` on: for inference agents
# pegged CPU IS the job, and spawn-heavy agents look like churn while working.
#
# alerts:
# command: 'terminal-notifier -title agent-usage-manager -message "$AUM_MSG"'
# cooldown: 600 # seconds, default 600
# flags: [hot, churn, leak] # default; add idle only if you really want it
# leak_floor_mb: 1536 # only alert on leak once the absolute footprint
# # is large — long-running agents legitimately
# # ratchet RSS as their context grows
5 changes: 4 additions & 1 deletion agent_usage_manager/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,10 @@ def _resolve_config() -> Path:
cwd_cfg = Path.cwd() / "agents.yaml"
if cwd_cfg.exists():
return cwd_cfg
return BASE / "agents.yaml"
local = BASE / "agents.yaml"
if local.exists():
return local
return BASE / "agents.default.yaml"


CONFIG_PATH = _resolve_config()
Expand Down
26 changes: 25 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ build-backend = "hatchling.build"

[project]
name = "agent-usage-manager"
version = "0.2.5"
version = "0.2.6"
description = "htop for AI agents — liveness, CPU/mem/GPU usage, and a kill switch for headless agents (openclaw, hermes, ollama, vllm, claude-code)."
readme = "README.md"
license = { text = "MIT" }
requires-python = ">=3.9"
keywords = ["ai-agents", "monitoring", "gpu", "ollama", "vllm", "llm", "observability"]
classifiers = [
"Development Status :: 4 - Beta",
"Environment :: Web Environment",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Operating System :: MacOS :: MacOS X",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Topic :: System :: Monitoring",
"Topic :: System :: Systems Administration",
]
dependencies = [
"fastapi>=0.110",
"uvicorn[standard]>=0.27",
Expand All @@ -23,9 +39,17 @@ dev = ["pytest>=7", "httpx>=0.27"]
[project.urls]
Homepage = "https://github.com/minglong51/agent-usage-manager"
Repository = "https://github.com/minglong51/agent-usage-manager"
Issues = "https://github.com/minglong51/agent-usage-manager/issues"
Newsletter = "https://buttondown.com/minglong51"

[project.scripts]
agent-usage-manager = "agent_usage_manager.cli:main"

[tool.hatch.build.targets.wheel]
packages = ["agent_usage_manager"]
# agents.yaml is the maintainer's live config in a source checkout, not a
# shippable default — the wheel carries agents.default.yaml instead.
exclude = ["agent_usage_manager/agents.yaml"]

[tool.hatch.build.targets.sdist]
exclude = ["agent_usage_manager/agents.yaml"]
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading