CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/koog

Koog 1.0 idioms, gotchas, and scaffolding skills for Kotlin agents on the JVM

88

1.95x
Quality

88%

Does it follow best practices?

Impact

88%

1.95x

Average score across 43 eval scenarios

SecuritybySnyk

Passed

No known issues

Overview
Quality
Evals
Security
Files

SKILL.mdskills/use-llm-node-variants/

name:
use-llm-node-variants
description:
Use a non-default LLM node variant inside a Koog 1.0 strategy — streaming output, multiple-choice sampling, content moderation, or forcing a specific tool call. Use when the user asks for "streaming", "multiple completions / sampling", "moderation", "force one tool", "force the LLM to call a specific tool", or names any of `nodeLLMRequestStreaming`, `nodeLLMRequestMultipleChoices`, `nodeLLMModerateText`, `nodeLLMRequestForceOneTool`.

Use LLM Node Variants Skill

This skill is an action router — pick the step that matches the user's intent and execute only that step. Do not run other steps; do not parallelize.

Available actions:

  • Step 1 — Streaming output (nodeLLMRequestStreaming)
  • Step 2 — Multiple-choice sampling (nodeLLMRequestMultipleChoices)
  • Step 3 — Content moderation (nodeLLMModerateText / nodeLLMModerateMessage)
  • Step 4 — Force a specific tool call (nodeLLMRequestForceOneTool)

Step 1 — Streaming Output

Use when the caller needs to see partial output as it arrives (Ktor server endpoint, CLI with live print, conference demo with text appearing live).

val strategy = strategy<String, String>("stream-reply") {
    val streamReply by nodeLLMRequestStreaming { chunk ->
        // chunk-handler — runs on every streamed chunk
        print(chunk.text)
    }

    edge(nodeStart forwardTo streamReply)
    edge(streamReply forwardTo nodeFinish)
}

The chunk-handler lambda runs synchronously on every streamed delta. Don't block inside it — the LLM's stream stalls if the handler is slow. For network I/O (writing chunks to a Ktor response), use a channel/flow downstream rather than calling respond* directly inside the handler.

Streaming doesn't compose with tool calls in the same node — for "stream the reply text but still allow tool calls", use a singleRunStrategy shape and stream from nodeLLMRequestStreaming only on the text-reply branch.

Finish here.

Step 2 — Multiple-Choice Sampling

Use when you want multiple completions for the same prompt and downstream code (or a critic node) picks the best.

val strategy = strategy<String, String>("sample-and-pick") {
    val sample by nodeLLMRequestMultipleChoices(numChoices = 3)
    val pick by node<List<Message.User>, Message.User>("pick-best") { choices ->
        // pick logic — e.g., longest reply, or run a critic LLM
        choices.maxBy { it.content.length }
    }

    edge(nodeStart forwardTo sample)
    edge(sample forwardTo pick)
    edge(pick forwardTo nodeFinish transformed { it.content })
}

Each choice costs one LLM completion at full token rates — sample 3 for 3× the cost. Use when the picker's signal is cheaper or more reliable than the per-completion quality.

Finish here.

Step 3 — Content Moderation

Two variants:

  • nodeLLMModerateText() — String input
  • nodeLLMModerateMessage()Message.User input

Both call a moderation classifier (provider-dependent) and produce a moderation result that downstream edges branch on.

val strategy = strategy<String, String>("moderated-chat") {
    val moderate by nodeLLMModerateText()
    val safeReply by nodeLLMRequest()

    edge(nodeStart forwardTo moderate)
    edge(moderate forwardTo nodeFinish onCondition { it.flagged } transformed { "I can't help with that request." })
    edge(moderate forwardTo safeReply onCondition { !it.flagged })
    edge(safeReply forwardTo nodeFinish onTextMessage { true })
}

The moderation node returns a typed result with a flagged boolean and category breakdown — branch on flagged to short-circuit, and on categories if you need finer-grained handling.

Finish here.

Step 4 — Force a Specific Tool Call

Use when the next step must invoke a specific tool — no LLM choice, no fallback to text. Common case: a "router" agent where the first step always calls classify_intent.

val classifyTool = ClassifyIntentTool()

val strategy = strategy<String, String>("force-router") {
    val forceClassify by nodeLLMRequestForceOneTool(classifyTool)
    val execute by nodeExecuteTools()
    val followUp by nodeLLMSendToolResults()

    edge(nodeStart forwardTo forceClassify)
    edge(forceClassify forwardTo execute)
    edge(execute forwardTo followUp)
    edge(followUp forwardTo nodeFinish onTextMessage { true })
}

The forced tool must already be in the agent's ToolRegistry. Forcing a tool not in the registry is a runtime error.

Finish here.

skills

use-llm-node-variants

README.md

tile.json