During an agentic workflow, the `AgentRunner` class and `AgentSession` interface
emit reactive event streams representing the model's reasoning process, tool
invocations, and textual responses. This page demonstrates how to collect events
and connect them to your UI using Kotlin Coroutines and Jetpack Compose.

## Understand the `AgentEvent` interface

The `AgentSession.runAsync()` and `AgentRunner.runSingleTurnAsync()` methods return a
`Flow<AgentEvent>` stream. Rather than waiting for the entire reasoning loop to finish,
your app receives real-time updates as the agent executes:

|---|---|---|
| **Event type** | **Description** | **Typical UI action** |
| `AgentEvent.TextMessage` | Emits incremental text chunks (`isPartial = true`) followed by the final complete response (`isPartial = false`) generated by Gemini Nano. | Append `event.text` to the active chat bubble when `event.isPartial` is `true` (or use the `textTokens()` extension function). |
| `AgentEvent.FunctionCall` | Emitted when Gemini Nano selects a tool and prepares to execute it (provides `event.name` and `event.args`). | Display an in-progress indicator (for example, "Checking order status..."). |
| `AgentEvent.FunctionResponse` | Emitted when the app finishes executing the tool function (provides `event.name` and `event.response`). | Transition the tool status to completed. |

> [!NOTE]
> **Note:** The `AgentEvent` interface is a non-exhaustive `sealed interface` so future event types can be added without breaking source compatibility. Always include an `else ->` branch when matching `AgentEvent` values in a `when` expression.

## Collect events in a `ViewModel` class

Use an Android `ViewModel` class to coordinate prompt dispatch and expose UI state to
your Compose screens:

    package com.example.ai.agent.ui

    import androidx.lifecycle.ViewModel
    import androidx.lifecycle.viewModelScope
    import com.google.mlkit.genai.agent.AgentEvent
    import com.google.mlkit.genai.agent.AgentRunner
    import com.google.mlkit.genai.agent.AgentSession
    import com.google.mlkit.genai.common.GenAiException
    import com.google.mlkit.genai.prompt.Content
    import kotlinx.coroutines.flow.MutableStateFlow
    import kotlinx.coroutines.flow.StateFlow
    import kotlinx.coroutines.flow.asStateFlow
    import kotlinx.coroutines.flow.catch
    import kotlinx.coroutines.flow.update
    import kotlinx.coroutines.launch

    data class AgentUiState(
        val messages: List<String> = emptyList(),
        val currentResponse: String = "",
        val activeToolStatus: String? = null,
        val isLoading: Boolean = false,
        val errorMessage: String? = null
    )

    class ChatViewModel(
        private val runner: AgentRunner,
        private val sessionId: String = "default_chat_session"
    ) : ViewModel() {

        private val session: AgentSession = runner.getOrCreateSession(sessionId)

        private val _uiState = MutableStateFlow(AgentUiState())
        val uiState: StateFlow<AgentUiState> = _uiState.asStateFlow()

        fun sendMessage(userText: String) {
            _uiState.update {
                it.copy(
                    messages = it.messages + userText,
                    currentResponse = "",
                    activeToolStatus = null,
                    isLoading = true,
                    errorMessage = null
                )
            }

            viewModelScope.launch {
                val content = Content.builder().text(userText).build()
                session.runAsync(content)
                    .catch { e ->
                        val message = when {
                            e is GenAiException && e.errorCode == GenAiException.ErrorCode.MAX_TURNS_EXCEEDED ->
                                "Agent exceeded the maximum reasoning turns allowed."
                            e is GenAiException && e.errorCode == GenAiException.ErrorCode.NOT_AVAILABLE ->
                                "On-device AI is currently unavailable."
                            e is GenAiException && e.errorCode == GenAiException.ErrorCode.BUSY ->
                                "On-device AI is busy. Please try again."
                            else -> e.localizedMessage ?: "Agent error"
                        }
                        _uiState.update {
                            it.copy(
                                isLoading = false,
                                activeToolStatus = null,
                                errorMessage = message
                            )
                        }
                    }
                    .collect { event ->
                        when (event) {
                            is AgentEvent.TextMessage -> {
                                _uiState.update {
                                    it.copy(
                                        currentResponse = if (event.isPartial) {
                                            it.currentResponse + event.text
                                        } else {
                                            event.text.ifEmpty { it.currentResponse }
                                        },
                                        activeToolStatus = null
                                    )
                                }
                            }
                            is AgentEvent.FunctionCall -> {
                                _uiState.update {
                                    it.copy(
                                        activeToolStatus =
                                            "Executing ${event.name}..."
                                    )
                                }
                            }
                            is AgentEvent.FunctionResponse -> {
                                _uiState.update {
                                    it.copy(
                                        activeToolStatus =
                                            "Received data from ${event.name}"
                                    )
                                }
                            }
                            else -> {}
                        }
                    }

                // Finalize message once stream completes
                _uiState.update {
                    it.copy(
                        messages = it.messages + it.currentResponse,
                        currentResponse = "",
                        activeToolStatus = null,
                        isLoading = false
                    )
                }
            }
        }

        override fun onCleared() {
            super.onCleared()
            // Release session memory and cancel active coroutines
            session.close()
        }
    }

## Extract text tokens directly

For conversational interfaces where tool execution details don't need to be
displayed separately in the UI, use the `textTokens()` extension function on the
`Flow<AgentEvent>` interface:

    import com.google.mlkit.genai.agent.textTokens
    import com.google.mlkit.genai.prompt.Content

    val content = Content.builder().text(userText).build()
    session.runAsync(content)
        .textTokens()
        .collect { token ->
            appendTokenToChatBubble(token)
        }

## Render in Jetpack Compose

Consume the `AgentUiState` state in a composable function to render streamed text and live
tool activity:

    package com.example.ai.agent.ui

    import androidx.compose.foundation.layout.*
    import androidx.compose.foundation.lazy.LazyColumn
    import androidx.compose.foundation.lazy.items
    import androidx.compose.material3.*
    import androidx.compose.runtime.*
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.unit.dp

    @Composable
    fun AgentChatScreen(
        viewModel: ChatViewModel,
        modifier: Modifier = Modifier
    ) {
        val state by viewModel.uiState.collectAsState()
        var inputText by remember { mutableStateOf("") }

        Column(modifier = modifier.fillMaxSize().padding(16.dp)) {
            LazyColumn(
                modifier = Modifier.weight(1f).fillMaxWidth(),
                verticalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                items(state.messages) { message ->
                    Card(modifier = Modifier.fillMaxWidth()) {
                        Text(text = message, modifier = Modifier.padding(12.dp))
                    }
                }

                if (state.activeToolStatus != null) {
                    item {
                        Text(
                            text = state.activeToolStatus ?: "",
                            style = MaterialTheme.typography.bodySmall,
                            color = MaterialTheme.colorScheme.secondary
                        )
                    }
                }

                if (state.currentResponse.isNotEmpty()) {
                    item {
                        Card(
                            colors = CardDefaults.cardColors(
                                containerColor =
                                    MaterialTheme.colorScheme.surfaceVariant
                            ),
                            modifier = Modifier.fillMaxWidth()
                        ) {
                            Text(
                                text = state.currentResponse,
                                modifier = Modifier.padding(12.dp)
                            )
                        }
                    }
                }
            }

            Row(
                modifier = Modifier.fillMaxWidth().padding(top = 8.dp),
                horizontalArrangement = Arrangement.spacedBy(8.dp)
            ) {
                TextField(
                    value = inputText,
                    onValueChange = { inputText = it },
                    modifier = Modifier.weight(1f),
                    placeholder = { Text("Ask about an order...") }
                )
                Button(
                    onClick = {
                        if (inputText.isNotBlank()) {
                            viewModel.sendMessage(inputText)
                            inputText = ""
                        }
                    },
                    enabled = !state.isLoading
                ) {
                    Text("Send")
                }
            }
        }
    }

## Next steps

- Learn how to define tools in [Define tools](https://developers.google.com/ml-kit/genai/agent/android/define-tools).
- Manage session memory and handle rotations in [Manage sessions and lifecycle](https://developers.google.com/ml-kit/genai/agent/android/manage-sessions).