@hydra-acp/budgeter
v0.1.7
Published
Cost-budget transformer extension for hydra-acp — warns on soft limit, rejects further prompts/sessions on hard limit.
Readme
hydra-acp-budgeter
Cost-budget transformer extension for hydra-acp. Watches every session's usage_update events, warns attached clients when total spend crosses a soft limit, and rejects further prompts once it crosses a hard limit — with a human-readable reason returned to the client.
Runs as a daemon-managed transformer (not a client extension): it connects once, declares its intercepts via transformer/initialize, and sits inside the daemon's message pipeline for every live session.
Install
From npm (recommended once published):
npm install -g @hydra-acp/cli @hydra-acp/budgeterThis drops the hydra-acp (and hydra) CLI plus a hydra-acp-budgeter binary on your PATH. The CLI dispatches hydra-acp <name> to any hydra-acp-<name> binary on PATH, so the budgeter is also reachable as hydra-acp budgeter.
Or from source:
git clone [email protected]:smagnuso/hydra-acp-budgeter.git ~/dev/hydra-acp-budgeter
cd ~/dev/hydra-acp-budgeter
npm install
npm run buildRegister the transformer with hydra. If installed via npm:
hydra-acp transformers add hydra-acp-budgeter --command hydra-acp-budgeterOr pointed at a local build:
hydra-acp transformers add hydra-acp-budgeter \
--command node \
--args ~/dev/hydra-acp-budgeter/dist/index.jsThat registers the transformer but does not wire it into sessions yet. You also need to add it to defaultTransformers in ~/.hydra-acp/config.json — there's no CLI command for this, edit the file directly:
{
"transformers": {
"hydra-acp-budgeter": {
"command": ["node"],
"args": ["/home/you/dev/hydra-acp-budgeter/dist/index.js"]
}
},
"defaultTransformers": ["hydra-acp-budgeter"]
}Without defaultTransformers, the transformer process runs but no sessions route through it.
On hydra-acp daemon start, hydra spawns hydra-acp-budgeter with these env
vars set: HYDRA_ACP_DAEMON_URL, HYDRA_ACP_TOKEN, HYDRA_ACP_WS_URL,
HYDRA_ACP_HOME, HYDRA_ACP_TRANSFORMER_NAME. Stdout/stderr land in
~/.hydra-acp/transformers/hydra-acp-budgeter.log. Lifecycle is managed with
hydra-acp transformers start|stop|restart hydra-acp-budgeter and
hydra-acp transformers log hydra-acp-budgeter -f to tail.
Behavior
Tracks each session's running cost from usage_update events (the cost.amount the agent reports, or _meta.hydra-acp.cumulativeCost when present), sums across sessions, and acts at two thresholds:
1. Soft limit crossed (total ≥ HYDRA_ACP_BUDGETER_SOFT)
- Emits a single warning
session/update(agent_message_chunk) to every attached client on the session that triggered the cross. - Body:
Spent $X.XX of $S.SS soft (hard: $H.HH). Heads up — prompts will be rejected at the hard limit. - Fires once per upward transition, not on every tick.
2. Hard limit crossed (total ≥ HYDRA_ACP_BUDGETER_HARD)
- Same one-shot warning, this time on the session that pushed us over.
- Body:
Spent $X.XX ≥ $H.HH hard limit. Further prompts will be rejected until the budget is reset.
3. Prompts while over hard limit
- Any
session/promptfrom any session is intercepted atrequest:session/promptand replaced with a stop response:{ "stopReason": "refusal", "_meta": { "hydra-acp": { "budgeter": { "message": "Budget exceeded: spent $X.XX ≥ $H.HH hard limit. Reset the budget or raise HYDRA_ACP_BUDGETER_HARD to continue." } } } } - The agent never sees the prompt. The client's pending
session/promptresolves with the stop payload; well-behaved renderers (TUI, Zed, agent-shell) will surface thestopReason: refusaland the_meta.hydra-acp.budgeter.messagebody.
4. New session opens while over hard limit
- The budgeter fires the same warning style on
session.openedso the user knows their next prompt will bounce, even before they send it.
State and reset
The per-session cost map is persisted to ~/.hydra-acp/budgeter-cost.json, atomically rewritten on every usage_update. The running budgeter reads it on startup and fs.watches it for external mutations — so daemon restarts preserve the running total, and a reset from elsewhere is picked up live without restarting.
Spend is sticky across session.closed: a closed session's cost stays in the total until you reset.
To zero the budget:
hydra-acp budgeter resetThat deletes the state file. If the transformer is running, its watcher adopts the deletion and the in-memory total drops to zero on the next tick (≤50ms). If it isn't running, the file is just gone and the next start begins at zero.
Configuration
Create ~/.hydra-acp/budgeter.conf (override path via HYDRA_ACP_BUDGETER_CONF):
# ~/.hydra-acp/budgeter.conf
SOFT=5
HARD=10
CURRENCY=USD
DEBUG=falseThe file is optional — all keys have defaults and the transformer works without it. Environment variables always win over file values, so you can temporarily override a limit without editing the file.
| Key / env var | Default | Purpose |
|---|---|---|
| SOFT / HYDRA_ACP_BUDGETER_SOFT | 5 | Soft limit (warning threshold) |
| HARD / HYDRA_ACP_BUDGETER_HARD | 10 | Hard limit (rejection threshold). Must be ≥ soft. |
| CURRENCY / HYDRA_ACP_BUDGETER_CURRENCY | USD | ISO-3 currency code for formatted messages |
| DEBUG | false | Verbose logging |
| HYDRA_ACP_DAEMON_URL | http://127.0.0.1:8765 | Daemon HTTP endpoint (injected by hydra) |
| HYDRA_ACP_TOKEN | (required) | Daemon auth token (injected by hydra) |
| HYDRA_ACP_WS_URL | derived | Override WS endpoint |
How it works
- Connects via WebSocket to
/acp, callsinitialize, thentransformer/initializedeclaring the intercepts:response:session/update— observeusage_updateto track costrequest:session/prompt— reject when over hard limitlifecycle:session.opened— warn brand-new sessions that are already over budgetlifecycle:session.closed— fires session_closed rule event (cost stays sticky)
- For every
transformer/messagethe daemon dispatches, the budgeter responds with{ action: "continue" }(observe-only on response side, allow on request side when under budget) or{ action: "stop", payload: { stopReason: "refusal", _meta: ... } }(when over hard limit). - Warnings are emitted via
hydra-acp/message/emitwithroute: "chain"andmethod: "session/update"so they flow back through the daemon's broadcast machinery and reach every attached client. - All cost state is in-memory; restart the transformer to reset.
For a working example of the transformer protocol the budgeter speaks, see hydra-acp/cli/examples/transformer-observe.mjs.
Reporting historical cost
The cost subcommand reads session metadata from ~/.hydra-acp/sessions/<id>/meta.json and optionally streams history.jsonl for time-bucketed or token-level queries. It is a pure reader — no new files are written.
Quick start
# All-time total cost across all sessions
hydra budgeter usage
# JSON output (machine-readable)
hydra budgeter usage --jsonTime-based queries
# Last 7 days
hydra budgeter usage --since 7d
# Last 30 days, grouped by day buckets
hydra budgeter usage --since 30d --bucket day
# Last 6 months, grouped by week
hydra budgeter usage --since 180d --bucket week
# Calendar-month buckets over the last 2 years
hydra budgeter usage --bucket monthGrouping
# By directory (depth-1 below $HOME)
hydra budgeter usage --by dir
# By session ID
hydra budgeter usage --by session
# By model
hydra budgeter usage --by model
# By agent
hydra budgeter usage --by agent
# Directory with custom depth
hydra budgeter usage --by dir --depth 2Filtering
# Only interactive sessions
hydra budgeter usage --interactive
# Only non-interactive (background) sessions
hydra budgeter usage --no-interactive
# Only sessions under a specific directory prefix
hydra budgeter usage --dir ~/dev/hydra-acp
# Combine filters
hydra budgeter usage --since 7d --dir ~/dev/hydra-acp --by sessionToken-level queries
# Total tokens across all sessions
hydra budgeter usage --metric tokens
# Tokens with histogram
hydra budgeter usage --metric tokens --histogramOutput formats
Text (default):
Total: $12.34 across 5 session(s)
──────────────────────────────────────────────────────────────────────────────
Label Cost Tokens
myapp $7.50 142k
other $4.84 89kJSON (--json):
{
"kind": "grouped",
"currency": "USD",
"groups": [
{
"label": "myapp",
"items": [{ "label": "myapp", "costAmount": 7.50, "deltaCost": 2.30 }]
},
{
"label": "other",
"items": [{ "label": "other", "costAmount": 4.84, "deltaCost": 1.10 }]
}
]
}Fast path vs slow path
- Fast path —
--by dir/session/model/agentwithout--since,--dir, or--interactive: reads onlymeta.jsonacross all sessions, returns instantly even with ~1000 sessions. - Slow path — any query with
--since,--bucket,--metric tokens,--dir, or--interactive: also streamshistory.jsonlfor delta-cost and token computation, pre-filtering sessions byupdatedAt.
