agentfences 0.5.0 → 0.7.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/README.md CHANGED
@@ -78,6 +78,8 @@ if (!approval.granted) return `I didn't issue the refund: ${approval.note}`;
78
78
 
79
79
  An approved amount raises the run's budget. It never hangs or throws: in local mode, outside a run, or on a server without approvals it's denied right away, and with no answer it's denied after `timeoutS`. The heartbeat's timer never keeps your process alive on its own.
80
80
 
81
+ **Waiting at a limit** (from 0.6.0): with `init({ onBreach: "hold" })` (or `governed(fn, { onBreach: "hold" })`), a limit the server says is crossed makes `checkpoint()` wait instead of returning the breach, so whoever is on call can raise it from the dashboard and the agent carries on. A guarded tool denied for budget waits the same way. A stop ends the wait; after `pauseTimeoutS` the agent gets the breach it was held at. Cloud only. Don't hold an agent a person is waiting on.
82
+
81
83
  **Containment hooks** (from 0.5.0) run in your process when a stop or quarantine arrives, to contain what Fences can't reach, such as a credential the agent holds:
82
84
 
83
85
  ```ts
@@ -105,6 +107,16 @@ const deploy = tool(deployImpl, { guard: true, timeoutS: 30 });
105
107
 
106
108
  A guarded or timed tool always returns a promise. What each part guarantees: [ENFORCEMENT.md](../ENFORCEMENT.md).
107
109
 
110
+ **MCP tools** (from 0.7.0): wrap an MCP `Client` and every `callTool` is recorded, with no `tool()` per tool. The tools you name are guarded as above:
111
+
112
+ ```ts
113
+ import { mcp } from "agentfences";
114
+
115
+ mcp.wrap(client, { guard: ["send_email"], costUsd: { refund: (args) => args.amount as number } });
116
+ ```
117
+
118
+ `guard: true` guards every tool, and `approve: [...]` names tools a person approves first. A result with `isError` is recorded as a failed call and still comes back to the agent.
119
+
108
120
  ## Not yet in the TypeScript SDK
109
121
 
110
122
  The Python SDK also records OpenAI/Anthropic calls automatically (`instrument`) and integrates with LangGraph and the OpenAI Agents SDK. Those are also how it checks a model call against the budget **before** it's sent, so the TypeScript SDK doesn't do that yet: a model call is checked afterwards, by `checkpoint()`, and a run can go over its budget by one call. Everything here follows the same server protocol ([PROTOCOL.md](../PROTOCOL.md)).
package/dist/control.d.ts CHANGED
@@ -34,6 +34,8 @@ export interface Control {
34
34
  paused: boolean;
35
35
  pauseExpired: boolean;
36
36
  quarantined: boolean;
37
+ /** While waiting at a limit: "breach" or "budget". */
38
+ held: "breach" | "budget" | null;
37
39
  /** approval id -> the answer, or null while waiting */
38
40
  approvals: Map<string, Approval | null>;
39
41
  awaiting: number;
@@ -66,6 +68,15 @@ export declare function apply(run: Run, commands: Reply[] | undefined): void;
66
68
  * its containment, and says so.
67
69
  */
68
70
  export declare function drain(timeoutMs?: number): Promise<void>;
71
+ /** Breaches a person can lift by raising a limit, so onBreach "hold" waits at them. */
72
+ export declare const HOLDABLE: Set<string>;
73
+ export declare const BUDGET_BREACHES: Set<string>;
74
+ /** Whether this run waits at a limit: onBreach "hold", in cloud mode (someone can raise it), and not stopped. */
75
+ export declare const holds: (run: Run) => boolean;
76
+ /** Pause the run at a limit: "breach" (a checkpoint breached) or "budget" (a call refused for budget). */
77
+ export declare function startHold(run: Run, kind: "breach" | "budget"): void;
78
+ /** After waiting: "released" (an operator raised the limit), "stopped", or "timeout". */
79
+ export declare function endHold(run: Run): "released" | "stopped" | "timeout";
69
80
  /** Hold the agent at its checkpoint while its run is paused, up to pauseTimeoutS. */
70
81
  export declare function waitIfPaused(run: Run): Promise<void>;
71
82
  /**
package/dist/control.js CHANGED
@@ -6,11 +6,13 @@
6
6
  * on as before. Same behaviour as the Python SDK (PROTOCOL.md, "Live control").
7
7
  */
8
8
  import { randomUUID } from "node:crypto";
9
+ /** Heartbeat statuses whose acks go again with the next heartbeat: rate limited, or a server error. */
10
+ const retryable = (status) => status === undefined || status === 429 || status >= 500;
9
11
  const FAST_MS = 2000; // heartbeat interval while a run is paused, waiting for an approval, or stopping
10
12
  const HOOK_TIMEOUT_MS = 30_000; // a hook still running after this is reported as timed out
11
13
  export const newControl = () => ({
12
14
  stopped: false, stoppedAt: 0, stopSeen: false, cancelled: false, onCancel: null, paused: false, pauseExpired: false,
13
- quarantined: false, approvals: new Map(), awaiting: 0, acked: new Set(),
15
+ quarantined: false, held: null, approvals: new Map(), awaiting: 0, acked: new Set(),
14
16
  });
15
17
  const live = new Map(); // run id -> runs in progress
16
18
  let client = null;
@@ -52,7 +54,8 @@ export function unsupported(kind, what, consequence) {
52
54
  const LIMITS = { budget_usd: "budgetUsd", max_iterations: "maxIterations", max_duration_ms: "maxDurationMs", max_tokens: "maxTokens" };
53
55
  /** Apply the commands addressed to this run (from a heartbeat or a checkpoint reply), and acknowledge them. Each is safe to apply twice. */
54
56
  export function apply(run, commands) {
55
- for (const c of commands ?? []) {
57
+ // Limits first: a resume in the same reply may release a call held for budget, which re-checks against them at once
58
+ for (const c of [...(commands ?? [])].sort((a, b) => Number(a.type !== "limits") - Number(b.type !== "limits"))) {
56
59
  if (c.run_id !== undefined && c.run_id !== run.runId)
57
60
  continue;
58
61
  const ctl = run.control;
@@ -153,7 +156,7 @@ async function sendAcks() {
153
156
  const batch = acks;
154
157
  acks = [];
155
158
  const resp = await client.heartbeat([], batch).catch(() => ({ network_error: "heartbeat failed" }));
156
- if (resp.network_error || (resp.ok === false && (resp.status ?? 500) >= 500))
159
+ if (resp.network_error || (resp.ok === false && retryable(resp.status)))
157
160
  acks = [...batch, ...acks];
158
161
  }
159
162
  // Node exits once nothing keeps it busy: beforeExit lets acks queued by a finished hook still go out.
@@ -201,10 +204,11 @@ async function beat() {
201
204
  acks = [];
202
205
  try {
203
206
  const resp = await client.heartbeat(runs.map((r) => ({ run_id: r.runId, iterations: r.iterations, spent_usd: r.costUsd,
204
- tokens_used: r.tokensUsed, paused: r.control.paused, stopped: r.control.stopped })), batch);
207
+ tokens_used: r.tokensUsed, paused: r.control.paused, stopped: r.control.stopped,
208
+ ...(r.control.held ? { held: r.control.held } : {}) })), batch);
205
209
  if (resp.network_error || (resp.ok === false && !resp.unsupported)) {
206
- if (resp.network_error || (resp.status ?? 500) >= 500)
207
- acks = [...batch, ...acks]; // at least once; a 4xx won't get better
210
+ if (resp.network_error || retryable(resp.status))
211
+ acks = [...batch, ...acks]; // at least once; any other 4xx won't get better
208
212
  }
209
213
  else if (resp.unsupported) {
210
214
  unsupported("heartbeat", "the heartbeat", "a stop from the dashboard applies at the next checkpoint and pause isn't available");
@@ -223,6 +227,32 @@ async function beat() {
223
227
  }
224
228
  schedule(nextDelay());
225
229
  }
230
+ // ── Waiting at a limit ────────────────────────────────────────────────────────
231
+ /** Breaches a person can lift by raising a limit, so onBreach "hold" waits at them. */
232
+ export const HOLDABLE = new Set(["budget_exceeded", "iteration_limit", "time_limit", "token_limit", "key_daily_budget",
233
+ "key_monthly_budget", "limit_reached"]);
234
+ export const BUDGET_BREACHES = new Set(["budget_exceeded", "key_daily_budget", "key_monthly_budget"]);
235
+ /** Whether this run waits at a limit: onBreach "hold", in cloud mode (someone can raise it), and not stopped. */
236
+ export const holds = (run) => run.onBreach === "hold" && client !== null && !run.control.stopped;
237
+ /** Pause the run at a limit: "breach" (a checkpoint breached) or "budget" (a call refused for budget). */
238
+ export function startHold(run, kind) {
239
+ run.control.held = kind;
240
+ if (!run.control.paused) {
241
+ run.control.paused = true;
242
+ schedule(0);
243
+ }
244
+ }
245
+ /** After waiting: "released" (an operator raised the limit), "stopped", or "timeout". */
246
+ export function endHold(run) {
247
+ run.control.held = null;
248
+ if (run.control.stopped)
249
+ return "stopped";
250
+ if (run.control.pauseExpired) {
251
+ run.control.pauseExpired = false; // the run ends with the limit it was held at, not "paused"
252
+ return "timeout";
253
+ }
254
+ return "released";
255
+ }
226
256
  // ── Pausing ───────────────────────────────────────────────────────────────────
227
257
  /** Hold the agent at its checkpoint while its run is paused, up to pauseTimeoutS. */
228
258
  export async function waitIfPaused(run) {
package/dist/core.d.ts CHANGED
@@ -60,6 +60,8 @@ export declare class Run {
60
60
  readonly control: control.Control;
61
61
  /** Held by guarded tools that are running. */
62
62
  reservedUsd: number;
63
+ /** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
64
+ onBreach: "hold" | null;
63
65
  private readonly abort;
64
66
  /**
65
67
  * Aborts when Fences halts the run after a stop it didn't reach a checkpoint for (see
@@ -109,6 +111,13 @@ export interface InitOptions {
109
111
  onStop?: control.Hook;
110
112
  /** The same, when the agent is quarantined (once per quarantine, however many runs it reaches). */
111
113
  onQuarantine?: control.Hook;
114
+ /**
115
+ * "hold" makes runs wait at a limit instead of ending: when the server says a limit is crossed,
116
+ * checkpoint() waits (as if paused) until someone raises it and resumes the run, then returns ok
117
+ * and the agent carries on. A guarded tool denied for budget waits the same way. A stop ends the
118
+ * wait, and after pauseTimeoutS the breach is returned as usual. Cloud only; override per agent in governed().
119
+ */
120
+ onBreach?: "hold" | null;
112
121
  }
113
122
  /** Call once at startup, before any governed function runs. */
114
123
  export declare function init(options?: InitOptions): void;
@@ -127,6 +136,7 @@ export declare const _client: () => FencesClient | null;
127
136
  */
128
137
  export declare function governed<A extends unknown[], R>(fn: (...args: A) => R | Promise<R>, options?: Limits & {
129
138
  name?: string;
139
+ onBreach?: "hold" | null;
130
140
  }): (...args: A) => Promise<Awaited<R>>;
131
141
  export interface Extra {
132
142
  /** Extra spend for this step in USD, e.g. a paid tool call. Without a response, the step's whole cost. */
package/dist/core.js CHANGED
@@ -62,6 +62,8 @@ export class Run {
62
62
  control = control.newControl();
63
63
  /** Held by guarded tools that are running. */
64
64
  reservedUsd = 0;
65
+ /** "hold": wait at a limit for someone to raise it (see InitOptions.onBreach). */
66
+ onBreach = null;
65
67
  abort = new AbortController();
66
68
  /**
67
69
  * Aborts when Fences halts the run after a stop it didn't reach a checkpoint for (see
@@ -89,12 +91,21 @@ export function getActiveRun() {
89
91
  let client = null;
90
92
  let localOnly = false;
91
93
  let failClosed = false;
94
+ let defaultOnBreach = null;
95
+ function checkOnBreach(value) {
96
+ if (value === undefined || value === null)
97
+ return null;
98
+ if (value !== "hold")
99
+ throw new TypeError(`onBreach must be "hold" or null, not ${JSON.stringify(value)}`);
100
+ return value;
101
+ }
92
102
  /** Call once at startup, before any governed function runs. */
93
103
  export function init(options = {}) {
94
104
  setCustomPrices(options.prices);
95
105
  events.configure(options.environment, options.release, options.redact);
96
106
  localOnly = !!options.localOnly;
97
107
  failClosed = !!options.failClosed;
108
+ defaultOnBreach = checkOnBreach(options.onBreach);
98
109
  if (localOnly) {
99
110
  client = null;
100
111
  }
@@ -137,9 +148,11 @@ function warnUnreachable(run, err) {
137
148
  */
138
149
  export function governed(fn, options = {}) {
139
150
  const name = options.name ?? (fn.name || "agent");
151
+ checkOnBreach(options.onBreach);
140
152
  return async (...args) => {
141
153
  requireInit();
142
154
  const run = await startRun(name, options);
155
+ run.onBreach = checkOnBreach(options.onBreach) ?? defaultOnBreach;
143
156
  // A stop the agent doesn't reach a checkpoint for within stopGraceS halts the call here
144
157
  const halted = new Promise((_, reject) => {
145
158
  run.control.onCancel = () => {
@@ -251,6 +264,17 @@ export async function checkpoint(response, extra = {}) {
251
264
  cost_delta_usd: stepCost, tokens_used: stepTokens, iterations: run.iterations, duration_ms: run.durationMs
252
265
  }) : null;
253
266
  const result = decide(run, resp);
267
+ // onBreach "hold": wait here for someone to raise the limit. Only a breach the server decided: an unreachable one can't be raised
268
+ if (result.breachType && control.HOLDABLE.has(result.breachType) && control.holds(run) && resp && !resp.network_error) {
269
+ control.startHold(run, "breach");
270
+ await control.waitIfPaused(run);
271
+ const outcome = control.endHold(run);
272
+ if (outcome === "released") { // an operator raised the limit and resumed the run
273
+ run.lastBreach = null;
274
+ return okResult(run.costUsd);
275
+ }
276
+ return outcome === "stopped" ? finalResult(run) : result; // timed out: the limit it was held at
277
+ }
254
278
  if (run.control.paused && !result.breached) { // paused from the dashboard: wait here until resumed
255
279
  await control.waitIfPaused(run);
256
280
  return finalResult(run);
package/dist/index.d.ts CHANGED
@@ -10,3 +10,4 @@ export { priceOf } from "./pricing.js";
10
10
  export type { CustomPrice } from "./pricing.js";
11
11
  export { FencesError, FencesAuthError, AgentQuarantined, FencesStop, ToolBlocked } from "./errors.js";
12
12
  export { VERSION } from "./version.js";
13
+ export * as mcp from "./mcp.js";
package/dist/index.js CHANGED
@@ -5,3 +5,4 @@ export { requestApproval } from "./control.js";
5
5
  export { priceOf } from "./pricing.js";
6
6
  export { FencesError, FencesAuthError, AgentQuarantined, FencesStop, ToolBlocked } from "./errors.js";
7
7
  export { VERSION } from "./version.js";
8
+ export * as mcp from "./mcp.js";
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,40 @@
1
+ type Args = Record<string, unknown>;
2
+ interface CallToolClient {
3
+ callTool(params: {
4
+ name: string;
5
+ arguments?: Args;
6
+ }, ...rest: any[]): Promise<any>;
7
+ }
8
+ export interface WrapOptions {
9
+ /** Tool names to check with Fences before each call, or true for every tool. */
10
+ guard?: boolean | string[];
11
+ /** Per tool, what a call costs the run: a number or a function of its arguments. Such a tool is guarded. */
12
+ costUsd?: Record<string, number | ((args: Args) => number)>;
13
+ /** Tool names a person approves from the dashboard first. Such a tool is guarded. */
14
+ approve?: string[];
15
+ }
16
+ /** An MCP tool answered with isError: recorded as the call's error, the result still goes back. */
17
+ export declare class MCPToolError extends Error {
18
+ readonly result: {
19
+ content?: {
20
+ text?: string;
21
+ }[];
22
+ };
23
+ constructor(result: {
24
+ content?: {
25
+ text?: string;
26
+ }[];
27
+ });
28
+ }
29
+ /**
30
+ * Record every tool call an MCP client makes, and guard the ones you name:
31
+ *
32
+ * mcp.wrap(client, { guard: ["refund", "send_email"], costUsd: { refund: (a) => a.amount as number } });
33
+ *
34
+ * Patches client.callTool in place and returns the client. Each call inside a governed run
35
+ * becomes a tool span, with no tool() per tool. A result with isError counts as a failed call.
36
+ * Guarded tools go through the same check and settle as tool(fn, { guard: true }); a refused
37
+ * one throws ToolBlocked and never reaches the MCP server. Outside a governed run calls pass straight through.
38
+ */
39
+ export declare function wrap<C extends CallToolClient>(client: C, options?: WrapOptions): C;
40
+ export {};
package/dist/mcp.js ADDED
@@ -0,0 +1,46 @@
1
+ import { getActiveRun } from "./core.js";
2
+ import { argsSummary, callAsync } from "./tools.js";
3
+ /** An MCP tool answered with isError: recorded as the call's error, the result still goes back. */
4
+ export class MCPToolError extends Error {
5
+ result;
6
+ constructor(result) {
7
+ super((result.content ?? []).map((c) => c.text ?? "").join(" ").trim() || "isError");
8
+ this.result = result;
9
+ this.name = "MCPToolError";
10
+ }
11
+ }
12
+ /**
13
+ * Record every tool call an MCP client makes, and guard the ones you name:
14
+ *
15
+ * mcp.wrap(client, { guard: ["refund", "send_email"], costUsd: { refund: (a) => a.amount as number } });
16
+ *
17
+ * Patches client.callTool in place and returns the client. Each call inside a governed run
18
+ * becomes a tool span, with no tool() per tool. A result with isError counts as a failed call.
19
+ * Guarded tools go through the same check and settle as tool(fn, { guard: true }); a refused
20
+ * one throws ToolBlocked and never reaches the MCP server. Outside a governed run calls pass straight through.
21
+ */
22
+ export function wrap(client, options = {}) {
23
+ const original = client.callTool.bind(client), costs = options.costUsd ?? {}, approve = new Set(options.approve);
24
+ const named = new Set(Array.isArray(options.guard) ? options.guard : []);
25
+ client.callTool = async (params, ...rest) => {
26
+ const run = getActiveRun();
27
+ if (!run)
28
+ return original(params, ...rest);
29
+ const { name } = params, args = params.arguments ?? {}, c = Object.hasOwn(costs, name) ? costs[name] : undefined;
30
+ const guarded = options.guard === true || named.has(name) || c !== undefined || approve.has(name);
31
+ try {
32
+ return await callAsync(run, name, argsSummary([args]), guarded ? () => Number((typeof c === "function" ? c(args) : c) ?? 0) : null, approve.has(name), async () => {
33
+ const result = await original(params, ...rest);
34
+ if (result?.isError)
35
+ throw new MCPToolError(result);
36
+ return result;
37
+ });
38
+ }
39
+ catch (e) {
40
+ if (e instanceof MCPToolError)
41
+ return e.result;
42
+ throw e;
43
+ }
44
+ };
45
+ return client;
46
+ }
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
@@ -36,7 +36,7 @@ async function open(run, name, summary, amount, needsApproval) {
36
36
  await control.waitIfPaused(run);
37
37
  if (run.control.pauseExpired)
38
38
  block(run, name, "the run was paused from the Fences dashboard and not resumed in time", "paused");
39
- let approvalId = needsApproval ? await approve(run, name, summary, amount) : null;
39
+ const approvalId = needsApproval ? await approve(run, name, summary, amount) : null;
40
40
  if (run.control.stopped)
41
41
  block(run, name, "the run was stopped from the Fences dashboard", "stopped_by_user");
42
42
  const client = _client();
@@ -49,6 +49,27 @@ async function open(run, name, summary, amount, needsApproval) {
49
49
  run.reservedUsd += amount;
50
50
  return randomUUID();
51
51
  }
52
+ const call = { guardId: randomUUID(), approvalId }; // one id per call: the server answers a re-check of a denied id afresh
53
+ for (;;) {
54
+ try {
55
+ return await check(run, client, name, summary, amount, call);
56
+ }
57
+ catch (e) {
58
+ if (!(e instanceof ToolBlocked && control.BUDGET_BREACHES.has(e.breachType ?? "") && control.holds(run)))
59
+ throw e;
60
+ control.startHold(run, "budget"); // onBreach "hold": wait for someone to raise the budget, then check again
61
+ await control.waitIfPaused(run);
62
+ const outcome = control.endHold(run);
63
+ if (outcome === "stopped")
64
+ block(run, name, "the run was stopped from the Fences dashboard", "stopped_by_user");
65
+ if (outcome !== "released")
66
+ throw e;
67
+ }
68
+ }
69
+ }
70
+ async function check(run, client, name, summary, amount, call) {
71
+ if (run.control.stopped)
72
+ block(run, name, "the run was stopped from the Fences dashboard", "stopped_by_user");
52
73
  // Check against what this process already holds before asking the server, which can't see it; reserve until it answers
53
74
  if (!fits(run, amount))
54
75
  block(run, name, `$${amount.toFixed(2)} more could take the run past its $${run.budgetUsd.toFixed(2)} budget`, "budget_exceeded");
@@ -57,15 +78,14 @@ async function open(run, name, summary, amount, needsApproval) {
57
78
  const sent = events.redact({ type: "guard", tool: name, arguments: summary }) ?? {}; // the check goes ahead without them
58
79
  let resp = {}; // eslint-disable-line @typescript-eslint/no-explicit-any
59
80
  for (let attempt = 0; attempt < 2; attempt++) { // once more after an approval the server asked for
60
- const guardId = randomUUID();
61
- resp = await client.guard(run.runId, { guard_id: guardId, phase: "check", tool: name, arguments: sent.arguments ?? null,
62
- amount_usd: amount, approval_id: approvalId });
81
+ resp = await client.guard(run.runId, { guard_id: call.guardId, phase: "check", tool: name, arguments: sent.arguments ?? null,
82
+ amount_usd: amount, approval_id: call.approvalId });
63
83
  control.apply(run, resp.commands);
64
84
  if (resp.allow === true)
65
- return guardId;
66
- if (!(resp.needs_approval && approvalId === null))
85
+ return call.guardId;
86
+ if (!(resp.needs_approval && call.approvalId === null))
67
87
  break;
68
- approvalId = await approve(run, name, summary, amount);
88
+ call.approvalId = await approve(run, name, summary, amount);
69
89
  }
70
90
  if (resp.unsupported) {
71
91
  control.unsupported("guard", "guarded tools", "they are blocked");
@@ -88,6 +108,28 @@ async function settle(run, guardId, amount, error, failed) {
88
108
  run.costUsd += charged;
89
109
  await _client()?.guard(run.runId, { guard_id: guardId, phase: "settle", outcome, cost_usd: charged });
90
110
  }
111
+ /** Record an async tool call; cost is null for an unguarded call, else a function giving its amount. */
112
+ export async function callAsync(run, name, summary, cost, needsApproval, call) {
113
+ const started = performance.now();
114
+ let gate = null;
115
+ try {
116
+ if (cost) {
117
+ const amount = cost();
118
+ gate = [await open(run, name, summary, amount, needsApproval), amount];
119
+ }
120
+ const out = await call();
121
+ recordToolCall(name, summary, started);
122
+ if (gate)
123
+ await settle(run, ...gate, undefined, false);
124
+ return out;
125
+ }
126
+ catch (e) {
127
+ recordToolCall(name, summary, started, e);
128
+ if (gate)
129
+ await settle(run, ...gate, e, true);
130
+ throw e;
131
+ }
132
+ }
91
133
  function timed(p, timeoutS, name) {
92
134
  if (timeoutS === undefined)
93
135
  return p;
@@ -105,25 +147,8 @@ export function tool(fn, options = {}) {
105
147
  const run = getActiveRun();
106
148
  if (!run)
107
149
  return timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
108
- const summary = argsSummary(args), started = performance.now();
109
- let gate = null;
110
- try {
111
- if (guarded) {
112
- const c = options.costUsd, amount = Number((typeof c === "function" ? c(...args) : c) ?? 0);
113
- gate = [await open(run, name, summary, amount, !!options.approve), amount];
114
- }
115
- const out = await timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
116
- recordToolCall(name, summary, started);
117
- if (gate)
118
- await settle(run, ...gate, undefined, false);
119
- return out;
120
- }
121
- catch (e) {
122
- recordToolCall(name, summary, started, e);
123
- if (gate)
124
- await settle(run, ...gate, e, true);
125
- throw e;
126
- }
150
+ const c = options.costUsd;
151
+ return callAsync(run, name, argsSummary(args), guarded ? () => Number((typeof c === "function" ? c(...args) : c) ?? 0) : null, !!options.approve, () => timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name));
127
152
  };
128
153
  }
129
154
  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.5.0";
2
+ export declare const VERSION = "0.7.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.5.0";
2
+ export const VERSION = "0.7.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfences",
3
- "version": "0.5.0",
3
+ "version": "0.7.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",