rudra-js

Letting an A2UI agent place the block

With A2UI, an AI agent builds an interface from a catalog of components your app renders. @rudra-js/a2ui adds the block to that catalog, as RudraRecommendations. How it works says what the agent decides and what your server decides.

This page sets up three pieces: the catalog your agent is prompted with, your endpoint, and the browser. It works with version 0.9 of the A2UI protocol, through @a2ui/react and @a2ui/web_core 0.14.

Install

npm install @rudra-js/a2ui@latest @rudra-js/core@latest @rudra-js/react@latest @rudra-js/attested@latest @a2ui/react@0.14 @a2ui/web_core@0.14 react react-dom
  • Run the same line to upgrade. If your app has @rudra-js/anthropic, add @rudra-js/anthropic@latest to it.
  • @0.14 is the A2UI release this package works with. react and react-dom carry no version, so npm and pnpm keep a React 18 app on 18.
  • Every dependency of @rudra-js/a2ui except zod is a peer, so your app holds one copy of each. Install every name on the line anyway. Your code imports most of them, and pnpm only lets it import what your package.json names.
  • On Yarn, leave out react and react-dom if your app already has React. Yarn moves a bare name to its latest version.
  • On pnpm, read zod on pnpm first.

Keep skipLibCheck: true in your tsconfig.json. Without it, A2UI's own type declarations fail to typecheck.

The catalog your agent is prompted with

import { Catalog } from '@a2ui/web_core/v0_9';
import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
import { RUDRA_RECOMMENDATIONS_INSTRUCTIONS, rudraRecommendationsApi } from '@rudra-js/a2ui/server';

export const agentCatalog = new Catalog(
  'https://shop.example/a2ui/catalog.json',
  'v0.9',
  [...basicCatalog.components.values(), rudraRecommendationsApi],
  [...basicCatalog.functions.values()],
  { instructions: RUDRA_RECOMMENDATIONS_INSTRUCTIONS },
);

Your endpoint

parseRecommendationRequest walks through this file, including where it goes in Next.js.

import { createComponentGenerator, parseTrackingInput } from '@rudra-js/core';
import {
  fitRecommendationRequest,
  parseRecommendationRequest,
  type RecommendationRequest,
  type RecommendationResult,
} from '@rudra-js/a2ui/server';
import { candidatesFor, shopperFor } from './your-shop.js';

const generator = createComponentGenerator({ provider: null });

export async function POST(request: Request): Promise<Response> {
  let asked: RecommendationRequest;
  try {
    asked = parseRecommendationRequest(await request.json());
  } catch {
    return new Response('Bad request', { status: 400 });
  }

  const shopper = await shopperFor(request);
  const candidates = await candidatesFor(shopper);
  const hints = fitRecommendationRequest(asked, candidates);
  const input = parseTrackingInput({
    user: { id: shopper.id },
    context: { surface: 'assistant', locale: shopper.locale, ...hints },
    candidates,
    signals: shopper.signals,
  });
  const spec = await generator.generate(input);

  const result: RecommendationResult = { spec, products: input.candidates, bundles: input.bundles };
  return Response.json(result);
}

Rate-limit the endpoint. Each new hint can be a new model call.

The browser

import type { ReactCatalogComponent } from '@a2ui/react/v0_9';
import { Catalog, MessageProcessor } from '@a2ui/web_core/v0_9';
import { basicCatalog } from '@a2ui/web_core/v0_9/basic_catalog';
import { createRudraRecommendationsComponent } from '@rudra-js/a2ui';

const recommendations = createRudraRecommendationsComponent({
  async loadRecommendations(request, signal) {
    const response = await fetch('/api/recommendations', {
      method: 'POST',
      headers: { 'content-type': 'application/json' },
      body: JSON.stringify(request),
      signal,
    });
    if (!response.ok) throw new Error(`recommendations: HTTP ${response.status}`);
    return response.json();
  },
  locale: 'en-US',
});

const shopCatalog = new Catalog<ReactCatalogComponent>(
  'https://shop.example/a2ui/catalog.json',
  'v0.9',
  [...basicCatalog.components.values(), recommendations],
  [...basicCatalog.functions.values()],
);

export const processor = new MessageProcessor<ReactCatalogComponent>([shopCatalog]);
  • Keep the browser catalog's id the same as the agent catalog's.
  • createRudraRecommendationsComponent walks through the rest of this file. It also lists every option, and says when the block loads and what it shows on a failure.

Where each piece runs

@rudra-js/a2ui loads only in the browser, because @a2ui/react needs a DOM. The agent catalog and your endpoint import from @rudra-js/a2ui/server, which runs anywhere.

In Next.js, load the browser file from a client component with next/dynamic and ssr: false.

On a product page crawlers should see, render the block on the server instead.

zod on pnpm

A2UI's schemas use zod 3, so this package depends on zod 3. The rest of rudra-js uses zod 4. npm installs both, each where its package finds it, and you don't need either one yourself.

On pnpm, @a2ui/react gets core's zod 4 as its zod peer, and the browser code stops typechecking with TS2344. If your app has no zod of its own, add zod 3:

pnpm add zod@^3.25.76

If your app uses zod 4, drop the zod peer from @a2ui/react in .pnpmfile.cjs. pnpm then gives it the zod 3 it depends on. The a2ui README has the hook to paste.