When would you implement Koog's SimpleTool or Tool instead of annotating a function?
answer
- who writes the descriptor
- reflection versus hand-written
- runtime-built and dependency-holding tools
- typed result instead of a string
- descriptor can drift from args
basics
~20 sSubclass when reflection is not enough: you need to hand-write the ToolDescriptor, build tools at runtime rather than at compile time, hold dependencies or state in the tool object, or return a typed ToolResult instead of a string.
solid answer
~40 sWith SimpleTool you supply three things yourself: a serializable Args type plus its argsSerializer, a ToolDescriptor giving the tool name, description and the required and optional ToolParameterDescriptor entries with their ToolParameterType, and a doExecute(args) that returns a String. The full Tool interface goes further and lets the result be a typed ToolResult, so surrounding Kotlin code can consume a structured value while the model still receives a serialized rendering. Reach for this when the schema needs hand-tuning that reflection cannot infer, when tools are constructed dynamically from configuration or a database, when the tool is a stateful object holding a client or connection, or when you want the result typed. For a fixed set of plain functions, the annotated ToolSet route is less code and should stay the default.
code
kotlin · 20 linesobject LookupOrder : SimpleTool<LookupOrder.Args>() {
@Serializable
data class Args(val orderId: String)
override val argsSerializer = Args.serializer()
override val descriptor = ToolDescriptor(
name = "lookup_order",
description = "Returns the status of a customer order by its id.",
requiredParameters = listOf(
ToolParameterDescriptor(
name = "orderId",
description = "Order id, for example ORD-4471",
type = ToolParameterType.String
)
)
)
override suspend fun doExecute(args: Args): String = "shipped"
}go deeper
Recognise that Koog has both styles and that annotated functions are the normal starting point; know a hand-built tool supplies its own descriptor.
List the three parts of a SimpleTool, namely the argument type with its serializer, the ToolDescriptor and doExecute, and give concrete reasons to prefer it over annotations.
Bring up descriptor drift and how you test for it, and explain when a typed ToolResult earns its keep over a formatted string.
Set the house rule: annotations by default, hand-built tools for dynamic or dependency-heavy cases, with descriptor-versus-args consistency enforced in CI rather than by review habit.
## Two ways to define a tool Koog lets you define a tool by annotating a Kotlin function inside a ToolSet, or by implementing a tool class directly. Both end up as entries in a ToolRegistry and the model cannot tell them apart. The difference is who writes the descriptor: reflection, or you. ## What SimpleTool asks of you A SimpleTool implementation carries an argument type, usually a serializable data class nested in the tool object, together with the serializer for it so Koog can turn the model's JSON arguments into a typed value. It carries a ToolDescriptor: the exposed tool name, the description, and lists of required and optional parameters, each a ToolParameterDescriptor with a name, a description and a ToolParameterType. And it carries doExecute(args), a suspending function returning a String, which Koog wraps as a text result for the conversation. That is more ceremony than an annotation, and you buy specific things with it. ## What the ceremony buys First, schema control. You choose exactly which parameters are required, what each type is, and what wording surrounds them, without that being a side effect of a Kotlin signature. When you are tuning a tool the model keeps calling wrongly, having the descriptor as data you can edit and test is a real advantage. Second, dynamic construction. A tool object can be created at runtime from configuration, a database row or a remote catalogue. Annotated methods have to exist at compile time; a registry built by looping over configuration cannot be written that way. If your product exposes per-tenant tools, this is the only route. Third, dependencies and state. A tool class takes constructor parameters, so it can hold an HTTP client, a repository, a rate limiter or a tenant id. With an annotated ToolSet you get the same by putting dependencies on the enclosing class, but the tool-per-object model is cleaner when each tool needs different collaborators. Fourth, typed results. SimpleTool returns a String, which is fine when the answer is text for the model. The general Tool interface lets you return a ToolResult subtype, so a result can be a structured value your own code inspects, which matters when surrounding code needs to branch on the outcome rather than re-parse prose. ## What it costs Hand-written descriptors drift. Nothing forces the ToolDescriptor to match the Args data class, so adding a field to Args without adding its ToolParameterDescriptor gives you a parameter the model is never told about, which then arrives absent or defaulted at runtime. That mismatch is the characteristic bug of this route, and it is worth a unit test asserting the descriptor's parameter names line up with the argument type. ## How to choose Default to the annotated ToolSet: it is shorter, and the schema cannot drift from the signature because it is derived from it. Move to SimpleTool or Tool for the specific reasons above, and be honest about which one applies. Subclassing every tool because it looks more serious just adds a descriptor to maintain.
- What is the characteristic bug of hand-written ToolDescriptors, and how do you catch it?Drift between the descriptor and the argument type. Adding a field to Args without a matching ToolParameterDescriptor means the model is never told the parameter exists, so it arrives missing or defaulted and the tool quietly misbehaves. Nothing in the compiler links the two, so catch it with a unit test asserting the descriptor's parameter names match the argument class fields.
- Why might you want a typed ToolResult rather than the String that SimpleTool returns?Because the string is written for the model, and your own code may need the outcome too. Returning a typed ToolResult lets surrounding Kotlin inspect a structured value, such as a status, an id or a parsed record, instead of pattern-matching prose the tool itself just formatted. SimpleTool is the shortcut for the common case where text is genuinely all you need.
saying these in an interview costs you the question
- Subclassing every tool when annotations would do
- Assuming the descriptor is derived from the args class
- Forgetting the serializer for the argument type
- Thinking dynamic per-tenant tools can be annotated
- Believing the model can tell the two styles apart