K
Kashif Mehmood
Guest

The future of mobile computing isnโt just about more processing powerโโโitโs about intelligence that lives right in your pocket.
Agentic AI is the new buzz in town. From automations and coding to research, you can see agents everywhere. There are new agentic platforms emerging day by day, and apps need to adapt. Gone are the days where everything was backend-driven, now things are changing. Apps are using AI in ways never imagined before.
You can now use on-device models in Android and iOS, and then thereโs Ollama and others for desktop to help you use AI for a better user experience. Todoist is using on device models for better suggestion of projects and also smart schedule .
While specific implementations vary, productivity apps are increasingly integrating AI-driven features to enhance user workflows. With that note, letโs get started.
Before getting started we need to understand what is an AI Agent and how it differs from the apps we are already used to.
AI agents are basically like A mom when sheโs planning a birthday party.
You tell your mom โI want an awesome 16th birthday partyโ and she becomes like an AI agent:
She has access to different โtoolsโ:
- Her phone (to call venues, caterers, friends)
- Her car (to drive places and pick things up)
- Her computer (to research and book online)
- Her network of friends (to get recommendations and help)
- Her credit card (to make purchases)
She works like an AI agent:
- Takes your goal: โawesome birthday partyโ
- Breaks it down into tasks: venue, food, guests, decorations, music
Uses her โtoolsโ strategically: calls the community center, drives to the bakery, texts other parents
- Keeps track of whatโs done and whatโs missing
- Adapts when problems come up (rain, cancelled vendor, etc.)
- Keeps going until the goal is achieved
The key: Your mom doesnโt just give you advice about parties, she actually takes actions using whatever tools she has available to make your party happen.
Thatโs exactly what AI agents do, they donโt just think or chat, they actually use tools (web search, databases, booking systems, etc.) to accomplish real tasks in the world.
Your mom = AI agent
Her phone/car/contacts = the digital tools an AI agent uses
Now that you have a gist of what an AI agent is and how it works letโs learn some Glossary before moving forward
- Agent: an AI entity that can interact with tools, handle complex workflows, and communicate with users.
- LLM (Large Language Model): the underlying AI model that powers agent capabilities.
- Message: a unit of communication in the agent system that represents data passed from a user, assistant, or system.
- Prompt: the conversation history provided to an LLM that consists of messages from a user, assistant, and system.
- System prompt: instructions provided to an agent to guide its behavior, define its role, and supply key information necessary for its tasks.
- Context: the environment in which LLM interactions occur, with access to the conversation history and tools.
- LLM session: a structured way to interact with LLMs that includes the conversation history, available tools, and methods to make requests.
All of these are already in the Koog Documentation where you can learn all these in details.
But where does Koog fit in? Koog is a AI Agentic framework built with Idiomatic Kotlin which means you can use it any where you want, Android, Spring, Quarkus and even on Desktop Apps. It comes with many features such building MCPs, Single Run Agents, Orchestrated Agents, Agents Memory and many more.
For this series we will be using Ollama with llama3.1:8b because Ollama is an open-source platform that simplifies running large language models (LLMs) locally on your computer. It allows users to download, manage, and interact with various LLMs without needing an internet connection or cloud infrastructure. This makes it ideal for developers, researchers, and businesses prioritizing data privacy, security, and offline access without having to worry about costs.So, letโs get Started
First we need to download Ollama, The setup is pretty simple you just have to install and run Ollama, Once thats done we need go to terminal and run this command
ollama list
It will list all the models that Ollama currently has but since we are starting fresh there will be no models listed. You can find list of models that Ollama supports here, but the one we will be using is listed here. Now to get our model we need to run this command

ollama run llama3.1:8b
It will try to run the model but since itโs not already installed it will pull the model and run, It will take some time as its size is 4GB+. If your machine you can also use GPT-OSS which is a powerful and first open source model by Open AI.
Once that done we need to create a new Kotlin Application and add the Koog dependency, I woul ask you to also add Coroutines and Kotlinx Serialization to the project.
implementation("ai.koog:koog-agents:0.3.0")
Along with Agent creation and MCPs, Koog also lets you chat with your LLM Model, So start by asking a simple question and build upon that
First we need to create something called an executor, There are multiple LLM providers and we have an executor built in Koog for most of them, All of these create an object of SingleLLMPromptExecutor:A wrapper class that takes an LLM client with required Configurations and uses it to execute prompts, returning either full responses or streaming chunks.

We will be using simpleOllamaAiExecutor but if you have API keys for other providers feel free to use those as well,
val client = simpleOllamaAIExecutor()
Next we need to create an LLM model, an LLM Model is just a data class with some properties nothing to fear about
/**
* Represents a Large Language Model (LLM) with a specific provider, identifier, and a set of capabilities.
*
* @property provider The provider of the LLM, such as Google, OpenAI, or Meta.
* @property id A unique identifier for the LLM instance. This typically represents the specific model version or name.
* @property capabilities A list of capabilities supported by the LLM, such as temperature adjustment, tools usage, or schema-based tasks.
*/
@Serializable
public data class LLModel(val provider: LLMProvider, val id: String, val capabilities: List<LLMCapability>)
So using the KDoc we know how to create the object
val llm = LLModel(
provider = LLMProvider.Ollama,
id = "llama3.1:8b",
capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Schema.JSON.Simple,
LLMCapability.Tools
),
)
Now our llm and client is ready we can use them to ask a simple question/prompt to our llm
client.executeStreaming(
ai.koog.prompt.dsl.Prompt(
messages = listOf(Message.User("What is the capital of France?", RequestMetaInfo.create(Clock.System))),
id = UUID.randomUUID().toString()
), llm
).collect {
println(it)
}
Your complete function should look like this
suspend fun simpleStreaming() {
val client = simpleOllamaAIExecutor()
val llm = LLModel(
provider = LLMProvider.Ollama,
id = "llama3.1:8b",
capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Schema.JSON.Simple,
LLMCapability.Tools
),
)
client.executeStreaming(
ai.koog.prompt.dsl.Prompt(
messages = listOf(Message.User("What is the capital of France?", RequestMetaInfo.create(Clock.System))),
id = UUID.randomUUID().toString()
), llm
).collect {
println(it)
}
}
Pure idiomatic Kotlin, no fuss just simplicity, one might thing here why messages with list of messages with a Mesage.User object. Itโs because there are multiple types of messages you can send to an LLM to help him know how we want it to act in koog we have these roles mainly
@Serializable
public enum class Role {
/**
* Role indicating a system message.
*/
System,
/**
* Role for messages generated by the user.
*/
User,
/**
* Role for messages generated by an assistant (e.g., an AI assistant).
*/
Assistant,
/**
* Role for messages related to tools (e.g., tool usage or tool results).
*/
Tool
}
You can check them in detail inside this package
package ai.koog.prompt.message
package ai.koog.prompt.message
Now the agent responds in stream and produces this output

Now that our baby Agent has started talking letโs start by planing the birthday party just like we did in the Analogy we used earlier to explain AI Agents. We discussed, the Mom needs tools and is an agent herslef, so letโs now create an agent and provide it with some tools. There are two types of Agenst Single-run and Complex Workflow Agents , as the name suggest single run agents can run one time and use the tools it has to complete one task but with Complex workflow agents you can use Strategies to guide them they can fallback check mistakes, get orchestrated go back in loops/graphs and go to a completely different path from the last one they took but more on that later.
Hor creating an agent the first two steps would be the same, I created an OllamaConfig class for simplicity to choose from environment or default
data class OllamaConfig(
val baseUrl: String = "http://localhost:11434",
val model: String = "llama3.1:8b",
val temperature: Double = 0.7
) {
companion object {
fun default() = OllamaConfig()
fun fromEnv() = OllamaConfig(
baseUrl = System.getenv("OLLAMA_URL") ?: "http://localhost:11434",
model = System.getenv("OLLAMA_MODEL") ?: "llama3.2:3b",
temperature = System.getenv("OLLAMA_TEMPERATURE")?.toDoubleOrNull() ?: 0.7
)
}
}
fun main(){
val ollamaConfig = OllamaConfig.default()
val executor = simpleOllamaAIExecutor(
baseUrl = ollamaConfig.baseUrl,
)
val llm = LLModel(
provider = LLMProvider.Ollama,
id = "llama3.1:8b",
capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Tools,
LLMCapability.Schema.JSON.Simple
),
)
}
Now we have to create and Agent configuration to adapt agent to our needs
val agentConfig = AIAgentConfig(
prompt = prompt(id = "mom-agent", params = LLMParams(temperature = ollamaConfig.temperature)) {
system(content = "You are a sweet mom planning a birthday party.")
},
model = llm,
maxAgentIterations = 10
)
Here you see we are using system because this is a system prompt for our Agent it tells the agent, what its personality should be like the Agent will consider this and act accordingly. Now that the config is there we need to create the Agent which will be acting as a Mom and arrange the birthday party but first we need to provide the mom with tools which can be used for arranging the party. AI agents use tools like web search, code execution, file manipulation, API calls, databases, calculators, email/messaging, calendar access, and system commands to interact with external systems and perform actions beyond text generation.
Koog comes with some built in tools, which can be provided in the ToolRegistry these are AskUser , SayToUser and ExitTool but it pretty simple to create your own Tools.

But letโs provide it with the inbuilt tools for now
val toolRegistry = ToolRegistry {
tool(SayToUser)
}
Now finally, letโs create our agent and ask it a question
val mom = AIAgent(
promptExecutor = executor,
strategy = singleRunStrategy(),// this means oue agent will run only once and perfom its task
agentConfig = agentConfig,
toolRegistry = toolRegistry
)
println("Enter your question:")
val input = readLine() ?: "Hello! whats the date today? should I bring a cake?"
val result = mom.run(input)
println("Response: $result")
This is the output produced,

Notice that the LLM does not know about todayโs date but since we provided the system prompt that we are planning a birthday party it says we can bring a cake, now we need to create a tool to get the date and time
object GetTodaysDate : SimpleTool<GetTodaysDate.Args>() {
@Serializable
object Args : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "get_todays_date",
description = "Gets today's current date and time",
requiredParameters = emptyList()
)
override suspend fun doExecute(args: Args): String {
val now = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault())
val date = now.date
val time = now.time
return "Today is ${date.dayOfWeek}, ${date.month} ${date.dayOfMonth}, ${date.year} at ${time.hour}:${time.minute.toString().padStart(2, '0')}"
}
}
Letโs break it down, we inherit from `SimpleTool` class I shared earlier and we provide the Args which is serializable and uses kotlinx Serialization then we add a descriptor so the Mom(Agent) knows which kind of tool this is and what can it do, it has no parameters since we dont need it for getting date, and inside doExecute we get the time using kotlinx Datetime and send it back to LLM, Now letโs re-run our agent after adding this tool to our ToolRegistery.
val toolRegistry = ToolRegistry {
tool(SayToUser)
tool(GetTodaysDate)
}
Our Agent now has access to date and time

Let us now create a more complex tool and verify it
object FamilyCalendar : SimpleTool<FamilyCalendar.Args>() {
@Serializable
data class Args(val name: String) : ToolArgs
private val birthdays = mapOf(
"Alice" to "1990-06-15",
"Bob" to "1985-12-01",
"Charlie" to "2000-04-20"
)
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "family_calendar",
description = "Checks birthday of a family member, It takes a name as input and returns if today is their birthday",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "name", description = "name of the family member to check if today is their birthday", type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String {
val today = Clock.System.now().toLocalDateTime(TimeZone.currentSystemDefault()).date
val birthdayStr = birthdays[args.name]
if (birthdayStr != null) {
val parts = birthdayStr.split("-")
if (parts.size == 3) {
val month = parts[1].toIntOrNull()
val day = parts[2].toIntOrNull()
if (month == today.monthNumber && day == today.dayOfMonth) {
return "Today is ${args.name}'s birthday!"
}
}
return "Today is not ${args.name}'s birthday."
}
return "I don't have birthday information for ${args.name}."
}
}
In this we have added Args as a dataclass to that has a name and inside doExecute llm provides us the name as a function parameter where we check if todays is a birthday of any of the family members or not, We also added
requiredParameters = listOf(
ToolParameterDescriptor(
name = "name", description = "name of the family member to check if today is their birthday", type = ToolParameterType.String
)
)
Represents a descriptor for a tool parameter. A tool parameter descriptor contains information about a specific tool parameter, such as its name, description, data type, and default value.
Note that parameters are deserialized using CamelCase to snake_case conversion, so use snake_case names.
Letโs test it

And itโs working fine, we have it working fine now, One improvement we can make is that we can make the tool calls parallel
val mom = AIAgent(
promptExecutor = executor,
strategy = singleRunStrategy(ToolCalls.PARALLEL),// tool calls in parallel
agentConfig = agentConfig,
toolRegistry = toolRegistry
)
If you looked closely in the output you shouldโve noticed that it checked only for Kashif not for Alice we can improve this by providing a better system prompt.
fun main() = runBlocking {
val ollamaConfig = OllamaConfig.default()
val executor = simpleOllamaAIExecutor(
baseUrl = ollamaConfig.baseUrl,
)
val llm = LLModel(
provider = LLMProvider.Ollama,
id = ollamaConfig.model,
capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Tools,
LLMCapability.Schema.JSON.Simple
),
)
val agentConfig = AIAgentConfig(
prompt = prompt(id = "mom-agent", params = LLMParams(temperature = ollamaConfig.temperature)) {
system(content = """You are a sweet mom planning birthday parties.
IMPORTANT: When someone asks about multiple people's birthdays, you MUST check each person individually by calling the family_calendar tool once for each person mentioned.
Available tools:
- get_todays_date: Gets today's current date
- family_calendar: Checks if today is a specific person's birthday (call once per person)
For example, if asked "Is it Kashif's or Alice's birthday?", you should:
1. Call family_calendar with name="Kashif"
2. Call family_calendar with name="Alice"
3. Then tell me the results for both people""")
},
model = llm,
maxAgentIterations = 10
)
val toolRegistry = ToolRegistry {
tool(AskUser)
tool(SayToUser)
tool(GetTodaysDate)
tool(FamilyCalendar)
}
val mom = AIAgent(
promptExecutor = executor,
strategy = singleRunStrategy(ToolCalls.PARALLEL),
agentConfig = agentConfig,
toolRegistry = toolRegistry
)
println("Enter your question:")
val input = readLine() ?: "Hello! whats the date today? should I bring a cake?"
val result = mom.run(input)
println("Response: $result")
}
And now it works fine,

And bang, itโs working fine now. Now letโs build rest of the tools
// Mom's "Phone" - to call venues, caterers, friends
object PhoneTool : SimpleTool<PhoneTool.Args>() {
@Serializable
data class Args(
val contactType: String, // "venue", "caterer", "friend", "entertainment"
val purpose: String // what you're calling about
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "make_phone_call",
description = "Mom's phone tool to call venues, caterers, friends, or entertainment services for party planning",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "contactType",
description = "Type of contact: 'venue', 'caterer', 'friend', 'entertainment', 'decoration_store'",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "purpose",
description = "What you're calling about (e.g., 'book birthday party venue', 'order birthday cake')",
type = ToolParameterType.String
)
)
)
private val contacts = mapOf(
"venue" to listOf("Community Center", "Pizza Palace", "Local Park Pavilion", "Chuck E. Cheese"),
"caterer" to listOf("Sweet Dreams Bakery", "Party Pizza Co.", "Hometown Deli"),
"entertainment" to listOf("DJ Mike", "Bounce House Rentals", "Face Painting by Sarah"),
"decoration_store" to listOf("Party City", "Dollar Store", "Walmart"),
"friend" to listOf("Sarah's Mom", "Jessica's Mom", "Tommy's Dad")
)
override suspend fun doExecute(args: Args): String {
val availableContacts = contacts[args.contactType] ?: return "I don't have contacts for ${args.contactType}"
println("

return when (args.contactType) {
"venue" -> {
val venue = availableContacts.random()
"

}
"caterer" -> {
val caterer = availableContacts.random()
"

}
"entertainment" -> {
val entertainer = availableContacts.random()
"

}
"decoration_store" -> {
val store = availableContacts.random()
"

}
"friend" -> {
val friend = availableContacts.random()
"

}
else -> "Called about ${args.purpose} - got some good information!"
}
}
}
Momโs โCarโโโโto drive places and pick things up
object CarTool : SimpleTool<CarTool.Args>() {
@Serializable
data class Args(
val destination: String,
val purpose: String
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "drive_to_location",
description = "Mom's car tool to drive to stores, venues, or other locations to pick things up or handle tasks",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "destination",
description = "Where to drive (e.g., 'bakery', 'party store', 'venue', 'grocery store')",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "purpose",
description = "What to do there (e.g., 'pick up cake', 'buy decorations', 'check out venue')",
type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String {
println("

return when (args.destination.lowercase()) {
"bakery" -> "

"party store", "party city" -> "

"grocery store" -> "

"venue" -> "

"dollar store" -> "

else -> "

}
}
}
Momโs โComputerโโโโto research and book online
object ComputerTool : SimpleTool<ComputerTool.Args>() {
@Serializable
data class Args(
val searchTopic: String,
val task: String // "research", "book", "compare_prices", "read_reviews"
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "use_computer",
description = "Mom's computer tool to research party ideas, compare prices, read reviews, or book services online",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "searchTopic",
description = "What to research (e.g., 'birthday party themes', 'party venues near me', 'DIY decorations')",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "task",
description = "What to do: 'research', 'book', 'compare_prices', 'read_reviews'",
type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String {
println("

return when (args.task) {
"research" -> {
"

}
"compare_prices" -> {
"

}
"read_reviews" -> {
"

}
"book" -> {
"

}
else -> "

}
}
}
Momโs โNetwork of Friendsโโโโto get recommendations and help
object FriendsNetworkTool : SimpleTool<FriendsNetworkTool.Args>() {
@Serializable
data class Args(
val requestType: String, // "recommendation", "help", "advice", "invitation"
val topic: String
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "contact_mom_friends",
description = "Mom's network tool to get recommendations, ask for help, or send invitations through her mom friends",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "requestType",
description = "Type of request: 'recommendation', 'help', 'advice', 'invitation'",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "topic",
description = "What you need (e.g., 'birthday venue recommendations', 'help with setup', 'party planning advice')",
type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String {
println("

return when (args.requestType) {
"recommendation" -> {
"

}
"help" -> {
"

}
"advice" -> {
"

}
"invitation" -> {
"

}
else -> "

}
}
}
Momโs โCredit Cardโโโโto make purchases
object CreditCardTool : SimpleTool<CreditCardTool.Args>() {
@Serializable
data class Args(
val item: String,
val amount: Double,
val store: String
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "make_purchase",
description = "Mom's credit card tool to purchase party supplies, food, decorations, or services",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "item",
description = "What to buy (e.g., 'birthday cake', 'decorations', 'venue booking', 'entertainment')",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "amount",
description = "Cost in dollars",
type = ToolParameterType.Float
),
ToolParameterDescriptor(
name = "store",
description = "Where purchasing from",
type = ToolParameterType.String
)
)
)
private var totalSpent = 0.0
override suspend fun doExecute(args: Args): String {
totalSpent += args.amount
println("

return "

"Transaction approved! Total party budget spent so far: $${String.format("%.2f", totalSpent)}"
}
}
Momโs โPlanning Brainโโโโto track and organize everything
object PlanningTool : SimpleTool<PlanningTool.Args>() {
@Serializable
data class Args(
val task: String, // "create_checklist", "check_progress", "adapt_plan", "set_timeline"
val details: String
) : ToolArgs
override val argsSerializer: KSerializer<Args> = Args.serializer()
override val descriptor: ToolDescriptor = ToolDescriptor(
name = "party_planning_organizer",
description = "Mom's organizational tool to create checklists, track progress, adapt plans, and manage timeline",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "task",
description = "Planning task: 'create_checklist', 'check_progress', 'adapt_plan', 'set_timeline'",
type = ToolParameterType.String
),
ToolParameterDescriptor(
name = "details",
description = "Specific details about the planning task",
type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String {
println("

return when (args.task) {
"create_checklist" -> {
"""








}
"check_progress" -> {
"

}
"adapt_plan" -> {
"

}
"set_timeline" -> {
"""







}
else -> "

}
}
}
Here are the tools Mom might need to organise a party, now letโs update the main entry point,
fun main() = runBlocking {
val ollamaConfig = OllamaConfig.default()
val executor = simpleOllamaAIExecutor(baseUrl = ollamaConfig.baseUrl)
val llm = LLModel(
provider = LLMProvider.Ollama,
id = ollamaConfig.model,
capabilities = listOf(
LLMCapability.Temperature,
LLMCapability.Tools,
LLMCapability.Schema.JSON.Simple
),
)
val agentConfig = AIAgentConfig(
prompt = prompt(id = "mom-party-planner", params = LLMParams(temperature = ollamaConfig.temperature)) {
system(
content = """You are a loving, organized mom who is an expert at planning amazing birthday parties!

You work through parties in phases, just like a real mom:
PHASE 1 - INITIAL PLANNING:
Start by using party_planning_organizer to create a checklist for the specific party type requested.
PHASE 2 - RESEARCH & RECOMMENDATIONS:
Use contact_mom_friends to get recommendations, then use_computer to research ideas that match the party theme.
PHASE 3 - BOOKING & SHOPPING:
Use make_phone_call to book venues/entertainment, then make_purchase to buy supplies and drive_to_location as needed.
PHASE 4 - FINAL PREPARATIONS:
Handle any remaining tasks, double-check everything, and make final purchases or calls.







๏ฟฝ YOUR PERSONALITY:
- Enthusiastic and action-oriented ("Let me get started right away!")
- Think step-by-step through each phase
- Use tools strategically to build toward a complete party plan
- Don't ask questions - make reasonable assumptions (budget $200-300, 10-12 kids, etc.)
IMPORTANT: Work through each phase systematically using your tools. Build upon what you learn in each phase!"""
)
},
model = llm,
maxAgentIterations = 30
)
val toolRegistry = ToolRegistry {
tool(PhoneTool)
tool(CarTool)
tool(ComputerTool)
tool(FriendsNetworkTool)
tool(CreditCardTool)
tool(PlanningTool)
tool(SayToUser)
}
}
Notice the main system prompt now has clear instructions on how to approach the problem and solve it, this s very critical when designing agents you need to be good and precise with the system prompt on what you want to achieve or else you might not get the results you want, also notice we added a new parameter maxAgentIterations this the number of times an Agent can iterate between tools, in the end we added all the tools to our tool registery
Now letโs create an Agent
val momAgent = AIAgent(
promptExecutor = executor,
strategy = createMomPartyPlanningStrategy(),
agentConfig = agentConfig,
toolRegistry = toolRegistry
)
You might be thinking whats with the createMomPartyPlanningStrategy() at times you might not want to rely on single run strategy and want to create custom flow, Koog comes up with a graph based workflow similar to LangGraph where you can specify your own strategy in a type safe manner
fun createMomPartyPlanningStrategy() = strategy<String, String>("mom-party-planning") {
// Mom's systematic party planning workflow
val nodeCallLLM by nodeLLMRequest("callLLM")
val nodeExecuteTool by nodeExecuteTool("executeTool")
val nodeSendToolResult by nodeLLMSendToolResult("sendToolResult")
// Define the mom's workflow - she can use multiple tools in sequence
edge(nodeStart forwardTo nodeCallLLM)
// When LLM wants to use a tool, execute it
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })
// When LLM gives a final response, finish
edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })
// After executing tool, send result back to LLM
edge(nodeExecuteTool forwardTo nodeSendToolResult)
// After sending tool result, LLM can either finish or use another tool
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
}
Letโs break it down
fun createMomPartyPlanningStrategy() = strategy<String, String>("mom-party-planning")
- Creates a strategy that takes String input (party request) and returns String output (final party plan)
- Named โmom-party-planningโ for identification
Node Definitions
val nodeCallLLM by nodeLLMRequest("callLLM")
val nodeExecuteTool by nodeExecuteTool("executeTool")
val nodeSendToolResult by nodeLLMSendToolResult("sendToolResult")
Three core nodes:
- nodeCallLLM: Sends requests to the LLM (mom's brain thinking)
- nodeExecuteTool: Executes the tools mom wants to use (phone, car, computer, etc.)
- nodeSendToolResult: Sends tool results back to the LLM (tells mom what happened)
Workflow Edges (The Momโs Decision Tree)
edge(nodeStart forwardTo nodeCallLLM)
// Start โ Call LLM: Begin by asking mom what to do with the party request
edge(nodeCallLLM forwardTo nodeExecuteTool onToolCall { true })
edge(nodeCallLLM forwardTo nodeFinish onAssistantMessage { true })
LLM Decision Point:
Mom decides either:
- Use a tool โ Go to nodeExecuteTool (she wants to take action)
- Give final answer โ Go to nodeFinish (she's done planning)
edge(nodeExecuteTool forwardTo nodeSendToolResult)
// Execute Tool โ Send Result: After using a tool, always tell mom what happened
edge(nodeSendToolResult forwardTo nodeFinish onAssistantMessage { true })
edge(nodeSendToolResult forwardTo nodeExecuteTool onToolCall { true })
Tool Result Decision: After getting tool results, mom can:
- Finish if sheโs satisfied with the party plan
- Use another tool if she needs to do more work
Letโs see this in action

Itโs proceeding in phases and since we arent using chat strategy, we need to update the system prompt.Here is the updated one.
val agentConfig = AIAgentConfig(
prompt = prompt(id = "mom-party-planner", params = LLMParams(temperature = ollamaConfig.temperature)) {
system(
content = """You are a loving, organized mom who is an expert at planning amazing birthday parties!

You work through parties in phases, just like a real mom:
PHASE 1 - INITIAL PLANNING:
Start by using party_planning_organizer to create a checklist for the specific party type requested.
PHASE 2 - RESEARCH & RECOMMENDATIONS:
Use contact_mom_friends to get recommendations, then use_computer to research ideas that match the party theme.
PHASE 3 - BOOKING & SHOPPING:
Use make_phone_call to book venues/entertainment, then make_purchase to buy supplies and drive_to_location as needed.
PHASE 4 - FINAL PREPARATIONS:
Handle any remaining tasks, double-check everything, and make final purchases or calls.








- Enthusiastic and action-oriented ("Let me get started right away!")
- Think step-by-step through each phase
- Use tools strategically to build toward a complete party plan
- Don't ask questions - make reasonable assumptions (budget $200-300, 10-12 kids, etc.)
- Don't put off tasks - get things done efficiently
- Do everything in one shot - no need to ask for confirmation, phases or details

After completing all phases and using your tools, you MUST provide a comprehensive final summary that includes:

- **Party Details:** Theme, venue, date, time, guest count
- **Venue & Location:** Where booked, cost, what's included
- **Entertainment:** What was booked, cost, duration
- **Food & Catering:** Cake details, menu, costs
- **Decorations:** What was purchased, theme items, total cost
- **Shopping Summary:** Complete list of everything bought with prices
- **Timeline:** Setup schedule and party day plan
- **Guest Information:** Who's invited, RSVPs, parent contacts
- **Budget Breakdown:** Total spent by category and overall total
- **Final Checklist:** Any remaining tasks or day-of reminders
IMPORTANT: Work through each phase systematically using your tools. Build upon what you learn in each phase! Always end with the complete detailed summary above - this is what parents need to see their party is fully planned!"""
)
},
model = llm,
maxAgentIterations = 30
)
Letโs test again, and here is the final output

What kind of party are you dreaming of? (e.g., 'I want an awesome 16th birthday party' or 'Plan a superhero party for my 8-year-old')
Plan a superhero party for my 8-year-old














Agent says: All tasks completed!
==================================================

==================================================
**


---
##

| Category | Details |
|----------|---------|
| **Party Details** | **Theme:** Superhero<br>**Date:** Saturday, 1 Oct 2025 (2 โ 6 pm)<br>**Time:** 2:00 pm โ 6:00 pm<br>**Guest Count:** 12 kids (ages 6โ9) + parents |
| **Venue & Location** | **Venue:** Pizza Palace (familyโfriendly indoor venue)<br>**Cost:** $150.00 (includes tables, chairs, cleanup, pizza buffet)<br>**What's Included:** 4 tables, 20 chairs, pizza & drinks, kitchen staff, cleaning crew |
| **Entertainment** | **Superhero Magic Show** (live magician in superhero costume) <br>**Cost:** $100.00 (includes 45โminute show + 15โminute Q&A)<br>**Duration:** 45 min, scheduled 3:30 pm โ 4:15 pm |
| **Food & Catering** | โข **Superhero Cake** โ $50 (custom "SuperโHero" design)<br>โข **Pizza Buffet** โ included in venue fee (12 slices per child + 4 adult slices)<br>โข **Drinks:** sodas, juice, water (included)<br>โข **Snacks:** popcorn, candy (included) |
| **Decorations** | โข Superhero banners, balloons, tablecloths โ $30.00<br>โข DIY "Hero" backdrop (DIY kit) โ $10.00 (purchased at Party Supplies Plus)<br>โข Table centerpieces (mini capes & masks) โ $10.00 |
| **Goodie Bags** | โข Superhero stickers, temporary tattoos, mini capes, candy โ $15.00 total (12 bags) |
| **Shopping Summary** | โข Cake โ $50.00 (Bakery Bliss)<br>โข Decorations โ $30.00 (Party Supplies Plus)<br>โข Goodie bags โ $15.00 (Party Supplies Plus)<br>โข Magic Show โ $100.00 (Superhero Magic Show)<br>โข Venue โ $150.00 (Pizza Palace)<br>**Total:** $345.00 |
| **Timeline** | โข **2 weeks before** โ Send invitations (digital & paper)<br>โข **1 week before** โ Confirm venue, order cake, buy decorations, confirm magician<br>โข **3 days before** โ Confirm entertainment, prepare goodie bags, create activity schedule<br>โข **1 day before** โ Set up decorations, prepare food station, test sound<br>โข **Party Day** โ 1:30 pm: Final setup & sound check<br> 2:00 pm: Guests arrive, pizza served<br> 3:00 pm: Games & activities (superhero obstacle course, maskโmaking)<br> 3:30 pm: Magic show<br> 4:15 pm: Cake cutting & singing "Happy Birthday"<br> 5:00 pm: Free play & photo booth<br> 6:00 pm: Cleanโup & thankโyou |
| **Guest Information** | โข **Invitations** sent to 12 parents (via email & printed cards)<br>โข **RSVP Deadline:** 4 Oct 2025<br>โข **Confirmed Guests:** 10 kids + 10 parents (all parents have provided contact numbers)<br>โข **Special Needs:** One child with mild food allergy (no dairy, no nuts) โ pizza selected without cheese, candy replaced with fruit snacks |
| **Budget Breakdown** | โข **Venue:** $150.00<br>โข **Entertainment:** $100.00<br>โข **Cake:** $50.00<br>โข **Decorations:** $40.00 (incl. DIY kit)<br>โข **Goodie Bags:** $15.00<br>โข **Miscellaneous (transport, contingency):** $30.00<br>**Total Spent:** $345.00 (within $200โ$300 budget plan โ extra funds kept for unexpected items) |
| **Final Checklist** | โข [x] Venue booked (Pizza Palace) โ confirmed 10 Oct 2025<br>โข [x] Cake ordered โ pickup scheduled 14 Oct 2025<br>โข [x] Decorations purchased โ ready for assembly<br>โข [x] Entertainment booked โ magician confirmed 13 Oct 2025<br>โข [x] Invitations sent โ RSVPs tracked<br>โข [x] Goodie bags assembled โ labeled with child names<br>โข [ ] Final dayโof checklist printed and shared with helpers<br>โข [ ] Backup plan for inclement weather (indoor already covered)<br>โข [ ] Thankโyou notes drafted for guests |
---
**All tasks are completed and the party is fully planned!**

Let me know if you'd like any lastโminute tweaks or additional details. Enjoy the superhero celebration!
Mom has now completed everything for arranging the party, With this small example we have covered almost all the concepts that you need to be aware of before getting started to build an AI agent, In the coming stories in this series we will build a complete deep research workflow using these concepts.
If you have learned some new thing and enjoyed the article please give a clap it will be of great encouragement.
Note: this is nowhere the best practices but just an example for you to get started.
Letโs connect: LinkedIn or X
code for this example is available on GitHub.
Happy coding โค
Building Agentic Deep Research with Kotlin and Koog: Part1 was originally published in ProAndroidDev on Medium, where people are continuing the conversation by highlighting and responding to this story.
Continue reading...