@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.
- package/LICENSE +21 -0
- package/README.md +144 -0
- package/dist/acp/protocol.js +13 -0
- package/dist/acp/protocol.js.map +1 -0
- package/dist/acp/transformer.js +188 -0
- package/dist/acp/transformer.js.map +1 -0
- package/dist/bridge.js +196 -0
- package/dist/bridge.js.map +1 -0
- package/dist/config.js +99 -0
- package/dist/config.js.map +1 -0
- package/dist/enforce.js +81 -0
- package/dist/enforce.js.map +1 -0
- package/dist/index.js +65 -0
- package/dist/index.js.map +1 -0
- package/dist/paths.js +12 -0
- package/dist/paths.js.map +1 -0
- package/dist/router.js +161 -0
- package/dist/router.js.map +1 -0
- package/dist/rule.js +86 -0
- package/dist/rule.js.map +1 -0
- package/dist/tracker.js +264 -0
- package/dist/tracker.js.map +1 -0
- package/dist/util/log.js +46 -0
- package/dist/util/log.js.map +1 -0
- package/package.json +41 -0
package/dist/config.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
const PRIMARY_CONF_PATH = resolve(homedir(), ".hydra-acp", "budgeter.conf");
|
|
5
|
+
export function configPath() {
|
|
6
|
+
const override = process.env.HYDRA_ACP_BUDGETER_CONF;
|
|
7
|
+
if (override) {
|
|
8
|
+
return override;
|
|
9
|
+
}
|
|
10
|
+
return PRIMARY_CONF_PATH;
|
|
11
|
+
}
|
|
12
|
+
function parseEnvFile(text) {
|
|
13
|
+
const out = new Map();
|
|
14
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
15
|
+
const line = rawLine.trim();
|
|
16
|
+
if (!line || line.startsWith("#")) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
const eq = line.indexOf("=");
|
|
20
|
+
if (eq === -1) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
const key = line.slice(0, eq).trim();
|
|
24
|
+
let val = line.slice(eq + 1).trim();
|
|
25
|
+
if ((val.startsWith('"') && val.endsWith('"')) ||
|
|
26
|
+
(val.startsWith("'") && val.endsWith("'"))) {
|
|
27
|
+
val = val.slice(1, -1);
|
|
28
|
+
}
|
|
29
|
+
out.set(key, val);
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function deriveWsUrl(httpUrl) {
|
|
34
|
+
if (httpUrl.startsWith("https://")) {
|
|
35
|
+
return "wss://" + httpUrl.slice("https://".length).replace(/\/$/, "") + "/acp";
|
|
36
|
+
}
|
|
37
|
+
if (httpUrl.startsWith("http://")) {
|
|
38
|
+
return "ws://" + httpUrl.slice("http://".length).replace(/\/$/, "") + "/acp";
|
|
39
|
+
}
|
|
40
|
+
throw new Error(`hydraDaemonUrl must start with http:// or https://: ${httpUrl}`);
|
|
41
|
+
}
|
|
42
|
+
function floatVal(map, envName, key, fallback) {
|
|
43
|
+
const v = process.env[envName] ?? map.get(key);
|
|
44
|
+
if (v === undefined || v === "") {
|
|
45
|
+
return fallback;
|
|
46
|
+
}
|
|
47
|
+
const n = Number.parseFloat(v);
|
|
48
|
+
return Number.isFinite(n) ? n : fallback;
|
|
49
|
+
}
|
|
50
|
+
const TRUTHY = new Set(["1", "true", "yes", "on", "t"]);
|
|
51
|
+
function boolVal(map, envName, key, fallback) {
|
|
52
|
+
const v = process.env[envName] ?? map.get(key);
|
|
53
|
+
if (v === undefined) {
|
|
54
|
+
return fallback;
|
|
55
|
+
}
|
|
56
|
+
return TRUTHY.has(v.toLowerCase());
|
|
57
|
+
}
|
|
58
|
+
function strVal(map, envName, key, fallback) {
|
|
59
|
+
return process.env[envName] ?? map.get(key) ?? fallback;
|
|
60
|
+
}
|
|
61
|
+
export function loadConfig(path = configPath()) {
|
|
62
|
+
let map = new Map();
|
|
63
|
+
if (existsSync(path)) {
|
|
64
|
+
try {
|
|
65
|
+
map = parseEnvFile(readFileSync(path, "utf8"));
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
// Non-fatal — log and continue with env vars / defaults.
|
|
69
|
+
process.stderr.write(`hydra-acp-budgeter: warning: could not read ${path}: ${err.message}\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
// Hydra-injected connection vars: env always wins, conf file is fallback.
|
|
73
|
+
const hydraDaemonUrl = process.env.HYDRA_ACP_DAEMON_URL ??
|
|
74
|
+
map.get("HYDRA_DAEMON_URL") ??
|
|
75
|
+
"http://127.0.0.1:8765";
|
|
76
|
+
const hydraToken = process.env.HYDRA_ACP_TOKEN ?? map.get("HYDRA_TOKEN") ?? "";
|
|
77
|
+
if (!hydraToken) {
|
|
78
|
+
throw new Error("Missing HYDRA_ACP_TOKEN env var (or HYDRA_TOKEN in budgeter.conf). " +
|
|
79
|
+
"When run as a hydra transformer, hydra injects this automatically.");
|
|
80
|
+
}
|
|
81
|
+
const hydraWsUrl = process.env.HYDRA_ACP_WS_URL ??
|
|
82
|
+
map.get("HYDRA_WS_URL") ??
|
|
83
|
+
deriveWsUrl(hydraDaemonUrl);
|
|
84
|
+
const softLimit = floatVal(map, "HYDRA_ACP_BUDGETER_SOFT", "SOFT", 5);
|
|
85
|
+
const hardLimit = floatVal(map, "HYDRA_ACP_BUDGETER_HARD", "HARD", 10);
|
|
86
|
+
if (hardLimit < softLimit) {
|
|
87
|
+
throw new Error(`HARD (${hardLimit}) must be >= SOFT (${softLimit})`);
|
|
88
|
+
}
|
|
89
|
+
return {
|
|
90
|
+
hydraDaemonUrl,
|
|
91
|
+
hydraWsUrl,
|
|
92
|
+
hydraToken,
|
|
93
|
+
softLimit,
|
|
94
|
+
hardLimit,
|
|
95
|
+
currency: strVal(map, "HYDRA_ACP_BUDGETER_CURRENCY", "CURRENCY", "USD"),
|
|
96
|
+
debug: boolVal(map, "DEBUG", "DEBUG", false),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"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"}
|
package/dist/enforce.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// Builds outbound emit_message payloads and stop responses for the
|
|
2
|
+
// router. Centralises the "what does a warning/rejection look like on
|
|
3
|
+
// the wire" decisions so the router only deals with verdicts.
|
|
4
|
+
export class Enforcer {
|
|
5
|
+
client;
|
|
6
|
+
log;
|
|
7
|
+
trace;
|
|
8
|
+
constructor(client, log, trace) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
this.log = log;
|
|
11
|
+
this.trace = trace;
|
|
12
|
+
}
|
|
13
|
+
// Emit a warning to every client attached to the session. Sent as a
|
|
14
|
+
// session/update with sessionUpdate="agent_message_chunk" so it
|
|
15
|
+
// surfaces in the conversation log on every well-behaved client. A
|
|
16
|
+
// _meta.hydra-acp.budgeter marker lets renderer-aware clients style it
|
|
17
|
+
// differently (or filter it out, if they prefer their own UI).
|
|
18
|
+
async warn(sessionId, warn) {
|
|
19
|
+
const text = warn.body ? `\n\n${warn.title}\n${warn.body}\n\n` : `\n\n${warn.title}\n\n`;
|
|
20
|
+
const envelope = {
|
|
21
|
+
sessionId,
|
|
22
|
+
update: {
|
|
23
|
+
sessionUpdate: "agent_message_chunk",
|
|
24
|
+
content: { type: "text", text },
|
|
25
|
+
_meta: {
|
|
26
|
+
"hydra-acp": {
|
|
27
|
+
budgeter: {
|
|
28
|
+
title: warn.title,
|
|
29
|
+
...(warn.body !== undefined ? { body: warn.body } : {}),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
if (this.trace) {
|
|
36
|
+
this.trace.emits.push({
|
|
37
|
+
sessionId,
|
|
38
|
+
method: "session/update",
|
|
39
|
+
envelope,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await this.client.request("hydra-acp/emit_message", {
|
|
44
|
+
sessionId,
|
|
45
|
+
method: "session/update",
|
|
46
|
+
envelope,
|
|
47
|
+
route: "chain",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
this.log.warn(`emit_message warn for ${sessionId} failed: ${err.message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Build the payload that the router returns as the stop action's
|
|
55
|
+
// payload. Becomes the response delivered to the originator of the
|
|
56
|
+
// session/prompt — clients render the stopReason and the _meta.
|
|
57
|
+
buildRejectPayload(reject) {
|
|
58
|
+
return {
|
|
59
|
+
stopReason: reject.stopReason ?? "refusal",
|
|
60
|
+
_meta: {
|
|
61
|
+
"hydra-acp": {
|
|
62
|
+
budgeter: { message: reject.message },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// Convenience: combine a warn + reject into the appropriate side
|
|
68
|
+
// effects. Called by the router on a prompt_request verdict that
|
|
69
|
+
// includes both — the warn fires asynchronously so the stop payload
|
|
70
|
+
// can be returned to the daemon without blocking on emit_message.
|
|
71
|
+
async dispatch(sessionId, verdict) {
|
|
72
|
+
if (verdict.warn) {
|
|
73
|
+
void this.warn(sessionId, verdict.warn);
|
|
74
|
+
}
|
|
75
|
+
if (verdict.reject) {
|
|
76
|
+
return this.buildRejectPayload(verdict.reject);
|
|
77
|
+
}
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=enforce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"enforce.js","sourceRoot":"","sources":["../src/enforce.ts"],"names":[],"mappings":"AAUA,mEAAmE;AACnE,sEAAsE;AACtE,8DAA8D;AAC9D,MAAM,OAAO,QAAQ;IAEA;IACA;IACA;IAHnB,YACmB,MAA0C,EAC1C,GAAW,EACX,KAAiB;QAFjB,WAAM,GAAN,MAAM,CAAoC;QAC1C,QAAG,GAAH,GAAG,CAAQ;QACX,UAAK,GAAL,KAAK,CAAY;IACjC,CAAC;IAEJ,oEAAoE;IACpE,gEAAgE;IAChE,mEAAmE;IACnE,uEAAuE;IACvE,+DAA+D;IAC/D,KAAK,CAAC,IAAI,CACR,SAAiB,EACjB,IAAsC;QAEtC,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,KAAK,IAAI,CAAC,IAAI,MAAM,CAAC,CAAC,CAAC,OAAO,IAAI,CAAC,KAAK,MAAM,CAAC;QACzF,MAAM,QAAQ,GAAG;YACf,SAAS;YACT,MAAM,EAAE;gBACN,aAAa,EAAE,qBAAqB;gBACpC,OAAO,EAAE,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;gBAC/B,KAAK,EAAE;oBACL,WAAW,EAAE;wBACX,QAAQ,EAAE;4BACR,KAAK,EAAE,IAAI,CAAC,KAAK;4BACjB,GAAG,CAAC,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;yBACxD;qBACF;iBACF;aACF;SACF,CAAC;QACF,IAAI,IAAI,CAAC,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC;gBACpB,SAAS;gBACT,MAAM,EAAE,gBAAgB;gBACxB,QAAQ;aACT,CAAC,CAAC;QACL,CAAC;QACD,IAAI,CAAC;YACH,MAAM,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,wBAAwB,EAAE;gBAClD,SAAS;gBACT,MAAM,EAAE,gBAAgB;gBACxB,QAAQ;gBACR,KAAK,EAAE,OAAO;aACf,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,yBAAyB,SAAS,YAAa,GAAa,CAAC,OAAO,EAAE,CACvE,CAAC;QACJ,CAAC;IACH,CAAC;IAED,iEAAiE;IACjE,mEAAmE;IACnE,gEAAgE;IAChE,kBAAkB,CAChB,MAAgD;QAEhD,OAAO;YACL,UAAU,EAAE,MAAM,CAAC,UAAU,IAAI,SAAS;YAC1C,KAAK,EAAE;gBACL,WAAW,EAAE;oBACX,QAAQ,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;iBACtC;aACF;SACF,CAAC;IACJ,CAAC;IAED,iEAAiE;IACjE,iEAAiE;IACjE,oEAAoE;IACpE,kEAAkE;IAClE,KAAK,CAAC,QAAQ,CACZ,SAAiB,EACjB,OAAsB;QAEtB,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,KAAK,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QAC1C,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,OAAO,IAAI,CAAC,kBAAkB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QACjD,CAAC;QACD,OAAO,SAAS,CAAC;IACnB,CAAC;CACF"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { dirname, resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { loadConfig } from "./config.js";
|
|
6
|
+
import { BudgeterBridge } from "./bridge.js";
|
|
7
|
+
import { stateFilePath } from "./paths.js";
|
|
8
|
+
import { DEFAULT_RULE } from "./rule.js";
|
|
9
|
+
import { deleteStateFile } from "./tracker.js";
|
|
10
|
+
import { logger, setDebug } from "./util/log.js";
|
|
11
|
+
const log = logger("main");
|
|
12
|
+
function readVersion() {
|
|
13
|
+
try {
|
|
14
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
15
|
+
const pkg = JSON.parse(readFileSync(resolve(here, "../package.json"), "utf8"));
|
|
16
|
+
return pkg.version ?? "unknown";
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
function runReset() {
|
|
23
|
+
deleteStateFile(stateFilePath());
|
|
24
|
+
process.stdout.write("hydra-acp-budgeter accumulated cost reset\n");
|
|
25
|
+
}
|
|
26
|
+
async function runTransformer() {
|
|
27
|
+
const config = loadConfig();
|
|
28
|
+
setDebug(config.debug);
|
|
29
|
+
const statePath = stateFilePath();
|
|
30
|
+
const bridge = new BudgeterBridge({
|
|
31
|
+
daemonWsUrl: config.hydraWsUrl,
|
|
32
|
+
token: config.hydraToken,
|
|
33
|
+
softLimit: config.softLimit,
|
|
34
|
+
hardLimit: config.hardLimit,
|
|
35
|
+
currency: config.currency,
|
|
36
|
+
rule: DEFAULT_RULE,
|
|
37
|
+
statePath,
|
|
38
|
+
});
|
|
39
|
+
bridge.start();
|
|
40
|
+
const shutdown = (sig) => {
|
|
41
|
+
log.info(`${sig} received — shutting down`);
|
|
42
|
+
bridge.stop();
|
|
43
|
+
setTimeout(() => process.exit(0), 200).unref();
|
|
44
|
+
};
|
|
45
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
46
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
47
|
+
log.info(`hydra-acp-budgeter up; daemon=${config.hydraDaemonUrl} soft=${config.softLimit} hard=${config.hardLimit} ${config.currency} state=${statePath}`);
|
|
48
|
+
}
|
|
49
|
+
async function main() {
|
|
50
|
+
const argv = process.argv.slice(2);
|
|
51
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
52
|
+
process.stdout.write(`hydra-acp-budgeter ${readVersion()}\n`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (argv[0] === "reset") {
|
|
56
|
+
runReset();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
await runTransformer();
|
|
60
|
+
}
|
|
61
|
+
main().catch((err) => {
|
|
62
|
+
process.stderr.write(`hydra-acp-budgeter: ${err.message}\n`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/paths.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
// hydra-acp injects HYDRA_ACP_HOME when it spawns transformers; this file
|
|
4
|
+
// is invoked both in that role and as a one-shot subcommand from the
|
|
5
|
+
// user's shell, so fall back to ~/.hydra-acp when the env isn't set.
|
|
6
|
+
function hydraHome() {
|
|
7
|
+
return process.env.HYDRA_ACP_HOME ?? resolve(homedir(), ".hydra-acp");
|
|
8
|
+
}
|
|
9
|
+
export function stateFilePath() {
|
|
10
|
+
return resolve(hydraHome(), "budgeter-cost.json");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=paths.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"paths.js","sourceRoot":"","sources":["../src/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAEpC,0EAA0E;AAC1E,qEAAqE;AACrE,qEAAqE;AACrE,SAAS,SAAS;IAChB,OAAO,OAAO,CAAC,GAAG,CAAC,cAAc,IAAI,OAAO,CAAC,OAAO,EAAE,EAAE,YAAY,CAAC,CAAC;AACxE,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,OAAO,CAAC,SAAS,EAAE,EAAE,oBAAoB,CAAC,CAAC;AACpD,CAAC"}
|
package/dist/router.js
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// Translates incoming transformer/message and transformer/session_event
|
|
2
|
+
// envelopes into BudgetEvents, runs them through the rule, and dispatches
|
|
3
|
+
// the resulting verdicts via the Enforcer. Unlike the notifier's
|
|
4
|
+
// EventRouter (one instance per session), this is a singleton — the
|
|
5
|
+
// transformer process holds one connection for every session, so the
|
|
6
|
+
// router keeps a sessionId → SessionMeta map.
|
|
7
|
+
export class EventRouter {
|
|
8
|
+
rule;
|
|
9
|
+
tracker;
|
|
10
|
+
enforcer;
|
|
11
|
+
log;
|
|
12
|
+
metas = new Map();
|
|
13
|
+
constructor(rule, tracker, enforcer, log) {
|
|
14
|
+
this.rule = rule;
|
|
15
|
+
this.tracker = tracker;
|
|
16
|
+
this.enforcer = enforcer;
|
|
17
|
+
this.log = log;
|
|
18
|
+
}
|
|
19
|
+
setMeta(sessionId, meta) {
|
|
20
|
+
this.metas.set(sessionId, meta);
|
|
21
|
+
}
|
|
22
|
+
forgetSession(sessionId) {
|
|
23
|
+
this.metas.delete(sessionId);
|
|
24
|
+
}
|
|
25
|
+
// Fired by the bridge when an agent→client session/update notification
|
|
26
|
+
// is being routed. usage_update is the only kind we care about for cost
|
|
27
|
+
// tracking; other kinds may still flow through the rule so a custom
|
|
28
|
+
// rule can observe (but the default rule ignores them).
|
|
29
|
+
async onResponseUpdate(sessionId, update) {
|
|
30
|
+
const kind = typeof update.sessionUpdate === "string" ? update.sessionUpdate : "";
|
|
31
|
+
// session_info_update may carry an updated title; mirror the notifier
|
|
32
|
+
// and refresh meta so subsequent warning messages reflect the latest.
|
|
33
|
+
if (kind === "session_info_update") {
|
|
34
|
+
this.applySessionInfoUpdate(sessionId, update);
|
|
35
|
+
}
|
|
36
|
+
if (kind !== "usage_update") {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.tracker.applyUsageUpdate(sessionId, update);
|
|
40
|
+
const transition = this.tracker.consumeStateTransition();
|
|
41
|
+
if (transition !== undefined) {
|
|
42
|
+
await this.fire({
|
|
43
|
+
sessionId,
|
|
44
|
+
kind: "threshold_cross",
|
|
45
|
+
raw: { to: transition, from: this.tracker.state },
|
|
46
|
+
meta: this.metaFor(sessionId),
|
|
47
|
+
budget: this.tracker.snapshotFor(sessionId),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
// Also run a plain usage_update event so a custom rule can act on
|
|
51
|
+
// every tick (e.g. emit a status notification every $1 spent). The
|
|
52
|
+
// default rule ignores this kind.
|
|
53
|
+
await this.fire({
|
|
54
|
+
sessionId,
|
|
55
|
+
kind: "usage_update",
|
|
56
|
+
raw: update,
|
|
57
|
+
meta: this.metaFor(sessionId),
|
|
58
|
+
budget: this.tracker.snapshotFor(sessionId),
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// Fired by the bridge for a request:session/prompt intercept. Returns
|
|
62
|
+
// the stop payload to send back to the daemon, or undefined to let the
|
|
63
|
+
// prompt continue. The rule decides — the default rule rejects when
|
|
64
|
+
// we're in the "hard" state.
|
|
65
|
+
async onPromptRequest(sessionId, envelope) {
|
|
66
|
+
const verdict = await this.runRule({
|
|
67
|
+
sessionId,
|
|
68
|
+
kind: "prompt_request",
|
|
69
|
+
raw: { envelope },
|
|
70
|
+
meta: this.metaFor(sessionId),
|
|
71
|
+
budget: this.tracker.snapshotFor(sessionId),
|
|
72
|
+
});
|
|
73
|
+
if (!verdict) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
return this.enforcer.dispatch(sessionId, verdict);
|
|
77
|
+
}
|
|
78
|
+
// Fired by the bridge for lifecycle:session.opened and session.closed.
|
|
79
|
+
async onLifecycle(event, sessionId) {
|
|
80
|
+
if (event === "session.opened") {
|
|
81
|
+
await this.fire({
|
|
82
|
+
sessionId,
|
|
83
|
+
kind: "session_opened",
|
|
84
|
+
raw: {},
|
|
85
|
+
meta: this.metaFor(sessionId),
|
|
86
|
+
budget: this.tracker.snapshotFor(sessionId),
|
|
87
|
+
});
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
if (event === "session.closed") {
|
|
91
|
+
// Note: we deliberately do NOT drop the session's cost from the
|
|
92
|
+
// tracker — total spend is sticky across session.closed so the
|
|
93
|
+
// budget reflects every dollar this transformer has seen. Reset
|
|
94
|
+
// via `hydra-acp-budgeter reset` (deletes the state file) when
|
|
95
|
+
// you want to zero it.
|
|
96
|
+
await this.fire({
|
|
97
|
+
sessionId,
|
|
98
|
+
kind: "session_closed",
|
|
99
|
+
raw: {},
|
|
100
|
+
meta: this.metaFor(sessionId),
|
|
101
|
+
budget: this.tracker.snapshotFor(sessionId),
|
|
102
|
+
});
|
|
103
|
+
this.forgetSession(sessionId);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
async fire(ev) {
|
|
107
|
+
const verdict = await this.runRule(ev);
|
|
108
|
+
if (!verdict) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
// For non-prompt events, only the warn side can fire — reject without
|
|
112
|
+
// a prompt to attach it to is a no-op. Log and skip so a misconfigured
|
|
113
|
+
// rule doesn't fail silently.
|
|
114
|
+
if (verdict.reject && ev.kind !== "prompt_request") {
|
|
115
|
+
this.log.warn(`rule returned reject on kind=${ev.kind} for ${ev.sessionId.slice(0, 12)} — only valid on prompt_request; ignored`);
|
|
116
|
+
}
|
|
117
|
+
if (verdict.warn) {
|
|
118
|
+
await this.enforcer.warn(ev.sessionId, verdict.warn);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
async runRule(ev) {
|
|
122
|
+
try {
|
|
123
|
+
return await this.rule(ev);
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
this.log.warn(`rule threw on kind=${ev.kind} sessionId=${ev.sessionId.slice(0, 12)}: ${err.message}; skipping`);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
metaFor(sessionId) {
|
|
131
|
+
return this.metas.get(sessionId) ?? {};
|
|
132
|
+
}
|
|
133
|
+
applySessionInfoUpdate(sessionId, update) {
|
|
134
|
+
const cur = { ...this.metaFor(sessionId) };
|
|
135
|
+
let changed = false;
|
|
136
|
+
if (typeof update.title === "string" && cur.title !== update.title) {
|
|
137
|
+
cur.title = update.title;
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
const agentId = readHydraAgentId(update._meta);
|
|
141
|
+
if (agentId !== undefined && cur.agentId !== agentId) {
|
|
142
|
+
cur.agentId = agentId;
|
|
143
|
+
changed = true;
|
|
144
|
+
}
|
|
145
|
+
if (changed) {
|
|
146
|
+
this.metas.set(sessionId, cur);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function readHydraAgentId(meta) {
|
|
151
|
+
if (!meta || typeof meta !== "object" || Array.isArray(meta)) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const ns = meta["hydra-acp"];
|
|
155
|
+
if (!ns || typeof ns !== "object" || Array.isArray(ns)) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const v = ns.agentId;
|
|
159
|
+
return typeof v === "string" ? v : undefined;
|
|
160
|
+
}
|
|
161
|
+
//# sourceMappingURL=router.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"router.js","sourceRoot":"","sources":["../src/router.ts"],"names":[],"mappings":"AAWA,wEAAwE;AACxE,0EAA0E;AAC1E,iEAAiE;AACjE,oEAAoE;AACpE,qEAAqE;AACrE,8CAA8C;AAC9C,MAAM,OAAO,WAAW;IAIH;IACA;IACA;IACA;IANX,KAAK,GAAG,IAAI,GAAG,EAAuB,CAAC;IAE/C,YACmB,IAAkB,EAClB,OAAoB,EACpB,QAAkB,EAClB,GAAW;QAHX,SAAI,GAAJ,IAAI,CAAc;QAClB,YAAO,GAAP,OAAO,CAAa;QACpB,aAAQ,GAAR,QAAQ,CAAU;QAClB,QAAG,GAAH,GAAG,CAAQ;IAC3B,CAAC;IAEJ,OAAO,CAAC,SAAiB,EAAE,IAAiB;QAC1C,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAClC,CAAC;IAED,aAAa,CAAC,SAAiB;QAC7B,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;IAC/B,CAAC;IAED,uEAAuE;IACvE,wEAAwE;IACxE,oEAAoE;IACpE,wDAAwD;IACxD,KAAK,CAAC,gBAAgB,CACpB,SAAiB,EACjB,MAA+B;QAE/B,MAAM,IAAI,GACR,OAAO,MAAM,CAAC,aAAa,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,CAAC;QACvE,sEAAsE;QACtE,sEAAsE;QACtE,IAAI,IAAI,KAAK,qBAAqB,EAAE,CAAC;YACnC,IAAI,CAAC,sBAAsB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACjD,CAAC;QACD,IAAI,IAAI,KAAK,cAAc,EAAE,CAAC;YAC5B,OAAO;QACT,CAAC;QACD,IAAI,CAAC,OAAO,CAAC,gBAAgB,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;QACjD,MAAM,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,CAAC;QACzD,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,CAAC,IAAI,CAAC;gBACd,SAAS;gBACT,IAAI,EAAE,iBAAiB;gBACvB,GAAG,EAAE,EAAE,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE;gBACjD,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;gBAC7B,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;aAC5C,CAAC,CAAC;QACL,CAAC;QACD,kEAAkE;QAClE,mEAAmE;QACnE,kCAAkC;QAClC,MAAM,IAAI,CAAC,IAAI,CAAC;YACd,SAAS;YACT,IAAI,EAAE,cAAc;YACpB,GAAG,EAAE,MAAM;YACX,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;YAC7B,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;SAC5C,CAAC,CAAC;IACL,CAAC;IAED,sEAAsE;IACtE,uEAAuE;IACvE,oEAAoE;IACpE,6BAA6B;IAC7B,KAAK,CAAC,eAAe,CACnB,SAAiB,EACjB,QAAiB;QAEjB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC;YACjC,SAAS;YACT,IAAI,EAAE,gBAAgB;YACtB,GAAG,EAAE,EAAE,QAAQ,EAA6B;YAC5C,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;YAC7B,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;SAC5C,CAAC,CAAC;QACH,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC,CAAC;IACpD,CAAC;IAED,uEAAuE;IACvE,KAAK,CAAC,WAAW,CACf,KAA0C,EAC1C,SAAiB;QAEjB,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;YAC/B,MAAM,IAAI,CAAC,IAAI,CAAC;gBACd,SAAS;gBACT,IAAI,EAAE,gBAAgB;gBACtB,GAAG,EAAE,EAAE;gBACP,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;gBAC7B,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;aAC5C,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,KAAK,KAAK,gBAAgB,EAAE,CAAC;YAC/B,gEAAgE;YAChE,+DAA+D;YAC/D,gEAAgE;YAChE,+DAA+D;YAC/D,uBAAuB;YACvB,MAAM,IAAI,CAAC,IAAI,CAAC;gBACd,SAAS;gBACT,IAAI,EAAE,gBAAgB;gBACtB,GAAG,EAAE,EAAE;gBACP,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC;gBAC7B,MAAM,EAAE,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,SAAS,CAAC;aAC5C,CAAC,CAAC;YACH,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,EAAe;QAChC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;QACvC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO;QACT,CAAC;QACD,sEAAsE;QACtE,uEAAuE;QACvE,8BAA8B;QAC9B,IAAI,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,IAAI,KAAK,gBAAgB,EAAE,CAAC;YACnD,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,gCAAgC,EAAE,CAAC,IAAI,QAAQ,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,0CAA0C,CACnH,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,IAAI,EAAE,CAAC;YACjB,MAAM,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC,SAAS,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC;QACvD,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,OAAO,CAAC,EAAe;QACnC,IAAI,CAAC;YACH,OAAO,MAAM,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAC7B,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,IAAI,CAAC,GAAG,CAAC,IAAI,CACX,sBAAsB,EAAE,CAAC,IAAI,cAAc,EAAE,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,KAAM,GAAa,CAAC,OAAO,YAAY,CAC5G,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;IACH,CAAC;IAEO,OAAO,CAAC,SAAiB;QAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;IACzC,CAAC;IAEO,sBAAsB,CAC5B,SAAiB,EACjB,MAA+B;QAE/B,MAAM,GAAG,GAAgB,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,CAAC;QACxD,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,GAAG,CAAC,KAAK,KAAK,MAAM,CAAC,KAAK,EAAE,CAAC;YACnE,GAAG,CAAC,KAAK,GAAG,MAAM,CAAC,KAAK,CAAC;YACzB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAC/C,IAAI,OAAO,KAAK,SAAS,IAAI,GAAG,CAAC,OAAO,KAAK,OAAO,EAAE,CAAC;YACrD,GAAG,CAAC,OAAO,GAAG,OAAO,CAAC;YACtB,OAAO,GAAG,IAAI,CAAC;QACjB,CAAC;QACD,IAAI,OAAO,EAAE,CAAC;YACZ,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QACjC,CAAC;IACH,CAAC;CACF;AAED,SAAS,gBAAgB,CAAC,IAAa;IACrC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;QAC7D,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,EAAE,GAAI,IAAgC,CAAC,WAAW,CAAC,CAAC;IAC1D,IAAI,CAAC,EAAE,IAAI,OAAO,EAAE,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,EAAE,CAAC,EAAE,CAAC;QACvD,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,MAAM,CAAC,GAAI,EAA8B,CAAC,OAAO,CAAC;IAClD,OAAO,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;AAC/C,CAAC"}
|
package/dist/rule.js
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const CURRENCY_SYMBOLS = {
|
|
2
|
+
USD: "$",
|
|
3
|
+
EUR: "€",
|
|
4
|
+
GBP: "£",
|
|
5
|
+
JPY: "¥",
|
|
6
|
+
CNY: "¥",
|
|
7
|
+
CAD: "C$",
|
|
8
|
+
AUD: "A$",
|
|
9
|
+
NZD: "NZ$",
|
|
10
|
+
CHF: "CHF ",
|
|
11
|
+
HKD: "HK$",
|
|
12
|
+
SGD: "S$",
|
|
13
|
+
INR: "₹",
|
|
14
|
+
KRW: "₩",
|
|
15
|
+
BRL: "R$",
|
|
16
|
+
MXN: "MX$",
|
|
17
|
+
};
|
|
18
|
+
function fmtMoney(amount, currency) {
|
|
19
|
+
const fixed = amount.toFixed(2);
|
|
20
|
+
const sym = CURRENCY_SYMBOLS[currency.toUpperCase()];
|
|
21
|
+
if (sym) {
|
|
22
|
+
return `${sym}${fixed}`;
|
|
23
|
+
}
|
|
24
|
+
return `${currency} ${fixed}`;
|
|
25
|
+
}
|
|
26
|
+
// The rule the budgeter runs on every event. Strategy:
|
|
27
|
+
// - threshold_cross to "soft": warn (one-shot, the tracker only fires
|
|
28
|
+
// crosses once per transition)
|
|
29
|
+
// - threshold_cross to "hard": warn (also one-shot)
|
|
30
|
+
// - prompt_request in "hard" state: reject with stopReason "refusal"
|
|
31
|
+
// and an explanation in the message
|
|
32
|
+
// - session_opened in "hard" state: warn the new session so the user
|
|
33
|
+
// understands why their next prompt will bounce
|
|
34
|
+
// - usage_update and session_closed: do nothing
|
|
35
|
+
export const DEFAULT_RULE = (ev) => {
|
|
36
|
+
const { budget } = ev;
|
|
37
|
+
const totalStr = fmtMoney(budget.total, budget.currency);
|
|
38
|
+
const softStr = fmtMoney(budget.soft, budget.currency);
|
|
39
|
+
const hardStr = fmtMoney(budget.hard, budget.currency);
|
|
40
|
+
if (ev.kind === "threshold_cross") {
|
|
41
|
+
const to = (ev.raw.to ?? "ok");
|
|
42
|
+
if (to === "hard") {
|
|
43
|
+
return {
|
|
44
|
+
warn: {
|
|
45
|
+
title: `🛑 Budget hard limit hit`,
|
|
46
|
+
body: `Spent ${totalStr} ≥ ${hardStr} hard limit. Further prompts will be rejected until the budget is reset.`,
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
// Warn on every turn that reports a cost while over the soft limit.
|
|
53
|
+
if (ev.kind === "usage_update" && budget.state !== "ok" && typeof ev.raw.cost?.amount === "number") {
|
|
54
|
+
const label = budget.state === "hard" ? "🛑 Over hard limit" : "💰 Over soft limit";
|
|
55
|
+
return {
|
|
56
|
+
warn: {
|
|
57
|
+
title: `${label} · ${totalStr} spent`,
|
|
58
|
+
body: budget.state === "hard"
|
|
59
|
+
? `Hard limit ${hardStr} reached. Prompts will be rejected until budget is reset.`
|
|
60
|
+
: `Soft limit ${softStr} reached (hard: ${hardStr}).`,
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
if (ev.kind === "prompt_request" && budget.state === "hard") {
|
|
65
|
+
return {
|
|
66
|
+
warn: {
|
|
67
|
+
title: `🛑 Prompt blocked — budget exceeded`,
|
|
68
|
+
body: `Spent ${totalStr} ≥ ${hardStr} hard limit. Reset the budget or raise HYDRA_ACP_BUDGETER_HARD to continue.`,
|
|
69
|
+
},
|
|
70
|
+
reject: {
|
|
71
|
+
message: `Budget exceeded: spent ${totalStr} ≥ ${hardStr} hard limit. Reset the budget or raise HYDRA_ACP_BUDGETER_HARD to continue.`,
|
|
72
|
+
stopReason: "refusal",
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
if (ev.kind === "session_opened" && budget.state === "hard") {
|
|
77
|
+
return {
|
|
78
|
+
warn: {
|
|
79
|
+
title: `🛑 Session opened over budget`,
|
|
80
|
+
body: `Total spend ${totalStr} ≥ ${hardStr}. Prompts on this session will be rejected.`,
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
return null;
|
|
85
|
+
};
|
|
86
|
+
//# sourceMappingURL=rule.js.map
|
package/dist/rule.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rule.js","sourceRoot":"","sources":["../src/rule.ts"],"names":[],"mappings":"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"}
|