Chrylo
03/27/2026, 7:46 AM@Tool
@LLMDescription(
"""
Search with query.
""",
)
fun search(
@LLMDescription("Query")
query: String,
): List<Model>Vadim Briliantov
03/27/2026, 8:44 AMsubgraphWithTask is a full-fledged loop that would call multiple tools.
If you need finer-grained steps, you can use nodeLLMRequest() (also there’s nodeLLMRequestMultiple() with multi-message/multi-tool result to then execute N tools in parallel) and then nodeExecuteTool() (also there’s a -Multiple version of it).
val askLLM by nodeLLMRequest()
val callTool by nodeExecuteTool()
edge(askLLM forwardTo callTool onToolCall { true }) // onToolCall check is required in compile time, because `askLLM` returns Message.Response which can be either Message.Assistant or Message.Tool.Call
edge(askLLM forwardTo ... onAssistantMessage { true }) // handle the case if LLM didn't want to call a tool (ex: retry)
edge(callTool forwardTo nodeFinish transformed { it.result }) // would return JSONElement that you can then parse using tool.decodeResult()
If you know one specific tool you want to call (specifically, search) — then there’s nodeLLMRequestForceOneTool(toolDescriptor) :
val searchTool = ::search.asTool()
val askLLM by nodeLLMRequestForceOneTool(tool = searchTool.descriptor) // would work if your model supports toolChoice (ex.: Ollama doesn't)
val callTool by nodeExecuteTool()
edge(askLLM forwardTo callTool onToolCall { true })
edge(callTool forwardTo nodeFinish transformed transformed { searchTool.decodeResult(it.result!!, config.serializer) })
Please note also that it.result (on the last line) is nullable. And there’s also it.resultKind that could be Success / Failure(public val error: AIAgentError?) / ValidationError(public val error: AIAgentError) — so if you want you can actually handle errors from your tool thenVadim Briliantov
03/27/2026, 8:55 AMsealed interface SearchToolResult {
class Success(val result: List<Model>) : SearchToolResult
class Failure(val error: AIAgentError?, val content: String) : SearchToolResult
}
...
val searchTool = ::search.asTool()
val guaranteedSearch by subgraphWithRetry(
condition = { result ->
when (result) {
is SearchToolResult.Success -> ConditionResult.Approve
is SearchToolResult.Failure -> ConditionResult.Reject("Fix the error: ${result.error?.message ?: result.content}")
}
},
maxRetries = 3,
name = "retry-search",
) {
val askLLM by nodeLLMRequestForceOneTool(tool = searchTool.descriptor)
val callTool by nodeExecuteTool()
edge(nodeStart forwardTo askLLM)
edge(askLLM forwardTo callTool onToolCall { true })
edge(callTool forwardTo nodeFinish transformed {
val resultKind = it.resultKind
when (resultKind) {
ToolResultKind.Success -> SearchToolResult.Success(
searchTool.decodeResult(
it.result!!,
config.serializer
)
)
is ToolResultKind.Failure -> SearchToolResult.Failure(resultKind.error, it.content)
is ToolResultKind.ValidationError -> SearchToolResult.Failure(resultKind.error, it.content)
}
})
}Chrylo
03/27/2026, 8:56 AMval selectNode by nodeLLMRequestForceOneTool(name = "SelectNode", SelectToolSet::search.asTool())
val executeToolNode by nodeExecuteTool("ExecuteToolNode")
edge(
selectNode forwardTo executeToolNode
onToolCall { true },
)
edge(
executeToolNode forwardTo sendSResultNode
transformed { toolsResult ->
val tool = XToolSet::search.asTool()
val toolResult = toolsResult.toSafeResult(tool, config.serializer).asSuccessful().result
Result(toolResult)
},
)Vadim Briliantov
03/27/2026, 8:57 AMsubgraphWithRetry as I shown above to be on the safe side before deploying to prod 🙂Vadim Briliantov
03/27/2026, 8:58 AM