agentfences 0.6.0 → 0.8.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.
@@ -0,0 +1,136 @@
1
+ /**
2
+ * Budget prechecks: before a model call goes out, work out the most it could cost and refuse it
3
+ * (FencesStop) if that could take the run past its budget. The amount is held while the call is in
4
+ * flight, so parallel calls can't overspend together, and released at the next checkpoint, which
5
+ * records the actual cost. Used by instrument(), which sees each request before it's sent.
6
+ * Same rules as the Python SDK's precheck.py.
7
+ */
8
+ import * as control from "./control.js";
9
+ import { FencesStop } from "./errors.js";
10
+ import { costOf, priceOf, readUsage } from "./pricing.js";
11
+ import { breachResult } from "./results.js";
12
+ import { getActiveRun } from "./run.js";
13
+ const DATA_URI = /data:[\w/+.-]+;base64,[A-Za-z0-9+/=\\]+/g;
14
+ const FILE_TOKENS = 2_000; // ponytail: a flat guess per embedded image or file; providers price them by size
15
+ const BYTES_PER_TOKEN = 2; // ponytail: tokenizers average 3-4 bytes a token for English and code; 2 keeps the estimate high
16
+ const DEFAULT_MAX_OUT = 32_768; // for the few models whose output cap isn't in the price table
17
+ const MAX_OUT_PARAMS = ["max_completion_tokens", "max_output_tokens", "max_tokens"];
18
+ /** The output-token cap a request sets, if any. */
19
+ export function maxOut(params) {
20
+ for (const k of MAX_OUT_PARAMS)
21
+ if (Number.isInteger(params[k]))
22
+ return params[k];
23
+ return null;
24
+ }
25
+ /**
26
+ * The most a call could cost: its input, estimated high from the request's size, at the dearest
27
+ * input rate, plus its output cap (or the model's) at the output rate. null for a model with no
28
+ * known price, which can't be prechecked.
29
+ */
30
+ export function worstCaseUsd(model, request, outCap = null, n = 1) {
31
+ const p = typeof model === "string" && model ? priceOf(model) : undefined;
32
+ if (!p)
33
+ return null;
34
+ let text;
35
+ try {
36
+ text = JSON.stringify(request, (_k, v) => typeof v === "bigint" ? String(v) : v) ?? "";
37
+ }
38
+ catch {
39
+ text = String(request); // circular: its size is a guess, but the call isn't broken by the check
40
+ }
41
+ const files = text.match(DATA_URI)?.length ?? 0;
42
+ // TextEncoder, not Buffer: edge runtimes have no Buffer
43
+ const tokensIn = Math.floor(new TextEncoder().encode(text.replace(DATA_URI, "")).length / BYTES_PER_TOKEN) + files * FILE_TOKENS;
44
+ const tokensOut = (outCap ?? p.max_out ?? DEFAULT_MAX_OUT) * Math.max(n, 1);
45
+ const rates = p.above !== undefined && p.tier && tokensIn > p.above ? { ...p, ...p.tier } : p;
46
+ return tokensIn * Math.max(rates.in, rates.cache_write ?? 0) + tokensOut * rates.out;
47
+ }
48
+ /** Hold worst if it fits in what's left of the budget; null if it doesn't. */
49
+ function take(run, worst) {
50
+ if (Math.round((run.costUsd + run.reservedUsd + worst) * 1e9) / 1e9 > run.budgetUsd)
51
+ return null;
52
+ const h = { amount: worst, finished: false };
53
+ run.holds.push(h);
54
+ run.reservedUsd += worst;
55
+ return h;
56
+ }
57
+ function refuse(run, worst, outCap) {
58
+ if (run.control.stopped) {
59
+ run.lastBreach = "stopped_by_user";
60
+ run.control.stopSeen = true;
61
+ throw new FencesStop(breachResult("stopped_by_user", run));
62
+ }
63
+ const room = Math.max(run.budgetUsd - run.costUsd - run.reservedUsd, 0);
64
+ const result = breachResult("budget_exceeded", run);
65
+ result.message = `I can't make another model call without risking going over my budget (spent $${run.costUsd.toFixed(4)} ` +
66
+ `of $${run.budgetUsd.toFixed(4)}; the next call could cost up to $${worst.toFixed(4)}). I've stopped here.`;
67
+ result.systemPrompt = "The run's budget can't cover another model call. Stop now; don't call any more tools or models.";
68
+ run.lastBreach = "budget_exceeded"; // the run ends breached even if the agent catches FencesStop and returns
69
+ console.warn(`agentfences: ${run.agentName}: refused a model call that could cost up to $${worst.toFixed(4)} with ` +
70
+ `$${room.toFixed(4)} of its budget left.${outCap ? "" : " Setting max_tokens on the call lowers its worst case."}`);
71
+ throw new FencesStop(result);
72
+ }
73
+ /** The call to check, or null when there's nothing to check (no run, no budget, a model with no known price). */
74
+ export function prepare(model, request, params = {}) {
75
+ const run = getActiveRun();
76
+ if (!run || run.budgetUsd === null)
77
+ return null;
78
+ const outCap = maxOut(params);
79
+ const worst = worstCaseUsd(model, request, outCap, Number.isInteger(params.n) ? params.n : 1);
80
+ return worst === null ? null : { run, worst, outCap };
81
+ }
82
+ /**
83
+ * Before a model call, without waiting: hold its worst case, or throw FencesStop if it doesn't fit.
84
+ * Returns "wait" when it doesn't fit but the run waits at a limit (onBreach "hold"): then use holdAsync.
85
+ */
86
+ export function holdNow(p) {
87
+ const h = take(p.run, p.worst);
88
+ if (h)
89
+ return h;
90
+ if (control.holds(p.run))
91
+ return "wait";
92
+ refuse(p.run, p.worst, p.outCap);
93
+ }
94
+ /** The same, waiting (onBreach "hold") for someone to raise the budget while the call doesn't fit. */
95
+ export async function holdAsync(p) {
96
+ for (;;) {
97
+ const h = take(p.run, p.worst);
98
+ if (h)
99
+ return h;
100
+ if (!control.holds(p.run))
101
+ refuse(p.run, p.worst, p.outCap);
102
+ control.startHold(p.run, "budget");
103
+ await control.waitIfPaused(p.run);
104
+ if (control.endHold(p.run) !== "released")
105
+ refuse(p.run, p.worst, p.outCap);
106
+ }
107
+ }
108
+ /**
109
+ * After the call. A failed call isn't billed, so its hold is released now. Otherwise the hold
110
+ * shrinks to the response's actual cost (a stream without usage keeps its worst case), and the
111
+ * next checkpoint, which records that cost, releases it.
112
+ */
113
+ export function finish(run, h, response, failed = false) {
114
+ const i = run.holds.indexOf(h); // by identity: parallel calls can hold equal amounts
115
+ if (i < 0)
116
+ return;
117
+ if (failed) {
118
+ run.holds.splice(i, 1);
119
+ run.reservedUsd -= h.amount;
120
+ return;
121
+ }
122
+ if (response !== undefined && readUsage(response)) {
123
+ const actual = costOf(response)[0];
124
+ run.reservedUsd += actual - h.amount;
125
+ h.amount = actual;
126
+ }
127
+ h.finished = true;
128
+ }
129
+ /** At a checkpoint, which records the actual cost: release the holds of calls that have finished. */
130
+ export function releaseFinished(run) {
131
+ const done = run.holds.filter((h) => h.finished);
132
+ if (!done.length)
133
+ return;
134
+ run.holds = run.holds.filter((h) => !h.finished);
135
+ run.reservedUsd -= done.reduce((sum, h) => sum + h.amount, 0);
136
+ }
package/dist/pricing.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface CustomPrice {
10
10
  cacheRead?: number;
11
11
  cacheWrite?: number;
12
12
  }
13
+ /** input and output are required; a missing or invalid price throws here, not at the agent's first step. */
13
14
  export declare function setCustomPrices(prices?: Record<string, CustomPrice>): void;
14
15
  export declare function priceOf(model: string): PriceEntry | undefined;
15
16
  export interface Usage {
package/dist/pricing.js CHANGED
@@ -5,23 +5,36 @@
5
5
  import { PRICES } from "./prices.generated.js";
6
6
  import { warnOnce } from "./events.js";
7
7
  const custom = {};
8
+ const validPrice = (v) => typeof v === "number" && Number.isFinite(v) && v >= 0;
9
+ /** input and output are required; a missing or invalid price throws here, not at the agent's first step. */
8
10
  export function setCustomPrices(prices = {}) {
9
- for (const k of Object.keys(custom))
10
- delete custom[k];
11
+ const table = {};
11
12
  for (const [model, p] of Object.entries(prices)) {
13
+ if (!p || typeof p !== "object" || p.input === undefined || p.output === undefined) {
14
+ throw new TypeError(`init({ prices }): '${model}' needs an input and an output price (USD per 1M tokens)`);
15
+ }
16
+ for (const key of ["input", "output", "cacheRead", "cacheWrite"]) {
17
+ if (p[key] !== undefined && !validPrice(p[key])) {
18
+ throw new TypeError(`init({ prices }): '${model}' ${key} must be a number, zero or more, not ${String(p[key])}`);
19
+ }
20
+ }
12
21
  const e = { in: p.input / 1e6, out: p.output / 1e6 };
13
22
  if (p.cacheRead !== undefined)
14
23
  e.cache_read = p.cacheRead / 1e6;
15
24
  if (p.cacheWrite !== undefined)
16
25
  e.cache_write = p.cacheWrite / 1e6;
17
- custom[model.toLowerCase()] = e;
26
+ table[model.toLowerCase()] = e;
18
27
  }
28
+ for (const k of Object.keys(custom))
29
+ delete custom[k]; // only once every entry is valid
30
+ Object.assign(custom, table);
19
31
  }
20
32
  export function priceOf(model) {
21
33
  const name = model.toLowerCase().split("/").pop() ?? ""; // "models/gemini-2.5-pro", "openai/gpt-4o"
22
34
  const undated = name.replace(/-(\d{4}-\d{2}-\d{2}|\d{8})$/, ""); // "claude-sonnet-4-5-20250929"
23
35
  for (const candidate of [name, undated]) {
24
- const hit = custom[candidate] ?? PRICES[candidate];
36
+ // hasOwn: a model named "constructor" mustn't find Object.prototype's
37
+ const hit = Object.hasOwn(custom, candidate) ? custom[candidate] : Object.hasOwn(PRICES, candidate) ? PRICES[candidate] : undefined;
25
38
  if (hit)
26
39
  return hit;
27
40
  }
@@ -0,0 +1,16 @@
1
+ /** What checkpoint() returns, and the words a breach comes with: for the user, and for the model. */
2
+ import type { Run } from "./run.js";
3
+ export interface CheckpointResult {
4
+ ok: boolean;
5
+ breached: boolean;
6
+ /** "budget_exceeded" | "iteration_limit" | "time_limit" | "token_limit" | "stopped_by_user" | ... (see PROTOCOL.md) */
7
+ breachType: string | null;
8
+ /** First-person text, ready to return to the user. */
9
+ message: string;
10
+ /** An instruction to give the model so it wraps up. */
11
+ systemPrompt: string;
12
+ /** The run's total spend so far. */
13
+ spentUsd: number;
14
+ }
15
+ export declare function breachResult(type: string, run: Run): CheckpointResult;
16
+ export declare const okResult: (spentUsd?: number) => CheckpointResult;
@@ -0,0 +1,33 @@
1
+ const SUMMARY = "I'll summarize what I found so far.";
2
+ const WRAP_UP = "Stop your current task immediately and summarize what you have found or completed so far.";
3
+ const money = (x) => `$${x.toFixed(4)}`;
4
+ const count = (n) => n.toLocaleString("en-US");
5
+ export function breachResult(type, run) {
6
+ const spent = run.costUsd, limit = run.budgetUsd ?? 0;
7
+ const text = {
8
+ budget_exceeded: [`I've reached my budget limit (spent ${money(spent)} of ${money(limit)}). ${SUMMARY}`,
9
+ `You have reached your budget limit (${money(spent)} of ${money(limit)} spent). Stop your current task immediately ` +
10
+ "and provide a clear summary of what you have found or completed so far. Tell the user you stopped due to budget and what you accomplished."],
11
+ iteration_limit: [`I've reached my iteration limit (${run.iterations} steps). ${SUMMARY}`,
12
+ `You have reached your iteration limit (${run.iterations} steps). ${WRAP_UP} Tell the user you stopped due to the iteration limit.`],
13
+ time_limit: [`I've reached my time limit (${run.durationMs}ms). ${SUMMARY}`,
14
+ `You have reached your time limit. ${WRAP_UP} Tell the user you stopped due to the time limit.`],
15
+ token_limit: [`I've reached my token limit (${count(run.tokensUsed)} tokens). ${SUMMARY}`,
16
+ `You have reached your token limit (${count(run.tokensUsed)} tokens used). ${WRAP_UP} Tell the user you stopped due to the token limit.`],
17
+ stopped_by_user: [`I was stopped from the Fences dashboard. ${SUMMARY}`,
18
+ `A person stopped this run from the Fences dashboard. ${WRAP_UP} Tell the user you were stopped by an operator.`],
19
+ key_daily_budget: [`My daily spending cap has been reached. ${SUMMARY}`,
20
+ `Your daily spending cap, across all of your runs, has been reached. ${WRAP_UP} Tell the user you stopped because the daily cap was reached.`],
21
+ key_monthly_budget: [`My monthly spending cap has been reached. ${SUMMARY}`,
22
+ `Your monthly spending cap, across all of your runs, has been reached. ${WRAP_UP} Tell the user you stopped because the monthly cap was reached.`],
23
+ fences_unreachable: [`I can't reach Fences to confirm I'm within my limits, so I'm stopping to be safe. ${SUMMARY}`,
24
+ `Your governance service can't be reached and you are configured to stop when that happens. ${WRAP_UP}`],
25
+ paused: [`I was paused from the Fences dashboard and not resumed in time. ${SUMMARY}`,
26
+ `A person paused this run from the Fences dashboard and it was not resumed in time. Stop your current task and summarize ` +
27
+ "what you have found or completed so far. Tell the user the run was paused by an operator."],
28
+ };
29
+ const [message, systemPrompt] = text[type] ??
30
+ ["Governance limit reached.", "A governance limit has been reached. Summarize what you have done so far."];
31
+ return { ok: false, breached: true, breachType: type, message, systemPrompt, spentUsd: run.costUsd };
32
+ }
33
+ export const okResult = (spentUsd = 0) => ({ ok: true, breached: false, breachType: null, message: "", systemPrompt: "", spentUsd });
package/dist/run.d.ts ADDED
@@ -0,0 +1,79 @@
1
+ /** A run's state, and which run is active in this async context. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import * as control from "./control.js";
4
+ import type * as events from "./events.js";
5
+ /** Every limit is optional; see governed(). */
6
+ export interface Limits {
7
+ /** Maximum spend for the run in USD. None anywhere: unlimited spend, with a warning. */
8
+ budgetUsd?: number | null;
9
+ /** Maximum checkpoints (steps). Default 100. */
10
+ maxIterations?: number | null;
11
+ /** Maximum wall-clock time in milliseconds. Default 300000 (5 minutes). */
12
+ maxDurationMs?: number | null;
13
+ /** Maximum tokens (input + output). 0 or unset: no limit. */
14
+ maxTokens?: number | null;
15
+ }
16
+ export declare const DEFAULTS: {
17
+ maxIterations: number;
18
+ maxDurationMs: number;
19
+ maxTokens: number;
20
+ };
21
+ export declare const MAX_EVENTS_PER_RUN = 1000;
22
+ export interface Span {
23
+ name: string;
24
+ start_ts: number;
25
+ end_ts: number;
26
+ status: "ok" | "error";
27
+ attributes: Record<string, unknown>;
28
+ }
29
+ /** A model call's budget hold: its worst case, then its actual cost once it finishes. */
30
+ export interface Hold {
31
+ amount: number;
32
+ finished: boolean;
33
+ }
34
+ export declare class Run {
35
+ readonly agentName: string;
36
+ budgetUsd: number | null;
37
+ maxIterations: number;
38
+ maxDurationMs: number;
39
+ maxTokens: number;
40
+ readonly context: Record<string, string>;
41
+ readonly runId: `${string}-${string}-${string}-${string}-${string}`;
42
+ costUsd: number;
43
+ iterations: number;
44
+ tokensUsed: number;
45
+ readonly startedAt: number;
46
+ readonly decisions: {
47
+ timestamp: number;
48
+ iteration: number;
49
+ reasoning: string;
50
+ action: string | null;
51
+ }[];
52
+ /** Tool and model calls as OpenTelemetry-shaped spans, newest 1,000 kept. */
53
+ readonly events: Span[];
54
+ lastBreach: string | null;
55
+ exception?: events.ExceptionInfo;
56
+ warned: boolean;
57
+ /** Live control from the dashboard (see control.ts). */
58
+ readonly control: control.Control;
59
+ /** Held by guarded tools and model calls in flight. */
60
+ reservedUsd: number;
61
+ /** Model calls' worst-case costs (see precheck.ts), released at the next checkpoint once they finish. */
62
+ holds: Hold[];
63
+ /** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
64
+ onBreach: "hold" | null;
65
+ private readonly abort;
66
+ /**
67
+ * Aborts when Fences halts the run after a stop it didn't reach a checkpoint for (see
68
+ * stopGraceS). Pass it to fetch and other cancellable work so a stuck run really stops:
69
+ * await fetch(url, { signal: getActiveRun()?.signal });
70
+ */
71
+ get signal(): AbortSignal;
72
+ /** @internal */
73
+ _abort(reason: unknown): void;
74
+ constructor(agentName: string, budgetUsd: number | null, maxIterations: number, maxDurationMs: number, maxTokens: number, context: Record<string, string>);
75
+ get durationMs(): number;
76
+ }
77
+ export declare const store: AsyncLocalStorage<Run>;
78
+ /** The run in progress in this async context, if any. */
79
+ export declare function getActiveRun(): Run | null;
package/dist/run.js ADDED
@@ -0,0 +1,56 @@
1
+ /** A run's state, and which run is active in this async context. */
2
+ import { AsyncLocalStorage } from "node:async_hooks";
3
+ import { randomUUID } from "node:crypto";
4
+ import * as control from "./control.js";
5
+ export const DEFAULTS = { maxIterations: 100, maxDurationMs: 300_000, maxTokens: 0 };
6
+ export const MAX_EVENTS_PER_RUN = 1000;
7
+ export class Run {
8
+ agentName;
9
+ budgetUsd;
10
+ maxIterations;
11
+ maxDurationMs;
12
+ maxTokens;
13
+ context;
14
+ runId = randomUUID();
15
+ costUsd = 0;
16
+ iterations = 0;
17
+ tokensUsed = 0;
18
+ startedAt = Date.now();
19
+ decisions = [];
20
+ /** Tool and model calls as OpenTelemetry-shaped spans, newest 1,000 kept. */
21
+ events = [];
22
+ lastBreach = null;
23
+ exception;
24
+ warned = false;
25
+ /** Live control from the dashboard (see control.ts). */
26
+ control = control.newControl();
27
+ /** Held by guarded tools and model calls in flight. */
28
+ reservedUsd = 0;
29
+ /** Model calls' worst-case costs (see precheck.ts), released at the next checkpoint once they finish. */
30
+ holds = [];
31
+ /** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
32
+ onBreach = null;
33
+ abort = new AbortController();
34
+ /**
35
+ * Aborts when Fences halts the run after a stop it didn't reach a checkpoint for (see
36
+ * stopGraceS). Pass it to fetch and other cancellable work so a stuck run really stops:
37
+ * await fetch(url, { signal: getActiveRun()?.signal });
38
+ */
39
+ get signal() { return this.abort.signal; }
40
+ /** @internal */
41
+ _abort(reason) { this.abort.abort(reason); }
42
+ constructor(agentName, budgetUsd, maxIterations, maxDurationMs, maxTokens, context) {
43
+ this.agentName = agentName;
44
+ this.budgetUsd = budgetUsd;
45
+ this.maxIterations = maxIterations;
46
+ this.maxDurationMs = maxDurationMs;
47
+ this.maxTokens = maxTokens;
48
+ this.context = context;
49
+ }
50
+ get durationMs() { return Date.now() - this.startedAt; }
51
+ }
52
+ export const store = new AsyncLocalStorage();
53
+ /** The run in progress in this async context, if any. */
54
+ export function getActiveRun() {
55
+ return store.getStore() ?? null;
56
+ }
package/dist/tools.d.ts CHANGED
@@ -1,5 +1,8 @@
1
+ import { type Run } from "./core.js";
1
2
  /** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
2
3
  export declare function argsSummary(args: unknown[]): string;
4
+ /** Record an async tool call; cost is null for an unguarded call, else a function giving its amount. */
5
+ export declare function callAsync<R>(run: Run, name: string, summary: string, cost: (() => number) | null, needsApproval: boolean, call: () => Promise<R>): Promise<R>;
3
6
  interface ToolFields<A extends unknown[]> {
4
7
  name?: string;
5
8
  /** Reject with a TimeoutError if the call takes longer. JS can't stop the tool's code; pass run.signal to its fetches. */
package/dist/tools.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { randomUUID } from "node:crypto";
2
+ import * as approvals from "./approvals.js";
2
3
  import * as control from "./control.js";
3
- import { _client, getActiveRun, localBreach } from "./core.js";
4
+ import { _client, checkAmount, getActiveRun, localBreach } from "./core.js";
4
5
  import { ToolBlocked } from "./errors.js";
5
6
  import * as events from "./events.js";
6
7
  import { MAX_ARGS, recordToolCall } from "./spans.js";
@@ -23,13 +24,14 @@ function block(run, name, reason, breach = null) {
23
24
  }
24
25
  const fits = (run, amount) => run.budgetUsd === null || Math.round((run.costUsd + run.reservedUsd + amount) * 1e9) / 1e9 <= run.budgetUsd;
25
26
  async function approve(run, name, summary, amount) {
26
- const answer = await control.requestApproval(`${name}(${summary})`, { amountUsd: amount || undefined });
27
+ const answer = await approvals.requestApproval(`${name}(${summary})`, { amountUsd: amount || undefined });
27
28
  if (!answer.granted)
28
29
  block(run, name, answer.note ? `not approved: ${answer.note}` : "not approved");
29
30
  return answer.approvalId;
30
31
  }
31
32
  /** Check a guarded call and reserve its amount; returns the guard id, or throws ToolBlocked. */
32
33
  async function open(run, name, summary, amount, needsApproval) {
34
+ checkAmount(amount, `${name}'s costUsd`); // a NaN or negative amount would let calls past the budget
33
35
  if (run.control.quarantined)
34
36
  block(run, name, "the agent is quarantined in Fences"); // now, not when the quarantine's pause ends
35
37
  if (run.control.paused)
@@ -91,8 +93,9 @@ async function check(run, client, name, summary, amount, call) {
91
93
  control.unsupported("guard", "guarded tools", "they are blocked");
92
94
  block(run, name, "this Fences server doesn't support guarded tools");
93
95
  }
94
- if (resp.network_error || !("allow" in resp))
95
- block(run, name, "couldn't reach Fences to check this call"); // no answer: fail closed
96
+ if (resp.network_error || !("allow" in resp)) { // no answer: fail closed
97
+ block(run, name, resp.status === 401 || resp.status === 403 ? "Fences rejected the API key" : "couldn't reach Fences to check this call");
98
+ }
96
99
  block(run, name, resp.reason ?? "not allowed", resp.breach ?? null);
97
100
  }
98
101
  catch (e) {
@@ -108,6 +111,28 @@ async function settle(run, guardId, amount, error, failed) {
108
111
  run.costUsd += charged;
109
112
  await _client()?.guard(run.runId, { guard_id: guardId, phase: "settle", outcome, cost_usd: charged });
110
113
  }
114
+ /** Record an async tool call; cost is null for an unguarded call, else a function giving its amount. */
115
+ export async function callAsync(run, name, summary, cost, needsApproval, call) {
116
+ const started = performance.now();
117
+ let gate = null;
118
+ try {
119
+ if (cost) {
120
+ const amount = cost();
121
+ gate = [await open(run, name, summary, amount, needsApproval), amount];
122
+ }
123
+ const out = await call();
124
+ recordToolCall(name, summary, started);
125
+ if (gate)
126
+ await settle(run, ...gate, undefined, false);
127
+ return out;
128
+ }
129
+ catch (e) {
130
+ recordToolCall(name, summary, started, e);
131
+ if (gate)
132
+ await settle(run, ...gate, e, true);
133
+ throw e;
134
+ }
135
+ }
111
136
  function timed(p, timeoutS, name) {
112
137
  if (timeoutS === undefined)
113
138
  return p;
@@ -125,25 +150,8 @@ export function tool(fn, options = {}) {
125
150
  const run = getActiveRun();
126
151
  if (!run)
127
152
  return timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
128
- const summary = argsSummary(args), started = performance.now();
129
- let gate = null;
130
- try {
131
- if (guarded) {
132
- const c = options.costUsd, amount = Number((typeof c === "function" ? c(...args) : c) ?? 0);
133
- gate = [await open(run, name, summary, amount, !!options.approve), amount];
134
- }
135
- const out = await timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
136
- recordToolCall(name, summary, started);
137
- if (gate)
138
- await settle(run, ...gate, undefined, false);
139
- return out;
140
- }
141
- catch (e) {
142
- recordToolCall(name, summary, started, e);
143
- if (gate)
144
- await settle(run, ...gate, e, true);
145
- throw e;
146
- }
153
+ const c = options.costUsd;
154
+ return callAsync(run, name, argsSummary(args), guarded ? () => (typeof c === "function" ? c(...args) : c) ?? 0 : null, !!options.approve, () => timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name));
147
155
  };
148
156
  }
149
157
  return (...args) => {
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
2
- export declare const VERSION = "0.6.0";
2
+ export declare const VERSION = "0.8.0";
package/dist/version.js CHANGED
@@ -1,2 +1,2 @@
1
1
  /** The one place the version lives; sent to Fences on every request. Keep in step with package.json. */
2
- export const VERSION = "0.6.0";
2
+ export const VERSION = "0.8.0";
package/package.json CHANGED
@@ -1,24 +1,46 @@
1
1
  {
2
2
  "name": "agentfences",
3
- "version": "0.6.0",
3
+ "version": "0.8.0",
4
4
  "description": "On-call for AI agents: limits your agents can't raise, cost tracking, a decision trail, and a stop button",
5
5
  "license": "MIT",
6
6
  "author": "Bhavik Sardar",
7
7
  "homepage": "https://github.com/bhaviksardar/fences",
8
- "repository": { "type": "git", "url": "git+https://github.com/bhaviksardar/fences.git", "directory": "sdk-js" },
9
- "keywords": ["ai", "agents", "governance", "llm", "budget", "on-call"],
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/bhaviksardar/fences.git",
11
+ "directory": "sdk-js"
12
+ },
13
+ "keywords": [
14
+ "ai",
15
+ "agents",
16
+ "governance",
17
+ "llm",
18
+ "budget",
19
+ "on-call"
20
+ ],
10
21
  "type": "module",
11
22
  "main": "./dist/index.js",
12
23
  "types": "./dist/index.d.ts",
13
- "exports": { ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" } },
14
- "files": ["dist"],
15
- "engines": { "node": ">=18" },
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.ts",
27
+ "default": "./dist/index.js"
28
+ }
29
+ },
30
+ "files": [
31
+ "dist"
32
+ ],
33
+ "engines": {
34
+ "node": ">=18"
35
+ },
16
36
  "scripts": {
17
37
  "build": "node scripts/prices.mjs && tsc",
18
38
  "test": "npm run build && node --test test/*.test.mjs"
19
39
  },
20
40
  "devDependencies": {
41
+ "@anthropic-ai/sdk": "^0.135.0",
21
42
  "@types/node": "^22.0.0",
43
+ "openai": "^7.36.0",
22
44
  "typescript": "^5.6.0"
23
45
  }
24
46
  }