veragent
v0.5.1
Published
Vendor-neutral control plane client for AI agents — capture and govern agent activity. Node/TypeScript SDK.
Maintainers
Readme
Veragent Node SDK
The client library that makes Veragent a vendor-neutral control plane for agents built in JavaScript/TypeScript — the Node peer of the Python SDK, speaking the exact same wire contract so your backend treats both identically.
The core is dependency-free (Node built-ins only: fetch, AsyncLocalStorage). Requires Node 18+.
Install
npm install veragentQuick start
import { Veragent } from "veragent";
const va = new Veragent({ agent: "support-agent" }); // reads VERAGENT_API_KEY
// Report any action. Non-blocking — buffers and returns immediately.
va.track("refund issued", {
eventType: "tool_call", // the governable surface
tool: "issue_refund",
inputs: { orderId: "A-1001", amount: 49 },
});Group a run
Nested track() calls inside run() automatically inherit one correlation id (via AsyncLocalStorage):
await va.run("nightly-job", async () => {
va.track("step one", { eventType: "tool_call", tool: "fetch" });
va.track("step two", { eventType: "tool_call", tool: "write" });
});Wrap a function
const issueRefund = va.trackAction("issue_refund", async (orderId: string, amount: number) => {
// ... your logic ...
return { ok: true };
});Authorize — ask permission before acting
va.authorize(...) is the pre-action decision point. It calls the live /api/authorize endpoint with the same agent name + API key you configured the client with — you never re-pass credentials. It stays inert (reporting-only, allowed: true) until you turn enforcement on, so it's safe to wire in first:
const va = new Veragent({ agent: "crypto-paper-trader", enforcementEnabled: true });Blocking (the simple case)
const decision = await va.authorize("polymarket.place_order", {
context: { market: "BTC-100k", size: 250 },
});
if (decision.allowed) {
placeOrder(); // ✅ authorized
} else {
// MUST handle the not-allowed branch — these mean different things:
if (decision.status === "denied") {
console.warn(`blocked by policy: ${decision.reason}`);
} else if (decision.status === "timed_out") {
console.warn("no human answered in time; server fail mode applied");
} else if (decision.status === "error") {
console.error(`couldn't reach Veragent: ${decision.reason}`);
}
skipOrRollback();
}If the action is auto-decided, the answer comes back on the first call with zero added latency. If it escalates to a human, authorize() blocks and polls until the decision is terminal or timeoutSeconds (default 300, server clamps 10–3600) elapses.
Async (high-throughput loops)
Don't block a hot loop on a human. Fire the request, do other work, poll later:
const decision = await va.authorize("refund.issue", {
context: { order: "A-1001" },
wait: "async",
}); // returns immediately — terminal OR status === "pending"
// ...later, poll yourself:
const latest = await va.getDecision(decision.decisionId);
if (latest.status === "pending") {
// still waiting on a human; check again later
} else if (latest.allowed) {
issueRefund();
}The decision object
interface Decision {
allowed: boolean; // the one thing you must check
status: string; // "allowed" | "denied" | "timed_out" | "error" | "pending"
decisionId: string;
reason: string;
decidedBy?: string | null;
resolvedAt?: string | null;
}Pass { raiseOnDeny: true } to throw PolicyDenied on a terminal not-allowed decision instead of returning it.
Treat
authorize()as fallible. It is a network call. A client-side inability to reach Veragent returnsstatus: "error", allowed: false(fail-safe — never silently "allowed"), which is not the same as a policydenied. Distinguishdenied(a real no) fromtimed_out(nobody answered → the server's fail mode decided) fromerror(couldn't get an answer at all). Preferwait: "async"for high-throughput loops, and keep privileged/irreversible actions late and rollback-able so a denial or error is cheap to honor. Unliketrack()(which drops failures silently), authorize failures are legible — surfaced in the returnedDecision.
Instrument Vercel AI SDK tools
Wrap the tools you pass to generateText / streamText — every tool call is captured, with no changes to the tools themselves:
import { generateText } from "ai";
import { instrumentTools } from "veragent/vercel-ai";
const result = await generateText({
model,
tools: instrumentTools(myTools, va), // capture every tool call
// tools: instrumentTools(myTools, va, { enforce: true }), // also gate them
prompt: "...",
});Tool execution is the governable surface, so calls are tracked as eventType: "tool_call". With enforce: true, each call is authorized first and a denied one is blocked before it runs. The adapter has no dependency on the ai package (it duck-types the tools object) and preserves your tools type.
Instrument MCP tool calls
Wrap an MCP Client so every tool call through it is captured (and optionally gated):
import { instrumentMcpClient } from "veragent/mcp";
instrumentMcpClient(client, va); // capture every tool call
// instrumentMcpClient(client, va, { enforce: true }); // also gate each callMCP is the framework-agnostic tool layer, so instrumenting client.callTool captures the governable surface no matter what drives the client. With enforce: true, a denied call is blocked before it runs (returns an isError result). Dependency-free — it duck-types the client.
Instrument LangChain.js
instrument(va) returns a LangChain callback handler — pass it in the callbacks array:
import { instrument } from "veragent/langchain";
await chain.invoke(input, { callbacks: [instrument(va)] });Every LLM call, tool call, and error in the run is captured; tool calls as eventType: "tool_call". Dependency-free (it's a plain callback-methods object, no @langchain/core import). Capture-only — callbacks fire alongside execution, not before it, so use authorize() or the MCP interceptor for enforcement.
Instrument the OpenAI Agents SDK
instrument(va) registers a Veragent tracing processor with @openai/agents, so every span the SDK emits is captured:
import { instrument } from "veragent/openai-agents";
instrument(va); // capture every span the Agents SDK emits
// ...then run agents as usual.Spans map onto events by type: function → tool_call, generation/response → llm_call, agent → lifecycle, handoff/guardrail → decision, anything else → action. Registration is additive (addTraceProcessor), so existing processors keep running. Dependency-free — it duck-types the SDK's TracingProcessor/Span shapes (the package is only needed at runtime). Capture-only by design: the tracing API notifies on span end, after the action happened, so there is no pre-action hook to gate against — use authorize() or the MCP interceptor for enforcement. Every span mapping is wrapped so a capture error never throws into your agent run.
Design guarantees
- Safe.
track()only buffers; all network I/O is async and batched. Instrumentation never blocks, slows, or throws into your code path. If Veragent is unreachable, events are dropped with a log line — your agent keeps running. - Non-blocking. Events flush on an interval / size threshold and once on
beforeExit. Callawait va.close()for a deterministic final flush (e.g. before a short-lived process exits). - Redaction-first. Inputs/outputs run through a redactor before leaving the process (sensitive keys masked, long strings truncated). On by default; pass
redact: falseor a custom function to override. - Enforcement-ready.
va.authorize(action, { context })is the pre-action decision point, calling the live/api/authorizeendpoint. It is safe to wire in today — whileenforcementEnabledis off it returnsallowed: true(reporting-only); turn it on to get real allow/deny verdicts. Unliketrack(), authorize failures are legible, not silent: if Veragent can't be reached you getstatus: "error", allowed: falseso the agent never proceeds thinking it was authorized. See Authorize. - Wire-compatible. Emits the same
{ agent, action, severity, metadata }envelope as the Python SDK, taggedsdk: "veragent-node/0.2.0".
Configuration
new Veragent({
apiKey, // or env VERAGENT_API_KEY
agent: "agent",
endpoint, // default https://www.veragent.io/api/ingest-event
redact: true, // true | false | (value) => value
flushInterval: 2, // seconds
maxBatch: 50,
maxQueue: 10000,
timeout: 10, // seconds
enforcementEnabled: false,
failClosed: false,
});Adapter roadmap
| Surface | Status |
|---|---|
| Core (track, run, trackAction, authorize) | ✅ shipped |
| Vercel AI SDK (instrumentTools) | ✅ shipped |
| LangChain.js (instrument callback) | ✅ shipped |
| MCP (TypeScript) interceptor (instrumentMcpClient) | ✅ shipped |
