Pedal Assist Coding logo

Pedal Assist Coding

Archives
RSS Feed
CommonsWare
Log in
Subscribe
July 30, 2026

Thursdays with Koog: Tools

In Which the Author Channels His Inner Tim Taylor

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:

  • You need to tell your agent framework (e.g., Koog) about the tools
  • The agent framework injects details about those tools, and how an LLM can request them, as part of the system prompt
  • The LLM sends a message that is a JSON object representing a tool request
  • The agent framework parses that JSON and uses it to invoke the tool that you provided
  • Your tool does something awesome, usually returning a result
  • The agent framework includes that result as part of the overall prompt sent back to the LLM
  • The LLM's next response can take that tool result into account

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

/**
 * 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, 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. 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.

Don't miss what's next. Subscribe to Pedal Assist Coding:
← Newer MCP vs. Bash Older → Thursdays with Koog: Prompts Everywhere

Add a comment:

Posting this comment will subscribe you to this newsletter with the email address you enter.
Bluesky
androiddev.social
commonsware.com
Powered by Buttondown, the easiest way to start and grow your newsletter.