Quick start: pure client¶
In this mode the agent runs elsewhere, on your server, a remote A2A agent, or a coding agent over
ACP, and the app renders its conversation. You need ai-elements-ui and the backend for your
protocol (ai-elements-core for AI SDK and AG-UI).
App setup¶
Declare network access in src/main/AndroidManifest.xml:
For the local reference server only, allow HTTP in the debug manifest
(src/debug/AndroidManifest.xml):
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:usesCleartextTraffic="true" />
</manifest>
Use HTTPS for a production endpoint. An Android emulator reaches your computer as 10.0.2.2;
localhost on the emulator refers to the emulator itself. If connecting fails, check the server's
/health, bind address and firewall before changing your backend.
One composable¶
Use these imports inside a Compose activity's setContent:
import dev.ai.elements.ui.theme.AiElementsTheme
import dev.ai.elements.ui.chat.Chat
import dev.ai.elements.ui.chat.rememberChat
import dev.ai.elements.core.protocol.agui.AgUiBackend
Chat wires the conversation, tools, approvals, sub-agents, plans and the prompt input to a
ChatController:
AiElementsTheme {
Chat(rememberChat { approver -> AgUiBackend("https://agents.example.com/api/agui", approver = approver) })
}
rememberChat gives the backend an approver: the app's human in the loop, which shows approvals
and forms in the conversation.
In a ViewModel¶
In an app, keep the controller in a ViewModel so that the conversation survives configuration
changes. Compose the pieces yourself when you need more control:
class ChatViewModel : ViewModel() {
// An AG-UI agent (Pydantic AI, LangGraph, CrewAI, Mastra…). Tools that need approval pause
// the run as an AG-UI interrupt; the UI shows a Confirmation and resumes it.
val chat = ChatController(backend = { approver ->
AgUiBackend("https://agents.example.com/api/agui", approver = approver)
}, scope = viewModelScope)
}
@Composable
fun ChatScreen(vm: ChatViewModel) {
val state by vm.chat.state.collectAsStateWithLifecycle()
var input by rememberSaveable { mutableStateOf("") }
AiElementsTheme {
Column(Modifier.fillMaxSize().imePadding()) {
Conversation(
state = state,
modifier = Modifier.weight(1f),
onRegenerate = vm.chat::regenerate,
onToolApproval = vm.chat::respondToApproval,
onToolDecision = vm.chat::respondToApproval, // deny with a reason, edit then approve
onInputResponse = vm.chat::respondToInput, // forms the agent asks the user to fill
onSelectVersion = vm.chat::selectVersion,
onRestoreCheckpoint = vm.chat::restoreCheckpoint,
)
PromptInput(
value = input,
onValueChange = { input = it },
onSubmit = { if (vm.chat.send(input)) input = "" },
onStop = vm.chat::stop,
busy = state.isBusy,
allowQueue = true,
)
}
}
}
Sub-agents, plans, shared state and activities from the server render on their own. For example, a
delegate_task call or an AG-UI SUBAGENT_* run becomes a Subagent card with the nested run.
Choose a backend¶
Every backend is a ChatBackend. Pick the one your agent speaks:
// AI SDK 5 / 6 UI Message Stream (any `toUIMessageStreamResponse()` server)
{ approver -> UiMessageStreamBackend("https://agents.example.com/api/chat", approver = approver) },
// AG-UI 1.x (Pydantic AI, LangGraph, CrewAI, Mastra, …)
{ approver -> AgUiBackend("https://agents.example.com/api/agui", approver = approver) },
// A2A 1.0 remote agent (ai-elements-a2a)
{ _ -> A2aBackend(A2aAgent("https://agents.example.com")) },
// Agent Client Protocol coding agent (ai-elements-acp)
{ approver -> AcpBackend(AcpAgent.webSocket("wss://dev-box.example.com/acp"), approver) },
// A model API on the device, with the built-in agent loop
{ approver ->
ProviderProfile("openai", "OpenAI", ProviderKind.OPENAI_RESPONSES, "https://api.openai.com/v1", "gpt-5.5")
.createBackend(apiKey = "…", approver = approver)
},
Protocols compares them and explains each one, including how the server side should be built.
Try it against the reference server¶
The repository includes a reference server, a Pydantic AI agent served over every protocol, with a scripted model that needs no API key:
Point a backend at http://10.0.2.2:8788/api/agui (the Android emulator's alias for your computer),
or use the LAN address of your computer on a device.
The samples/pure-client app in the repository is a complete, minimal app for this mode.
Lifecycle and errors¶
rememberChat is scoped to the composition and is suitable for a prototype. Use a ViewModel for
configuration changes. A ViewModel does not survive process death: persist the messages in your
app's store and supply them as initialMessages when creating a new controller.
Call the controller from the main thread and use viewModelScope. Stop cancels the current turn;
transport errors become ChatState.error. Retry requests a new turn; it does not reconnect to the
middle of the previous stream. See storage and replay.