8.7.12. dasLLAMA-11 — Tool Calling

A tool-calling conversation is a loop: you declare functions, the model replies with a call instead of an answer, you run the function and hand the result back, and the model answers with it. This tutorial walks one full round of that loop.

Run it with a tool-capable GGUF (Qwen3-0.6B-Q8_0 works):

daslang.exe -jit tutorials/dasLLAMA/11_tool_calling.das -- path/to/Qwen3-0.6B-Q8_0.gguf

8.7.12.1. Declare the tools

set_tools takes the conversation’s tools — verbatim OpenAI tools[] JSON objects — before the first turn renders; the system turn then carries the model family’s tool block. A family with no tool format ignores the declaration. render_turn (tutorial 02) shows the exact result:

let WEATHER_TOOL = ("\{\"type\": \"function\", \"function\": \{\"name\": \"get_weather\", " +
    "\"description\": \"Get the current weather in a city\", " +
    "\"parameters\": \{\"type\": \"object\", \"properties\": \{\"city\": \{\"type\": \"string\"}}, " +
    "\"required\": [\"city\"]}}}")
var chat = create_chat(m, "You are a helpful assistant.", 512l)
var tools <- [WEATHER_TOOL]
set_tools(chat, tools)
add_user(chat, "What is the weather in Paris right now?")
print("{decode(m, render_turn(m, chat))}\n")   // the system turn now carries the defs

8.7.12.2. The model calls the tool

The reply to a tool-needing question is a call, spelled in the family’s wire format — hermes (Qwen) wraps a JSON object in <tool_call> markers. parse_calls splits any complete reply into reasoning, content, and calls per that format; each call’s args comes back normalized to JSON object text:

let reply = respond(m, chat, SamplingParams()) $(_piece) => true
var tr <- parse_calls(chat, reply)
for (c in tr.calls) {
    print("call: {c.name}({c.args})\n")   // call: get_weather(\{"city": "Paris"})
}

8.7.12.3. Answer the call

Tool results are the reply to the model: queue them with add_tool_results in place of add_user — one result per call, in call order — then respond again. The model reads the results and answers the original question:

var results <- ["\{\"temp_c\": 21, \"sky\": \"clear\"}"]
add_tool_results(chat, results)
respond(m, chat, SamplingParams()) $(piece) {
    fprint(fstdout(), piece)
    fflush(fstdout())
    return true
}
// -> The current weather in Paris is clear with a temperature of 21°C.

8.7.12.4. Beyond one round

The loop generalizes: parse every reply, and while it carries calls, run them and add_tool_results; when it carries none, the content is the answer. Hosts that key results by call id use the named overload — add_tool_results(chat, results, names) — so the name-spelling families (harmony, gemma-4) pair results correctly even out of call order. A stateless server replays a past call turn with render_assistant_calls.

Five wire formats ride the same verbs: hermes (Qwen), Harmony (gpt-oss), gemma-4’s bracketed DSL, Mistral’s [TOOL_CALLS] arrays, and Llama-3’s whole-reply JSON. The OpenAI-compatible server (utils/dasllama-server) serves tools[] requests over exactly this surface.

See also

Full source: tutorials/dasLLAMA/11_tool_calling.das

Next tutorial: dasLLAMA-12 — Voice Activity Detection

Thinking models and the reasoning split: dasLLAMA-10 — Thinking Models

Chat basics: dasLLAMA-02 — Chat and Templates