floe-guard 0.2.1 → 0.3.0

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/dist/index.d.cts CHANGED
@@ -64,6 +64,32 @@ declare namespace pricing {
64
64
  * same epsilon handling, same fail-closed default.
65
65
  */
66
66
 
67
+ /**
68
+ * One priced spend event in the guard's per-call ledger.
69
+ *
70
+ * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
71
+ * {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
72
+ * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
73
+ * evicted old events). The schema is identical in the Python
74
+ * package (`SpendEvent` in `src/floe_guard/guard.py`) and
75
+ * {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in
76
+ * both languages, so every agent emits the same shape regardless of stack.
77
+ */
78
+ interface SpendEvent {
79
+ /** Unix epoch seconds (UTC). */
80
+ readonly timestamp: number;
81
+ readonly kind: "llm" | "tool";
82
+ readonly modelOrTool: string;
83
+ /** `null` for tool events. */
84
+ readonly promptTokens: number | null;
85
+ /** `null` for tool events. */
86
+ readonly completionTokens: number | null;
87
+ readonly costUsd: number;
88
+ /** Caller-supplied tag (agent/task name). */
89
+ readonly label?: string;
90
+ /** The reservation settled by this call, if any. */
91
+ readonly reserved?: number;
92
+ }
67
93
  interface BudgetGuardOptions {
68
94
  /** Per-model manual prices for models the bundled cost map cannot price. */
69
95
  priceOverrides?: Record<string, ManualPrice>;
@@ -84,6 +110,13 @@ interface BudgetGuardOptions {
84
110
  * flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.
85
111
  */
86
112
  nearLimitBps?: number;
113
+ /**
114
+ * Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).
115
+ * When set, the ledger is a ring buffer keeping the most recent N events so a
116
+ * long-running agent's memory stays bounded; the running totals are
117
+ * unaffected. Default: keep every event.
118
+ */
119
+ maxLogEvents?: number;
87
120
  }
88
121
  /**
89
122
  * A context-aware spend signal for the single local budget.
@@ -120,6 +153,9 @@ declare class BudgetGuard {
120
153
  private lastCost;
121
154
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
122
155
  private reserved;
156
+ /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
157
+ private readonly spendEvents;
158
+ private readonly maxLogEvents?;
123
159
  /**
124
160
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
125
161
  */
@@ -152,11 +188,15 @@ declare class BudgetGuard {
152
188
  * Release a reservation and record the actual cost. `record` is `settle` with
153
189
  * no reservation. Returns the USD cost of this call; unpriceable-model handling
154
190
  * matches {@link BudgetGuard.record}, and any held reservation is released even
155
- * on the warn-and-skip path.
191
+ * on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
192
+ * {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
193
+ * the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
194
+ * in lockstep with `spentUsd`.
156
195
  */
157
196
  settle(model: string, promptTokens: number, completionTokens: number, options?: {
158
197
  reserved?: number;
159
198
  price?: ManualPrice;
199
+ label?: string;
160
200
  }): number;
161
201
  /**
162
202
  * Price one response's tokens offline and add the cost to the total.
@@ -167,6 +207,21 @@ declare class BudgetGuard {
167
207
  */
168
208
  record(model: string, promptTokens: number, completionTokens: number, options?: {
169
209
  price?: ManualPrice;
210
+ label?: string;
211
+ }): number;
212
+ /**
213
+ * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
214
+ *
215
+ * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
216
+ * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
217
+ * `check()` / `reserve()` see them) and appends a `kind: "tool"`
218
+ * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
219
+ * cost: tools have no token usage to price. Deliberately does NOT update the
220
+ * next-call estimate — that predicts the next *LLM* call, and a tool's price
221
+ * would skew it. Returns `costUsd`.
222
+ */
223
+ recordTool(tool: string, costUsd: number, options?: {
224
+ label?: string;
170
225
  }): number;
171
226
  /**
172
227
  * Drop an in-flight reservation without recording spend (e.g. the call failed
@@ -175,6 +230,24 @@ declare class BudgetGuard {
175
230
  release(reserved: number): void;
176
231
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
177
232
  get remainingUsd(): number;
233
+ /**
234
+ * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
235
+ * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
236
+ * it cannot corrupt the ledger.
237
+ */
238
+ get spendLog(): SpendEvent[];
239
+ /**
240
+ * The spend ledger as JSONL — one event per line, newline-terminated.
241
+ *
242
+ * The schema is stable and language-independent (snake_case keys, fixed order;
243
+ * optional fields omitted when absent), identical to the Python package's
244
+ * `export_log()`, so heterogeneous agents produce logs you can concatenate and
245
+ * analyse as one stream. (The *schema* is the contract, not the bytes: the two
246
+ * runtimes may render the same float differently, e.g. JS `0.0000025` vs
247
+ * Python `2.5e-06`.) Empty ledger yields `""`.
248
+ */
249
+ exportLog(): string;
250
+ private appendEvent;
178
251
  /**
179
252
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
180
253
  *
@@ -290,4 +363,4 @@ interface BudgetGuardMiddleware {
290
363
  */
291
364
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
292
365
 
293
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
366
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
package/dist/index.d.ts CHANGED
@@ -64,6 +64,32 @@ declare namespace pricing {
64
64
  * same epsilon handling, same fail-closed default.
65
65
  */
66
66
 
67
+ /**
68
+ * One priced spend event in the guard's per-call ledger.
69
+ *
70
+ * Every {@link BudgetGuard.record} / {@link BudgetGuard.settle} /
71
+ * {@link BudgetGuard.recordTool} that accrues spend appends exactly one event, so
72
+ * the ledger's costs sum to `spentUsd` (unless a `maxLogEvents` ring buffer has
73
+ * evicted old events). The schema is identical in the Python
74
+ * package (`SpendEvent` in `src/floe_guard/guard.py`) and
75
+ * {@link BudgetGuard.exportLog} serialises it with the same snake_case keys in
76
+ * both languages, so every agent emits the same shape regardless of stack.
77
+ */
78
+ interface SpendEvent {
79
+ /** Unix epoch seconds (UTC). */
80
+ readonly timestamp: number;
81
+ readonly kind: "llm" | "tool";
82
+ readonly modelOrTool: string;
83
+ /** `null` for tool events. */
84
+ readonly promptTokens: number | null;
85
+ /** `null` for tool events. */
86
+ readonly completionTokens: number | null;
87
+ readonly costUsd: number;
88
+ /** Caller-supplied tag (agent/task name). */
89
+ readonly label?: string;
90
+ /** The reservation settled by this call, if any. */
91
+ readonly reserved?: number;
92
+ }
67
93
  interface BudgetGuardOptions {
68
94
  /** Per-model manual prices for models the bundled cost map cannot price. */
69
95
  priceOverrides?: Record<string, ManualPrice>;
@@ -84,6 +110,13 @@ interface BudgetGuardOptions {
84
110
  * flags `nearLimit` so an agent can taper before the hard-stop. Default 8000.
85
111
  */
86
112
  nearLimitBps?: number;
113
+ /**
114
+ * Optional cap on the per-call spend ledger ({@link BudgetGuard.spendLog}).
115
+ * When set, the ledger is a ring buffer keeping the most recent N events so a
116
+ * long-running agent's memory stays bounded; the running totals are
117
+ * unaffected. Default: keep every event.
118
+ */
119
+ maxLogEvents?: number;
87
120
  }
88
121
  /**
89
122
  * A context-aware spend signal for the single local budget.
@@ -120,6 +153,9 @@ declare class BudgetGuard {
120
153
  private lastCost;
121
154
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
122
155
  private reserved;
156
+ /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
157
+ private readonly spendEvents;
158
+ private readonly maxLogEvents?;
123
159
  /**
124
160
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
125
161
  */
@@ -152,11 +188,15 @@ declare class BudgetGuard {
152
188
  * Release a reservation and record the actual cost. `record` is `settle` with
153
189
  * no reservation. Returns the USD cost of this call; unpriceable-model handling
154
190
  * matches {@link BudgetGuard.record}, and any held reservation is released even
155
- * on the warn-and-skip path.
191
+ * on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
192
+ * {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
193
+ * the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
194
+ * in lockstep with `spentUsd`.
156
195
  */
157
196
  settle(model: string, promptTokens: number, completionTokens: number, options?: {
158
197
  reserved?: number;
159
198
  price?: ManualPrice;
199
+ label?: string;
160
200
  }): number;
161
201
  /**
162
202
  * Price one response's tokens offline and add the cost to the total.
@@ -167,6 +207,21 @@ declare class BudgetGuard {
167
207
  */
168
208
  record(model: string, promptTokens: number, completionTokens: number, options?: {
169
209
  price?: ManualPrice;
210
+ label?: string;
211
+ }): number;
212
+ /**
213
+ * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
214
+ *
215
+ * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
216
+ * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
217
+ * `check()` / `reserve()` see them) and appends a `kind: "tool"`
218
+ * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
219
+ * cost: tools have no token usage to price. Deliberately does NOT update the
220
+ * next-call estimate — that predicts the next *LLM* call, and a tool's price
221
+ * would skew it. Returns `costUsd`.
222
+ */
223
+ recordTool(tool: string, costUsd: number, options?: {
224
+ label?: string;
170
225
  }): number;
171
226
  /**
172
227
  * Drop an in-flight reservation without recording spend (e.g. the call failed
@@ -175,6 +230,24 @@ declare class BudgetGuard {
175
230
  release(reserved: number): void;
176
231
  /** USD left before the ceiling, net of in-flight reservations (never negative). */
177
232
  get remainingUsd(): number;
233
+ /**
234
+ * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
235
+ * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
236
+ * it cannot corrupt the ledger.
237
+ */
238
+ get spendLog(): SpendEvent[];
239
+ /**
240
+ * The spend ledger as JSONL — one event per line, newline-terminated.
241
+ *
242
+ * The schema is stable and language-independent (snake_case keys, fixed order;
243
+ * optional fields omitted when absent), identical to the Python package's
244
+ * `export_log()`, so heterogeneous agents produce logs you can concatenate and
245
+ * analyse as one stream. (The *schema* is the contract, not the bytes: the two
246
+ * runtimes may render the same float differently, e.g. JS `0.0000025` vs
247
+ * Python `2.5e-06`.) Empty ledger yields `""`.
248
+ */
249
+ exportLog(): string;
250
+ private appendEvent;
178
251
  /**
179
252
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
180
253
  *
@@ -290,4 +363,4 @@ interface BudgetGuardMiddleware {
290
363
  */
291
364
  declare function budgetGuardMiddleware(guard: BudgetGuard): BudgetGuardMiddleware;
292
365
 
293
- export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
366
+ export { type BudgetAdvisory, BudgetExceeded, BudgetGuard, type BudgetGuardMiddleware, type BudgetGuardOptions, FloeGuardError, type ManualPrice, type PricedModel, type SpendEvent, UnpriceableModelError, budgetGuardMiddleware, priceTokens, pricing, resolvePrice };
package/dist/index.js CHANGED
@@ -952,6 +952,9 @@ var BudgetGuard = class {
952
952
  lastCost = 0;
953
953
  /** USD held for in-flight calls (reserved, not yet settled). Counts toward the ceiling. */
954
954
  reserved = 0;
955
+ /** Per-call ledger, oldest first; a ring buffer when maxLogEvents is set. */
956
+ spendEvents = [];
957
+ maxLogEvents;
955
958
  /**
956
959
  * @param limitUsd the spend ceiling, in USD. `0` blocks the very first call.
957
960
  */
@@ -967,7 +970,13 @@ var BudgetGuard = class {
967
970
  `nearLimitBps must be an integer in 0..10000, got ${nearLimitBps}`
968
971
  );
969
972
  }
973
+ if (options.maxLogEvents !== void 0 && (!Number.isInteger(options.maxLogEvents) || options.maxLogEvents < 0)) {
974
+ throw new RangeError(
975
+ `maxLogEvents must be a non-negative integer, got ${options.maxLogEvents}`
976
+ );
977
+ }
970
978
  this.limitUsd = limitUsd;
979
+ this.maxLogEvents = options.maxLogEvents;
971
980
  this.priceOverrides = options.priceOverrides;
972
981
  this.failClosed = options.failClosed ?? true;
973
982
  this.onBlock = options.onBlock ?? defaultOnBlock;
@@ -1029,7 +1038,10 @@ var BudgetGuard = class {
1029
1038
  * Release a reservation and record the actual cost. `record` is `settle` with
1030
1039
  * no reservation. Returns the USD cost of this call; unpriceable-model handling
1031
1040
  * matches {@link BudgetGuard.record}, and any held reservation is released even
1032
- * on the warn-and-skip path.
1041
+ * on the warn-and-skip path. A priced call appends one {@link SpendEvent} to
1042
+ * {@link BudgetGuard.spendLog} (`label` tags it, e.g. with an agent/task name);
1043
+ * the warn-and-skip path accrues nothing and logs nothing, so the ledger stays
1044
+ * in lockstep with `spentUsd`.
1033
1045
  */
1034
1046
  settle(model, promptTokens, completionTokens, options = {}) {
1035
1047
  const reserved = options.reserved ?? 0;
@@ -1066,6 +1078,18 @@ var BudgetGuard = class {
1066
1078
  this.spentUsd = this.limitUsd;
1067
1079
  }
1068
1080
  this.lastCost = cost;
1081
+ this.appendEvent({
1082
+ timestamp: Date.now() / 1e3,
1083
+ kind: "llm",
1084
+ modelOrTool: model,
1085
+ promptTokens,
1086
+ completionTokens,
1087
+ costUsd: cost,
1088
+ ...options.label !== void 0 ? { label: options.label } : {},
1089
+ // 0 means "no reservation" (the plain record() path) — omit rather than
1090
+ // log a meaningless zero.
1091
+ ...reserved ? { reserved } : {}
1092
+ });
1069
1093
  return cost;
1070
1094
  }
1071
1095
  /**
@@ -1078,9 +1102,40 @@ var BudgetGuard = class {
1078
1102
  record(model, promptTokens, completionTokens, options = {}) {
1079
1103
  return this.settle(model, promptTokens, completionTokens, {
1080
1104
  reserved: 0,
1081
- price: options.price
1105
+ price: options.price,
1106
+ label: options.label
1082
1107
  });
1083
1108
  }
1109
+ /**
1110
+ * Accrue a non-LLM cost (a paid tool/API call) against the same ceiling.
1111
+ *
1112
+ * Tools with direct dollar costs — search APIs, scrapers, sandboxes — spend the
1113
+ * same budget the LLM calls do; `recordTool` folds them into `spentUsd` (so
1114
+ * `check()` / `reserve()` see them) and appends a `kind: "tool"`
1115
+ * {@link SpendEvent} to {@link BudgetGuard.spendLog}. The caller supplies the
1116
+ * cost: tools have no token usage to price. Deliberately does NOT update the
1117
+ * next-call estimate — that predicts the next *LLM* call, and a tool's price
1118
+ * would skew it. Returns `costUsd`.
1119
+ */
1120
+ recordTool(tool, costUsd, options = {}) {
1121
+ if (!Number.isFinite(costUsd) || costUsd < 0) {
1122
+ throw new RangeError(`costUsd must be a finite, non-negative number, got ${costUsd}`);
1123
+ }
1124
+ this.spentUsd += costUsd;
1125
+ if (this.spentUsd - this.limitUsd > 0 && this.spentUsd - this.limitUsd < EPS) {
1126
+ this.spentUsd = this.limitUsd;
1127
+ }
1128
+ this.appendEvent({
1129
+ timestamp: Date.now() / 1e3,
1130
+ kind: "tool",
1131
+ modelOrTool: tool,
1132
+ promptTokens: null,
1133
+ completionTokens: null,
1134
+ costUsd,
1135
+ ...options.label !== void 0 ? { label: options.label } : {}
1136
+ });
1137
+ return costUsd;
1138
+ }
1084
1139
  /**
1085
1140
  * Drop an in-flight reservation without recording spend (e.g. the call failed
1086
1141
  * before producing usage). Safe to call with `0`.
@@ -1096,6 +1151,46 @@ var BudgetGuard = class {
1096
1151
  get remainingUsd() {
1097
1152
  return Math.max(0, this.limitUsd - this.spentUsd - this.reserved);
1098
1153
  }
1154
+ /**
1155
+ * The per-call spend ledger, oldest first — one {@link SpendEvent} per priced
1156
+ * `record()` / `settle()` / `recordTool()`. Returns a snapshot copy: mutating
1157
+ * it cannot corrupt the ledger.
1158
+ */
1159
+ get spendLog() {
1160
+ return [...this.spendEvents];
1161
+ }
1162
+ /**
1163
+ * The spend ledger as JSONL — one event per line, newline-terminated.
1164
+ *
1165
+ * The schema is stable and language-independent (snake_case keys, fixed order;
1166
+ * optional fields omitted when absent), identical to the Python package's
1167
+ * `export_log()`, so heterogeneous agents produce logs you can concatenate and
1168
+ * analyse as one stream. (The *schema* is the contract, not the bytes: the two
1169
+ * runtimes may render the same float differently, e.g. JS `0.0000025` vs
1170
+ * Python `2.5e-06`.) Empty ledger yields `""`.
1171
+ */
1172
+ exportLog() {
1173
+ return this.spendEvents.map((e) => {
1174
+ const row = {
1175
+ timestamp: e.timestamp,
1176
+ kind: e.kind,
1177
+ model_or_tool: e.modelOrTool,
1178
+ prompt_tokens: e.promptTokens,
1179
+ completion_tokens: e.completionTokens,
1180
+ cost_usd: e.costUsd
1181
+ };
1182
+ if (e.label !== void 0) row.label = e.label;
1183
+ if (e.reserved !== void 0) row.reserved = e.reserved;
1184
+ return `${JSON.stringify(row)}
1185
+ `;
1186
+ }).join("");
1187
+ }
1188
+ appendEvent(event) {
1189
+ this.spendEvents.push(Object.freeze(event));
1190
+ if (this.maxLogEvents !== void 0 && this.spendEvents.length > this.maxLogEvents) {
1191
+ this.spendEvents.splice(0, this.spendEvents.length - this.maxLogEvents);
1192
+ }
1193
+ }
1099
1194
  /**
1100
1195
  * Context-aware spend advisory for this budget — see {@link BudgetAdvisory}.
1101
1196
  *