Skip to main content
What every setting, method and field does for the TypeScript SDK. If you are instrumenting for the first time, start with the guide — this page is for looking things up.

Custom agents guide

Install, instrument, the event methods, a worked example, and common problems.

Using Python?

The same events, the same wire format, the same spool — from Python.
Node 20.9 or newer. ESM and CommonJS. No runtime dependencies.
This SDK and the Python one write the same events into the same spool. A fleet with Node agents and Python agents produces one set of sessions, not two, and nothing in the dashboard distinguishes them. Pick per service, not per company.

Install

The framework adapters ship in the package itself. The frameworks are optional peer dependencies — declared so the supported ranges are visible, never installed on your behalf, and imported only when you call instrument().

Connect the Failproof daemon

Identical to the Python SDK: create an events:add key under Admin → Keys, then connect the daemon on the agent machine. The SDK writes to disk; the daemon ships.

Configuration

Nothing is applied unless all of it validates, so a rejected call leaves the SDK exactly as it was rather than with a new baseDir and the old interval. Set by environment variable instead:
No commas in environment. Ingest splits that field on commas to build its filters, and skips any event whose label contains one — so a whole run silently vanishes. Write prod-eu, not prod,eu.configure({ environment: "prod,eu" }) throws so you find out immediately. AGENTEYE_ENVIRONMENT cannot throw — nothing is calling you — so it warns once and falls back to dev.
Route the SDK’s own log lines into your logger with failproofai.setLogger({ debug, info, warn, error }).

Shutdown

Buffered events are flushed on process.on("exit"). A process killed by a signal never reaches that, and Node’s default for SIGTERM is to terminate without running exit handlers — so a containerised agent loses whatever the last interval had not written.
This SDK will not install a signal handler for you. Registering one changes your process’s behaviour: a listener suppresses Node’s default termination, so a library that added one would silently stop Ctrl-C from working. Add your own:
A short-lived script or a serverless handler should await failproofai.flush() before returning — the interval alone does not guarantee delivery.

Identity

Every event belongs to a session and an agent. The scopes fill both in, so you rarely pass them:
Passing sessionId or agentId explicitly still works and wins. With neither bound nor passed, the call throws rather than emitting an event Cloud would quietly discard.
Identity rides on AsyncLocalStorage. It follows await, .then(), timers and any callback created inside the scope. It does not follow a callback stored during one run and invoked during another, or work handed across a worker_threads boundary — wrap those in failproofai.propagate() or their events land unattached.

Scopes

A synchronous body stays synchronous: agent("x", () => 1) returns 1, not a promise. toolCall records the body’s resolved value as the tool’s output, unless you assign call.output yourself.
The error is always re-thrown.A tool failure is recorded on the leaf — tool_result with an error string — and emits no run-level error event. One the agent loop catches is not a run failure, and one that propagates is reported exactly once, by the enclosing agent().
When the work is not a single function — a scope opened in a constructor and closed in a teardown, or one that straddles existing control flow:
Both forms emit byte-identical events. Prefer the callback form: it runs inside AsyncLocalStorage.run(), so there is nothing to unwind and the whole class of “opened here, closed over there” bugs is unreachable.A using block that catches its own failure reports it with span.fail(error) — the disposer has no exception channel of its own.

Event catalog

The same fifteen methods as the Python SDK, in camelCase. Most come in pairs — you call the opener, then the closer, and the SDK times the gap. Three stand alone: error, humanPause, humanInterrupt.
Every method also takes sessionId and agentId, which the scopes fill in for you. Anything omitted is dropped rather than sent as JSON null.Any other key you add becomes a custom payload field. Namespace anything framework-specific fw_*; a name that collides with a declared field is refused rather than silently overwriting a promoted column.
duration_ms is computed, not accepted. The four closing methods time the gap from their opener and refuse a caller-supplied duration_ms — a reported duration is unfalsifiable.Pairs are matched on the session and the id, never on the agent. A tool opened under planner and closed under worker still pairs, which is what nested multi-agent runs actually do.

Framework adapters

Every range is tested against real framework releases, at both ends, as an ES module and as CommonJS, on every CI run. The mapping is the Python SDK’s, so the same program draws the same tree in either language. A construct is an agent only if it owns an LLM decision loop — a graph or chain run, an AI SDK generateText/streamText call, a Mastra agent, a LlamaIndex agent run. A LangGraph node or a workflow step is a hook (hook_triggered/hook_completed), never a nested agent. Model calls are model_request/model_response pairs with token counts; tool calls carry the model’s own tool call id. A failure is recorded once, on the event it happened in. An adapter that fails to install is logged and skipped; the others still install, because a broken LlamaIndex should not cost you LangGraph.
instrument() with no argument detects a framework by whether it resolves, not by whether it is already imported — Node exposes no equivalent of Python’s sys.modules for ES modules. A framework you have installed but do not use will be imported and patched. Name the one you want if that matters.
Most of these frameworks ship an ES-module build and a CommonJS build, which Node loads as two unrelated copies. The adapters patch the copy your application loads (and the CommonJS copy too if something already required it), so both module systems work. A framework bundled into your own output by esbuild or webpack is out of reach — use the call-site helpers there: langchainHandler(), telemetry(), wrapTool().

LangChain without patching

The handler works with or without instrument() and never double-records. instrument("langchain") takes sessionId, captureContent, includeChains, graphCallbacks and captureLimit, as the Python adapter does; metadata: { failproofai_sdk_session_id } on a call picks the session for that invocation.

Vercel AI SDK

The AI SDK exports plain functions from an ES module, and an ES module namespace is immutable by specification — there is nowhere to patch. It uses the extension points the SDK itself documents:
That is the complete integration: an agent span, a model request/response pair per step with token counts, and every tool call. One call site works on every major — ai 4–6 read the tracer it carries, ai 7 the telemetry integration. instrument("ai") does the same process-wide on ai 7: every call, through the AI SDK’s global telemetry-integration list, which is additive and takes nothing from anybody else’s. On ai 4–6, instrument("ai") records nothing by itself, and logs one warning saying so. The only process-wide hook those majors have is the global OpenTelemetry tracer provider — a single slot OpenTelemetry refuses to hand over once taken. Registering ours would silently refuse your own NodeSDK.start() later in startup and send your http/database spans to a tracer that exports nothing. Use telemetry() at the call site or wrapModel there. If the process runs no OpenTelemetry of its own, opt in with instrument("ai", { registerGlobalTracer: true }): it then records every call that passes experimental_telemetry: { isEnabled: true }, and only takes the slot if it is still empty. registerGlobalTracer: false keeps the default and silences the warning. If you would rather wrap the model once, wrapModel sees model calls only, because tool calls happen above the model layer. A wrapped model called with nothing around it is recorded as its own run. A streamed call closes however the stream stops — stop_reason: "cancelled" when the consumer cancels it, "error" with the error when it fails part-way:
Using both is fine: the middleware notices the call is already being recorded and defers, so each call is recorded once. functionId names the agent span. Keep it low-cardinality — it lands in agent_id, the primary dashboard facet.

Next.js

next build bundles your server’s dependencies by default, and a framework bundled into the build is a copy instrument() cannot reach. Wrap the config once and call instrument() from Next’s startup hook:
withFailproofai adds LangChain, Mastra, LlamaIndex and the SDK itself to serverExternalPackages, keeping your own list. Without it, instrument() warns once per framework it cannot reach rather than failing silently; if you list the packages yourself, set FAILPROOFAI_NEXT_EXTERNALS=1. The Vercel AI SDK and the call-site helpers work either way. An Edge route gets a no-op build: importing the SDK is safe and records nothing.

Token counts on streamed calls

OpenAI-compatible APIs only report usage on a stream when the client asks. LangChain and the Vercel AI SDK ask; for LlamaIndex pass additionalChatOptions: { stream_options: { include_usage: true } } to its OpenAI LLM, and for Mastra build the model with usage enabled (for example createOpenAICompatible({ includeUsage: true })). Otherwise streamed model calls carry no token counts.

Runtimes

Node ≥ 20.9, Bun and Deno — every framework, as an ES module and as CommonJS, is tested on each against Node’s trace. The SDK runs beside the failproofaid daemon, which ships what it writes.

Your own agent — no framework

For an agent loop you wrote yourself, or a framework without an adapter. You emit the events with the same API the adapters use underneath, so the trace has the same shape and quality. You don’t need to know how the agent is organised. Every hand-built agent already has three places, whatever its functions are called, and those three are the whole integration:
Identity is ambient: everything inside agent() lands on that run’s session without taking an id, and nothing else in the program changes — including whatever the agent already writes to its own database.
  • A service or a worker: pass your own request or job id as sessionId, so a session on the dashboard and the record in your own logs or database are the same string.
  • Sub-agents: nest agent() calls. The inner one joins the session with the outer as its parent_id.
  • Emit the pairs. A modelRequest with no modelResponse is a span the dashboard shows as running forever — hence the catch.
sdk/typescript/examples/research-agent.ts in the repository is the complete, runnable version: a real OpenAI tool loop instrumented exactly like this, run in CI on every change as an ES module and as CommonJS.

Evaluations

See the Evaluator SDK reference for the protocol, the worker settings and the result types.
An evaluation must yield. A synchronous function that never returns blocks the one thread Node has, and no timeout can fire while it does. Write async evaluations.

What it will not do to your process