Building Agentic Deep Research with Kotlin and Koog: Part1

K

Kashif Mehmood

Guest
1*n0p2BTgWEWcpD0kLOsEf0w.jpeg


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

1*UOE_GUSmvc81Rt7I433eOw.png


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.

1*4Ln3RhVf-hd2ifte2o9h0w.png


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

1*12BgQbEtPXRpByNLtlaZjg.png


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.

1*cviu7U826OTUHqD0FqYWSg.png


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,

1*3B7Irdvy4BgLcA-SyNSHzw.png


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

1*OmEitoORQL81eCHfX-wLEA.png


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

1*CCT4f4aQYQggDR96k8tAxg.png


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,

1*eXcHXH5qsDLa9MizvKeYWA.png


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("๐Ÿ“ž Mom is calling ${args.contactType} about: ${args.purpose}")

return when (args.contactType) {
"venue" -> {
val venue = availableContacts.random()
"โœ… Called $venue: Available Saturday 2-6pm for \$150. Includes tables, chairs, and cleanup!"
}

"caterer" -> {
val caterer = availableContacts.random()
"โœ… Called $caterer: Can make a custom birthday cake for \$45, pizza party package for 12 kids is \$80"
}

"entertainment" -> {
val entertainer = availableContacts.random()
"โœ… Called $entertainer: Available for 3-hour party booking at \$200, includes setup and cleanup"
}

"decoration_store" -> {
val store = availableContacts.random()
"โœ… Called $store: Birthday theme decorations in stock - balloons, banners, party hats for about \$50"
}

"friend" -> {
val friend = availableContacts.random()
"โœ… Called $friend: They can help with setup and their kid is excited to come! They'll bring chips and drinks."
}

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("๐Ÿš— Mom is driving to ${args.destination} to ${args.purpose}")

return when (args.destination.lowercase()) {
"bakery" -> "โœ… Drove to bakery: Picked up the custom birthday cake - it looks amazing! The kids will love it."
"party store", "party city" -> "โœ… Drove to party store: Got balloons, streamers, party hats, and themed decorations. Total: \$52"
"grocery store" -> "โœ… Drove to grocery store: Picked up snacks, drinks, ice cream, and paper plates. Everything we need!"
"venue" -> "โœ… Drove to venue: Checked it out in person - perfect space, clean, and the staff is helpful!"
"dollar store" -> "โœ… Drove to dollar store: Found great budget decorations and party favors for the goodie bags!"
else -> "โœ… Drove to ${args.destination}: Completed task - ${args.purpose}. Everything is coming together!"
}
}
}


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("๐Ÿ’ป Mom is using computer to ${args.task} about: ${args.searchTopic}")

return when (args.task) {
"research" -> {
"โœ… Researched ${args.searchTopic}: Found great ideas! Pinterest has amazing DIY decorations, and there are themes like superheroes, princess, sports, or gaming that kids love."
}

"compare_prices" -> {
"โœ… Compared prices for ${args.searchTopic}: Found the best deals - local venues are cheaper than chains, and buying decorations in bulk saves money!"
}

"read_reviews" -> {
"โœ… Read reviews for ${args.searchTopic}: Great feedback! Most parents say kids had amazing time, good value for money, and staff was helpful."
}

"book" -> {
"โœ… Booked ${args.searchTopic} online: Confirmed reservation! Got confirmation email and all the details."
}

else -> "โœ… Used computer for ${args.task}: Got the information we needed for ${args.searchTopic}!"
}
}
}

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("๐Ÿ‘ฅ Mom is reaching out to her network for ${args.requestType} about: ${args.topic}")

return when (args.requestType) {
"recommendation" -> {
"โœ… Asked mom friends for recommendations: Sarah recommends Pizza Palace (great for kids), Lisa loves the community center (affordable), and Jenny suggests outdoor parties (weather permitting)!"
}

"help" -> {
"โœ… Asked for help with ${args.topic}: Three moms volunteered! Sarah will help with decorations, Lisa can assist with setup, and Jenny will help with cleanup."
}

"advice" -> {
"โœ… Got advice about ${args.topic}: Experienced moms say keep it simple, have backup activities, and don't forget goodie bags. Most importantly - take lots of pictures!"
}

"invitation" -> {
"โœ… Sent invitations through mom network: All the kids' friends are invited! Parents confirmed attendance and offered to help bring snacks and drinks."
}

else -> "โœ… Contacted mom network about ${args.topic}: Got great support from other parents!"
}
}
}


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("๐Ÿ’ณ Mom is purchasing ${args.item} for $${args.amount} from ${args.store}")

return "โœ… Purchased ${args.item} for $${args.amount} from ${args.store}. " +
"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("๐Ÿ“‹ Mom is organizing: ${args.task} - ${args.details}")

return when (args.task) {
"create_checklist" -> {
"""โœ… Created party planning checklist:
๐Ÿ“ Venue: [ ] Book location
๐ŸŽ‚ Food: [ ] Order cake, [ ] Plan menu, [ ] Buy snacks
๐ŸŽ‰ Decorations: [ ] Choose theme, [ ] Buy supplies, [ ] Set up
๐ŸŽต Entertainment: [ ] Book DJ/activities, [ ] Plan games
๐Ÿ‘ฅ Guests: [ ] Send invitations, [ ] Track RSVPs
๐Ÿ›๏ธ Goodie bags: [ ] Buy items, [ ] Assemble bags
๐Ÿ“ธ Memories: [ ] Charge camera, [ ] Designate photographer"""
}

"check_progress" -> {
"โœ… Progress check: We've made great progress! Venue is booked, decorations are bought, cake is ordered. Still need to finalize guest list and prepare goodie bags."
}

"adapt_plan" -> {
"โœ… Plan adapted for: ${args.details}. No worries! Moms are experts at handling changes. We have backup options and can make it work!"
}

"set_timeline" -> {
"""โœ… Party timeline set:
๐Ÿ“… 2 weeks before: Send invitations
๐Ÿ“… 1 week before: Confirm venue, order cake, buy decorations
๐Ÿ“… 3 days before: Confirm entertainment, prepare goodie bags
๐Ÿ“… 1 day before: Set up decorations, prepare food
๐Ÿ“… Party day: Final setup, enjoy the celebration! ๐ŸŽ‰"""
}

else -> "โœ… Organized ${args.task}: Everything is under control!"
}
}
}

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!

๐ŸŽฏ YOUR PARTY PLANNING PROCESS:
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 TOOLS (use them strategically in each phase):
๐Ÿ“ž make_phone_call - Your phone to call venues, caterers, entertainment, stores, friends
๐Ÿš— drive_to_location - Your car to go places and pick things up
๐Ÿ’ป use_computer - Your computer to research, compare prices, read reviews, book online
๐Ÿ‘ฅ contact_mom_friends - Your network of mom friends for recommendations and help
๐Ÿ’ณ make_purchase - Your credit card to buy everything needed
๐Ÿ“‹ party_planning_organizer - Your organizational skills to plan and track everything

๏ฟฝ 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

1*vTW1W9Ofh3hqPxnyMPFS8w.png


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!

๐ŸŽฏ YOUR PARTY PLANNING PROCESS:
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 TOOLS (use them strategically in each phase):
๐Ÿ“ž make_phone_call - Your phone to call venues, caterers, entertainment, stores, friends
๐Ÿš— drive_to_location - Your car to go places and pick things up
๐Ÿ’ป use_computer - Your computer to research, compare prices, read reviews, book online
๐Ÿ‘ฅ contact_mom_friends - Your network of mom friends for recommendations and help
๐Ÿ’ณ make_purchase - Your credit card to buy everything needed
๐Ÿ“‹ party_planning_organizer - Your organizational skills to plan and track everything

๐ŸŽ‰ 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.)
- Don't put off tasks - get things done efficiently
- Do everything in one shot - no need to ask for confirmation, phases or details

๐ŸŽฏ CRITICAL FINAL STEP:
After completing all phases and using your tools, you MUST provide a comprehensive final summary that includes:

๐Ÿ“‹ COMPLETE PARTY PLAN SUMMARY:
- **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

๐ŸŽ‰ Hi honey! I'm your party planning mom! Tell me about the birthday party you want and I'll make it happen!

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

๐ŸŽˆ Mom is getting to work on your party! Let me use all my tools to make this happen...

๐Ÿ“‹ Mom is organizing: create_checklist - Superhero birthday party for 8-year-old
๐Ÿ‘ฅ Mom is reaching out to her network for recommendation about: superhero party venues near me
๐Ÿ’ป Mom is using computer to research about: superhero party themes and ideas
๐Ÿ“ž Mom is calling venue about: book superhero birthday party venue
๐Ÿ’ณ Mom is purchasing superhero cake for $50.0 from Bakery Bliss
๐Ÿš— Mom is driving to Bakery Bliss to pick up cake
๐Ÿ’ณ Mom is purchasing superhero decorations for $30.0 from Party Supplies Plus
๐Ÿ“‹ Mom is organizing: set_timeline - Set timeline for superhero party
๐Ÿ‘ฅ Mom is reaching out to her network for recommendation about: superhero party entertainment
๐Ÿ“ž Mom is calling entertainment about: book superhero face painter
๐Ÿ’ณ Mom is purchasing goodie bags for $15.0 from Party Supplies Plus
๐Ÿ“ž Mom is calling venue about: book community center for superhero birthday party
๐Ÿ“ž Mom is calling entertainment about: cancel superhero face painter
Agent says: All tasks completed!

==================================================
๐ŸŽŠ MOM'S FINAL PARTY PLAN:
==================================================
**๐ŸŽ‰ Superhero Birthday Party โ€“ 8โ€‘Yearโ€‘Old ๐ŸŽ‰**

---

## ๐Ÿ“‹ COMPLETE PARTY PLAN SUMMARY

| 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 โค

stat



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...
 


Join ๐•‹๐•„๐•‹ on Telegram
Channel PREVIEW:
Back
Top