In a Koog strategy graph, which edges close the LLM tool-calling loop?
answer
- Three nodes, two conditional exits
- The model response can be either shape
- The cycle is not through the request node
- Send-result node needs both edges
- Missing exit means maxIterations aborts
basics
~10 sSend nodeLLMRequest to nodeExecuteTool on onToolCall, nodeExecuteTool straight to nodeLLMSendToolResult, and nodeLLMSendToolResult back to nodeExecuteTool on onToolCall. Both LLM nodes also need an onAssistantMessage edge to nodeFinish, or the loop never exits.
solid answer
~40 sThe canonical Koog loop uses three nodes: `nodeLLMRequest()` sends the prompt, `nodeExecuteTool()` runs a single requested tool, and `nodeLLMSendToolResult()` feeds that result back to the model. You wire `nodeStart` into the request node, then `edge(callLLM forwardTo runTool onToolCall { true })` and `edge(callLLM forwardTo nodeFinish onAssistantMessage { true })`. The tool node forwards unconditionally to the send-result node, which needs the *same pair* of outgoing edges: back to `nodeExecuteTool` on `onToolCall`, and out to `nodeFinish` on `onAssistantMessage`. The cycle is `sendResult -> runTool -> sendResult`, and the only way out is the assistant-message edge. Forget that pair on the send-result node and the agent has no exit: it spins until `maxIterations` aborts the run.
code
kotlin · 12 linesval toolLoop = strategy("tool-loop") {
val callLLM by nodeLLMRequest()
val runTool by nodeExecuteTool()
val sendResult by nodeLLMSendToolResult()
edge(nodeStart forwardTo callLLM)
edge(callLLM forwardTo nodeFinish onAssistantMessage { true })
edge(callLLM forwardTo runTool onToolCall { true })
edge(runTool forwardTo sendResult)
edge(sendResult forwardTo nodeFinish onAssistantMessage { true })
edge(sendResult forwardTo runTool onToolCall { true })
}go deeper
Recall the three node names — request, execute tool, send tool result — and that edges out of an LLM node are guarded by whether the model asked for a tool or gave an answer.
Be ready to draw the graph from memory and explain why the cycle runs between the tool node and the send-result node, and why both LLM-producing nodes need an edge to nodeFinish.
Diagnose from a symptom: an agent that hangs only when a tool is involved points straight at a missing assistant-message exit on the send-result node, and parallel tool calls point at the singular-versus-plural node choice.
Argue about what belongs in the graph at all — validation, approval and history compression grafted onto this skeleton are enforceable, while the same rules written as prompt instructions are advisory and unobservable.
## The three nodes Koog ships the tool loop as three predefined node builders, used as delegated properties inside `strategy("name") { }`: - `nodeLLMRequest()` — takes the input, appends it to the prompt, calls the model, and outputs the model's response. - `nodeExecuteTool()` — takes a tool call from that response, looks the tool up in the agent's registry, executes it, and outputs the result. - `nodeLLMSendToolResult()` — takes a tool result, appends it to the conversation, calls the model again, and outputs the new response. Notice the shape: the two LLM nodes both emit a model response that may be *either* a tool call or a plain assistant message. That is the whole reason the loop needs conditional edges. ## The wiring ``` nodeStart -> callLLM callLLM --onAssistantMessage--> nodeFinish callLLM --onToolCall---------> runTool runTool ----------------------> sendResult sendResult --onAssistantMessage--> nodeFinish sendResult --onToolCall---------> runTool ``` In DSL form each line is an `edge(a forwardTo b)` with an optional condition block, `onToolCall { true }` or `onAssistantMessage { true }`. The condition lambda receives the node's output, so you can be selective — `onToolCall { it.tool == "search" }` routes only one tool somewhere special — but `{ true }` is the normal catch-all. ## Where the cycle actually is The cycle is not `callLLM -> runTool -> callLLM`. `nodeLLMRequest` appends a *user* turn to the prompt; re-entering it mid-loop would corrupt the conversation. The cycle is `runTool -> sendResult -> runTool`: the send-result node is the one that keeps calling the model with accumulated tool output. `nodeLLMRequest` is entered once, from the start. This is the single most common wiring mistake to talk about in an interview: candidates draw the loop back to the request node because that is how they picture a ReAct cycle abstractly, and the resulting agent either re-asks the original question every turn or fails to type-check, because `nodeLLMRequest` expects the strategy's input type, not a tool result. ## Both exits matter Every node that can emit an assistant message needs an edge out to `nodeFinish`. Beginners wire the exit on `callLLM` — the case where the model answers without touching a tool — and forget it on `sendResult`, which is the case that fires on *every* successful tool run. The result is an agent that answers trivial questions fine and hangs on anything that uses a tool, until `maxIterations` aborts it. When someone reports "my Koog agent works until it calls a tool", this is the first thing to check. ## Multiple tool calls in one turn Modern models often return several tool calls in a single response. `nodeExecuteTool` handles one call; Koog also provides the plural variants (`nodeExecuteMultipleTools`, `nodeLLMSendMultipleToolResults`) and a matching multiple-tool-call edge condition for that shape. The loop topology is identical — request, execute, send results, branch — only the arity changes. If your model provider returns parallel calls and you wired only the singular nodes, you will see calls dropped or the graph failing to match an edge. ## Edge conditions are checked, not guessed `onToolCall` and `onAssistantMessage` are not magic strings; they are typed conditions over the node's output, and the compiler enforces that the downstream node accepts what the edge produces. That is Koog's pitch: this whole loop is a compile-time-checked object graph, so a mistyped hop is a build error. What the compiler cannot catch is a *logical* gap — a node whose conditions never all match, or a missing exit — and that is what `maxIterations` exists to bound. ## Practical shape Once you have written this loop twice, you realise it is exactly what Koog's default agent does for you. The reason to spell it out is to insert something into it: a validation node between tool execution and the model, a history-compression node once the transcript grows, or a branch that routes an expensive tool through a human approval step. The loop above is the skeleton you graft those onto.
- Why does the loop cycle back to nodeExecuteTool rather than to nodeLLMRequest?`nodeLLMRequest` appends a fresh user turn to the prompt and expects the strategy's input, so re-entering it mid-loop would re-ask the original question and mangle the conversation. `nodeLLMSendToolResult` is the node designed to continue an in-flight exchange: it appends the tool result and calls the model again, so the cycle lives between it and `nodeExecuteTool`.
- What happens if you omit the onAssistantMessage edge from the send-result node?The graph has no exit once a tool has run. Each model response that is a plain answer matches no outgoing edge, so the agent cannot advance to `nodeFinish` and keeps iterating until `maxIterations` trips and the run fails. The tell is an agent that answers tool-free questions but dies on anything that touches a tool.
- How do you handle a model that returns several tool calls in one response?Use Koog's plural node builders — the execute-multiple-tools and send-multiple-tool-results nodes — with the matching multiple-tool-call edge condition. The topology is unchanged: request, execute, send back, branch on tool call versus assistant message. Wiring only the singular nodes against a provider that emits parallel calls leaves calls unmatched.
saying these in an interview costs you the question
- Draws the loop back to nodeLLMRequest instead of nodeExecuteTool
- Adds an exit edge only on the first LLM node
- Thinks onToolCall and onAssistantMessage are mutually exclusive per node
- Believes a tool result is sent to the model automatically without a node
- Says the graph guarantees termination on its own