
AG-UI in Drupal: any component can be an answer if a tool feeds it
How the AG-UI module renders agent answers as Single Directory Components: a tool returns data, and a tool component turns it into markup.
Most AI on a Drupal site is a chat bubble in the corner that answers in paragraphs. Ask it about upcoming events and you get a wall of text, even though the site already has an events listing built for exactly that.
The talk was about closing that gap. AG-UI is an open protocol for streaming an agent’s run to a frontend: text, state changes, and tool calls as they start, stream and finish. The AG-UI module puts a chat Single Directory Component on the Drupal side of that stream. The part I care about is what it does with the tool calls.
Tools are the contract
An agent answering a question can call tools. A tool has a name, arguments, and a result. In our setup the agents run outside Drupal, and the chat component talks to them over AG-UI. When the agent calls a tool, the chat sees the call arrive, sees its status change, and gets the result when it completes.
That result is just data. A weather tool returns a location, a temperature, conditions, humidity and wind as JSON. A search over the site’s own content returns a list of items, each with a heading, a bit of text, a link and maybe an image. The agent decides which tool to call and with what arguments. The tool decides the shape of what comes back.
So the rule is simple: any SDC can render an agent answer, as long as there is a tool whose result matches what the component needs. Build the tool, then hook a component up to it.
Hooking a component up to a tool
On the Drupal side, a tool component is an SDC that registers a renderer for one tool name.
The module ships a weather example. Its Twig holds <template> elements for the loading,
error and complete states, so the markup stays in Twig where a theme can override it:
<template data-weather-template="complete"> <div class="weather-card weather-card--complete"> <div class="weather-card__location" data-weather-location>Location</div> <div class="weather-card__temp" data-weather-temp>--°</div> <div class="weather-card__conditions" data-weather-conditions>Conditions</div> </div></template>Its JavaScript registers against the tool name when the chat says it’s ready, and gets called on every status change of that tool call:
const weatherRenderer = { containerClass: 'agui-weather-widget', render({ args, status, result, container }) { if (status === 'pending' || status === 'streaming') { // Clone the loading template, show args.location. return; } // Parse result, clone the complete template, fill the data-weather-* slots. },};
window.addEventListener('agui:tools:ready', (event) => { event.detail.api.register('get_weather', weatherRenderer, { override: true });});Place the component in the chat’s tools slot and it’s live:
{% embed 'agui:chat' with { endpoint: '/agui/api/chat', agentId: 'my_agent' } %} {% block tools %} {% embed 'agui:weather-tool' %}{% endembed %} {% endblock %}{% endembed %}The loading state appears the moment the agent decides to call the tool, using the arguments it has streamed so far, before any result exists. The page responds while the agent is still working, and the answer lands as a real component instead of a description of one.
Building more than weather
Weather is the example because everyone gets it. The ones we built for our platform follow the same pattern with real site components: cards, an accordion, a table, statistics, a carousel, an image gallery, an event listing, a map, a webform, and a program finder. Each is a small SDC with a Twig template and a renderer, registered against one tool.
Some of those render in the chat thread. Others target a canvas panel beside it, so a set of cards or a map gets room to be a page section instead of a message. The renderer picks the target; the agent doesn’t know or care.
The agents don’t have to know about markup at all. One that answers questions from the site’s content is told to call the cards tool with the documents it cited, and the tool turns those into heading, text and link objects. The card component turns those into the same Bootstrap card the rest of the site uses. Change the card’s Twig and every agent answer that produces cards changes with it.
Where the guardrails live
The demo makes rendering look like the hard part. It isn’t. Once the tool contract exists, a new component is a template and a renderer.
The harder part is what the agent is allowed to say before it ever reaches a tool. The content agent behind the cards only answers from the institution’s own indexed pages, refuses anything outside that scope in one sentence, runs a moderation check before the model sees the message, and only cites links that appear verbatim in the documents it retrieved. A card can only exist for a document it actually cited.
None of that comes from the component schema. A schema can stop a prop from receiving the wrong type of value; it can’t stop an agent from making something up. That has to happen in the agent and in the tools that feed the components, which is where I’d spend most of the time if you’re planning one of these for a real site.
What this came from
- ModuleAG-UI
- ArticleAG-UI protocol documentation