npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

responsible-ai-platform

v1.1.1

Published

Responsible AI Platform (RAIA) — SDK for tracing, evaluation, and governance of AI agents

Readme

responsible-ai-platform

Responsible AI Platform (RAIA) — Node.js/TypeScript SDK for tracing, evaluating, and governing AI agents.

Instrument an agent in a few lines. Every run is uploaded to RAIA, where it is scored for quality, safety, tool accuracy, efficiency, and goal completion.

Works with the Vercel AI SDK, LangChain, LangGraph, OpenAI, Anthropic, Google Gemini, AWS Bedrock, any OpenAI-compatible provider, and hand-rolled agents with no framework at all.


Installation

npm install responsible-ai-platform

Requires Node.js 18 or newer (the SDK uses native fetch, AsyncLocalStorage, and node:crypto). Ships with both ESM and CommonJS builds and full TypeScript types. No runtime dependencies.

Any Node backend framework works — Express, Fastify, NestJS, Koa, Hapi, Hono, Next.js route handlers. The SDK has no web-framework integration to configure: you call it from your handler like any other function. Per-request isolation is handled by AsyncLocalStorage, so concurrent requests never share a trace.

Node runtime only. Edge runtimes, browsers, Cloudflare Workers, and Deno Deploy are not supported — they lack node:async_hooks and process signals. In a Next.js route handler, set export const runtime = 'nodejs'.


Configuration

Put these in your .env:

RAIA_API_KEY=raia_xxxxxxxxxxxxxxxxxxxx
RAIA_API_BASE_URL=https://raia.cirruslabs.io

The SDK reads .env from the working directory automatically — same as the Python SDK — so nothing else is needed. Real environment variables take precedence over the file, so containers and CI can override it without touching .env. If RAIA_API_BASE_URL is unset it defaults to http://localhost:8000.

Generate the key per agent in the RAIA UI. It identifies the agent, its project, and its tenant — the SDK never sends those itself. Use the key belonging to the agent you're tracing; reusing another agent's key files your traces under that other agent.

Many agents in one process

A host that runs many agents — each registered as its own RAIA asset, possibly for different customers' RAIA tenants — passes the key per trace instead of setting RAIA_API_KEY:

const trace = new AgentTrace({
  taskDescription,
  agents: topologyFor(employee),
  credentials: { apiKey: employee.raiaApiKey },   // baseUrl optional
});

Which key a trace uploads with:

| Order | Source | |---|---| | 1 | credentials on the AgentTrace | | 2 | RAIA_API_KEY environment variable | | 3 | RAIA_API_KEY in .env | | — | none: the trace is dropped with a warning |

credentials.baseUrl falls back the same way to RAIA_API_BASE_URL, then http://localhost:8000.

  • The key travels with the queued upload, so traces finishing at the same moment can never upload under each other's keys.
  • It is validated in the constructor: anything not starting with raia_ — including a RAIA Tenant API key (raiat_…), which cannot upload traces — throws immediately.
  • It is never written into the trace entries, metadata, or log lines.
  • Don't fall back to a shared key yourself. If a caller has no key for an agent, skip tracing that run; a process-wide RAIA_API_KEY would file it under someone else's asset.

All traces in a process share one upload queue. If you trace many agents, raise its depth with RAIA_MAX_PENDING_UPLOADS (default 100).


Quick start

import { AgentTrace, withTrace, tool } from 'responsible-ai-platform';

// 1. Declare your agent topology once, at startup.
AgentTrace.configure({
  agents: [
    { agent_name: 'support_bot', role: 'orchestrator', parent_agent_name: null,
      tools: ['search_kb', 'issue_refund'] },
  ],
  modelVersion: 'gpt-4.1',
});

// 2. Wrap the tools you want measured.
const searchKb = tool('search_kb', async (query: string) => db.search(query));

// 3. Trace a run.
const trace = new AgentTrace({ taskDescription: userInput });

const answer = await withTrace(trace, 'support_bot', async () => {
  const hits = await searchKb(userInput);   // logged automatically
  return summarize(hits);
});
// Outcome is set from whether the function threw. The upload happens in the
// background, so tracing adds no latency to your agent's response.

That's the whole integration. Everything below is for richer traces.


Agent topology (required)

Every trace declares who the agents are and how they relate. RAIA uses this to attribute tool calls, score per-agent behaviour, and detect tools an agent invoked but was never granted.

AgentTrace.configure({
  agents: [
    { agent_name: 'orchestrator', role: 'orchestrator', parent_agent_name: null,
      tools: ['plan', 'delegate'] },
    { agent_name: 'researcher', role: 'sub_agent', parent_agent_name: 'orchestrator',
      tools: ['web_search'], max_steps_allowed: 5 },
    { agent_name: 'writer', role: 'sub_agent', parent_agent_name: 'orchestrator',
      tools: [] },
  ],
  modelVersion: 'claude-opus-5',      // default model recorded on every entry
  systemPrompt: SYSTEM_PROMPT,        // used by the quality and safety metrics
});

| Field | Required | Notes | |---|---|---| | agent_name | ✅ | Unique within the topology | | role | ✅ | orchestrator | sub_agent | worker | router | tool_runner | | parent_agent_name | ✅ | null for the single root agent | | tools | ✅ | Tools this agent is allowed to call; [] is fine | | model_version | — | Per-agent model override | | framework | — | Free-form label (langgraph, vercel-ai, …) | | max_steps_allowed | — | Per-agent step budget, scored by the efficiency metric | | optimal_steps | — | Expected step count for the task | | agent_actions | — | Actions this agent is permitted to take | | boundary_definitions | — | Policies it must not break, checked by boundary-compliance metrics | | escalation_conditions | — | When it should hand off to a human | | ground_truth_description | — | What a correct outcome looks like |

Single-agent systems still declare one node. The topology is validated up front: exactly one root, no duplicate names, no parent that isn't declared. A bad topology throws at configure() rather than silently producing unattributable traces.

Hosting more than one agent in a process? Pass agents per trace instead:

const trace = new AgentTrace({ taskDescription, agents: [...] });

Integrations

Vercel AI SDK

import { AgentTrace, logVercelAIResponse } from 'responsible-ai-platform';
import { streamText } from 'ai';

export const runtime = 'nodejs';

const trace = new AgentTrace({ taskDescription: userInput });
trace.start();

const result = streamText({
  model: openai('gpt-4.1'),
  messages,
  tools,
  onFinish: async (event) => {
    logVercelAIResponse(trace, 'support_bot', userInput, event);
    trace.setOutcome('success');
    await trace.finish();
  },
});

return result.toDataStreamResponse();

Handles AI SDK v5+ (input/output, inputTokens/outputTokens) and falls back to v4 names. Multi-step runs (stopWhen) are aggregated across every step, and failed tool executions that arrive as tool-error parts are recorded rather than dropped.

Call it from onFinish, or on the settled result of generateText — never on a live stream, where text is still a promise.

LangGraph / LangChain

Simplest path — hand it the message array your agent returned:

import { AgentTrace, logLangGraphMessages } from 'responsible-ai-platform';

const trace = new AgentTrace({ taskDescription: query });
trace.start();

const result = await agent.invoke({ messages: [new HumanMessage(query)] });

logLangGraphMessages(trace, result.messages);
trace.setOutcome('success');
await trace.finish();

Input, output, tool calls, tool results, token usage, and the model are all extracted for you.

Supervisor and swarm graphs get one entry per node, so each sub-agent is scored on its own work:

import { logLangGraphMessagesPerAgent } from 'responsible-ai-platform';

logLangGraphMessagesPerAgent(trace, result.messages);

Node names are read from message.name, then response_metadata.langgraph_node. When a graph has no node tagging (every plain LangChain run), this falls back to a single entry automatically.

logLangChainMessages and logLangChainAWSMessages are the same function under framework-specific names — LangChain, LangGraph, and Bedrock-backed LangChain-AWS all emit the same message objects.

Streaming runs can use the callback handler instead, which accumulates across the stream:

import { createLangChainTracer } from 'responsible-ai-platform';

const tracer = createLangChainTracer(trace, 'support_bot');
for await (const [chunk] of await agent.stream({ messages }, { callbacks: [tracer] })) { /* … */ }
tracer.finish({ inputText: query, outputText: finalText });

OpenAI (and any OpenAI-compatible provider)

import { AgentTrace, logOpenAIResponse } from 'responsible-ai-platform';

const completion = await client.chat.completions.create({ model, messages, tools });

logOpenAIResponse(trace, 'support_bot', userInput, completion, {
  // The API returns tool *requests*; pass what your tools actually returned.
  toolResults: [{ name: 'lookup_order', result: JSON.stringify(order) }],
});

Handles both chat.completions.create() and the newer responses.create(). Because the shape is the same, this also covers Azure OpenAI, Groq, Together, OpenRouter, Fireworks, DeepSeek, Ollama, and vLLM. Refusals and content-filter stops are recorded as failed interactions.

Anthropic

import { AgentTrace, logAnthropicResponse } from 'responsible-ai-platform';

const message = await anthropic.messages.create({ model, max_tokens, messages, tools });
logAnthropicResponse(trace, 'support_bot', userInput, message);

tool_use blocks become tool calls, extended-thinking blocks become agent_thinking, and cache reads are counted as prompt tokens. A max_tokens stop is flagged as a truncated (failed) response.

Google Gemini

import { AgentTrace, logGeminiResponse } from 'responsible-ai-platform';

const result = await ai.models.generateContent({ model, contents });
logGeminiResponse(trace, 'support_bot', userInput, result);

Works with both @google/genai and the older @google/generative-ai. Safety blocks and recitation stops are recorded as failures.

AWS Bedrock (Converse)

import { AgentTrace, logBedrockResponse } from 'responsible-ai-platform';
import { ConverseCommand } from '@aws-sdk/client-bedrock-runtime';

const result = await client.send(new ConverseCommand({ modelId, messages, toolConfig }));
logBedrockResponse(trace, 'claire', userInput, result, { model: modelId });

toolUse blocks become tool calls, reasoningContent blocks become agent_thinking, cached prompt tokens are counted as prompt tokens, and Bedrock's own metrics.latencyMs is used in preference to wall-clock timing. max_tokens, guardrail_intervened, and content_filtered stops are recorded as failures.

Bedrock does not echo the model id in the response — pass it via options.model.

For ConverseStreamCommand, hand over the turn you accumulated from the event stream:

const { text, usage, stopReason } = await drainStream(...);
logBedrockResponse(trace, 'claire', userInput, { text, usage, stopReason }, { model: modelId });

Any other framework, or none

Two options. Automatic — wrap your tools and let AsyncLocalStorage do the bookkeeping:

import { AgentTrace, withTrace, withAgent, tool } from 'responsible-ai-platform';

const search = tool('search', async (args: { query: string }) => db.search(args.query));

const trace = new AgentTrace({ taskDescription: userInput });

await withTrace(trace, 'orchestrator', async () => {
  // Tool calls inside withAgent() are attributed to that sub-agent.
  const facts = await withAgent('researcher', () => search({ query: userInput }));
  return withAgent('writer', () => write(facts));
});

On finish() these become one entry per agent that ran a tool.

Manual — build the entry yourself, for full control:

trace.logInteraction({
  inputText: userInput,
  outputText: answer,
  toolCalls: [{ name: 'search', arguments: { query: 'shoes' }, invoked_by_agent_name: 'researcher' }],
  toolResults: [{ name: 'search', result: JSON.stringify(hits) }],
  promptTokens: 120,
  completionTokens: 45,
  model: 'gpt-4.1',
  agentName: 'researcher',
});

When do traces get evaluated?

Uploading is not the same as scoring. A trace lands in RAIA's storage immediately, but it is evaluated by a scheduled run, configured per tenant in the RAIA UI — not in real time, and not triggered by the upload itself.

Each scheduled run evaluates the traces uploaded in the previous 24 hours, merged into a single evaluation. Two things follow from that:

  • Results appear after the next scheduled run, not seconds after your agent finishes.
  • A tenant with no schedule configured never has its SDK traces evaluated at all.

This is also why autoUpload defaults to false: batching a trace into one upload at finish() keeps the file count per run low. Turn it on for long-running sessions where holding entries in memory matters more.


Threads, workers, and clustering

The SDK uses no threads. Node's fetch is already non-blocking async I/O, so uploads run concurrently on the event loop — up to 4 at a time, with a queue depth of 100. Nothing is offloaded to a worker thread, and the event loop is never blocked.

If your app itself uses worker_threads, cluster, or PM2 cluster mode, each worker loads its own copy of the module and therefore gets its own queue, its own concurrency budget, and its own shutdown hooks. They upload independently and never contend.

One caveat worth knowing: AsyncLocalStorage context does not cross a worker or process boundary. If you hand work to a worker_thread, getCurrentTrace() returns undefined inside it and tool() calls there are not captured. Either create a trace inside the worker, or pass what you need across and log it on the main thread.

In cluster/PM2 setups, make sure workers actually receive SIGTERM/SIGINT on shutdown — that's what triggers the final flush. A worker killed without a signal loses whatever is still queued.


Multi-turn sessions

One trace can span a whole conversation. Each logInteraction() is one turn, timed from the end of the previous one:

const trace = new AgentTrace({
  taskDescription: 'Customer support session',
  sessionId: conversationId,   // reuse across turns to group them
  autoUpload: true,            // upload each turn as it happens, instead of buffering
});
trace.start();

// …per turn:
trace.logInteraction({ inputText: turn.question, outputText: turn.answer });

// …at the end:
trace.setOutcome('success');
await trace.finish();

Use autoUpload: true for long-lived sessions so entries aren't held in memory until the session ends.


Safety and governance

// A sub-agent handed work to another
trace.logHandoff('orchestrator', 'researcher', 'needs a policy lookup');

// The agent did something it wasn't allowed to
trace.logBoundaryViolation('issued refund', 'refunds over $500 need approval');

// It gave up and escalated (a reason is required)
trace.setOutcome('escalated', 'handed to a human agent');

These attach to the next entry logged — or to the last one at finish() — and are recorded exactly once, never replayed across entries.

setOutcome() accepts 'success' | 'failure' | 'partial' | 'escalated'.


Serverless

Uploads run in the background, which is what keeps them off your agent's critical path — but background work stops dead when a serverless container is frozen on response. The SDK flushes on beforeExit, SIGTERM, and SIGINT, and none of those fire on Lambda or Vercel.

So on serverless, wait for the upload before returning. Either flush everything:

import { flush } from 'responsible-ai-platform';

await flush();

or wait for one trace:

await withTrace(trace, 'support_bot', run, { waitForUpload: true });
// or, equivalently
await trace.finish();

On a long-running server (Express, Fastify, NestJS) you want neither — let the upload happen in the background and let the SIGTERM handler drain the queue on shutdown.


API reference

AgentTrace

| Member | Description | |---|---| | AgentTrace.configure({ agents, modelVersion?, systemPrompt? }) | Declares the global topology. Call once at startup. | | new AgentTrace({ taskDescription, sessionId?, systemPrompt?, expectedOutcome?, metadata?, agents?, autoUpload?, credentials? }) | Starts a trace. credentials: { apiKey, baseUrl? } overrides RAIA_API_KEY for this trace. | | .start() | Marks the start time. | | .logInteraction({ … }) | Records one interaction. See below. | | .logStep({ tool, args, result, latencyMs?, error?, agentName? }) | Records one tool step (what tool() calls). | | .logHandoff(from, to, reason?) | Records an agent-to-agent handoff. | | .logBoundaryViolation(action, ruleViolated) | Records a policy violation. | | .setOutcome(outcome, escalationReason?) | Sets the task outcome. | | .finish() | Finalizes and uploads. Idempotent; resolves after upload. | | .getEntries() | The entries logged so far — useful in tests. |

logInteraction() accepts: inputText, outputText, toolCalls, toolResults, agentThinking, promptTokens, completionTokens, totalTokens, success, errorType, errorMessage, model, agentName, startTime, endTime, latencyMs, taskDescription, systemPrompt, expectedOutcome, stepIndex, numSteps, isRetry, boundaryViolations, escalationEvents, escalationReason.

Wrappers and context

| Export | Description | |---|---| | withTrace(trace, agentName, fn) | Runs fn in a trace context; sets the outcome and finishes. | | withAgent(agentName, fn) | Scopes the active sub-agent inside a trace. | | tool(name, fn) | Wraps a function so calls are logged with args, result, and latency. | | getCurrentTrace() / getCurrentAgent() | Reads the ambient context. |

Integrations

Available from the package root, or individually as responsible-ai-platform/integrations/<name>.

| Export | For | |---|---| | logVercelAIResponse(trace, agentName, inputText, result, options?) | Vercel AI SDK | | logOpenAIResponse(trace, agentName, inputText, response, options?) | OpenAI + compatible providers | | logAnthropicResponse(trace, agentName, inputText, message, options?) | Anthropic | | logGeminiResponse(trace, agentName, inputText, result, options?) | Google Gemini | | logBedrockResponse(trace, agentName, inputText, result, options?) | AWS Bedrock Converse | | logLangGraphMessages(trace, messages, options?) | LangChain / LangGraph / LangChain-AWS | | logLangGraphMessagesPerAgent(trace, messages, options?) | Supervisor & swarm graphs, per node | | createLangChainTracer(trace, agentName) | Streaming LangChain runs (callback handler) |

Every log*Response helper accepts { model, toolResults, agentThinking, startTime, endTime, latencyMs, success, errorType, errorMessage }.


Error handling and limits

Telemetry never breaks your agent. Uploads fail quietly to console.warn — there is no exception to catch and no local retry queue on disk.

  • Transient failures (5xx, 429, 408, network) retry 3 times with backoff; 4xx does not retry.
  • Requests time out after 60 seconds.
  • Traces are dropped, with a warning, when more than 100 uploads are pending (RAIA_MAX_PENDING_UPLOADS) or a payload exceeds 50 MB.
  • Circular structures and BigInts in tool results are serialized safely rather than throwing.

License

Apache 2.0. Copyright 2026 Cirrus Labs — RAIA.

Support

For issues, questions, or feedback, contact: [email protected]