agentfences 0.0.0-stage → 0.4.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
@@ -1,3 +1,83 @@
1
- # Temporary Holding Version
1
+ # agentfences (TypeScript / JavaScript)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The TypeScript SDK for [Fences](https://github.com/bhaviksardar/fences), on-call for AI agents: limits your agents can't raise, cost tracking from real usage, a decision trail, and a stop button. Works offline too: local limits, no account. Node 18+, no dependencies.
4
+
5
+ ```bash
6
+ npm install agentfences
7
+ ```
8
+
9
+ ## Quickstart (no account needed)
10
+
11
+ ```ts
12
+ import { init, governed, checkpoint, logDecision } from "agentfences";
13
+
14
+ init({ localOnly: true });
15
+
16
+ const research = governed(async (question: string) => {
17
+ for (;;) {
18
+ logDecision("asking the model for the next step", "llm_call");
19
+ const response = await openai.chat.completions.create({ model: "gpt-4o", messages });
20
+ const result = await checkpoint(response); // Fences reads the usage and prices the call
21
+ if (result.breached) return result.message; // "I've reached my budget limit (spent $0.5100 of $0.5000)..."
22
+ // ...
23
+ }
24
+ }, { budgetUsd: 0.5, maxIterations: 20 });
25
+ ```
26
+
27
+ `checkpoint()` never throws for a breach: it returns a result with `ok`, `breached`, `breachType`, `message` (ready for the user), `systemPrompt` (to give the model so it wraps up) and `spentUsd`.
28
+
29
+ ## Cost tracking
30
+
31
+ Pass the model's response to `checkpoint()` and Fences works out what the call cost from its model and token usage, including cache reads and writes. It understands results from the OpenAI and Anthropic SDKs, the Vercel AI SDK (`generateText`), Gemini and LangChain.js, or any object with the same usage fields. Prices come from a built-in table of about 290 models (the same one as the Python SDK).
32
+
33
+ - Unknown model: its tokens are counted and Fences warns once. Add prices in USD per 1M tokens: `init({ localOnly: true, prices: { "my-finetune": { input: 2, output: 6 } } })`.
34
+ - Extra costs, or no response at all: `checkpoint(response, { costDeltaUsd: 0.01 })` or `checkpoint(null, { costDeltaUsd: 0.02, tokensUsed: 450 })`.
35
+
36
+ ## Limits
37
+
38
+ `budgetUsd`, `maxIterations` (default 100), `maxDurationMs` (default 5 minutes) and `maxTokens` (default none). All are optional. A limit trips once it's exceeded, not when reached. With no budget anywhere, the run's spend isn't capped and Fences warns once per agent. On Fences Cloud, limits left out come from the agent's page in the dashboard.
39
+
40
+ ## Tools, context, errors and redaction
41
+
42
+ ```ts
43
+ import { tool, context } from "agentfences";
44
+
45
+ const webSearch = tool(async ({ query }: { query: string }) => search(query)); // each call recorded as a span
46
+
47
+ await context({ user_id: "u_123", session_id: "s_9" }, () => research(question)); // tags every run inside
48
+ ```
49
+
50
+ - Tool and model calls are recorded as OpenTelemetry-shaped spans (`execute_tool webSearch`, `gen_ai.*` attributes), on `getActiveRun().events`.
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
+ - `init({ redact: (event) => ... })` sees everything before it leaves the process and can change or drop it.
53
+
54
+ ## Fences Cloud
55
+
56
+ ```ts
57
+ init({ apiKey: process.env.FENCES_API_KEY, endpoint: "https://your-fences-host", environment: "prod", release: "v1.4.2" });
58
+ ```
59
+
60
+ The server is authoritative for limits and sees spend across processes. If it can't be reached, limits are enforced locally; pass `failClosed: true` to stop instead. A rejected key throws `FencesAuthError`; a quarantined agent throws `AgentQuarantined` before any of its code runs. In serverless handlers, `await flush()` before returning so queued decisions and spans are sent.
61
+
62
+ ## Live control (from 0.4.0)
63
+
64
+ From version 0.4.0, which isn't on npm yet: in cloud mode a heartbeat (every 15 seconds, `init({ heartbeatS })`) lets whoever is on call act on a running agent from the dashboard:
65
+
66
+ - **Stop:** the next `checkpoint()` returns `breachType: "stopped_by_user"`, so the agent wraps up gracefully.
67
+ - **Pause and resume:** a paused run waits inside its next `checkpoint()` until it's resumed; after `pauseTimeoutS` (default one hour) it returns `breachType: "paused"`.
68
+ - **Limits** changed in the dashboard apply to the run immediately. **Quarantine** makes the governed call throw `AgentQuarantined` before any agent code runs.
69
+
70
+ An agent can ask a person before doing something risky:
71
+
72
+ ```ts
73
+ import { requestApproval } from "agentfences";
74
+
75
+ const approval = await requestApproval("Refund $240 to order 1182?", { amountUsd: 240, timeoutS: 600 });
76
+ if (!approval.granted) return `I didn't issue the refund: ${approval.note}`;
77
+ ```
78
+
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
+
81
+ ## Not yet in the TypeScript SDK
82
+
83
+ The Python SDK also records OpenAI/Anthropic calls automatically (`instrument`) and integrates with LangGraph and the OpenAI Agents SDK. Everything here follows the same server protocol ([PROTOCOL.md](../PROTOCOL.md)).
@@ -0,0 +1,20 @@
1
+ export type Reply = Record<string, any>;
2
+ export declare class FencesClient {
3
+ readonly endpoint: string;
4
+ private readonly timeoutMs;
5
+ private readonly headers;
6
+ constructor(apiKey: string, endpoint: string, timeoutMs?: number);
7
+ startRun(body: Record<string, unknown>): Promise<Reply>;
8
+ checkpoint(runId: string, body: Record<string, unknown>): Promise<Reply>;
9
+ logDecision(runId: string, body: Record<string, unknown>): Promise<Reply>;
10
+ endRun(runId: string, body: Record<string, unknown>): Promise<Reply>;
11
+ logSpans(runId: string, spans: unknown[]): Promise<Reply>;
12
+ /** Report live runs; the reply may carry commands for them. */
13
+ heartbeat(runs: unknown[]): Promise<Reply>;
14
+ requestApproval(runId: string, body: Record<string, unknown>): Promise<Reply>;
15
+ /**
16
+ * optional: an endpoint older servers may not have. A missing route returns
17
+ * { unsupported: true } and nothing throws, not even a rejected key.
18
+ */
19
+ post(path: string, payload: unknown, optional?: boolean): Promise<Reply>;
20
+ }
package/dist/client.js ADDED
@@ -0,0 +1,62 @@
1
+ /** HTTP calls to a Fences server, as specified in PROTOCOL.md. Uses the fetch built into Node 18+. */
2
+ import { FencesAuthError } from "./errors.js";
3
+ import { VERSION } from "./version.js";
4
+ export class FencesClient {
5
+ endpoint;
6
+ timeoutMs;
7
+ headers;
8
+ constructor(apiKey, endpoint, timeoutMs = 3000) {
9
+ this.endpoint = endpoint;
10
+ this.timeoutMs = timeoutMs;
11
+ this.endpoint = endpoint.replace(/\/+$/, "");
12
+ // The server uses the SDK version to warn about old SDKs and to skip commands they don't understand
13
+ this.headers = { "Content-Type": "application/json", "X-API-Key": apiKey, "X-Fences-SDK": `js/${VERSION}` };
14
+ }
15
+ startRun(body) { return this.post("/api/runs/start", body); }
16
+ checkpoint(runId, body) { return this.post(`/api/runs/${runId}/checkpoint`, body); }
17
+ logDecision(runId, body) { return this.post(`/api/runs/${runId}/decisions`, body); }
18
+ endRun(runId, body) { return this.post(`/api/runs/${runId}/end`, body); }
19
+ logSpans(runId, spans) { return this.post(`/api/runs/${runId}/spans`, { spans }, true); }
20
+ /** Report live runs; the reply may carry commands for them. */
21
+ heartbeat(runs) { return this.post("/api/heartbeat", { runs }, true); }
22
+ requestApproval(runId, body) { return this.post(`/api/runs/${runId}/approvals`, body, true); }
23
+ /**
24
+ * optional: an endpoint older servers may not have. A missing route returns
25
+ * { unsupported: true } and nothing throws, not even a rejected key.
26
+ */
27
+ async post(path, payload, optional = false) {
28
+ let resp;
29
+ try {
30
+ resp = await fetch(this.endpoint + path, {
31
+ method: "POST", headers: this.headers, body: JSON.stringify(payload), signal: AbortSignal.timeout(this.timeoutMs),
32
+ });
33
+ }
34
+ catch (e) {
35
+ return { network_error: String(e) };
36
+ }
37
+ const text = await resp.text().catch(() => "");
38
+ let body = {}, isJson = false;
39
+ try {
40
+ const parsed = JSON.parse(text);
41
+ if (parsed && typeof parsed === "object")
42
+ [body, isJson] = [parsed, true];
43
+ }
44
+ catch { /* not JSON: the status code still decides */ }
45
+ const detail = typeof body.detail === "string" ? body.detail : "";
46
+ if (optional) {
47
+ if ((resp.status === 404 || resp.status === 405) && (detail === "Not Found" || detail === "Method Not Allowed")) {
48
+ return { unsupported: true };
49
+ }
50
+ return resp.ok ? body : { ok: false, status: resp.status };
51
+ }
52
+ if (resp.status === 401 || resp.status === 403)
53
+ throw new FencesAuthError(`Fences API key rejected: ${text}`);
54
+ if (resp.status === 423)
55
+ return { ok: false, quarantined: true, detail }; // the agent is quarantined: the run must not start
56
+ if (resp.status === 409)
57
+ return { ok: false, conflict: detail }; // e.g. checkpoint on a run already fenced
58
+ if (!resp.ok || !isJson)
59
+ return { network_error: resp.ok ? "response was not JSON" : `HTTP ${resp.status}` };
60
+ return body;
61
+ }
62
+ }
@@ -0,0 +1,43 @@
1
+ import type { FencesClient, Reply } from "./client.js";
2
+ import type { Run } from "./core.js";
3
+ export interface Approval {
4
+ granted: boolean;
5
+ /** Who answered, as the dashboard reports it. */
6
+ by?: string | null;
7
+ /** Their note, or why no one could answer. */
8
+ note?: string | null;
9
+ /** The amount approved, if one was asked for. */
10
+ amountUsd?: number | null;
11
+ }
12
+ /** Live-control state a run carries. */
13
+ export interface Control {
14
+ stopped: boolean;
15
+ paused: boolean;
16
+ pauseExpired: boolean;
17
+ /** approval id -> the answer, or null while waiting */
18
+ approvals: Map<string, Approval | null>;
19
+ awaiting: number;
20
+ }
21
+ export declare const newControl: () => Control;
22
+ export declare let pauseTimeoutMs: number;
23
+ export declare function configure(c: FencesClient | null, heartbeatS: number, pauseTimeoutS: number): void;
24
+ export declare function register(run: Run): void;
25
+ export declare function unregister(run: Run): void;
26
+ /** Apply the commands addressed to this run (from a heartbeat or a checkpoint reply). Each is safe to apply twice. */
27
+ export declare function apply(run: Run, commands: Reply[] | undefined): void;
28
+ /** Hold the agent at its checkpoint while its run is paused, up to pauseTimeoutS. */
29
+ export declare function waitIfPaused(run: Run): Promise<void>;
30
+ /**
31
+ * Ask a person, through the Fences dashboard, to approve something, and wait for the answer:
32
+ *
33
+ * const approval = await requestApproval("Refund $240 to order 1182?", { amountUsd: 240 });
34
+ * if (!approval.granted) return `I didn't issue the refund: ${approval.note}`;
35
+ *
36
+ * If an amount is approved, the run's budget goes up by that much. Returns denied right away in
37
+ * local mode, outside a run, or when the server can't take requests, and denied after timeoutS
38
+ * (default 600) with no answer: it never hangs and never throws.
39
+ */
40
+ export declare function requestApproval(reason: string, options?: {
41
+ amountUsd?: number;
42
+ timeoutS?: number;
43
+ }): Promise<Approval>;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Live control from the Fences dashboard: a heartbeat that reports live runs and brings back
3
+ * commands (stop, pause, resume, limits, approval answers), pausing at checkpoints, and
4
+ * approval requests. Cloud mode only; a server without these endpoints gets one warning and
5
+ * runs carry on as before. Same behaviour as the Python SDK (PROTOCOL.md, "Live control").
6
+ */
7
+ import { randomUUID } from "node:crypto";
8
+ const FAST_MS = 2000; // heartbeat interval while a run is paused or waiting for an approval
9
+ export const newControl = () => ({ stopped: false, paused: false, pauseExpired: false, approvals: new Map(), awaiting: 0 });
10
+ const live = new Map(); // run id -> runs in progress
11
+ let client = null;
12
+ let intervalMs = 15_000;
13
+ export let pauseTimeoutMs = 3_600_000;
14
+ const supported = { heartbeat: true, approvals: true };
15
+ let timer = null;
16
+ let beating = false;
17
+ export function configure(c, heartbeatS, pauseTimeoutS) {
18
+ client = c;
19
+ intervalMs = heartbeatS * 1000;
20
+ pauseTimeoutMs = pauseTimeoutS * 1000;
21
+ supported.heartbeat = supported.approvals = true;
22
+ schedule(0); // a new client or interval takes effect now, not after the old wait
23
+ }
24
+ export function register(run) {
25
+ live.set(run.runId, run);
26
+ if (!timer)
27
+ schedule(nextDelay());
28
+ }
29
+ export function unregister(run) {
30
+ live.delete(run.runId);
31
+ }
32
+ function unsupported(kind, what, consequence) {
33
+ if (!supported[kind])
34
+ return;
35
+ supported[kind] = false;
36
+ console.warn(`agentfences: this Fences server doesn't support ${what} yet, so ${consequence}`);
37
+ }
38
+ // ── Commands ──────────────────────────────────────────────────────────────────
39
+ const LIMITS = { budget_usd: "budgetUsd", max_iterations: "maxIterations", max_duration_ms: "maxDurationMs", max_tokens: "maxTokens" };
40
+ /** Apply the commands addressed to this run (from a heartbeat or a checkpoint reply). Each is safe to apply twice. */
41
+ export function apply(run, commands) {
42
+ for (const c of commands ?? []) {
43
+ if (c.run_id !== undefined && c.run_id !== run.runId)
44
+ continue;
45
+ const ctl = run.control;
46
+ if (c.type === "stop") {
47
+ ctl.stopped = true;
48
+ ctl.paused = false;
49
+ }
50
+ else if (c.type === "pause") {
51
+ ctl.paused = true;
52
+ schedule(0);
53
+ }
54
+ else if (c.type === "resume") {
55
+ ctl.paused = false;
56
+ }
57
+ else if (c.type === "limits") {
58
+ for (const [wire, field] of Object.entries(LIMITS)) {
59
+ if (c[wire] !== undefined && c[wire] !== null)
60
+ run[field] = c[wire]; // eslint-disable-line @typescript-eslint/no-explicit-any
61
+ }
62
+ }
63
+ else if (c.type === "approval" && ctl.approvals.has(c.approval_id)) {
64
+ ctl.approvals.set(c.approval_id, { granted: !!c.granted, by: c.by ?? null, note: c.note ?? null, amountUsd: c.amount_usd ?? null });
65
+ }
66
+ }
67
+ }
68
+ const busy = () => [...live.values()].some((r) => r.control.paused || r.control.awaiting > 0);
69
+ const nextDelay = () => (busy() ? Math.min(FAST_MS, intervalMs) : intervalMs); // faster while busy, never slower
70
+ function schedule(delayMs) {
71
+ if (timer)
72
+ clearTimeout(timer);
73
+ timer = null;
74
+ if (!client || !supported.heartbeat || !live.size)
75
+ return;
76
+ timer = setTimeout(beat, delayMs);
77
+ timer.unref(); // the heartbeat never keeps the process alive on its own
78
+ }
79
+ async function beat() {
80
+ timer = null;
81
+ if (beating || !client || !live.size)
82
+ return schedule(nextDelay());
83
+ beating = true;
84
+ try {
85
+ const runs = [...live.values()];
86
+ const resp = await client.heartbeat(runs.map((r) => ({ run_id: r.runId, iterations: r.iterations, spent_usd: r.costUsd,
87
+ tokens_used: r.tokensUsed, paused: r.control.paused })));
88
+ if (resp.unsupported) {
89
+ unsupported("heartbeat", "the heartbeat", "a stop from the dashboard applies at the next checkpoint and pause isn't available");
90
+ return;
91
+ }
92
+ for (const run of runs)
93
+ apply(run, resp.commands?.filter((c) => c.run_id === run.runId));
94
+ }
95
+ catch { /* a failed heartbeat is retried next time */ }
96
+ finally {
97
+ beating = false;
98
+ }
99
+ schedule(nextDelay());
100
+ }
101
+ // ── Pausing ───────────────────────────────────────────────────────────────────
102
+ /** Hold the agent at its checkpoint while its run is paused, up to pauseTimeoutS. */
103
+ export async function waitIfPaused(run) {
104
+ const deadline = Date.now() + pauseTimeoutMs;
105
+ while (run.control.paused) {
106
+ if (Date.now() >= deadline) {
107
+ run.control.paused = false;
108
+ run.control.pauseExpired = true;
109
+ return;
110
+ }
111
+ await new Promise((ok) => setTimeout(ok, 200));
112
+ }
113
+ }
114
+ // ── Approvals ─────────────────────────────────────────────────────────────────
115
+ /**
116
+ * Ask a person, through the Fences dashboard, to approve something, and wait for the answer:
117
+ *
118
+ * const approval = await requestApproval("Refund $240 to order 1182?", { amountUsd: 240 });
119
+ * if (!approval.granted) return `I didn't issue the refund: ${approval.note}`;
120
+ *
121
+ * If an amount is approved, the run's budget goes up by that much. Returns denied right away in
122
+ * local mode, outside a run, or when the server can't take requests, and denied after timeoutS
123
+ * (default 600) with no answer: it never hangs and never throws.
124
+ */
125
+ export async function requestApproval(reason, options = {}) {
126
+ const { getActiveRun, recordSpan } = await import("./core.js");
127
+ const run = getActiveRun();
128
+ const timeoutS = options.timeoutS ?? 600, amountUsd = options.amountUsd ?? null;
129
+ if (!run)
130
+ return { granted: false, note: "requestApproval() was called outside a governed run" };
131
+ if (!client)
132
+ return { granted: false, note: "Local mode: there is no one to approve this request" };
133
+ if (!supported.approvals)
134
+ return { granted: false, note: "This Fences server doesn't support approvals" };
135
+ const approvalId = randomUUID(), started = performance.now();
136
+ // timeout_s lets the server expire the request when we stop waiting
137
+ const resp = await client.requestApproval(run.runId, { approval_id: approvalId, reason, amount_usd: amountUsd, timeout_s: timeoutS });
138
+ if (resp.unsupported) {
139
+ unsupported("approvals", "approval requests", "they are denied");
140
+ return { granted: false, note: "This Fences server doesn't support approvals" };
141
+ }
142
+ if (resp.network_error)
143
+ return { granted: false, note: "Couldn't reach Fences to ask for approval" };
144
+ run.control.approvals.set(approvalId, null);
145
+ run.control.awaiting += 1;
146
+ schedule(0); // poll fast while someone decides
147
+ const deadline = Date.now() + timeoutS * 1000;
148
+ while (run.control.approvals.get(approvalId) === null && Date.now() < deadline) {
149
+ await new Promise((ok) => setTimeout(ok, 100));
150
+ }
151
+ run.control.awaiting -= 1;
152
+ const answer = run.control.approvals.get(approvalId) ?? { granted: false, note: "No answer before the timeout" };
153
+ run.control.approvals.delete(approvalId);
154
+ if (answer.granted && answer.amountUsd && run.budgetUsd !== null)
155
+ run.budgetUsd += answer.amountUsd; // an unlimited run stays unlimited
156
+ recordSpan("fences.approval", performance.now() - started, {
157
+ "fences.approval.id": approvalId, "fences.approval.reason": reason, "fences.approval.amount_usd": amountUsd,
158
+ "fences.approval.granted": answer.granted, "fences.approval.by": answer.by, "fences.approval.note": answer.note,
159
+ "fences.approval.granted_usd": answer.amountUsd,
160
+ });
161
+ return answer;
162
+ }
package/dist/core.d.ts ADDED
@@ -0,0 +1,130 @@
1
+ import { FencesClient } from "./client.js";
2
+ import * as control from "./control.js";
3
+ import * as events from "./events.js";
4
+ import { type CustomPrice } from "./pricing.js";
5
+ export interface CheckpointResult {
6
+ ok: boolean;
7
+ breached: boolean;
8
+ /** "budget_exceeded" | "iteration_limit" | "time_limit" | "token_limit" | "stopped_by_user" | ... (see PROTOCOL.md) */
9
+ breachType: string | null;
10
+ /** First-person text, ready to return to the user. */
11
+ message: string;
12
+ /** An instruction to give the model so it wraps up. */
13
+ systemPrompt: string;
14
+ /** The run's total spend so far. */
15
+ spentUsd: number;
16
+ }
17
+ /** Every limit is optional; see governed(). */
18
+ export interface Limits {
19
+ /** Maximum spend for the run in USD. None anywhere: unlimited spend, with a warning. */
20
+ budgetUsd?: number | null;
21
+ /** Maximum checkpoints (steps). Default 100. */
22
+ maxIterations?: number | null;
23
+ /** Maximum wall-clock time in milliseconds. Default 300000 (5 minutes). */
24
+ maxDurationMs?: number | null;
25
+ /** Maximum tokens (input + output). 0 or unset: no limit. */
26
+ maxTokens?: number | null;
27
+ }
28
+ export declare const MAX_EVENTS_PER_RUN = 1000;
29
+ export interface Span {
30
+ name: string;
31
+ start_ts: number;
32
+ end_ts: number;
33
+ status: "ok" | "error";
34
+ attributes: Record<string, unknown>;
35
+ }
36
+ export declare class Run {
37
+ readonly agentName: string;
38
+ budgetUsd: number | null;
39
+ maxIterations: number;
40
+ maxDurationMs: number;
41
+ maxTokens: number;
42
+ readonly context: Record<string, string>;
43
+ readonly runId: `${string}-${string}-${string}-${string}-${string}`;
44
+ costUsd: number;
45
+ iterations: number;
46
+ tokensUsed: number;
47
+ readonly startedAt: number;
48
+ readonly decisions: {
49
+ timestamp: number;
50
+ iteration: number;
51
+ reasoning: string;
52
+ action: string | null;
53
+ }[];
54
+ /** Tool and model calls as OpenTelemetry-shaped spans, newest 1,000 kept. */
55
+ readonly events: Span[];
56
+ lastBreach: string | null;
57
+ exception?: events.ExceptionInfo;
58
+ warned: boolean;
59
+ /** Live control from the dashboard (see control.ts). */
60
+ readonly control: control.Control;
61
+ constructor(agentName: string, budgetUsd: number | null, maxIterations: number, maxDurationMs: number, maxTokens: number, context: Record<string, string>);
62
+ get durationMs(): number;
63
+ }
64
+ /** The run in progress in this async context, if any. */
65
+ export declare function getActiveRun(): Run | null;
66
+ export interface InitOptions {
67
+ /** Your Fences API key (fc_...). Required unless localOnly. */
68
+ apiKey?: string;
69
+ /** Your Fences server. Defaults to http://localhost:8000. */
70
+ endpoint?: string;
71
+ /** Enforce limits in-process only: no account, nothing leaves the process. */
72
+ localOnly?: boolean;
73
+ /** Treat an unreachable server as a breach (fences_unreachable) instead of enforcing locally. */
74
+ failClosed?: boolean;
75
+ /** Add or override model prices, USD per 1M tokens. */
76
+ prices?: Record<string, CustomPrice>;
77
+ /** Tag every run, e.g. "prod". */
78
+ environment?: string;
79
+ /** Tag every run, e.g. "v1.4.2". */
80
+ release?: string;
81
+ /** Sees every outgoing event and returns it (changed or not), or null to drop it. */
82
+ redact?: events.RedactHook;
83
+ /** Seconds between heartbeats reporting live runs (cloud mode). Default 15. */
84
+ heartbeatS?: number;
85
+ /** How long a paused run waits at its checkpoint before stopping with breach "paused". Default 3600. */
86
+ pauseTimeoutS?: number;
87
+ }
88
+ /** Call once at startup, before any governed function runs. */
89
+ export declare function init(options?: InitOptions): void;
90
+ /** @internal For tests: point the SDK at a fake server client. */
91
+ export declare function _setClient(c: FencesClient | null): void;
92
+ /**
93
+ * Make each call of fn a governed run with these limits:
94
+ *
95
+ * const research = governed(async (q: string) => { ... }, { budgetUsd: 0.5, maxIterations: 20 });
96
+ *
97
+ * Every limit is optional. On Fences Cloud, a limit left out comes from the agent's page in
98
+ * the dashboard. The run is named after the function (or `name`). checkpoint() never throws:
99
+ * a crossed limit comes back as a breached result, so the agent can wrap up gracefully.
100
+ */
101
+ export declare function governed<A extends unknown[], R>(fn: (...args: A) => R | Promise<R>, options?: Limits & {
102
+ name?: string;
103
+ }): (...args: A) => Promise<Awaited<R>>;
104
+ export interface Extra {
105
+ /** Extra spend for this step in USD, e.g. a paid tool call. Without a response, the step's whole cost. */
106
+ costDeltaUsd?: number;
107
+ /** Extra tokens for this step. */
108
+ tokensUsed?: number;
109
+ }
110
+ /**
111
+ * Record one step, then check every limit. Pass the model's response and Fences works out
112
+ * the cost from its model and token usage:
113
+ *
114
+ * const response = await openai.chat.completions.create({ ... });
115
+ * const result = await checkpoint(response);
116
+ * if (result.breached) return result.message;
117
+ *
118
+ * Without a response: checkpoint(null, { costDeltaUsd: 0.02, tokensUsed: 450 }).
119
+ * Never throws for a breach. Outside a governed run it does nothing.
120
+ */
121
+ export declare function checkpoint(response?: unknown, extra?: Extra): Promise<CheckpointResult>;
122
+ /** Wait up to timeoutMs for queued decisions and spans to reach the server. Call before a serverless handler returns. */
123
+ export declare function flush(timeoutMs?: number): Promise<void>;
124
+ /** Record the agent's reasoning at this step. Does nothing outside a governed run. */
125
+ export declare function logDecision(reasoning: string, action?: string | null): void;
126
+ /**
127
+ * Add a tool or model call to the active run as an OpenTelemetry-shaped span (GenAI
128
+ * semantic conventions): kept on the run, and sent through the redact hook in cloud mode.
129
+ */
130
+ export declare function recordSpan(name: string, elapsedMs: number, attributes: Record<string, unknown>, error?: unknown): void;