agentfences 0.8.0 → 0.9.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
@@ -47,7 +47,7 @@ const webSearch = tool(async ({ query }: { query: string }) => search(query));
47
47
  await context({ user_id: "u_123", session_id: "s_9" }, () => research(question)); // tags every run inside
48
48
  ```
49
49
 
50
- - Tool and model calls are recorded as OpenTelemetry-shaped spans (`execute_tool webSearch`, `gen_ai.*` attributes), on `getActiveRun().events`.
50
+ - Tool and model calls are recorded as OpenTelemetry-shaped spans (`execute_tool webSearch`, `gen_ai.*` attributes), on `getActiveRun().events`. A tool call carries its argument names (`query, limit`), not their values; `init({ toolArguments: "full" })` records the values as JSON (300 characters at most), `"none"` neither (from 0.9.0). A guarded tool's check and an approval request still send the values.
51
51
  - When a governed run throws, Fences records the error's type, message and stack with the run; the error still reaches your code.
52
52
  - `init({ redact: (event) => ... })` sees everything before it leaves the process and can change or drop it.
53
53
 
package/dist/client.d.ts CHANGED
@@ -1,10 +1,28 @@
1
1
  export type Reply = Record<string, any>;
2
+ export declare const FAILURES_TO_OPEN = 3;
3
+ /**
4
+ * A circuit breaker: after FAILURES_TO_OPEN failed calls in a row (no connection, a timeout, a 5xx,
5
+ * or a 429 on anything but the heartbeat), skip calls for about 30 seconds, then let one through to test the server. While it's open
6
+ * a call fails at once instead of after the timeout: checkpoints are enforced locally and guarded
7
+ * tools are blocked, without adding seconds to every step.
8
+ */
9
+ export declare class Breaker {
10
+ failures: number;
11
+ openUntil: number;
12
+ private probing;
13
+ allow(): boolean;
14
+ record(ok: boolean): void;
15
+ }
2
16
  export declare class FencesClient {
3
17
  readonly endpoint: string;
4
18
  private readonly timeoutMs;
5
19
  private readonly headers;
20
+ readonly breaker: Breaker;
21
+ /** The server echoed a step_id: it counts a repeated step once, so retrying a checkpoint is safe. */
22
+ dedupesSteps: boolean;
6
23
  constructor(apiKey: string, endpoint: string, timeoutMs?: number);
7
24
  startRun(body: Record<string, unknown>): Promise<Reply>;
25
+ /** body.step_id, if set, lets the server count the step once however often it's sent. */
8
26
  checkpoint(runId: string, body: Record<string, unknown>): Promise<Reply>;
9
27
  logDecision(runId: string, body: Record<string, unknown>): Promise<Reply>;
10
28
  endRun(runId: string, body: Record<string, unknown>): Promise<Reply>;
@@ -19,6 +37,8 @@ export declare class FencesClient {
19
37
  * and any other error { ok: false, status }.
20
38
  * start: a rejected key throws FencesAuthError, before any agent code runs. Later in a run it
21
39
  * returns { network_error, auth_rejected: true }: throwing there would replace the agent's own result or error.
40
+ * retry: a call that's safe to repeat goes once more after a connection error (not after a timeout,
41
+ * which would double the wait).
22
42
  */
23
- post(path: string, payload: unknown, optional?: boolean, start?: boolean): Promise<Reply>;
43
+ post(path: string, payload: unknown, optional?: boolean, start?: boolean, retry?: boolean): Promise<Reply>;
24
44
  }
package/dist/client.js CHANGED
@@ -4,10 +4,52 @@
4
4
  */
5
5
  import { FencesAuthError } from "./errors.js";
6
6
  import { VERSION } from "./version.js";
7
+ export const FAILURES_TO_OPEN = 3; // failed calls in a row before the breaker opens
8
+ const OPEN_MS = 30_000; // how long calls are skipped once it's open (with jitter), before one goes through to test
9
+ /**
10
+ * A circuit breaker: after FAILURES_TO_OPEN failed calls in a row (no connection, a timeout, a 5xx,
11
+ * or a 429 on anything but the heartbeat), skip calls for about 30 seconds, then let one through to test the server. While it's open
12
+ * a call fails at once instead of after the timeout: checkpoints are enforced locally and guarded
13
+ * tools are blocked, without adding seconds to every step.
14
+ */
15
+ export class Breaker {
16
+ failures = 0;
17
+ openUntil = 0;
18
+ probing = false;
19
+ allow() {
20
+ if (this.failures < FAILURES_TO_OPEN)
21
+ return true;
22
+ if (this.probing || Date.now() < this.openUntil)
23
+ return false;
24
+ this.probing = true; // one test call at a time
25
+ return true;
26
+ }
27
+ record(ok) {
28
+ const wasOpen = this.failures >= FAILURES_TO_OPEN;
29
+ this.probing = false;
30
+ if (ok) {
31
+ this.failures = 0;
32
+ if (wasOpen)
33
+ console.warn("agentfences: Fences is reachable again");
34
+ return;
35
+ }
36
+ this.failures += 1;
37
+ if (this.failures < FAILURES_TO_OPEN)
38
+ return;
39
+ this.openUntil = Date.now() + OPEN_MS * (0.75 + Math.random() / 2); // jitter: SDKs cut off together don't all come back at once
40
+ if (!wasOpen) {
41
+ console.warn(`agentfences: Fences failed ${this.failures} calls in a row; skipping calls for about ${OPEN_MS / 1000}s ` +
42
+ "(limits enforced locally, guarded tools blocked)");
43
+ }
44
+ }
45
+ }
7
46
  export class FencesClient {
8
47
  endpoint;
9
48
  timeoutMs;
10
49
  headers;
50
+ breaker = new Breaker();
51
+ /** The server echoed a step_id: it counts a repeated step once, so retrying a checkpoint is safe. */
52
+ dedupesSteps = false;
11
53
  constructor(apiKey, endpoint, timeoutMs = 3000) {
12
54
  this.endpoint = endpoint;
13
55
  this.timeoutMs = timeoutMs;
@@ -16,7 +58,13 @@ export class FencesClient {
16
58
  this.headers = { "Content-Type": "application/json", "X-API-Key": apiKey, "X-Fences-SDK": `js/${VERSION}` };
17
59
  }
18
60
  startRun(body) { return this.post("/api/runs/start", body, false, true); }
19
- checkpoint(runId, body) { return this.post(`/api/runs/${runId}/checkpoint`, body); }
61
+ /** body.step_id, if set, lets the server count the step once however often it's sent. */
62
+ async checkpoint(runId, body) {
63
+ const resp = await this.post(`/api/runs/${runId}/checkpoint`, body, false, false, !!body.step_id && this.dedupesSteps);
64
+ if (body.step_id && resp.step_id === body.step_id)
65
+ this.dedupesSteps = true;
66
+ return resp;
67
+ }
20
68
  logDecision(runId, body) { return this.post(`/api/runs/${runId}/decisions`, body); }
21
69
  endRun(runId, body) { return this.post(`/api/runs/${runId}/end`, body); }
22
70
  logSpans(runId, spans) { return this.post(`/api/runs/${runId}/spans`, { spans }, true); }
@@ -25,24 +73,48 @@ export class FencesClient {
25
73
  return this.post("/api/heartbeat", acks.length ? { runs, acks } : { runs }, true);
26
74
  }
27
75
  /** Check a guarded tool call before it runs ({ phase: "check" }), or settle it after ({ phase: "settle" }). */
28
- guard(runId, body) { return this.post(`/api/runs/${runId}/guard`, body, true); }
76
+ guard(runId, body) { return this.post(`/api/runs/${runId}/guard`, body, true, false, true); } // guard_id: safe to repeat
29
77
  requestApproval(runId, body) { return this.post(`/api/runs/${runId}/approvals`, body, true); }
30
78
  /**
31
79
  * optional: an endpoint older servers may not have. A missing route returns { unsupported: true },
32
80
  * and any other error { ok: false, status }.
33
81
  * start: a rejected key throws FencesAuthError, before any agent code runs. Later in a run it
34
82
  * returns { network_error, auth_rejected: true }: throwing there would replace the agent's own result or error.
83
+ * retry: a call that's safe to repeat goes once more after a connection error (not after a timeout,
84
+ * which would double the wait).
35
85
  */
36
- async post(path, payload, optional = false, start = false) {
86
+ async post(path, payload, optional = false, start = false, retry = false) {
87
+ if (!this.breaker.allow())
88
+ return { network_error: "Fences unreachable: calls paused after repeated failures" };
89
+ let json;
90
+ try {
91
+ json = JSON.stringify(payload);
92
+ }
93
+ catch (e) { // a payload JSON can't encode: not the server's fault
94
+ this.breaker.record(true);
95
+ return { network_error: String(e) };
96
+ }
97
+ const send = () => fetch(this.endpoint + path, {
98
+ method: "POST", headers: this.headers, body: json, signal: AbortSignal.timeout(this.timeoutMs),
99
+ });
37
100
  let resp;
38
101
  try {
39
- resp = await fetch(this.endpoint + path, {
40
- method: "POST", headers: this.headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(this.timeoutMs),
41
- });
102
+ try {
103
+ resp = await send();
104
+ }
105
+ catch (e) {
106
+ if (!retry || e?.name === "TimeoutError")
107
+ throw e;
108
+ await new Promise((ok) => setTimeout(ok, 100 + Math.random() * 200)); // a blip: once more, after a moment
109
+ resp = await send();
110
+ }
42
111
  }
43
112
  catch (e) {
113
+ this.breaker.record(false);
44
114
  return { network_error: String(e) };
45
115
  }
116
+ // A 429 on the heartbeat is its own budget, not an outage: it mustn't cut off checkpoints
117
+ this.breaker.record(resp.status < 500 && (resp.status !== 429 || path === "/api/heartbeat"));
46
118
  const text = await resp.text().catch(() => "");
47
119
  let body = {}, isJson = false;
48
120
  try {
package/dist/control.js CHANGED
@@ -2,6 +2,7 @@
2
2
  const retryable = (status) => status === undefined || status === 429 || status >= 500;
3
3
  const FAST_MS = 2000; // heartbeat interval while a run is paused, waiting for an approval, or stopping
4
4
  const HOOK_TIMEOUT_MS = 30_000; // a hook still running after this is reported as timed out
5
+ const MAX_BACKOFF_MS = 60_000; // longest wait between heartbeats after failures (jittered)
5
6
  export const newControl = () => ({
6
7
  stopped: false, stoppedAt: 0, stopSeen: false, cancelled: false, onCancel: null, paused: false, pauseExpired: false,
7
8
  quarantined: false, held: null, approvals: new Map(), awaiting: 0, acked: new Set(),
@@ -17,6 +18,7 @@ const hooks = { stop: null, quarantine: null };
17
18
  export const supported = { heartbeat: true, approvals: true, guard: true };
18
19
  let timer = null;
19
20
  let beating = false;
21
+ const backoff = { failed: 0, nextTry: 0 }; // heartbeats failed in a row, and when the next may go
20
22
  let acks = []; // acknowledgements waiting for the next heartbeat
21
23
  const hooked = new Set(); // quarantine ids whose hook has run in this process
22
24
  const runningHooks = new Set();
@@ -28,6 +30,7 @@ export function configure(c, o) {
28
30
  hooks.stop = o.onStop ?? null;
29
31
  hooks.quarantine = o.onQuarantine ?? null;
30
32
  supported.heartbeat = supported.approvals = supported.guard = true;
33
+ Object.assign(backoff, { failed: 0, nextTry: 0 });
31
34
  schedule(0); // a new client or interval takes effect now, not after the old wait
32
35
  }
33
36
  export function register(run) {
@@ -186,6 +189,13 @@ export function schedule(delayMs) {
186
189
  timer = setTimeout(beat, delayMs);
187
190
  timer.unref(); // the heartbeat never keeps the process alive on its own
188
191
  }
192
+ /** After a failed heartbeat: wait longer before the next, doubling from the shortest interval in use, with jitter,
193
+ * so SDKs coming back from the same outage don't all beat at once. */
194
+ function backOff() {
195
+ backoff.failed += 1;
196
+ const wait = Math.min(Math.min(FAST_MS, intervalMs) * 2 ** (backoff.failed - 1), MAX_BACKOFF_MS);
197
+ backoff.nextTry = Date.now() + wait * (0.5 + Math.random() / 2);
198
+ }
189
199
  /** Halt governed calls that haven't reached a checkpoint within stopGraceS of a stop. */
190
200
  function cancelOverdue() {
191
201
  if (stopGraceMs === null)
@@ -199,8 +209,8 @@ function cancelOverdue() {
199
209
  }
200
210
  async function beat() {
201
211
  timer = null;
202
- cancelOverdue();
203
- if (beating || !client || !(live.size || acks.length))
212
+ cancelOverdue(); // local, so it keeps its schedule while heartbeats back off
213
+ if (beating || !client || !(live.size || acks.length) || Date.now() < backoff.nextTry)
204
214
  return schedule(nextDelay());
205
215
  beating = true;
206
216
  const runs = [...live.values()], batch = acks;
@@ -210,20 +220,24 @@ async function beat() {
210
220
  tokens_used: r.tokensUsed, paused: r.control.paused, stopped: r.control.stopped,
211
221
  ...(r.control.held ? { held: r.control.held } : {}) })), batch);
212
222
  if (resp.network_error || (resp.ok === false && !resp.unsupported)) {
213
- if (resp.network_error || retryable(resp.status))
223
+ if (resp.network_error || retryable(resp.status)) {
214
224
  acks = [...batch, ...acks]; // at least once; any other 4xx won't get better
225
+ backOff();
226
+ }
215
227
  }
216
228
  else if (resp.unsupported) {
217
229
  unsupported("heartbeat", "the heartbeat", "a stop from the dashboard applies at the next checkpoint and pause isn't available");
218
230
  return;
219
231
  }
220
232
  else {
233
+ Object.assign(backoff, { failed: 0, nextTry: 0 });
221
234
  for (const run of runs)
222
235
  apply(run, resp.commands?.filter((c) => c.run_id === run.runId));
223
236
  }
224
237
  }
225
238
  catch {
226
239
  acks = [...batch, ...acks]; // a failed heartbeat is retried next time
240
+ backOff();
227
241
  }
228
242
  finally {
229
243
  beating = false;
package/dist/core.d.ts CHANGED
@@ -1,12 +1,8 @@
1
- /**
2
- * init(), governed() and checkpoint(): starting and ending runs, checking every step against
3
- * its limits (the server decides in cloud mode, the same rules apply here otherwise), and
4
- * recording decisions and spans.
5
- */
6
1
  import { FencesClient } from "./client.js";
7
2
  import * as control from "./control.js";
8
3
  import * as events from "./events.js";
9
4
  import { type CustomPrice } from "./pricing.js";
5
+ import { type ToolArguments } from "./spans.js";
10
6
  import { type CheckpointResult } from "./results.js";
11
7
  import { Run, type Limits } from "./run.js";
12
8
  export type { CheckpointResult } from "./results.js";
@@ -53,6 +49,12 @@ export interface InitOptions {
53
49
  * wait, and after pauseTimeoutS the breach is returned as usual. Cloud only; override per agent in governed().
54
50
  */
55
51
  onBreach?: "hold" | null;
52
+ /**
53
+ * What a recorded tool call carries of its arguments: "names" (the default: `query, limit`),
54
+ * "full" (the arguments as JSON, capped) or "none". A guarded tool's check and an approval request
55
+ * still send the values, which rules and approvers need; the redact hook can change or drop them.
56
+ */
57
+ toolArguments?: ToolArguments;
56
58
  }
57
59
  /** Call once at startup, before any governed function runs. */
58
60
  export declare function init(options?: InitOptions): void;
package/dist/core.js CHANGED
@@ -3,6 +3,7 @@
3
3
  * its limits (the server decides in cloud mode, the same rules apply here otherwise), and
4
4
  * recording decisions and spans.
5
5
  */
6
+ import { randomUUID } from "node:crypto";
6
7
  import { FencesClient } from "./client.js";
7
8
  import * as control from "./control.js";
8
9
  import { AgentQuarantined, FencesStop } from "./errors.js";
@@ -10,6 +11,7 @@ import * as events from "./events.js";
10
11
  import * as outbox from "./outbox.js";
11
12
  import * as precheck from "./precheck.js";
12
13
  import { costOf, setCustomPrices } from "./pricing.js";
14
+ import { setToolArguments } from "./spans.js";
13
15
  import { breachResult, okResult } from "./results.js";
14
16
  import { DEFAULTS, MAX_EVENTS_PER_RUN, Run, getActiveRun, store } from "./run.js";
15
17
  export { Run, getActiveRun } from "./run.js";
@@ -26,6 +28,7 @@ function checkOnBreach(value) {
26
28
  }
27
29
  /** Call once at startup, before any governed function runs. */
28
30
  export function init(options = {}) {
31
+ setToolArguments(options.toolArguments ?? "names");
29
32
  setCustomPrices(options.prices);
30
33
  events.configure(options.environment, options.release, options.redact);
31
34
  localOnly = !!options.localOnly;
@@ -182,9 +185,9 @@ export async function checkpoint(response, extra = {}) {
182
185
  precheck.releaseFinished(run);
183
186
  run.tokensUsed += stepTokens;
184
187
  run.iterations += 1;
185
- const resp = client ? await client.checkpoint(run.runId, {
186
- cost_delta_usd: stepCost, tokens_used: stepTokens, iterations: run.iterations, duration_ms: run.durationMs
187
- }) : null;
188
+ // A step_id per step: a retried checkpoint is counted once on servers that support it
189
+ const resp = client ? await client.checkpoint(run.runId, { cost_delta_usd: stepCost, tokens_used: stepTokens,
190
+ iterations: run.iterations, duration_ms: run.durationMs, step_id: randomUUID().replaceAll("-", "") }) : null;
188
191
  const result = decide(run, resp);
189
192
  // onBreach "hold": wait here for someone to raise the limit. Only a breach the server decided: an unreachable one can't be raised
190
193
  if (result.breachType && control.HOLDABLE.has(result.breachType) && control.holds(run) && resp && !resp.network_error) {
package/dist/mcp.js CHANGED
@@ -34,7 +34,7 @@ export function wrap(client, options = {}) {
34
34
  if (result?.isError)
35
35
  throw new MCPToolError(result);
36
36
  return result;
37
- });
37
+ }, Object.keys(args).join(", "));
38
38
  }
39
39
  catch (e) {
40
40
  if (e instanceof MCPToolError)
package/dist/spans.d.ts CHANGED
@@ -1,11 +1,21 @@
1
1
  export declare const MAX_ARGS = 300;
2
+ export type ToolArguments = "names" | "full" | "none";
3
+ /** init({ toolArguments }): what tool call spans carry of the arguments. */
4
+ export declare let toolArguments: ToolArguments;
5
+ /** @internal */
6
+ export declare function setToolArguments(value: unknown): void;
2
7
  /** A `chat {model}` span. started is a performance.now() reading; response carries the usage. */
3
8
  export declare function recordLlmCall(provider: string, requestModel: string | undefined, started: number, response?: unknown, error?: unknown, opts?: {
4
9
  stream?: boolean;
5
10
  integration?: string;
6
11
  }): void;
7
- /** An `execute_tool {name}` span; args is a short summary of the call's arguments. */
12
+ /**
13
+ * An `execute_tool {name}` span. args is a short summary of the call's arguments and opts.argNames
14
+ * just their names: the span carries the names by default, the values only with
15
+ * init({ toolArguments: "full" }), and neither with "none".
16
+ */
8
17
  export declare function recordToolCall(name: string, args: string, started: number, error?: unknown, opts?: {
9
18
  callId?: string;
10
19
  integration?: string;
20
+ argNames?: string;
11
21
  }): void;
package/dist/spans.js CHANGED
@@ -5,6 +5,15 @@
5
5
  import { recordSpan } from "./core.js";
6
6
  import { costOf, readUsage } from "./pricing.js";
7
7
  export const MAX_ARGS = 300; // characters of a tool call's argument summary
8
+ /** init({ toolArguments }): what tool call spans carry of the arguments. */
9
+ export let toolArguments = "names";
10
+ /** @internal */
11
+ export function setToolArguments(value) {
12
+ if (value !== "names" && value !== "full" && value !== "none") {
13
+ throw new TypeError(`toolArguments must be "names", "full" or "none", not ${JSON.stringify(value)}`);
14
+ }
15
+ toolArguments = value;
16
+ }
8
17
  /** A `chat {model}` span. started is a performance.now() reading; response carries the usage. */
9
18
  export function recordLlmCall(provider, requestModel, started, response, error, opts = {}) {
10
19
  const attrs = { "gen_ai.operation.name": "chat", "gen_ai.provider.name": provider,
@@ -25,11 +34,16 @@ export function recordLlmCall(provider, requestModel, started, response, error,
25
34
  const model = attrs["gen_ai.response.model"] || requestModel || "unknown";
26
35
  recordSpan(`chat ${model}`, performance.now() - started, attrs, error);
27
36
  }
28
- /** An `execute_tool {name}` span; args is a short summary of the call's arguments. */
37
+ /**
38
+ * An `execute_tool {name}` span. args is a short summary of the call's arguments and opts.argNames
39
+ * just their names: the span carries the names by default, the values only with
40
+ * init({ toolArguments: "full" }), and neither with "none".
41
+ */
29
42
  export function recordToolCall(name, args, started, error, opts = {}) {
43
+ const sent = toolArguments === "full" ? args : toolArguments === "names" ? opts.argNames : undefined;
30
44
  recordSpan(`execute_tool ${name}`, performance.now() - started, {
31
45
  "gen_ai.operation.name": "execute_tool", "gen_ai.tool.name": name, "gen_ai.tool.call.id": opts.callId,
32
- "gen_ai.tool.call.arguments": args.length <= MAX_ARGS ? args : args.slice(0, MAX_ARGS - 1) + "…",
46
+ "gen_ai.tool.call.arguments": sent === undefined || sent.length <= MAX_ARGS ? sent : sent.slice(0, MAX_ARGS - 1) + "…",
33
47
  "fences.integration": opts.integration,
34
48
  }, error);
35
49
  }
package/dist/tools.d.ts CHANGED
@@ -1,8 +1,11 @@
1
1
  import { type Run } from "./core.js";
2
+ /** `query, limit` (one object argument: its keys), or `0, 1` (positions): what a tool call span carries by default. */
3
+ export declare function argNames(args: unknown[]): string;
2
4
  /** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
3
5
  export declare function argsSummary(args: unknown[]): string;
4
6
  /** 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>;
7
+ /** summary (the values) goes to the guard and an approval request; the span gets names. */
8
+ export declare function callAsync<R>(run: Run, name: string, summary: string, cost: (() => number) | null, needsApproval: boolean, call: () => Promise<R>, names?: string): Promise<R>;
6
9
  interface ToolFields<A extends unknown[]> {
7
10
  name?: string;
8
11
  /** 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
@@ -5,6 +5,13 @@ import { _client, checkAmount, getActiveRun, localBreach } from "./core.js";
5
5
  import { ToolBlocked } from "./errors.js";
6
6
  import * as events from "./events.js";
7
7
  import { MAX_ARGS, recordToolCall } from "./spans.js";
8
+ /** `query, limit` (one object argument: its keys), or `0, 1` (positions): what a tool call span carries by default. */
9
+ export function argNames(args) {
10
+ const [only] = args;
11
+ if (args.length === 1 && only && typeof only === "object" && !Array.isArray(only))
12
+ return Object.keys(only).join(", ");
13
+ return args.map((_, i) => String(i)).join(", ");
14
+ }
8
15
  /** `{"query":"cats","limit":5}`: the arguments as compact JSON (one object argument on its own), capped. */
9
16
  export function argsSummary(args) {
10
17
  let text;
@@ -112,7 +119,8 @@ async function settle(run, guardId, amount, error, failed) {
112
119
  await _client()?.guard(run.runId, { guard_id: guardId, phase: "settle", outcome, cost_usd: charged });
113
120
  }
114
121
  /** 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) {
122
+ /** summary (the values) goes to the guard and an approval request; the span gets names. */
123
+ export async function callAsync(run, name, summary, cost, needsApproval, call, names) {
116
124
  const started = performance.now();
117
125
  let gate = null;
118
126
  try {
@@ -121,13 +129,13 @@ export async function callAsync(run, name, summary, cost, needsApproval, call) {
121
129
  gate = [await open(run, name, summary, amount, needsApproval), amount];
122
130
  }
123
131
  const out = await call();
124
- recordToolCall(name, summary, started);
132
+ recordToolCall(name, summary, started, undefined, { argNames: names });
125
133
  if (gate)
126
134
  await settle(run, ...gate, undefined, false);
127
135
  return out;
128
136
  }
129
137
  catch (e) {
130
- recordToolCall(name, summary, started, e);
138
+ recordToolCall(name, summary, started, e, { argNames: names });
131
139
  if (gate)
132
140
  await settle(run, ...gate, e, true);
133
141
  throw e;
@@ -151,25 +159,25 @@ export function tool(fn, options = {}) {
151
159
  if (!run)
152
160
  return timed(Promise.resolve().then(() => fn(...args)), options.timeoutS, name);
153
161
  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));
162
+ 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), argNames(args));
155
163
  };
156
164
  }
157
165
  return (...args) => {
158
166
  if (!getActiveRun())
159
167
  return fn(...args);
160
- const summary = argsSummary(args), started = performance.now();
168
+ const summary = argsSummary(args), started = performance.now(), opts = { argNames: argNames(args) };
161
169
  let out;
162
170
  try {
163
171
  out = fn(...args);
164
172
  }
165
173
  catch (e) {
166
- recordToolCall(name, summary, started, e);
174
+ recordToolCall(name, summary, started, e, opts);
167
175
  throw e;
168
176
  }
169
177
  if (out instanceof Promise) {
170
- return out.then((v) => { recordToolCall(name, summary, started); return v; }, (e) => { recordToolCall(name, summary, started, e); throw e; });
178
+ return out.then((v) => { recordToolCall(name, summary, started, undefined, opts); return v; }, (e) => { recordToolCall(name, summary, started, e, opts); throw e; });
171
179
  }
172
- recordToolCall(name, summary, started);
180
+ recordToolCall(name, summary, started, undefined, opts);
173
181
  return out;
174
182
  };
175
183
  }
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.8.0";
2
+ export declare const VERSION = "0.9.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.8.0";
2
+ export const VERSION = "0.9.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentfences",
3
- "version": "0.8.0",
3
+ "version": "0.9.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",