@hydra-acp/budgeter 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,264 @@
1
+ import { mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+ import { logger } from "./util/log.js";
4
+ const log = logger("tracker");
5
+ export class CostTracker {
6
+ opts;
7
+ sessions = new Map();
8
+ currentState = "ok";
9
+ // Last JSON we wrote to disk. The watcher uses this to ignore its own
10
+ // writes — if the file's content matches, we did it.
11
+ lastWrittenJson = "";
12
+ constructor(opts) {
13
+ this.opts = opts;
14
+ if (opts.statePath) {
15
+ this.loadFromDisk();
16
+ }
17
+ }
18
+ applyUsageUpdate(sessionId, update) {
19
+ log.debug(`usage_update session=${sessionId.slice(0, 12)} raw=${JSON.stringify(update).slice(0, 200)}`);
20
+ const cost = readCost(update);
21
+ if (cost === undefined) {
22
+ log.debug(`readCost returned undefined for session=${sessionId.slice(0, 12)}`);
23
+ return this.snapshotFor(sessionId);
24
+ }
25
+ const prior = this.sessions.get(sessionId) ?? { cost: 0, baseline: 0, currency: undefined };
26
+ const next = {
27
+ cost: Math.max(prior.cost, cost.amount),
28
+ baseline: prior.baseline,
29
+ currency: cost.currency ?? prior.currency,
30
+ };
31
+ this.sessions.set(sessionId, next);
32
+ this.persist();
33
+ return this.snapshotFor(sessionId);
34
+ }
35
+ // Public snapshot accessor for the synthetic events (session_opened,
36
+ // prompt_request) that don't carry a cost in their own envelope.
37
+ snapshotFor(sessionId) {
38
+ const per = this.sessions.get(sessionId);
39
+ const total = this.totalCost();
40
+ return {
41
+ total,
42
+ perSession: per ? Math.max(0, per.cost - per.baseline) : 0,
43
+ currency: per?.currency ?? this.opts.currency,
44
+ soft: this.opts.softLimit,
45
+ hard: this.opts.hardLimit,
46
+ state: deriveState(total, this.opts.softLimit, this.opts.hardLimit),
47
+ };
48
+ }
49
+ // Returns the new state if this call transitioned to a higher tier
50
+ // (ok→soft, ok→hard, soft→hard). Returns undefined if the state didn't
51
+ // change or if it dropped (e.g. a reset zeroed everything).
52
+ consumeStateTransition() {
53
+ const next = deriveState(this.totalCost(), this.opts.softLimit, this.opts.hardLimit);
54
+ if (rank(next) > rank(this.currentState)) {
55
+ this.currentState = next;
56
+ return next;
57
+ }
58
+ if (next !== this.currentState) {
59
+ this.currentState = next;
60
+ }
61
+ return undefined;
62
+ }
63
+ get state() {
64
+ return this.currentState;
65
+ }
66
+ // Baseline all sessions from their current cost so effective spend
67
+ // reads zero without discarding the agent's running total.
68
+ reset() {
69
+ this.baselineFromCurrent();
70
+ this.persist();
71
+ }
72
+ baselineFromCurrent() {
73
+ for (const [id, s] of this.sessions) {
74
+ this.sessions.set(id, { ...s, baseline: s.cost });
75
+ }
76
+ this.currentState = "ok";
77
+ }
78
+ // Re-read the state file and replace in-memory state if it changed
79
+ // from what we last wrote. Called from the fs.watch handler in bridge.
80
+ // Returns true when state was adopted (caller can react if it cares).
81
+ adoptFromDisk() {
82
+ if (!this.opts.statePath) {
83
+ return false;
84
+ }
85
+ let raw;
86
+ try {
87
+ raw = readFileSync(this.opts.statePath, "utf8");
88
+ }
89
+ catch (err) {
90
+ const e = err;
91
+ if (e.code === "ENOENT") {
92
+ // File deleted = reset. Baseline all sessions from current cost
93
+ // so effective spend reads zero without discarding the agent's
94
+ // running total (it will keep reporting the same numbers).
95
+ if (this.sessions.size === 0) {
96
+ return false;
97
+ }
98
+ log.info("state file removed externally — baselining from current totals");
99
+ this.baselineFromCurrent();
100
+ this.lastWrittenJson = "";
101
+ return true;
102
+ }
103
+ log.warn(`read state failed: ${e.message}`);
104
+ return false;
105
+ }
106
+ if (raw === this.lastWrittenJson) {
107
+ return false;
108
+ }
109
+ // Don't advance currentState — leave it at the pre-load value so
110
+ // consumeStateTransition detects any upward crossing on the next tick.
111
+ const adopted = this.applyPersisted(raw, false);
112
+ if (adopted) {
113
+ this.lastWrittenJson = raw;
114
+ log.info(`adopted state from disk (total=${this.totalCost().toFixed(2)})`);
115
+ }
116
+ return adopted;
117
+ }
118
+ loadFromDisk() {
119
+ if (!this.opts.statePath) {
120
+ return;
121
+ }
122
+ let raw;
123
+ try {
124
+ raw = readFileSync(this.opts.statePath, "utf8");
125
+ }
126
+ catch (err) {
127
+ const e = err;
128
+ if (e.code !== "ENOENT") {
129
+ log.warn(`read state failed: ${e.message}; starting fresh`);
130
+ }
131
+ return;
132
+ }
133
+ if (this.applyPersisted(raw)) {
134
+ this.lastWrittenJson = raw;
135
+ log.info(`loaded state from ${this.opts.statePath} (total=${this.totalCost().toFixed(2)})`);
136
+ }
137
+ }
138
+ applyPersisted(raw, updateState = true) {
139
+ let parsed;
140
+ try {
141
+ parsed = JSON.parse(raw);
142
+ }
143
+ catch (err) {
144
+ log.warn(`state file malformed: ${err.message}; ignoring`);
145
+ return false;
146
+ }
147
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
148
+ log.warn("state file is not an object; ignoring");
149
+ return false;
150
+ }
151
+ const obj = parsed;
152
+ const sessions = obj.sessions ?? {};
153
+ if (typeof sessions !== "object" || Array.isArray(sessions)) {
154
+ log.warn("state.sessions is not an object; ignoring");
155
+ return false;
156
+ }
157
+ this.sessions.clear();
158
+ for (const [id, val] of Object.entries(sessions)) {
159
+ if (val && typeof val === "object" && typeof val.cost === "number") {
160
+ const v = val;
161
+ this.sessions.set(id, {
162
+ cost: v.cost,
163
+ baseline: typeof v.baseline === "number" ? v.baseline : 0,
164
+ currency: typeof v.currency === "string" ? v.currency : undefined,
165
+ });
166
+ }
167
+ }
168
+ if (updateState) {
169
+ this.currentState = deriveState(this.totalCost(), this.opts.softLimit, this.opts.hardLimit);
170
+ }
171
+ return true;
172
+ }
173
+ persist() {
174
+ if (!this.opts.statePath) {
175
+ return;
176
+ }
177
+ const payload = {
178
+ version: 1,
179
+ sessions: Object.fromEntries(this.sessions),
180
+ };
181
+ const json = JSON.stringify(payload, null, 2);
182
+ if (json === this.lastWrittenJson) {
183
+ return;
184
+ }
185
+ try {
186
+ mkdirSync(dirname(this.opts.statePath), { recursive: true });
187
+ const tmp = `${this.opts.statePath}.tmp`;
188
+ writeFileSync(tmp, json, { encoding: "utf8", mode: 0o600 });
189
+ renameSync(tmp, this.opts.statePath);
190
+ this.lastWrittenJson = json;
191
+ }
192
+ catch (err) {
193
+ log.warn(`persist failed: ${err.message}`);
194
+ }
195
+ }
196
+ totalCost() {
197
+ let sum = 0;
198
+ for (const s of this.sessions.values()) {
199
+ sum += Math.max(0, s.cost - s.baseline);
200
+ }
201
+ return sum;
202
+ }
203
+ }
204
+ // Delete the persisted state file. Used by the `reset` subcommand when
205
+ // no live process is running (or as the message the live process picks
206
+ // up via fs.watch).
207
+ export function deleteStateFile(statePath) {
208
+ try {
209
+ unlinkSync(statePath);
210
+ return true;
211
+ }
212
+ catch (err) {
213
+ const e = err;
214
+ if (e.code === "ENOENT") {
215
+ return false;
216
+ }
217
+ throw err;
218
+ }
219
+ }
220
+ function deriveState(total, soft, hard) {
221
+ if (total >= hard) {
222
+ return "hard";
223
+ }
224
+ if (total >= soft) {
225
+ return "soft";
226
+ }
227
+ return "ok";
228
+ }
229
+ function rank(state) {
230
+ if (state === "hard")
231
+ return 2;
232
+ if (state === "soft")
233
+ return 1;
234
+ return 0;
235
+ }
236
+ function readCost(update) {
237
+ // hydra injects cumulative cost via _meta.hydra-acp.cumulativeCost on
238
+ // usage_update for sessions that have lived across multiple agents.
239
+ // Prefer that when present so resurrects don't undercount.
240
+ const cumulative = readCumulativeFromMeta(update._meta);
241
+ const cost = (update.cost ?? undefined);
242
+ const amount = cumulative !== undefined
243
+ ? cumulative
244
+ : typeof cost?.amount === "number"
245
+ ? cost.amount
246
+ : undefined;
247
+ if (amount === undefined) {
248
+ return undefined;
249
+ }
250
+ const currency = typeof cost?.currency === "string" ? cost.currency : undefined;
251
+ return { amount, currency };
252
+ }
253
+ function readCumulativeFromMeta(meta) {
254
+ if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
255
+ return undefined;
256
+ }
257
+ const ns = meta["hydra-acp"];
258
+ if (!ns || typeof ns !== "object" || Array.isArray(ns)) {
259
+ return undefined;
260
+ }
261
+ const v = ns.cumulativeCost;
262
+ return typeof v === "number" ? v : undefined;
263
+ }
264
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../src/tracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ let debugEnabled = false;
2
+ export function setDebug(on) {
3
+ debugEnabled = on;
4
+ }
5
+ function emit(level, scope, args) {
6
+ const ts = new Date().toISOString();
7
+ const stream = level === "error" || level === "warn" ? process.stderr : process.stdout;
8
+ stream.write(`[${ts}] ${level} [${scope}] ${formatArgs(args)}\n`);
9
+ }
10
+ function formatArgs(args) {
11
+ return args
12
+ .map((a) => {
13
+ if (typeof a === "string") {
14
+ return a;
15
+ }
16
+ if (a instanceof Error) {
17
+ return a.stack ?? a.message;
18
+ }
19
+ try {
20
+ return JSON.stringify(a);
21
+ }
22
+ catch {
23
+ return String(a);
24
+ }
25
+ })
26
+ .join(" ");
27
+ }
28
+ export function logger(scope) {
29
+ return {
30
+ debug(...args) {
31
+ if (debugEnabled) {
32
+ emit("debug", scope, args);
33
+ }
34
+ },
35
+ info(...args) {
36
+ emit("info", scope, args);
37
+ },
38
+ warn(...args) {
39
+ emit("warn", scope, args);
40
+ },
41
+ error(...args) {
42
+ emit("error", scope, args);
43
+ },
44
+ };
45
+ }
46
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../../src/util/log.ts"],"names":[],"mappings":"AAEA,IAAI,YAAY,GAAG,KAAK,CAAC;AAEzB,MAAM,UAAU,QAAQ,CAAC,EAAW;IAClC,YAAY,GAAG,EAAE,CAAC;AACpB,CAAC;AAED,SAAS,IAAI,CAAC,KAAY,EAAE,KAAa,EAAE,IAAe;IACxD,MAAM,EAAE,GAAG,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,MAAM,MAAM,GAAG,KAAK,KAAK,OAAO,IAAI,KAAK,KAAK,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;IACvF,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,KAAK,KAAK,KAAK,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACpE,CAAC;AAED,SAAS,UAAU,CAAC,IAAe;IACjC,OAAO,IAAI;SACR,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;QACT,IAAI,OAAO,CAAC,KAAK,QAAQ,EAAE,CAAC;YAC1B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,IAAI,CAAC,YAAY,KAAK,EAAE,CAAC;YACvB,OAAO,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,OAAO,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC;YACH,OAAO,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAC3B,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;QACnB,CAAC;IACH,CAAC,CAAC;SACD,IAAI,CAAC,GAAG,CAAC,CAAC;AACf,CAAC;AASD,MAAM,UAAU,MAAM,CAAC,KAAa;IAClC,OAAO;QACL,KAAK,CAAC,GAAG,IAAe;YACtB,IAAI,YAAY,EAAE,CAAC;gBACjB,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;QACD,IAAI,CAAC,GAAG,IAAe;YACrB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,GAAG,IAAe;YACrB,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC5B,CAAC;QACD,KAAK,CAAC,GAAG,IAAe;YACtB,IAAI,CAAC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC;QAC7B,CAAC;KACF,CAAC;AACJ,CAAC"}
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@hydra-acp/budgeter",
3
+ "version": "0.1.2",
4
+ "description": "Cost-budget transformer extension for hydra-acp — warns on soft limit, rejects further prompts/sessions on hard limit.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public",
9
+ "registry": "https://registry.npmjs.org"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "main": "dist/index.js",
17
+ "bin": {
18
+ "hydra-acp-budgeter": "dist/index.js"
19
+ },
20
+ "scripts": {
21
+ "build": "tsc -p .",
22
+ "watch": "tsc -p . --watch",
23
+ "start": "node dist/index.js",
24
+ "dev": "tsc -p . && node dist/index.js",
25
+ "test": "node --test --import tsx 'test/**/*.test.ts'",
26
+ "lint": "tsc -p . --noEmit",
27
+ "prepublishOnly": "npm run build && npm test"
28
+ },
29
+ "engines": {
30
+ "node": ">=20"
31
+ },
32
+ "dependencies": {
33
+ "ws": "^8.20.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/node": "^22.10.0",
37
+ "@types/ws": "^8.18.1",
38
+ "tsx": "^4.20.0",
39
+ "typescript": "^5.6.3"
40
+ }
41
+ }