@phnx-labs/agents-cli 1.20.17 → 1.20.19
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/CHANGELOG.md +19 -0
- package/README.md +1 -1
- package/dist/commands/budget.d.ts +14 -0
- package/dist/commands/budget.js +137 -0
- package/dist/commands/cost.d.ts +12 -0
- package/dist/commands/cost.js +139 -0
- package/dist/commands/exec.d.ts +20 -0
- package/dist/commands/exec.js +382 -5
- package/dist/commands/secrets.d.ts +15 -0
- package/dist/commands/secrets.js +343 -16
- package/dist/commands/sessions.js +4 -0
- package/dist/index.js +4 -0
- package/dist/lib/budget/config.d.ts +9 -0
- package/dist/lib/budget/config.js +115 -0
- package/dist/lib/budget/enforce.d.ts +94 -0
- package/dist/lib/budget/enforce.js +151 -0
- package/dist/lib/budget/ledger.d.ts +61 -0
- package/dist/lib/budget/ledger.js +107 -0
- package/dist/lib/budget/preflight.d.ts +110 -0
- package/dist/lib/budget/preflight.js +200 -0
- package/dist/lib/checkpoint.d.ts +54 -0
- package/dist/lib/checkpoint.js +56 -0
- package/dist/lib/cloud/rush.js +18 -0
- package/dist/lib/exec.d.ts +36 -0
- package/dist/lib/exec.js +192 -4
- package/dist/lib/git.d.ts +18 -0
- package/dist/lib/git.js +67 -4
- package/dist/lib/loop.d.ts +145 -0
- package/dist/lib/loop.js +330 -0
- package/dist/lib/mcp.d.ts +7 -0
- package/dist/lib/mcp.js +24 -0
- package/dist/lib/models.d.ts +11 -0
- package/dist/lib/models.js +21 -0
- package/dist/lib/plugins.js +5 -2
- package/dist/lib/pricing/cost.d.ts +46 -0
- package/dist/lib/pricing/cost.js +71 -0
- package/dist/lib/pricing/index.d.ts +8 -0
- package/dist/lib/pricing/index.js +8 -0
- package/dist/lib/pricing/prices.json +138 -0
- package/dist/lib/pricing/table.d.ts +17 -0
- package/dist/lib/pricing/table.js +73 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +147 -0
- package/dist/lib/secrets/agent.js +500 -0
- package/dist/lib/secrets/bundles.d.ts +58 -7
- package/dist/lib/secrets/bundles.js +264 -75
- package/dist/lib/secrets/filestore.d.ts +82 -0
- package/dist/lib/secrets/filestore.js +295 -0
- package/dist/lib/secrets/linux.d.ts +6 -24
- package/dist/lib/secrets/linux.js +22 -265
- package/dist/lib/session/db.d.ts +40 -0
- package/dist/lib/session/db.js +84 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +126 -2
- package/dist/lib/session/render.d.ts +2 -0
- package/dist/lib/session/render.js +1 -1
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/teams/agents.d.ts +32 -0
- package/dist/lib/teams/agents.js +66 -3
- package/dist/lib/teams/api.js +20 -0
- package/dist/lib/teams/parsers.js +16 -4
- package/dist/lib/types.d.ts +48 -0
- package/dist/lib/workflows.d.ts +56 -0
- package/dist/lib/workflows.js +72 -5
- package/package.json +2 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Budget config resolution (issue #346).
|
|
3
|
+
*
|
|
4
|
+
* The `budget:` block can live in the user/global agents.yaml (`readMeta().budget`)
|
|
5
|
+
* and in any project-local agents.yaml walked from cwd upward. Precedence is
|
|
6
|
+
* project > user, matching `run:` resolution (lib/run-config.ts). Caps merge
|
|
7
|
+
* field-by-field — a project that sets only `per_run` inherits the user's
|
|
8
|
+
* `per_day`/`per_project`/`per_agent` rather than wiping them.
|
|
9
|
+
*
|
|
10
|
+
* This is the single resolver the pre-flight gate, the live watcher, and the
|
|
11
|
+
* `agents budget` command all route through, so the effective cap set is
|
|
12
|
+
* computed in exactly one place.
|
|
13
|
+
*/
|
|
14
|
+
import * as fs from 'fs';
|
|
15
|
+
import * as path from 'path';
|
|
16
|
+
import * as yaml from 'yaml';
|
|
17
|
+
import { getUserAgentsDir, readMeta } from '../state.js';
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Coerce a raw parsed `budget:` block into a typed BudgetConfig, dropping any
|
|
23
|
+
* field whose value is the wrong shape. Malformed entries are ignored, not
|
|
24
|
+
* thrown — a typo in one cap must never crash a run (no-fallbacks applies to
|
|
25
|
+
* the data path, not to user-typed config we choose to be lenient about).
|
|
26
|
+
*/
|
|
27
|
+
function coerceBudget(raw) {
|
|
28
|
+
if (!isRecord(raw))
|
|
29
|
+
return {};
|
|
30
|
+
const out = {};
|
|
31
|
+
if (typeof raw.currency === 'string')
|
|
32
|
+
out.currency = raw.currency;
|
|
33
|
+
if (typeof raw.per_run === 'number' && raw.per_run >= 0)
|
|
34
|
+
out.per_run = raw.per_run;
|
|
35
|
+
if (typeof raw.per_day === 'number' && raw.per_day >= 0)
|
|
36
|
+
out.per_day = raw.per_day;
|
|
37
|
+
if (typeof raw.per_project === 'number' && raw.per_project >= 0)
|
|
38
|
+
out.per_project = raw.per_project;
|
|
39
|
+
if (raw.on_exceed === 'block' || raw.on_exceed === 'warn')
|
|
40
|
+
out.on_exceed = raw.on_exceed;
|
|
41
|
+
if (typeof raw.require_confirm_over === 'number' && raw.require_confirm_over >= 0) {
|
|
42
|
+
out.require_confirm_over = raw.require_confirm_over;
|
|
43
|
+
}
|
|
44
|
+
if (isRecord(raw.per_agent)) {
|
|
45
|
+
const perAgent = {};
|
|
46
|
+
for (const [k, v] of Object.entries(raw.per_agent)) {
|
|
47
|
+
if (typeof v === 'number' && v >= 0)
|
|
48
|
+
perAgent[k] = v;
|
|
49
|
+
}
|
|
50
|
+
if (Object.keys(perAgent).length > 0)
|
|
51
|
+
out.per_agent = perAgent;
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
/** Merge a higher-precedence budget over a base. Set fields win; per_agent merges key-by-key. */
|
|
56
|
+
function mergeBudget(base, over) {
|
|
57
|
+
const merged = { ...base, ...stripUndefined(over) };
|
|
58
|
+
if (base.per_agent || over.per_agent) {
|
|
59
|
+
merged.per_agent = { ...(base.per_agent ?? {}), ...(over.per_agent ?? {}) };
|
|
60
|
+
}
|
|
61
|
+
return merged;
|
|
62
|
+
}
|
|
63
|
+
function stripUndefined(cfg) {
|
|
64
|
+
const out = {};
|
|
65
|
+
for (const [k, v] of Object.entries(cfg)) {
|
|
66
|
+
if (v !== undefined)
|
|
67
|
+
out[k] = v;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Read project-local `budget:` blocks from nearest dir upward, nearest LAST (highest precedence). */
|
|
72
|
+
function getProjectBudgets(startPath) {
|
|
73
|
+
const configs = [];
|
|
74
|
+
let dir = path.resolve(startPath);
|
|
75
|
+
const userAgentsYaml = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
76
|
+
while (dir !== path.dirname(dir)) {
|
|
77
|
+
const manifestPath = path.join(dir, 'agents.yaml');
|
|
78
|
+
if (manifestPath !== userAgentsYaml && fs.existsSync(manifestPath)) {
|
|
79
|
+
try {
|
|
80
|
+
const parsed = yaml.parse(fs.readFileSync(manifestPath, 'utf-8'));
|
|
81
|
+
if (isRecord(parsed) && parsed.budget !== undefined) {
|
|
82
|
+
configs.push(coerceBudget(parsed.budget));
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Malformed project config — ignore and keep walking.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
dir = path.dirname(dir);
|
|
90
|
+
}
|
|
91
|
+
// configs[0] is the nearest dir. Reverse so the nearest applies LAST (wins).
|
|
92
|
+
return configs.reverse();
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Effective budget for `cwd`: user/global base, then each project-local block
|
|
96
|
+
* from farthest ancestor to nearest, nearest winning. `on_exceed` defaults to
|
|
97
|
+
* `block` when nothing sets it (fail-closed: the safe default is to enforce).
|
|
98
|
+
*/
|
|
99
|
+
export function resolveBudgetConfig(cwd = process.cwd()) {
|
|
100
|
+
const userBudget = coerceBudget(readMeta().budget);
|
|
101
|
+
let merged = userBudget;
|
|
102
|
+
for (const projectBudget of getProjectBudgets(cwd)) {
|
|
103
|
+
merged = mergeBudget(merged, projectBudget);
|
|
104
|
+
}
|
|
105
|
+
if (merged.on_exceed === undefined)
|
|
106
|
+
merged.on_exceed = 'block';
|
|
107
|
+
return merged;
|
|
108
|
+
}
|
|
109
|
+
/** True when at least one enforceable cap is set. No caps => budget feature is dormant. */
|
|
110
|
+
export function hasAnyCap(cfg) {
|
|
111
|
+
return (cfg.per_run !== undefined ||
|
|
112
|
+
cfg.per_day !== undefined ||
|
|
113
|
+
cfg.per_project !== undefined ||
|
|
114
|
+
(cfg.per_agent !== undefined && Object.keys(cfg.per_agent).length > 0));
|
|
115
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live spend watcher + cap math (issue #346).
|
|
3
|
+
*
|
|
4
|
+
* This is the provider-agnostic shared surface the loop driver (#332) will
|
|
5
|
+
* reuse for its budget guard. It knows nothing about child processes, agents,
|
|
6
|
+
* or the ledger — it accepts parsed usage events and a caps object, accumulates
|
|
7
|
+
* cost via the canonical pricing module, and fires `onBreach` exactly once when
|
|
8
|
+
* any active cap is crossed.
|
|
9
|
+
*
|
|
10
|
+
* The accumulation is the cross-vendor primitive: feed Claude usage and Codex
|
|
11
|
+
* usage to the same watcher under one `per_project` / `per_run` cap and the
|
|
12
|
+
* spend aggregates across both — no single-vendor control can do that.
|
|
13
|
+
*/
|
|
14
|
+
import type { AgentId, BudgetConfig } from '../types.js';
|
|
15
|
+
/** A parsed usage event from any agent's stream (fields match session/parse). */
|
|
16
|
+
export interface UsageEvent {
|
|
17
|
+
agent?: AgentId | string;
|
|
18
|
+
model?: string;
|
|
19
|
+
inputTokens?: number;
|
|
20
|
+
outputTokens?: number;
|
|
21
|
+
cacheReadTokens?: number;
|
|
22
|
+
cacheCreationTokens?: number;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Caps the watcher enforces. `priorDaySpend` / `priorProjectSpend` seed the
|
|
26
|
+
* accumulators with spend already on the ledger BEFORE this run started, so a
|
|
27
|
+
* per_day cap counts today's earlier runs too — not just this process. Per-cap
|
|
28
|
+
* fields are USD; undefined means "not enforced".
|
|
29
|
+
*/
|
|
30
|
+
export interface LiveCaps {
|
|
31
|
+
perRun?: number;
|
|
32
|
+
perDay?: number;
|
|
33
|
+
perProject?: number;
|
|
34
|
+
/** Per-agent daily caps. Each agent's running spend is checked against its own cap. */
|
|
35
|
+
perAgent?: Partial<Record<string, number>>;
|
|
36
|
+
/** Day spend already on the ledger before this run (cross-vendor). */
|
|
37
|
+
priorDaySpend?: number;
|
|
38
|
+
/** Project spend already on the ledger before this run (cross-vendor). */
|
|
39
|
+
priorProjectSpend?: number;
|
|
40
|
+
/** Per-agent day spend already on the ledger before this run, keyed by agent. */
|
|
41
|
+
priorAgentDaySpend?: Partial<Record<string, number>>;
|
|
42
|
+
}
|
|
43
|
+
/** Which cap tripped, and the spend figures at the moment of the breach. */
|
|
44
|
+
export interface BreachInfo {
|
|
45
|
+
cap: 'per_run' | 'per_day' | 'per_project' | 'per_agent';
|
|
46
|
+
/** The configured limit that was crossed (USD). */
|
|
47
|
+
limit: number;
|
|
48
|
+
/** The spend that crossed it (USD). */
|
|
49
|
+
spend: number;
|
|
50
|
+
/** Agent attributed to the breach (only meaningful for per_agent). */
|
|
51
|
+
agent?: string;
|
|
52
|
+
/** This run's accumulated spend so far (USD). */
|
|
53
|
+
runSpend: number;
|
|
54
|
+
}
|
|
55
|
+
/** Public watcher surface. `feedUsage` is idempotent after a breach (no double-fire). */
|
|
56
|
+
export interface LiveSpendWatcher {
|
|
57
|
+
/** Feed one parsed usage event; accrues cost and may fire onBreach. */
|
|
58
|
+
feedUsage(event: UsageEvent): void;
|
|
59
|
+
/** Total USD this run has accumulated across all fed events. */
|
|
60
|
+
runSpend(): number;
|
|
61
|
+
/** True once a cap has been breached. */
|
|
62
|
+
breached(): boolean;
|
|
63
|
+
/** Stop accepting events / release references. Idempotent. */
|
|
64
|
+
dispose(): void;
|
|
65
|
+
}
|
|
66
|
+
/** Convert a resolved BudgetConfig + prior ledger spend into the caps the watcher needs. */
|
|
67
|
+
export declare function capsFromConfig(cfg: BudgetConfig, prior?: {
|
|
68
|
+
daySpend?: number;
|
|
69
|
+
projectSpend?: number;
|
|
70
|
+
agentDaySpend?: Partial<Record<string, number>>;
|
|
71
|
+
}): LiveCaps;
|
|
72
|
+
/**
|
|
73
|
+
* Create a live spend watcher. `onBreach` fires at most once, on the first
|
|
74
|
+
* event that pushes any active cap from at-or-under to over. After it fires the
|
|
75
|
+
* watcher keeps accumulating (so `runSpend()` stays accurate for the final
|
|
76
|
+
* ledger record) but never calls `onBreach` again.
|
|
77
|
+
*/
|
|
78
|
+
export declare function makeLiveSpendWatcher(args: {
|
|
79
|
+
caps: LiveCaps;
|
|
80
|
+
onBreach: (breach: BreachInfo) => void;
|
|
81
|
+
}): LiveSpendWatcher;
|
|
82
|
+
/**
|
|
83
|
+
* Incrementally extract usage events from a stream-json chunk. Buffers a partial
|
|
84
|
+
* trailing line across calls (returned in `rest`), parses each complete line,
|
|
85
|
+
* and yields one UsageEvent per line that carries token counts. Provider shapes
|
|
86
|
+
* handled: Claude/`--json` assistant turns (`message.usage` with
|
|
87
|
+
* `input_tokens`/`output_tokens`/`cache_*_input_tokens`) and the flatter
|
|
88
|
+
* `usage.record` shape (`usage.input_tokens`/`output`). Lines that aren't JSON
|
|
89
|
+
* or carry no usage are skipped — this never throws on agent output.
|
|
90
|
+
*/
|
|
91
|
+
export declare function extractUsageEvents(chunk: string, pending: string, fallbackModel?: string, fallbackAgent?: string): {
|
|
92
|
+
events: UsageEvent[];
|
|
93
|
+
rest: string;
|
|
94
|
+
};
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { actualCost } from '../pricing/index.js';
|
|
2
|
+
/** Convert a resolved BudgetConfig + prior ledger spend into the caps the watcher needs. */
|
|
3
|
+
export function capsFromConfig(cfg, prior) {
|
|
4
|
+
return {
|
|
5
|
+
perRun: cfg.per_run,
|
|
6
|
+
perDay: cfg.per_day,
|
|
7
|
+
perProject: cfg.per_project,
|
|
8
|
+
perAgent: cfg.per_agent,
|
|
9
|
+
priorDaySpend: prior?.daySpend ?? 0,
|
|
10
|
+
priorProjectSpend: prior?.projectSpend ?? 0,
|
|
11
|
+
priorAgentDaySpend: prior?.agentDaySpend ?? {},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Create a live spend watcher. `onBreach` fires at most once, on the first
|
|
16
|
+
* event that pushes any active cap from at-or-under to over. After it fires the
|
|
17
|
+
* watcher keeps accumulating (so `runSpend()` stays accurate for the final
|
|
18
|
+
* ledger record) but never calls `onBreach` again.
|
|
19
|
+
*/
|
|
20
|
+
export function makeLiveSpendWatcher(args) {
|
|
21
|
+
const { caps, onBreach } = args;
|
|
22
|
+
let run = 0;
|
|
23
|
+
// Cross-vendor accumulators, seeded with pre-run ledger spend.
|
|
24
|
+
let day = caps.priorDaySpend ?? 0;
|
|
25
|
+
let project = caps.priorProjectSpend ?? 0;
|
|
26
|
+
const agentDay = {};
|
|
27
|
+
for (const [k, v] of Object.entries(caps.priorAgentDaySpend ?? {})) {
|
|
28
|
+
if (typeof v === 'number')
|
|
29
|
+
agentDay[k] = v;
|
|
30
|
+
}
|
|
31
|
+
let didBreach = false;
|
|
32
|
+
let disposed = false;
|
|
33
|
+
function checkBreach(agent) {
|
|
34
|
+
if (caps.perRun !== undefined && run > caps.perRun) {
|
|
35
|
+
return { cap: 'per_run', limit: caps.perRun, spend: run, runSpend: run };
|
|
36
|
+
}
|
|
37
|
+
if (caps.perDay !== undefined && day > caps.perDay) {
|
|
38
|
+
return { cap: 'per_day', limit: caps.perDay, spend: day, runSpend: run };
|
|
39
|
+
}
|
|
40
|
+
if (caps.perProject !== undefined && project > caps.perProject) {
|
|
41
|
+
return { cap: 'per_project', limit: caps.perProject, spend: project, runSpend: run };
|
|
42
|
+
}
|
|
43
|
+
if (agent && caps.perAgent && caps.perAgent[agent] !== undefined) {
|
|
44
|
+
const limit = caps.perAgent[agent];
|
|
45
|
+
if ((agentDay[agent] ?? 0) > limit) {
|
|
46
|
+
return { cap: 'per_agent', limit, spend: agentDay[agent], agent, runSpend: run };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
feedUsage(event) {
|
|
53
|
+
if (disposed)
|
|
54
|
+
return;
|
|
55
|
+
const { usd } = actualCost(event.model ?? '', {
|
|
56
|
+
inputTokens: event.inputTokens ?? 0,
|
|
57
|
+
outputTokens: event.outputTokens ?? 0,
|
|
58
|
+
cacheReadTokens: event.cacheReadTokens,
|
|
59
|
+
cacheCreationTokens: event.cacheCreationTokens,
|
|
60
|
+
});
|
|
61
|
+
if (usd <= 0)
|
|
62
|
+
return;
|
|
63
|
+
const agent = event.agent ? String(event.agent) : undefined;
|
|
64
|
+
run += usd;
|
|
65
|
+
day += usd;
|
|
66
|
+
project += usd;
|
|
67
|
+
if (agent)
|
|
68
|
+
agentDay[agent] = (agentDay[agent] ?? 0) + usd;
|
|
69
|
+
if (didBreach)
|
|
70
|
+
return;
|
|
71
|
+
const breach = checkBreach(agent);
|
|
72
|
+
if (breach) {
|
|
73
|
+
didBreach = true;
|
|
74
|
+
onBreach(breach);
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
runSpend: () => run,
|
|
78
|
+
breached: () => didBreach,
|
|
79
|
+
dispose() {
|
|
80
|
+
disposed = true;
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Incrementally extract usage events from a stream-json chunk. Buffers a partial
|
|
86
|
+
* trailing line across calls (returned in `rest`), parses each complete line,
|
|
87
|
+
* and yields one UsageEvent per line that carries token counts. Provider shapes
|
|
88
|
+
* handled: Claude/`--json` assistant turns (`message.usage` with
|
|
89
|
+
* `input_tokens`/`output_tokens`/`cache_*_input_tokens`) and the flatter
|
|
90
|
+
* `usage.record` shape (`usage.input_tokens`/`output`). Lines that aren't JSON
|
|
91
|
+
* or carry no usage are skipped — this never throws on agent output.
|
|
92
|
+
*/
|
|
93
|
+
export function extractUsageEvents(chunk, pending, fallbackModel, fallbackAgent) {
|
|
94
|
+
const combined = pending + chunk;
|
|
95
|
+
const lines = combined.split('\n');
|
|
96
|
+
const rest = lines.pop() ?? '';
|
|
97
|
+
const events = [];
|
|
98
|
+
for (const line of lines) {
|
|
99
|
+
const trimmed = line.trim();
|
|
100
|
+
if (!trimmed || trimmed[0] !== '{')
|
|
101
|
+
continue;
|
|
102
|
+
let obj;
|
|
103
|
+
try {
|
|
104
|
+
obj = JSON.parse(trimmed);
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const ev = usageFromObject(obj, fallbackModel, fallbackAgent);
|
|
110
|
+
if (ev)
|
|
111
|
+
events.push(ev);
|
|
112
|
+
}
|
|
113
|
+
return { events, rest };
|
|
114
|
+
}
|
|
115
|
+
function usageFromObject(obj, fallbackModel, fallbackAgent) {
|
|
116
|
+
// Claude emits a final `type:"result"` event carrying a TOP-LEVEL cumulative
|
|
117
|
+
// `usage` that already sums every per-turn `message.usage`. Counting both the
|
|
118
|
+
// per-turn turns AND this cumulative total double-counts a multi-turn run
|
|
119
|
+
// (~2x). The canonical session parser (src/lib/session/parse.ts) reads usage
|
|
120
|
+
// ONLY from `message.usage` and extracts nothing from the result line — mirror
|
|
121
|
+
// that here: skip result lines entirely for usage.
|
|
122
|
+
if (obj?.type === 'result')
|
|
123
|
+
return null;
|
|
124
|
+
// Claude stream-json assistant turn.
|
|
125
|
+
const mu = obj?.message?.usage;
|
|
126
|
+
if (mu && (typeof mu.input_tokens === 'number' || typeof mu.output_tokens === 'number')) {
|
|
127
|
+
return {
|
|
128
|
+
agent: fallbackAgent,
|
|
129
|
+
model: obj.message.model ?? fallbackModel,
|
|
130
|
+
inputTokens: mu.input_tokens ?? 0,
|
|
131
|
+
outputTokens: mu.output_tokens ?? 0,
|
|
132
|
+
cacheReadTokens: mu.cache_read_input_tokens,
|
|
133
|
+
cacheCreationTokens: mu.cache_creation_input_tokens,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
// Flatter usage.record / usage shape (Codex / `usage.record`). The result-line
|
|
137
|
+
// guard above already excludes Claude's cumulative result usage, so this only
|
|
138
|
+
// matches genuine per-event usage records.
|
|
139
|
+
const u = obj?.usage;
|
|
140
|
+
if (u && (typeof u.input_tokens === 'number' || typeof u.output === 'number' || typeof u.output_tokens === 'number')) {
|
|
141
|
+
return {
|
|
142
|
+
agent: fallbackAgent,
|
|
143
|
+
model: obj.model ?? u.model ?? fallbackModel,
|
|
144
|
+
inputTokens: u.input_tokens ?? u.inputOther ?? 0,
|
|
145
|
+
outputTokens: u.output_tokens ?? u.output ?? 0,
|
|
146
|
+
cacheReadTokens: u.cache_read_input_tokens,
|
|
147
|
+
cacheCreationTokens: u.cache_creation_input_tokens,
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/** A single spend observation. Append-only; never mutated in place. */
|
|
2
|
+
export interface SpendEntry {
|
|
3
|
+
/** Run identifier — groups multiple usage observations from one dispatch. */
|
|
4
|
+
runId: string;
|
|
5
|
+
/** Agent id (claude, codex, ...). The cross-vendor attribution key. */
|
|
6
|
+
agent: string;
|
|
7
|
+
/** Project key (absolute path or repo slug). Empty string when unknown. */
|
|
8
|
+
project: string;
|
|
9
|
+
/** Local calendar day, YYYY-MM-DD. */
|
|
10
|
+
day: string;
|
|
11
|
+
/** Model id as reported by the stream (may carry vendor prefix / date suffix). */
|
|
12
|
+
model: string;
|
|
13
|
+
inputTok: number;
|
|
14
|
+
outputTok: number;
|
|
15
|
+
/** Combined cache read + creation tokens (kept as one field for the ledger). */
|
|
16
|
+
cacheTok: number;
|
|
17
|
+
/** USD cost of THIS observation, via actualCost() at write time. */
|
|
18
|
+
costUsd: number;
|
|
19
|
+
/** Where the spend came from: local run, teams teammate, or cloud dispatch. */
|
|
20
|
+
source: 'run' | 'teams' | 'cloud';
|
|
21
|
+
/** ISO timestamp of the observation. */
|
|
22
|
+
ts: string;
|
|
23
|
+
}
|
|
24
|
+
/** Token bundle for a single observation (matches session/parse usage fields). */
|
|
25
|
+
export interface UsageObservation {
|
|
26
|
+
model?: string;
|
|
27
|
+
inputTokens?: number;
|
|
28
|
+
outputTokens?: number;
|
|
29
|
+
cacheReadTokens?: number;
|
|
30
|
+
cacheCreationTokens?: number;
|
|
31
|
+
}
|
|
32
|
+
/** Default ledger path: <history>/spend/ledger.jsonl. */
|
|
33
|
+
export declare function defaultLedgerPath(): string;
|
|
34
|
+
/** Local YYYY-MM-DD for a Date (defaults to now). Local, not UTC — caps are a human-day notion. */
|
|
35
|
+
export declare function localDay(d?: Date): string;
|
|
36
|
+
/**
|
|
37
|
+
* Append one spend observation. Computes `costUsd` from the usage via the
|
|
38
|
+
* canonical pricing module (unpriced models contribute $0). Returns the written
|
|
39
|
+
* entry. Creates the spend dir on first write.
|
|
40
|
+
*/
|
|
41
|
+
export declare function recordSpend(input: {
|
|
42
|
+
runId: string;
|
|
43
|
+
agent: string;
|
|
44
|
+
project?: string;
|
|
45
|
+
model: string;
|
|
46
|
+
usage: UsageObservation;
|
|
47
|
+
source: SpendEntry['source'];
|
|
48
|
+
ts?: Date;
|
|
49
|
+
}, ledgerPath?: string): SpendEntry;
|
|
50
|
+
/** Load every entry. Skips malformed lines (a half-written final line never breaks a rollup). */
|
|
51
|
+
export declare function loadLedger(ledgerPath?: string): SpendEntry[];
|
|
52
|
+
/** Total USD spend on a given local day across ALL agents (cross-vendor). */
|
|
53
|
+
export declare function spendForDay(day: string, ledger?: SpendEntry[]): number;
|
|
54
|
+
/** Total USD spend on a given day for ONE agent (per-agent cap accounting). */
|
|
55
|
+
export declare function spendForAgentDay(agent: string, day: string, ledger?: SpendEntry[]): number;
|
|
56
|
+
/** Total USD spend attributed to an agent across all time. */
|
|
57
|
+
export declare function spendForAgent(agent: string, ledger?: SpendEntry[]): number;
|
|
58
|
+
/** Total USD spend attributed to a project across all time (cross-vendor). */
|
|
59
|
+
export declare function spendForProject(project: string, ledger?: SpendEntry[]): number;
|
|
60
|
+
/** Total USD spend for a single run id (all of its usage observations). */
|
|
61
|
+
export declare function spendForRun(runId: string, ledger?: SpendEntry[]): number;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only spend ledger (issue #346).
|
|
3
|
+
*
|
|
4
|
+
* Every dispatched run that produces token usage records one JSONL line under
|
|
5
|
+
* `<history>/spend/ledger.jsonl`. The ledger is the shared artifact #323's
|
|
6
|
+
* `agents cost` can later read for $ rollups, so the entry shape stays clean
|
|
7
|
+
* and stable: one record = one usage observation attributed to a run.
|
|
8
|
+
*
|
|
9
|
+
* `costUsd` is computed at write time via the canonical pricing module
|
|
10
|
+
* (lib/pricing) so the ledger is self-contained — a reader never needs the
|
|
11
|
+
* pricing table to sum spend. Rollups (`spendForDay`/`spendForAgent`/...) are
|
|
12
|
+
* pure folds over the file; for the modest line counts a developer accrues this
|
|
13
|
+
* is plenty fast, and there's no index to corrupt.
|
|
14
|
+
*/
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
import { getHistoryDir } from '../state.js';
|
|
18
|
+
import { actualCost } from '../pricing/index.js';
|
|
19
|
+
/** Default ledger path: <history>/spend/ledger.jsonl. */
|
|
20
|
+
export function defaultLedgerPath() {
|
|
21
|
+
return path.join(getHistoryDir(), 'spend', 'ledger.jsonl');
|
|
22
|
+
}
|
|
23
|
+
/** Local YYYY-MM-DD for a Date (defaults to now). Local, not UTC — caps are a human-day notion. */
|
|
24
|
+
export function localDay(d = new Date()) {
|
|
25
|
+
const y = d.getFullYear();
|
|
26
|
+
const m = String(d.getMonth() + 1).padStart(2, '0');
|
|
27
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
28
|
+
return `${y}-${m}-${day}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Append one spend observation. Computes `costUsd` from the usage via the
|
|
32
|
+
* canonical pricing module (unpriced models contribute $0). Returns the written
|
|
33
|
+
* entry. Creates the spend dir on first write.
|
|
34
|
+
*/
|
|
35
|
+
export function recordSpend(input, ledgerPath = defaultLedgerPath()) {
|
|
36
|
+
const ts = input.ts ?? new Date();
|
|
37
|
+
const cacheTok = (input.usage.cacheReadTokens ?? 0) + (input.usage.cacheCreationTokens ?? 0);
|
|
38
|
+
const { usd } = actualCost(input.model, {
|
|
39
|
+
inputTokens: input.usage.inputTokens ?? 0,
|
|
40
|
+
outputTokens: input.usage.outputTokens ?? 0,
|
|
41
|
+
cacheReadTokens: input.usage.cacheReadTokens,
|
|
42
|
+
cacheCreationTokens: input.usage.cacheCreationTokens,
|
|
43
|
+
});
|
|
44
|
+
const entry = {
|
|
45
|
+
runId: input.runId,
|
|
46
|
+
agent: input.agent,
|
|
47
|
+
project: input.project ?? '',
|
|
48
|
+
day: localDay(ts),
|
|
49
|
+
model: input.model,
|
|
50
|
+
inputTok: input.usage.inputTokens ?? 0,
|
|
51
|
+
outputTok: input.usage.outputTokens ?? 0,
|
|
52
|
+
cacheTok,
|
|
53
|
+
costUsd: usd,
|
|
54
|
+
source: input.source,
|
|
55
|
+
ts: ts.toISOString(),
|
|
56
|
+
};
|
|
57
|
+
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
|
|
58
|
+
fs.appendFileSync(ledgerPath, JSON.stringify(entry) + '\n');
|
|
59
|
+
return entry;
|
|
60
|
+
}
|
|
61
|
+
/** Load every entry. Skips malformed lines (a half-written final line never breaks a rollup). */
|
|
62
|
+
export function loadLedger(ledgerPath = defaultLedgerPath()) {
|
|
63
|
+
if (!fs.existsSync(ledgerPath))
|
|
64
|
+
return [];
|
|
65
|
+
const out = [];
|
|
66
|
+
for (const line of fs.readFileSync(ledgerPath, 'utf-8').split('\n')) {
|
|
67
|
+
const trimmed = line.trim();
|
|
68
|
+
if (!trimmed)
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(trimmed);
|
|
72
|
+
if (typeof parsed.costUsd === 'number')
|
|
73
|
+
out.push(parsed);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
// Tolerate a torn final line; everything before it is intact.
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
function sum(entries, pred) {
|
|
82
|
+
let total = 0;
|
|
83
|
+
for (const e of entries)
|
|
84
|
+
if (pred(e))
|
|
85
|
+
total += e.costUsd;
|
|
86
|
+
return total;
|
|
87
|
+
}
|
|
88
|
+
/** Total USD spend on a given local day across ALL agents (cross-vendor). */
|
|
89
|
+
export function spendForDay(day, ledger = loadLedger()) {
|
|
90
|
+
return sum(ledger, (e) => e.day === day);
|
|
91
|
+
}
|
|
92
|
+
/** Total USD spend on a given day for ONE agent (per-agent cap accounting). */
|
|
93
|
+
export function spendForAgentDay(agent, day, ledger = loadLedger()) {
|
|
94
|
+
return sum(ledger, (e) => e.agent === agent && e.day === day);
|
|
95
|
+
}
|
|
96
|
+
/** Total USD spend attributed to an agent across all time. */
|
|
97
|
+
export function spendForAgent(agent, ledger = loadLedger()) {
|
|
98
|
+
return sum(ledger, (e) => e.agent === agent);
|
|
99
|
+
}
|
|
100
|
+
/** Total USD spend attributed to a project across all time (cross-vendor). */
|
|
101
|
+
export function spendForProject(project, ledger = loadLedger()) {
|
|
102
|
+
return sum(ledger, (e) => e.project === project);
|
|
103
|
+
}
|
|
104
|
+
/** Total USD spend for a single run id (all of its usage observations). */
|
|
105
|
+
export function spendForRun(runId, ledger = loadLedger()) {
|
|
106
|
+
return sum(ledger, (e) => e.runId === runId);
|
|
107
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-flight cost estimate + gate (issue #346).
|
|
3
|
+
*
|
|
4
|
+
* Before a run spawns we estimate its cost and decide whether to allow it. The
|
|
5
|
+
* estimate's token basis comes from recent ledger averages for the same agent
|
|
6
|
+
* (the most accurate signal we have), falling back to a prompt-character
|
|
7
|
+
* heuristic when there's no history. Cost is computed via the canonical pricing
|
|
8
|
+
* module — never reimplemented here.
|
|
9
|
+
*
|
|
10
|
+
* `enforcePreflight` is the decision: with `on_exceed: block`, if launching
|
|
11
|
+
* this run would push any cap (per_run / per_day / per_agent / per_project)
|
|
12
|
+
* over the line, it denies. With `on_exceed: warn` it always allows but reports
|
|
13
|
+
* the projected overrun.
|
|
14
|
+
*/
|
|
15
|
+
import type { BudgetConfig } from '../types.js';
|
|
16
|
+
import type { SpendEntry } from './ledger.js';
|
|
17
|
+
/** A pre-flight cost estimate for one run. */
|
|
18
|
+
export interface RunEstimate {
|
|
19
|
+
/** Estimated USD for this run. 0 when the model is unpriced. */
|
|
20
|
+
estUsd: number;
|
|
21
|
+
/** How the token count was derived. */
|
|
22
|
+
basis: 'ledger-average' | 'prompt-heuristic' | 'none';
|
|
23
|
+
/** True when the model resolved to a priced entry. */
|
|
24
|
+
priced: boolean;
|
|
25
|
+
estInputTokens: number;
|
|
26
|
+
estOutputTokens: number;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Estimate the cost of a run. When the ledger has prior runs for this agent we
|
|
30
|
+
* use their average input/output tokens; otherwise we fall back to a
|
|
31
|
+
* prompt-character heuristic. `recentAvgTokens` lets callers inject a
|
|
32
|
+
* precomputed average (e.g. from a scoped ledger) for testability.
|
|
33
|
+
*/
|
|
34
|
+
export declare function estimateRunCost(args: {
|
|
35
|
+
agent: string;
|
|
36
|
+
model: string;
|
|
37
|
+
mode?: string;
|
|
38
|
+
promptChars?: number;
|
|
39
|
+
recentAvgTokens?: {
|
|
40
|
+
input: number;
|
|
41
|
+
output: number;
|
|
42
|
+
};
|
|
43
|
+
ledger?: SpendEntry[];
|
|
44
|
+
}): RunEstimate;
|
|
45
|
+
/** Average input/output tokens per RUN for an agent, from the ledger. Null when no history. */
|
|
46
|
+
export declare function ledgerAverageTokens(agent: string, ledger: SpendEntry[]): {
|
|
47
|
+
input: number;
|
|
48
|
+
output: number;
|
|
49
|
+
} | null;
|
|
50
|
+
/** Decision returned by the pre-flight gate. */
|
|
51
|
+
export interface PreflightDecision {
|
|
52
|
+
/** Whether the run may proceed. */
|
|
53
|
+
allow: boolean;
|
|
54
|
+
/** Whether the caller must interactively confirm (estimate >= require_confirm_over). */
|
|
55
|
+
needsConfirm: boolean;
|
|
56
|
+
/** Human reason when blocked or confirming. */
|
|
57
|
+
reason?: string;
|
|
58
|
+
/** Which cap blocked, if any. */
|
|
59
|
+
blockedCap?: 'per_run' | 'per_day' | 'per_agent' | 'per_project';
|
|
60
|
+
/** Projected day spend if this run lands at its estimate. */
|
|
61
|
+
projectedDaySpend: number;
|
|
62
|
+
/** Projected project spend if this run lands at its estimate. */
|
|
63
|
+
projectedProjectSpend: number;
|
|
64
|
+
}
|
|
65
|
+
/** Current spend snapshot the gate compares the estimate against. */
|
|
66
|
+
export interface LedgerState {
|
|
67
|
+
/** Agent this snapshot is for (used to pick the matching per_agent cap). */
|
|
68
|
+
agent: string;
|
|
69
|
+
daySpend: number;
|
|
70
|
+
projectSpend: number;
|
|
71
|
+
agentDaySpend: number;
|
|
72
|
+
}
|
|
73
|
+
/** Read the ledger snapshot the gate needs for `agent` / `project` / today. */
|
|
74
|
+
export declare function ledgerStateFor(agent: string, project: string, ledger?: SpendEntry[]): LedgerState;
|
|
75
|
+
/**
|
|
76
|
+
* The pre-flight gate. Projects this run's estimate on top of current spend and
|
|
77
|
+
* decides allow/deny. `on_exceed: warn` never blocks (allow:true) but still
|
|
78
|
+
* reports the projected overrun via `reason`. A hard block sets allow:false —
|
|
79
|
+
* `--yes` MUST NOT override it (the caller enforces that; this function only
|
|
80
|
+
* reports the truth).
|
|
81
|
+
*/
|
|
82
|
+
export declare function enforcePreflight(cfg: BudgetConfig, state: LedgerState, est: RunEstimate): PreflightDecision;
|
|
83
|
+
/** Build a one-line human estimate banner for `agents run` preamble. */
|
|
84
|
+
export declare function formatEstimateBanner(agent: string, model: string, est: RunEstimate): string;
|
|
85
|
+
/** Result of the high-level run gate consumed by `agents run` / teams / cloud. */
|
|
86
|
+
export interface PreflightGateResult {
|
|
87
|
+
/** True when no caps are configured — budget feature dormant, nothing to do. */
|
|
88
|
+
dormant: boolean;
|
|
89
|
+
cfg: BudgetConfig;
|
|
90
|
+
estimate: RunEstimate;
|
|
91
|
+
decision: PreflightDecision;
|
|
92
|
+
banner: string;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* High-level pre-flight gate: resolve the effective budget for `cwd`, estimate
|
|
96
|
+
* the run, and evaluate every cap. Returns `dormant:true` (and skips all work)
|
|
97
|
+
* when no caps are set, so the gate is zero-cost for users who never configure
|
|
98
|
+
* a budget. The CLI layer decides how to act on `decision` (print banner,
|
|
99
|
+
* confirm, or block + exit non-zero).
|
|
100
|
+
*/
|
|
101
|
+
export declare function runPreflightGate(args: {
|
|
102
|
+
agent: string;
|
|
103
|
+
model: string;
|
|
104
|
+
mode?: string;
|
|
105
|
+
prompt?: string;
|
|
106
|
+
project: string;
|
|
107
|
+
cwd?: string;
|
|
108
|
+
ledger?: SpendEntry[];
|
|
109
|
+
}): PreflightGateResult;
|
|
110
|
+
export type { SpendEntry };
|