# Thursdays with Koog: Tools

> Source: <https://pac.commonsware.com/archive/thursdays-with-koog-tools/>
> Published: 2026-07-30 13:00:00+00:00

A coding agent usually is only as good as its tools and how well the LLM can use those tools. Which begs the question: how do tools work, anyway?

There are several steps for tools:

From our standpoint as consumers of Koog, Koog handles most of the dirty work. We tell Koog what tools we offer, and Koog invokes those as needed based on LLM requests. Beyond that, the rest of the work is internal to the tools and tends to be specific to your particular Koog-using program. A coding agent will have different tools (doing different things) than will a song lyric generator, or a 3D printer model generator, or a customer service chatbot.

In Koog, there are a couple of high-level ways you can define tools.

Knosh uses the [class-based approach](https://docs.koog.ai/tools/class-based-tools/). You can extend `Tool`

or `SimpleTool`

, the latter handling the common case where your return is a chunk of text to send back to the LLM. For example, here is the class declaration for [ CreateDirectoryTool in Knosh 0.3.0](https://codeberg.org/commonsguy/knosh/src/tag/0.3.0/lib/knosh-tools/src/main/kotlin/com/commonsware/knosh/tools/CreateDirectoryTool.kt):

```
/**
 * Tool that creates one or more directories, including all required parent directories. Each path is attempted
 * independently; failures are reported but do not stop the remaining creations.
 *
 * @param dispatcher the coroutine dispatcher used for file I/O
 * @param context the runtime context
 */
public class CreateDirectoryTool(private val dispatcher: CoroutineDispatcher, private val context: RuntimeContext) :
  SimpleTool<CreateDirectoryTool.Args>(
    argsType = typeToken<Args>(),
    name = "create-directory",
    description = DESCRIPTION,
  ) {
  ```

The `name` and `description` constructor parameters provide details to the LLM of what the tool is and under what circumstances it should be used. Here, you are not talking to a human user, but rather to the LLM itself. Hence, you don't need to be polite, and sometimes you do need to be a bit forceful:

``` kotlin
private val DESCRIPTION =
  """
  You MUST use this tool when you need to create directories on the filesystem. You do not have access to a shell, Bash, or any command-line tools — do not attempt to create directories by any other means.

  Usage:
  - Parent directories are created automatically as needed (equivalent to `mkdir -p`).
  - Paths must be absolute. Relative paths are resolved against the current
    working directory.
  - If a directory already exists at a given path, that path is treated as a
    success (idempotent).
  - If a directory cannot be created (e.g. due to permissions), the failure is
    noted and the tool continues with the remaining paths. All failures are
    reported.
  """
    .trimIndent()
```

`argsType`

indicates what sorts of arguments your tool expects to be supplied by the LLM when it tries to invoke your tool. This can be any `@Serializable`

type, and you use the `LLMDescription`

annotation to provide descriptive information to the LLM of the role of each argument:

```
 /**
   * Arguments for [CreateDirectoryTool].
   *
   * @property paths the list of filesystem paths at which to create directories
   */
  @Serializable
  @Poko
  public class Args(
    @property:LLMDescription(
      "REQUIRED. The list of filesystem paths at which to create directories. " +
        "Paths must be absolute. Relative paths are resolved against the current working directory. " +
        "Parent directories are created automatically."
    )
    public val paths: List<String>
  ) : HasFilesystemPaths {
    override fun getFilesystemPaths(): List<String> = paths
  }
```

In this case, `HasFilesystemPaths`

is a Knosh interface, denoting argument types that contain filesystem paths and providing those paths on demand. This is tied to how Knosh secures its tools, which we will see in a future issue.

Your business logic goes in an `override suspend fun execute(args: Args): String`

function on your `SimpleTool`

subtype. Your job is to take those arguments, do something fun, and return a `String`

that will get passed back to the LLM. This response can have different content (e.g., success vs. failure) and even different formats (e.g., JSON for success, Markdown for failure).

In many cases, you can get away with using Koog's [annotation-based tools](https://docs.koog.ai/tools/annotation-based-tools/), where you can define a tool as a simple function by means of the `@Tool`

annotation:

```
@Tool
@LLMDescription("Processes input data")
fun processTool(
    @LLMDescription("The input data to process")
    input: String,

    @LLMDescription("Optional configuration parameters")
    config: String = ""
): String {
    // Function implementation
    return "Processed: $input with config: $config"
}
```

(the above sample is from the Koog documentation)

These functions go on some implementation of the `ToolSet`

interface:

```
@LLMDescription("Tools for getting weather information")
class MyFirstToolSet : ToolSet {
    @Tool
    @LLMDescription("Get the current weather for a location")
    fun getWeather(
        @LLMDescription("The city and state/country")
        location: String
    ): String {
        // In a real implementation, you would call a weather API
        return "The weather in $location is sunny and 72°F"
    }
}
```

(the above sample is from the Koog documentation)

Teaching Koog about your tools is a matter of combining them into a `ToolRegistry`

, then passing that registry to your `AIAgent`

via a constructor parameter:

```
// Agent initialization
val agent = AIAgent(
    promptExecutor = simpleOpenAIExecutor(System.getenv("OPENAI_API_KEY")),
    systemPrompt = "You are a helpful assistant with strong mathematical skills.",
    llmModel = OpenAIModels.Chat.GPT4o,
    // Pass your tool registry to the agent
    toolRegistry = ToolRegistry {
        tools(toolOne, toolTwo, toolTime)
    }
)
```

(the above sample is based on the Koog documentation)

Here, `toolOne`

, `toolTwo`

, and `toolTime`

are instances of your `ToolSet`

implementation (for the annotation-based tools) or your `Tool`

subclass.

At this point, Koog takes over. Your tool's function (e.g., `execute()`

on a `SimpleTool`

) gets invoked when the LLM sends down a properly-formatted tool call, and your function's response gets sent back to the LLM as part of the prompt.

All that is necessary. It is unlikely to be *sufficient*. You may need to decide at runtime which tools are relevant, based on the circumstances in which you are creating your `AIAgent`

. You **must** take steps to ensure that your tools are not abused by the LLM, lest [the LLM run amok](https://arstechnica.com/security/2026/07/jfrog-tries-to-spin-openai-0-day-exploit-of-its-app-into-a-success-story/). In a future issue, I will explain how Knosh decides which tools to offer an agent and how it defends against LLM-based attacks.

But, next week, I want to talk a bit about metrics: how we can find out what the LLM is doing while it is chugging away on our prompt, and how we can find out details of the work, such as how many tokens were involved.
