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@latestto it. @0.14is the A2UI release this package works with.reactandreact-domcarry no version, so npm and pnpm keep a React 18 app on 18.- Every dependency of
@rudra-js/a2uiexcept 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 yourpackage.jsonnames. - On Yarn, leave out
reactandreact-domif 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 },
);rudraRecommendationsApisays what to put in your agent's prompt.RUDRA_RECOMMENDATIONS_INSTRUCTIONStells the agent how to use the block.- The warnings the first
toJson()logs are harmless.
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);
}fitRecommendationRequestkeeps the agent's hints to products you sell.- Send back
input.candidatesandinput.bundles, not your raw catalog. - Pass a provider in place of
nullto turn the model on. - The agent's
searchQueryreaches the model only withgeneration: 'per-shopper'.
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.
createRudraRecommendationsComponentwalks 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.76If 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.