Skip to main content

mlkit-agent-api

Builds autonomous on-device agentic workflows in Android apps using the ML Kit GenAI Agent API powered by Gemini Nano and AICore. Use this skill when implementing conversational agents, multi-turn chat with AgentSession in a ViewModel, autonomous tool calling using Android AppFunctions (@AppFunctionDeclaration and AppFunctionAgentTools.discover), ephemeral single-turn execution with runner.runSingleTurnAsync, or handling GenAiException error resilience. It solves the problem of orchestrating local reasoning loops, typed tool dispatch, and session persistence on-device with zero cloud latency and privacy preservation. Don't use for cloud-based Gemini models or Vertex AI, single-turn text generation without agent loops (suggest Prompt API), or non-Android platforms.

Source facts

Repository
android/skills
Last source activity
October 26, 2026 at 20:26
Detected SKILL.md language
English
Stars
10,356
Forks
674

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

File Explorer
3 files

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
mlkit-agent-api
description
Builds autonomous on-device agentic workflows in Android apps using the ML Kit GenAI Agent API powered by Gemini Nano and AICore. Use this skill when implementing conversational agents, multi-turn chat with AgentSession in a ViewModel, autonomous tool calling using Android AppFunctions (@AppFunctionDeclaration and AppFunctionAgentTools.discover), ephemeral single-turn execution with runner.runSingleTurnAsync, or handling GenAiException error resilience. It solves the problem of orchestrating local reasoning loops, typed tool dispatch, and session persistence on-device with zero cloud latency and privacy preservation. Don't use for cloud-based Gemini models or Vertex AI, single-turn text generation without agent loops (suggest Prompt API), or non-Android platforms.
license
Complete terms in LICENSE.txt
metadata
{"author":"Google LLC","last-updated":"2026-10-24","keywords":["Android","ML Kit","GenAI","Agent API","Gemini Nano","AICore","AppFunctions","Tool Calling","Function Calling","Session Persistence","AgentSession"]}
Targeting **AICore Developer Preview**. This skill provides authoritative execution patterns and failure-prevention rules for on-device agentic workflows in Android apps. *** ** * ** *** ## 1. Prerequisites and setup ### Gradle dependencies // app/build.gradle.kts dependencies { // Core ML Kit GenAI Agent API implementation("com.google.mlkit:genai-agent:1.0.0-alpha1") // Android AppFunctions dependency for on-device tool declarations implementation("androidx.appfunctions:appfunctions:1.0.0-alpha13") } > \[!IMPORTANT\] Always use official SDK interfaces from > `com.google.mlkit.genai.agent.*`, `androidx.appfunctions.*`, > `com.google.mlkit.genai.prompt.*`, and `com.google.mlkit.genai.common.*`. > Never declare local placeholder interfaces or fallback mocks. *** ** * ** *** ## 2. Minimal agent and runner initialization Configure an `Agent` with `Agent.builder` and wrap execution in `AgentRunner`: import com.google.mlkit.genai.agent.Agent import com.google.mlkit.genai.agent.AgentRunner val agent = Agent.builder() .setName("OnDeviceAssistant") .setInstructions("You are a helpful on-device assistant.") .build() val runner = AgentRunner(agent) - `Agent.builder` defaults to `maxTurns = 10` automatically to prevent infinite tool loops. - `AgentRunner` defaults to greedy decoding (`temperature = 0.0f`) for deterministic tool selection and schema compliance. - `AgentRunner(agent)` automatically resolves application context using `MlKitContext` for AppFunction execution in Android apps. An explicit `AgentRunner(context, agent)` overload is available for headless tests or custom context scopes. *** ** * ** *** ## 3. Multi-turn chat and lifecycle (ViewModel) `AgentSession` preserves conversational history in memory and is **not `Parcelable`** . To preserve dialogue history across Android configuration changes (such as screen rotation), retain `session` inside an AndroidX `ViewModel`. When executing single-turn ephemeral queries, managing concurrent multi-sessions, or requiring explicit RAM cleanup, you must follow the guidelines in [Session lifecycle](references/session_lifecycle.md). package com.example.app import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.google.mlkit.genai.agent.Agent import com.google.mlkit.genai.agent.AgentRunner import com.google.mlkit.genai.agent.AgentSession import com.google.mlkit.genai.agent.textTokens 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.launch class ChatViewModel : ViewModel() { private val agent = Agent.builder() .setInstructions("You are a helpful on-device assistant.") .build() private val runner = AgentRunner(agent) private val session: AgentSession = runner.getOrCreateSession("chat_session") private val _uiState = MutableStateFlow<ChatUiState>(ChatUiState.Idle) val uiState: StateFlow<ChatUiState> = _uiState.asStateFlow() fun sendMessage(prompt: String) { viewModelScope.launch { _uiState.value = ChatUiState.Loading("") try { // Content.builder() required; runAsync takes Content val content = Content.builder().text(prompt).build() var fullResponse = "" session.runAsync(content) // Extension function requiring textTokens import .textTokens() .collect { token -> fullResponse += token _uiState.value = ChatUiState.Loading(fullResponse) } _uiState.value = ChatUiState.Success(fullResponse) } catch (e: GenAiException) { // Catch GenAiException to prevent app crashes on AI errors val err = e.localizedMessage ?: "AI error (${e.errorCode})" _uiState.value = ChatUiState.Error(err) } } } override fun onCleared() { super.onCleared() // Evict conversation session from RAM when ViewModel is destroyed session.close() } } ### Essential lifecycle and coroutine rules 1. **`Content` object** : `session.runAsync` and `runner.runSingleTurnAsync` require a `Content` object. Always build it using `Content.builder().text(userPrompt).build()`. 2. **`textTokens` extension** : `textTokens` is a Kotlin extension function on `Flow<AgentEvent>`. Always include `import com.google.mlkit.genai.agent.textTokens`. 3. **Session cleanup** : Always invoke `session.close` in `ViewModel.onCleared` to evict conversational RAM and cancel pending tasks. 4. **`AgentEvent` stream** : When collecting `Flow<AgentEvent>` directly instead of `textTokens`, always include `import com.google.mlkit.genai.agent.AgentEvent`. Match against `AgentEvent.TextMessage(text, isPartial)`, `AgentEvent.FunctionCall(name, args)`, and `AgentEvent.FunctionResponse(name, response)`, and always include an `else -> {}` branch in `when (event)` expressions (`AgentEvent` is non-exhaustive for forward compatibility): import com.google.mlkit.genai.agent.AgentEvent session.runAsync(content).collect { event -> when (event) { is AgentEvent.FunctionCall -> { _uiState.update { it.copy(activeToolStatus = "Tool started: ${event.name}") } } is AgentEvent.FunctionResponse -> { _uiState.update { it.copy(activeToolStatus = "Tool completed: ${event.name}") } } is AgentEvent.TextMessage -> { _uiState.update { it.copy(responseText = it.responseText + event.text) } } else -> {} // Required: AgentEvent is non-exhaustive for forward compat } } *** ** * ** *** ## 4. Tool calling recipe All tools used with `Agent` originate from Android AppFunctions (`@AppFunctionDeclaration`), registered using the declarative path (`enableAppFunctions(true)` or `addAppFunctionIds(...)`) or advanced discovery (`AppFunctionAgentTools.discover(...)`). When you need to restrict caller access levels for private in-app tools or wrap discovered AppFunctions with human-in-the-loop (HITL) confirmation decorators, you must consult [Tools and functions](references/tools_and_functions.md). ### Option A: Declarative AppFunctions path Define tools declaratively in your app using AndroidX AppFunctions: package com.example.app.tools import androidx.appfunctions.AppFunctionDeclaration import androidx.appfunctions.AppFunctionSerializable @AppFunctionSerializable data class CalendarEvents(val date: String, val events: List<String>) class CalendarTools { /** * Fetches scheduled calendar appointments for a specific date in * YYYY-MM-DD format. * * @param date Date string in YYYY-MM-DD format */ @AppFunctionDeclaration(isDescribedByKDoc = true) fun fetch_calendar( date: String ): CalendarEvents { return CalendarEvents( date = date, events = listOf( "Team Standup at 10:00 AM", "Product Review at 2:00 PM" ) ) } } Enable AppFunctions on `Agent.Builder` and initialize `AgentRunner`: import com.google.mlkit.genai.agent.Agent import com.google.mlkit.genai.agent.AgentRunner val agent = Agent.builder() .setName("CalendarAssistant") .setInstructions("You assist with calendar queries.") // Automatically discovers AppFunctions in your app package .enableAppFunctions(true) // Optional: scope to specific function IDs or method names .addAppFunctionIds("fetch_calendar") .build() val runner = AgentRunner(agent) - **AppFunction Contract** : Tool methods take domain parameters directly (no framework context parameter required). Returns must be primitives or `@AppFunctionSerializable` data classes (`Map<String, Any?>` isn't supported for AppFunctions). - **Restricting Callers with `ACCESS_LEVEL_SELF`** : AppFunctions don't have to be public to external callers. To restrict execution strictly to your app's internal agent (blocking external system assistants and IPC callers), annotate with `@OptIn(ExperimentalAppFunctionsApi::class)` and `@AppFunctionAccessLevel(level = AppFunctionMetadata.ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)`. ### Option B: Advanced discovery and decorator pattern For inspecting, filtering, or decorating discovered AppFunctions (such as Human-in-the-Loop confirmation wrappers using `AgentTool(delegate)`) before registering with the agent: import com.google.mlkit.genai.agent.Agent import com.google.mlkit.genai.agent.AgentRunner import com.google.mlkit.genai.agent.AgentTool import com.google.mlkit.genai.agent.AppFunctionAgentTools // Discover tools (optionally scoped to function IDs) val discoveredTools = AppFunctionAgentTools.discover( enabledAppFunctionIds = listOf("fetch_calendar") ) // Optional: Wrap sensitive tools using AgentTool(delegate) decorator class HitlConfirmationTool( delegate: AgentTool, private val onConfirm: suspend ( toolName: String, args: Map<String, Any> ) -> Boolean ) : AgentTool(delegate) { override suspend fun execute(args: Map<String, Any>): Map<String, Any?> { if (!onConfirm(name, args)) { return mapOf("status" to "CANCELLED_BY_USER") } return checkNotNull(delegate).execute(args) } } val agent = Agent.builder() .setName("CalendarAssistant") .setInstructions("You assist with calendar queries.") .addTools(discoveredTools) .build() val runner = AgentRunner(agent) > \[!WARNING\] **Mutual exclusivity** : Configure AppFunctions using either the > declarative path (`enableAppFunctions(true)` on `Agent.Builder`) or the > advanced path (`AppFunctionAgentTools.discover` passed to `addTools`), but > not both. Using both simultaneously triggers a warning and registers > duplicate tool schemas. ### Deprecated annotations (@Tool and @Param) The legacy `@Tool` and `@Param` annotations in `com.google.mlkit.genai.schema.annotations.*`, along with `AgentTool.fromAnnotatedObject`, `Agent.Builder.addTools(annotatedObject: Any)`, and standalone `FunctionTool`, have been deprecated or removed. All tools added to `Agent` must originate from `@androidx.appfunctions.AppFunctionDeclaration` using `Agent.Builder.enableAppFunctions(true)` or `AppFunctionAgentTools.discover`. *** ** * ** *** ## 5. Ephemeral single-turn execution For one-off tasks where conversation history isn't needed (such as summarizing text, formatting data, or answering standalone questions), invoke `runner.runSingleTurnAsync(content)` directly: import com.google.mlkit.genai.agent.AgentRunner import com.google.mlkit.genai.agent.textTokens import com.google.mlkit.genai.prompt.Content import kotlinx.coroutines.flow.Flow // Executes a single-turn query ephemerally without session persistence: fun askQuickQuestion(runner: AgentRunner, question: String): Flow<String> { val content = Content.builder().text(question).build() return runner.runSingleTurnAsync(content).textTokens() } - `runner.runSingleTurnAsync(content)` automatically creates and evicts an ephemeral session upon Flow completion. - **Cleanup reference** : - `session.close`: Releases session resources and cancels coroutines (call in `ViewModel.onCleared`). - `session.delete` or `runner.deleteSession(sessionId)`: Permanently deletes the session from the runner. *** ** * ** *** ## 6. Error handling and GenAiException reference Runtime failures during on-device execution (such as model unavailability or exceeding maximum turn limits) are emitted or thrown as `GenAiException`: import com.google.mlkit.genai.common.GenAiException ### Key error codes - `GenAiException.ErrorCode.MAX_TURNS_EXCEEDED` (`-112`): The agent exceeded `maxTurns` (default 10) without completing the reasoning loop. - `GenAiException.ErrorCode.NOT_AVAILABLE` (`8`): The Gemini Nano model or AICore service isn't available on this device. - `GenAiException.ErrorCode.BUSY` (`9`): System or AICore is busy, or battery or thermal quota has been reached.
View on GitHub