Skip to content
Material 3 Expressivev1.2.2
AG-UI guide

AG-UI components and tool renderers

Compose a thread, render tools as components, share state, and handle approvals.

Use the assembled chat for a familiar conversation layout, or compose the individual components around your app. The components below come from @language-lit/material3-expressive-ag-ui@0.1.0. Custom forecast, project, and invitation cards in the demo are examples built with the core Material components, not extra exports from the AG-UI package.

Start with the setup guide to install the packages, load both stylesheets, and bind an agent.

Conversation components

ComponentWhat it does
AgentChatCombines the thread, run status, and composer. Accepts an emptyState and composer options.
MessageThreadRenders the timeline, including registered tool renderers. Follows new content while the reader is at the bottom.
UserMessageDisplays a user turn on a Material surface. Takes a UserNode.
AssistantMessageDisplays an assistant reply and a caret while it streams. Takes an AssistantTextNode.
ReasoningDisclosureLets the reader open reasoning text. Takes a ReasoningNode; defaultExpanded defaults to false.
ToolCallCardShows the tool name, progress, arguments, and result. Takes a ToolCallNode; details start collapsed.
ActivityRowDisplays a backend-defined activity type and its structured content. Takes an ActivityNode.
InterruptPromptShows an approval question with Approve and Cancel actions. Takes an InterruptNode and optional action labels.
RunStatusAnnounces a running step or failure. Accepts a workingLabel.
ComposerSupplies a labeled message field and send/stop control. Accepts label, placeholder, disabled, and action labels.

AgentProvider binds an agent and supplies it to the components that need it. MessageThread chooses the appropriate component for each timeline entry, so you do not need to build or pass individual nodes for a standard chat.

Render a tool as a component

Register a function under the exact tool name in AgentProvider.toolRenderers. It receives node, the current tool call, and agent, the bound agent state and actions. Names without a renderer use ToolCallCard.

import { Surface, Text } from '@language-lit/material3-expressive'
import type { ToolRendererProps } from '@language-lit/material3-expressive-ag-ui'

export function WeatherResult({ node }: ToolRendererProps) {
  const city = typeof node.args?.city === 'string'
    ? node.args.city : 'Loading city'
  const temperature = typeof node.args?.temperature === 'number'
    ? `${node.args.temperature} °C` : 'Loading temperature'

  return (
    <Surface color="tertiary-container" shape="large" style={{ padding: 24 }}>
      <Text as="h3" variant="titleLarge">{city}</Text>
      <Text as="p" variant="headlineMedium">{temperature}</Text>
      <Text as="p" variant="bodySmall">
        {node.status === 'complete' ? 'Forecast received' : 'Receiving forecast'}
      </Text>
    </Surface>
  )
}

Register it on the provider from the setup guide:

<AgentProvider
  agent={agent}
  toolRenderers={{ show_weather: WeatherResult }}
>
  <AgentChat />
</AgentProvider>

The renderer runs while arguments are arriving. node.args can be undefined or incomplete, and node.argsComplete tells you whether the JSON document has finished. Check values before rendering them or passing them to another component. In the demo, forecast rows appear only once their required fields are present.

Read tool state

FieldMeaning
name, idThe tool name and stable call ID.
rawArgsArgument text exactly as received, including an unfinished JSON tail.
argsA best-effort parsed object, or undefined.
argsCompleteWhether the argument document is complete.
statusstreaming, awaiting-result, complete, or error.
result, errorResult or error text from the tool response, when available.

The run and a tool call have separate lifecycles. A tool can have complete arguments while its result is still pending. Use status === 'complete' when a control needs the finished tool result.

Connect controls to shared state

The bound agent exposes state and setState. Incoming STATE_SNAPSHOT and STATE_DELTA events update the same state your components read. The project demo uses it for completed task IDs, so the checklist, schedule, and progress indicator stay in agreement.

import { Checkbox, LinearProgress } from '@language-lit/material3-expressive'
import { useAgentContext } from '@language-lit/material3-expressive-ag-ui'

export function ReviewTask() {
  const { state, setState, isRunning } = useAgentContext()
  const complete = state?.reviewComplete === true

  return (
    <div>
      <label>
        <Checkbox
          checked={complete}
          disabled={isRunning}
          onCheckedChange={(checked) =>
            setState({ ...state, reviewComplete: checked })
          }
        />
        Review with the team
      </label>
      <LinearProgress
        aria-label="Review progress"
        value={complete ? 1 : 0}
      />
    </div>
  )
}

setState changes the local agent state. It does not save a task to a server or start a run. Call run() when your workflow should send the updated state to the agent, or persist it through your application's own data layer.

Pause for approval

An AG-UI run requests approval by finishing with an interrupt outcome. MessageThread renders each pending interrupt with InterruptPrompt. The default actions answer it and resume through the SDK.

For your own approval controls, use resolveInterrupt from useAgentContext():

// Inside a component that reads useAgentContext().
await resolveInterrupt(interruptId, { status: 'resolved' })

// Or decline the action.
await resolveInterrupt(interruptId, { status: 'cancelled' })

The backend decides what an approval means and whether the action can proceed. An enabled button in a browser is not authorization for a server operation. The demo resumes its local script and updates the invitation preview without sending a message.

Reading and keyboard behavior

Keep RunStatus alongside the thread. The thread itself is not a live region; announcing the whole transcript for every text fragment would make it hard to read with a screen reader. If your layout needs a completion or approval announcement, add a brief status message for that transition.

Use labeled controls inside passive Cards or Surfaces. Do not put buttons or inputs inside an interactive Card, which is itself a button. The demo's Tabs, Checkboxes, and segmented buttons keep their normal keyboard behavior inside tool renderers.

When an approval removes the focused control, restore focus to a useful place in your layout. The demo returns it to the conversation. Keep motion optional and preserve the user's ability to scroll back while a reply arrives.

Version 0.1.0 text behavior

Assistant and user text is displayed as plain text, preserving line breaks. It is not a Markdown renderer. Supply your own content renderer if your app needs rich text, and handle untrusted content appropriately.

The package source contains the complete exported TypeScript definitions and tests.