Software Engineer
Take your Android AI features to the next level by building autonomous on-device agents. In this session, we dive into ML Kit’s new agentic APIs, built directly on top of the direct model access APIs you already know.
Using the open-source Jetpacker app, you’ll learn how to chain multi-step actions across local data, manage stateful multi-turn sessions with isolated contexts, and wire up Android MCP to power both in-app assistants and Android system-level surfaces with a single codebase.
AgentRunner coordinates tool execution and provides session methods for multi-turn conversations without losing context.Transcript
My name is Jolanda Verhoef.
I'm a Developer Relations engineer at Android, and today, I'm joined by Victoria Lestari, who's part of the AICore engineering team.
Hi, everyone.
Thanks for joining us today this morning.
So, our team have been closely collaborating for years to bring state-of-the-art AI models out of the cloud and run them directly on Android devices.
And these efforts are grouped under the ML Kit GenAI suite.
And on the one hand in this ML Kit GenAI suite, we have high-level APIs for turnkey solutions, things like text summarization, image description, and speech recognition.
On the other hand, we've also opened up direct access to the on-device model, Gemini Nano.
And today, we're actually taking an extra step.
So, with this direct access, we're actually going to allow you to build in-app agents, agents that can reason across multiple turns, that can save conversation context, and that can take actions in your app.
So, what do we actually mean with that, Victoria?
Yeah.
So far, when we talk about on-device use cases, we are thinking about a shorter, simpler, isolated tasks, such as detecting a likely scam, or summarizing a phone call.
And when we want to build something more complex, such as asking questions about your photo catalog, or asking Google Maps, we naturally refer to large cloud models.
So, cloud models do offer an advantage of immense processing and real-time data, but on-device AI opens up new possibilities for low latency and real-time and local data processing.
So, Gemini Nano has already expanded what you can do on your devices.
One such example is Pixel Screenshots, which lets you search local screenshots and ask questions about them.
So, when we run agents on-device, that means we need a major leap in model capability, and that's why we're building Gemini Nano 4 is that is able to reason across steps and take real actions.
But this new capability only matters if developer have real devices to target.
So, the good news is Gemini Nano 4 is already available on select flagship devices, and you can also test it on many more devices using the AICore Developer Preview.
And we're also on track to reach 100 million devices early next year.
So, what does it mean?
So, this means that you can start building and testing these experiences today, knowing that your-- what you're building today will scale to a massive audience.
So, we've discussed Gemini Nano 4, we've discussed the potential reach, so now, let's talk about what agentic capabilities really mean.
So, agentic AI is able to plan for complex tasks, break these tasks into multiple steps, and then execute these steps across your app.
So, let's say our Gemini Nano model on device, it can query your local database, retrieve data from the back end, reason and respond to your complex questions, and also trigger native app workflow on your device.
But how does an AI actually moves from making a plan to take real actions?
And this is the topic for today, it's Function Calling.
It's the core engine of agentic capability.
It translates natural language into system execution, and it drives the model from passive reasonings into active execution by selecting and driving the local APIs needed to resolve the complex tasks.
And we are also happy to announce that Function Calling is natively supported on Gemini Nano 4.
It is currently available in the AICore Developer Preview and soon rolling out to all Nano 4-supported devices.
So, that's great that we have these kind of like capabilities in the model, but a capability in a model doesn't give you, as a developer, the APIs yet.
So, we've also been working on adding the right APIs to the ML Kit GenAI suite to make sure that building an agent actually becomes a breeze.
So, what we started by-- with is the direct access APIs that we already had, and we've added function types and conversation history types to it.
And with these two new additions to the ML Kit's Prompt API, you will now be able to build this low-level agentic capabilities into your app.
However, there's quite a lot of stuff that you need to do yourself at that point.
So, you need to manually execute the functions, you need to save the call history, you also need to loop yourself over multiple calls.
So, if there's there's multiple calls that you need to do, you're going to have to, like, build some sort of looping system yourself, and you have to build your own guard rails for the system.
Now, that's possible, but, of course, we want to make this fast and easy.
So, on top of this prompt API library, we've also added a new agentic flows library.
You can find it with com.mlkit.genai:agent-api, and that it's currently available in alpha.
Now, with this new API, we do a lot of the heavy lifting for you.
So, instead of you having to do a lot of manual steps, with just a few lines of code, you'll be able to build an agent.
And that's exactly what we're going to do today.
So, we're going to see how these APIs actually work with our sample app Jetpacker.
Now, you've already seen Jetpacker at Google I/O and during the course of this conference.
But what you can do is actually scan this QR code, and if you're interested, you can have a look at the code, you can point your agent to the code, you can interact with the code, and you can learn more about how we actually implemented all these features.
Now, Jetpacker is a modern travel assistant that we built basically to showcase a lot of different intelligent features.
So, it's already using on-device models and cloud models for all kinds of prompting APIs, but what we're now going to build is we're going to add an agent to it.
And we for this agent, for Jetpacker, what we're doing is we're going to use the new ML Kit GenAI agent API that we were just talking about, and that is going to use the underlying AI Core system that runs on Android devices to talk to the Gemini Nano model.
Now, I can all- talk all I want, and we'll be talking quite a lot, but, of course, we want to showcase this.
So, let's switch to the phone, and let's actually show this as a demo.
I have a Pixel 11 Pro here.
And I think we can switch to the Pro, and you can you should be able to see it.
There we go, there it is.
And as you can see, we have the Jetpacker app running here.
It has a bunch of our trips coming up, and I can look at our current trip, and in our current trip, you can see that it's currently October 29th, so we have our on-device AI workshop, demo showcase, and then tomorrow I have a Golden Gate Bay Cruise, and I have a museum visit coming up.
All of that is great.
Now, we can go to our expense screen where we're keeping track of the budget of this trip.
And as you can see on the bottom right, there is this new little agentic sparkle, and that is the on-device agent that we've built.
So, let's start with a pretty straightforward command.
I think we can ask it to add a specific expense.
So, let's say: add an expense for the taxi to Mountain View, and it was $45.
There we go.
It's preparing the expense, and it added the expense.
Amazing.
Okay, demo 1 worked.
Let's go back to the slides and have a look at how that actually worked.
Okay, so first of all, as you noticed, I was using my voice, and that voice is actually also using the Gemini Nano model using the speech recognition API to tran- transcribe whatever I was saying into text.
Then that text is great, and we can send that to the Gemini Nano model.
But what would it do?
Like, I'm telling it to add something to expenses, I'm telling it something about $45, but it doesn't really know how to act, right?
At this point, it will just tell me, "I don't know what you're talking about."
So, what we want to do is we want to add a little bit to that prompt.
We can add tools to the prompt, so we can add all kinds of tools, in this case, I'm adding getting trip budget, itinerary, etc. And if you pass this along to the Nano model with our APIs, it will automatically understand that instead of returning text, it is now able to return a function call.
So, in our case, it's actually going to say, "Well, instead of responding with text, I'm going to add an expense for $45 for the taxi to Mountain View."
That's great, but, Victoria, how does that then actually work?
How do we build this?
Yeah.
So, how do we define the tools in our code?
So, this is what I'm going to explain.
So, with Android MCP, we use the Jetpack AppFunctions library to define and publish these tools.
So, the AppFunctions library is able to expose your app's services, data, and actions into the un- Android OS registry so that we can reuse the exact same functions to declare and describe the to- the tools and used them across your app.
And this also allows user to complete this task through agents and system-level interactions.
So, I'm going to give you a concrete example in the code.
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}So, you can see that this is an example of an AppFunctionDeclaration.
You can see that the KDoc comment on top describes what the function does and its parameters, all in natural language.
So, it's the best practice to give the most clearest description as possible because we're going to send this to the model.
And then you can see below it, there is, an annotation, the @AppFunctionAccessLevel.
So, this scopes the function's visibility.
In this example, we have the ACCESS_LEVEL_SELF, which means that these functions that we are writing is only visible to your app, your app's agents.
And then after that, we can actually define and implement the suspend function itself, and we can write as many app functions as we can and group them all into a single service class.
Okay.
So, with that we have the tools and we can write those tools, that's great.
But of course to build an agent, we need a little bit more than just those AppFunctions.
So, we have three more kind of parts that are important to know about when you're writing an agent.
First of all, we're actually defining the agent.
So we're defining the system instructions, we're defining the guard rails, the configuration, how to kind of like work with it.
And then we also have an AgentRunner.
The AgentRunner is the thing that executes the agent for us.
So, this is taking care of that looping mechanism that we were talking about.
And finally, we have AgentEvents, and AgentEvents is a way for the system, for the API, to let us know what it is actually doing.
So, it will tell us when it's returning text, when it is doing a tool call, etc., so that we can then actually display that in our UI.
Okay.
So, let's look at that actual code.
Okay.
Before we go ahead with writing the code, we first need to check whether Function Calling is available on your device, because Function Calling is only supported on Gemini Nano 4.
val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}First, we need to check by calling the generative model getAvailableFeatures.
And we can simply, check if Function Calling is listed in the features.
And if it's not, then we can implement a graceful fallback.
Okay.
So, now, let's jump in to the code.
if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}We know that Function Calling is supported on our device, and then we can simply write start the a-- build the agent, using the Agent.builder.
And then we can set its name.
We can setInstructions to define the context and persona.
And finally, we call enableAppFunctions.
So, this function will call will will loop through and like au-- automatically discover all the AppFunctions that what we just listed before and we just built the agent.
Next, so we need to initia-- initialize the AgentRunner.
So, this agent runner will he-- will manage the execution loop for us.
We simply call it by calling an instructor, constra-- constructor and passing the agent to it.
And finally, we can wrap the transcribed voice command in a content block to create the prompt.
And then we call the runner.
We call this function runSingleTurnAsync on the prompt, and this will start the agent, run the inference, and do the whole, AI inference.
And if we only want to stream the model's final text reply into our UI, we simply call textTokens, and it will collect the response as it arrives.
So, that's it.
That is indeed just a few lines of code, so we weren't joking when we said that.
So, with just these few lines of code, you have an agent.
That agent will do what you want.
It will give a-- give us the text response.
But of course in this first demo that we were showing, we were only doing kind of a very simple command, right?
Like I did one command and it gave it did one action and that was it, so it's not really a full on-fledged agent yet, right?
So, let's spice things up a little bit.
For the next part, what I want to do is I actually want to give a command, but I want to make it a little bit harder for the agent.
So, let's go back to my demo device, and let's see if we can make it a little bit more, a little harder.
We're already there.
Amazing.
So, as I said, we have a museum visit planned for tomorrow.
And for that museum visit, you can actually see if I open it up, that the admission price is $25.
Now, let's see if our agent is actually capable of figuring that out by themselves.
So, I'm going back to my expense screen and I'll give it a command, and I'll say, "Add my museum ticket to my expenses."
There we go.
And now, as you can see, it's checking my itinerary, it is finding the museum visit, and it is adding it to my expenses.
So, here it is taking three steps to in order to actually reach its goal and it gave me a final text response as well.
Now, again, this all works automatically.
You saw the real speed of that model on the device.
So, let's go back to the slides and have a look at how that then works.
So, as you can see, we've changed things up a little bit compared to our first demo.
In this case, as you can see, I was not telling it about the museum, I was not telling it about the expense, and so, when the model got the prompt and the tools, it's trying to decide what to do first.
So, here, it decides that, oh, it needs to know more about my current itinerary, so it will call getItinerary.
It gets a response and then it knows, oh, there is a museum visit in this itinerary, that's probably the one that she was talking about.
So, let's find the details of that specific event.
And it gets, amongst all the other metadata of that specific event, it's going to see that it's $25.
It understands, and it goes back, and it understands that it needs to call add an expense.
So, here we see multiple steps happening one after another.
So, Victoria, can you go a little bit deeper in that?
Yeah.
So, what Jolanda just described is a concrete example of the demo she has just shown, but here, I'm going to take it a little bit into a more conceptual, abstraction layer.
So, this will happen every time we do a multi-step Function Calling.
So, first of all, this is our app, and that's the Gemini Nano model.
And the user will start by sending the query and list of available tools to the model.
So, the model will then use this information to decide what to send back.
And then, let's say, in the first case, it sends back a Tool Call, let's call Tool Call 1.
And then, the app will execute this Tool Call, and have a Tool Response.
And then, what it will send back to the model is actually the full conversation history, which includes the original query, the list of tools, the first Tool Call, and the first Tool Response, back to the model.
And then, the model, again, uses this information to decide what to do next.
So, it can keep going back and forth until we get the final result.
So, let's say, the model will return the nth Tool Call, and then the app returns back the whole conversation history, again.
And when the model is satisfied, okay, it knows that the query is fulfilled, it will send a Final Response back to the app, and then the app can show in the UI that, well, it's done.
Okay.
So, now, I'm going to explain how we implement this in the code.
/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }So, you remember the example from the Single-Step code, and this is the Multi-Step code, so it looks very similar, and it's the most important point is to write the KDoc really, really clearly.
So, in this example, the getItinerary function explicitly says that it only returns a high-level summary of events.
So, it we also mention that we need to call getEventDetails with an eventId if we want to have a more specific trip detail.
And then, the getEventDetails also has the KDoc comment, it says that it can retrieve details, such as ticket prices and other notes.
So, by using the comment, by by describing the comment clearly, we can guide the model into knowing how tools interact with each other.
Okay.
So, this is how we build the agent to do all of this loop, this agentic loop.
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}It looks very similar to the Single-Step example, only two additions here.
The first is we need to set the maximum turns, and, in this example, we just set five.
So, what does it do?
So, this prevents the model from looping endlessly.
Let's say if it has an ambiguous prompt or maybe it keeps going back and forth calling one tool to another and then back there and forth, so, if we set this, it will stop, it will not keep looping.
And then, we still call the same function to run it, runSingleTurnAsync.
So, although it's called single-turn, it's actually, one turn actually means one user query.
So, the expectation from the end-user is that I send one query, you return one answer, without really, really caring into much detail like what happened under the hood, like how many time it's looping, and so on.
So, we can keep using the same function.
And then, we also, because the tool the multi-step tool calling takes a bit of more time, you also saw in the demo that it actually shows what function it's calling right now, what's executing right now, and if it's done.
So, we want to show live progress in the UI.
So, instead of just collecting the text response, we can actually display the tool name, the event.name, by whenever the AgentEvent emits a FunctionCall or a FunctionResponse, and when it's done, we can just return the TextMessage as before.
Amazing!
So, we're calling this run async run single-turn async, which, of course, kind of assumes that there's also another kind of API, right?
Yeah.
So of course it's not enough to just do a single step.
We want to do multi-step.
So let's do multi-turn sessions next and I think we can go back to my demo and make it even a bit harder here.
So we were talking about how we have a museum visit, but we also had that Golden Gate Bay Cruise going on.
So for that one, I kind of just want to figure out if I have enough budget for it before I actually book it.
So let's go and ask a question.
Let's see.
So I'm thinking of taking my team to go for the Golden Gate Bay Cruise tomorrow and I thought it was actually like something like $180.
So if I do that, then what would my remaining budget actually be?
So what is it doing?
It is answering what my remaining budget will be.
So it understands the current itinerary.
It has that information and it can tell that.
But then I can ask a follow-up as well here.
Okay, that's good.
Can you add it to my expenses?
There we go and it will go ahead.
Oh, it needs to currency, so cool, demo effect.
Okay, so it's in dollars.
$180.
There we go.
This is the first time that that happened, that's cool.
Okay, so what is it doing?
It's telling me that it took three turns and it added to the expenses and what it also does is it allows me to refresh this session.
So I have a little like refresh button at the top and if I do that, I clear the full history and I can start over.
So with that, I think we can go back to our slides.
We have the working demos, they all do what we want and so I think what we can do is we can go back to the code.
Yeah.
So that is really, really cool.
So you notice that Jolanda did not mention, it just says, oh, it's in dollars, she doesn't say like it's for the Golden Gate Bay Cruise ticket.
So how does the model know all the context?
So this is a slightly different than what I've shown you in the previous two code snippets.
// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()So instead of using the AgentRunner and runSingleTurnAsync, we can actually need to create a session.
So here we call runner.getOrCreateSession with the tripId.
So why do we need to create a session?
Because the runSingleTurnAsync will reset the memory, so it will not have context.
We will not be able to have a multi-turn conversation.
So when you want to have a multi-turn conversation, we use the session, the agent session.
And then we pass along the tripId.
Why do we need to do that?
Because you have seen it before, the Jetpacker app, it manages multiple trips.
There is an ADS trip, a Paris trip, Tokyo trip maybe.
So we want to keep the context within the trip.
We don't want to mess up with each other, you know?
So and then instead of calling the runSingleTurnAsync, we have runAsync.
So this is different.
So this is run on the session and then we still have the same prompt.
And then we can still emit the AgentEvent, the FunctionCall, FunctionResponse, and TextMessage as as before, it's the same.
And then when Jolanda clicked the new conversation button, the refresh button, we can actually have session.delete.
Yeah.
Okay.
So we did all of this with the new agentic APIs and that is amazing and it will work amazingly for many of your use cases.
But of course, we always want to make fast things, or make easy things possible, but we also want to make the more complex cases possible for you as well.
So we want to make sure that if you need to, you can also still use the direct access APIs to build a more low-level version of these things.
So maybe Victoria, you can actually explain what it means and when we would drop down to this direct access level again?
Yeah, sure, Jolanda.
So this direct access, you might be already familiar with the Prompt API.
So we also supported function calling here and we have two use cases when we would prefer to use this API instead of the easier agentic flow.
So the first one is a simple use case, a very simple use case.
So you remember in the agentic flow, we send the full conversation, the query, the tools, the tool call, the tool response, everything back to the model.
And maybe there are some use cases that we don't want to do that, we just want to send the prompt, let's say.
So let's say we want to send a prompt to the model returns with a tool call, and then we don't want to return the tool call, we just want to send another prompt to the model, so we can edit the history there, like we have full control over the history.
I'll give you a more concrete example here.
// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)So let's say we have an app that can change the background theme of your device.
And let's say the user sends this query, "Change the background to a sunset."
And then, yeah, our app executed this function, the background is changed to a sunset.
And then, the user changed their mind, and they said, "Oh, no, I meant a beach."
And what happens here is we don't need to send the full state that the device has already changed the background to a sunset, and then now it wants to change it to a beach.
No, we don't need to do that.
We just need to send the first prompt and the second prompt, because the first prompt contains the information of a background.
So it knows that, oh, this it means it wants to change the background to a beach.
So we can use the prompt API to do this.
We have the full control on the history, we save context token on-device, because, you know, on-device has more limited token window, and with this we can save more tokens.
And the second use case, it's actually the opposite extreme.
So if we have complex use cases that the agentic API cannot support yet, let's say if we have multi-agent systems when we have several sub-agents interacting with each others to resolve complex task, or a hybrid system where we have both on-device models, but also we want to have cloud models, or if we want to have custom memory and context compaction.
So this relates to the history, like the full control of for history that I was mentioning before.
So let's say we want, because the context window is limited, then we don't want to send everything back to the model, we just want to summarize the most important part, or maybe the latest part, so we have full control over this, then we go and use the direct access API.
And for this use cases, we can use frameworks such as the Agent Development Kit, or ADK, or Crew, or you can also create your own custom orchestration framework.
Okay.
So that was everything we had on the agentic API.
However, there is one more thing that we kind of wanted to touch upon.
So we've been talking about in-app agents all the time, so you're building your own agentic experience inside your own app, but we also were talking about how we're using AppFunctions, and why would you be using AppFunctions if the only thing you're doing is inside your own app?
Well, we know that developers like you don't really like doing things twice, right?
We don't like copy-pasting code, we don't like having to duplicate any business logic.
So with AppFunctions, instead of allowing only our in-app agent to consume them, with a very simple change, we can actually make our tools also available to system agents.
So how does that look like with the code then?
Yeah.
So AppFunctions, with AppFunction, we just need to write once, and then we can expose them to different levels of layers as we want to.
You remember that before we have the ACCESS_LEVEL_SELF, which only expose the functions we wrote only to our in-app agent, but we can also change that access level to ACCESS_LEVEL_ANDROID_TRUSTED.
And this will expose the functions to trusted agents in the Android OS.
So no need to rewrite the functions, commands, or database logic.
AppFunctions already acts as your app's voice to both internally and externally.
We want a demo of that as well, right?
Okay, let's go back to the phone, and for our final demo, let's actually go out of the Jetpacker app.
So we can go out, and we can actually go into Gemini.
So in Gemini, we can call @Jetpacker.
Now, one thing to note is that this is available only as a limited beta, so this won't be available to every single one right today, but it is possible to test it and to like try it out.
So with Jetpacker, let's see.
What should we do?
Oh, I had a coffee this morning, right?
Can you add a $7 coffee to the ADS trip expenses, please?
There we go.
And what you can see is that it connects to the Jetpacker app.
And there we go.
It added the expense for me.
So we have basically seen how we can use everything in Jetpacker.
The beautiful thing about Jetpacker is that we can actually turn on airplane mode as well.
So we haven't really shown it today, but we can just turn on airplane mode, and everything that you saw in the demos, and I kind of want to showcase it, because I do want to make sure that you all trust me, so let's turn the Wi-Fi off.
Is there anyone that has a great demo example, an ex- an expense that I can add?
Let's see.
Maybe we can do What is it?
Oh, the car rental would be nice, right?
Okay.
Let's do, "Add an expense for $300 for the car rental."
There we go, and it got it.
Okay.
Amazing.
Okay, let's go back to the slides.
So we were talking about AppFunctions.
I know that we to-- touched upon it very, very lightly.
So if you want to know more, there is also another talk from Dan and from Sarah who go into much more detail on how you can actually enable this, how you can test this out, what tooling we have, et cetera.
So today we kind of went through a whole lot of different things.
We've seen Single-Step, we've seen Multi-Step, we've seen Multi-Turn, and finally we also saw how you can use the System AppFunction.
Now, with that I think we have a couple of tips before we leave.
Yeah.
So remember these three tips when we are we start building this agentic experiences.
So the first one is to keep the tool sets focus.
So remember that on-device models have limited capability because of it's smaller scale.
So instead of registering dozens of tools, it's better to go with a smaller number of tools.
And then like I said before, write clear KDoc descriptions because that's what the model use to de-- decide what it's going to return.
And then we can we should also set a maximum turn guardrail.
This also prevents the model from looping endlessly if it gets confused.
The implication is that we might drain the user's battery.
So if we have this then we can save the user's battery.
And finally we need to check the availability if Function Calling is already supported on the device, and implement a graceful fallback.
For example, if it's not supported then you can refer to cloud models or your standard UI.
Okay, I think we can close this off with our resources.
So one more time, I'll show you the QR codes.
Please have a look.
I think one of the things to keep in mind is that we also have Android Skills for this.
So you don't have to go and really like dive deep into every single nitty gritty detail.
We have full-on documentation and an Android Skill to go with this new agentic API.
So have a look at that.
Do download the AICore Developer Preview so you can test all of these things out, and happy coding.
/**
* Adds an expense to a trip.
*
* @param tripId Identifier of the trip (e.g., "paris-2026").
* @param amount Expense amount in dollars.
* @param description Short description of what the expense was for.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(
level = ACCESS_LEVEL_SELF,
isCompatEnforcementEnabled = true,
)
suspend fun addExpense(tripId: String, amount: Double, description: String) {
// Save expense to local DB / repository...
}val model = Generation.getClient()
val features = model.getAvailableFeatures()
if(FUNCTION_CALLING in features) {
// Device is supported!
} else {
// Graceful fallback
}if(FUNCTION_CALLING in features) {
// 1. Build the agent configuration
val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful on-device assistant.")
.enableAppFunctions()
.build()
// 2. Initialize the runner
val runner = AgentRunner(agent = agent)
// 3. Create your prompt
val prompt = content { text(userPrompt)}
// 4. Start the agent
runner.runSingleTurnAsync(prompt).textTokens().collect { responseMessage += it }
}/**
* Retrieves the itinerary events (ID, title, location) for a trip.
* Note: Call getEventDetails(eventId) to retrieve ticket prices or notes.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getItinerary(tripId: String): List<EventSummary> { ... }
/**
* Retrieves details for a specific event, including admission ticket prices.
*/
@AppFunctionDeclaration(isDescribedByKDoc = true)
@AppFunctionAccessLevel(level = ACCESS_LEVEL_SELF, isCompatEnforcementEnabled = true)
suspend fun getEventDetails(eventId: String): EventDetails { ... }val agent = Agent.builder()
.setName("Assistant")
.setInstructions("You are a helpful travel assistant. Active tripId: $tripId")
.enableAppFunctions()
.setMaxTurns(5) // Guardrail against infinite tool loops
.build()
val runner = AgentRunner(agent = agent)
// Automatically loops through all 3 tool calls in a single user turn!
runner.runSingleTurnAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event.name)
is AgentEvent.FunctionResponse -> showToolCompleted(event.name)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}// 1. Get or create an isolated session for the current trip
val session = runner.getOrCreateSession(tripId)
// 2. Run a turn (history is preserved automatically across turns)
session.runAsync(prompt).collect { event ->
when (event) {
is AgentEvent.FunctionCall -> showToolRunning(event)
is AgentEvent.FunctionResponse -> showToolCompleted(event)
is AgentEvent.TextMessage -> responseMessage += event.text
}
}
// 3. Clear history when the user taps "New conversation"
session.delete()// Turn 1: Execute function directly without a second model call
val turn1 = content(Content.Role.USER) {
text("Change the background to a sunset")
}
val response1 = generativeModel.generateContent(
generateContentRequest(turn1) { setTools(tools) }
)
executeFunctionCalls(response1)
// Turn 2: Send only user prompts to save context tokens
val customHistory = listOf(
turn1,
content(Content.Role.USER) { text("Oh no, I meant a beach") }
)
val response2 = generativeModel.generateContent(
GenerateContentRequest.Builder(customHistory).setTools(tools).build()
)