---
slug: 51-llm-tool-calling
title: Tool calling
description: Dejá que el modelo elija una tool de forma segura con llm_step + call_tool — un loop in-language con allow-list, mínimo privilegio por tool, intent congelado y presupuesto acotado.
example_ids: [tool-calling]
---

# Tool calling

Las cuatro ops devuelven texto. Para que el modelo elija una **tool** (una decisión estructurada `{tool, args}`), Synsema agrega `llm_step` + `call_tool`. El loop de seguridad se escribe **in-language**, así el modelo nunca gana poderes nuevos.

```synsema
-- Doc example: tool-calling. A "tool" is just a task; call() invokes it with named
-- args. The llm_step loop that lets the MODEL pick a tool is shown in the prose.
intent: "doc example: tool-calling"
require llm

task get_weather(city)
    give "weather in " + city + ": sunny"

print(call(get_weather, {"city": "Lima"}))    -- run shows: weather in Lima: sunny

test "a tool is a normal task; call() invokes it with named args from a map"
    assert_eq(get_weather("Paris"), "weather in Paris: sunny")
    assert_eq(call(get_weather, {"city": "Lima"}), "weather in Lima: sunny")
```

## Las primitivas

- `llm_step(prompt, catalog, context)` → la decisión del modelo: `{kind: "final", text, tokens}` o `{kind: "tool", name, args, tokens}`. `catalog` es data plana; `tokens` te deja imponer un presupuesto.
- `call(task, args_map)` → llama una task con args nombrados desde un map.
- `call_tool(task, args_map)` → despacha una tool elegida con **mínimo privilegio**: corre solo con las capacidades que declaró (∩ las del agente), aunque el agente tenga más.

## El loop seguro

```synsema
require llm

task get_weather(city)
    require net("api.weather.com")               -- la declaración propia de la tool
    give "weather in " + city + ": sunny"

let tools be {"get_weather": get_weather}        -- ALLOW-LIST (nombre → task)
let catalog be [{"name": "get_weather", "describe": "Weather for a city", "params": ["city"]}]

task run_agent(question, max_steps, budget)
    let spent be 0
    let n be 0
    let ctx be ""
    while (n < max_steps) and (spent <= budget)  -- loop acotado (guardia de presupuesto en la condición)
        set n to n + 1
        let step be llm_step(question, catalog, ctx)
        set spent to spent + step["tokens"]
        when step["kind"] == "final"
            give step["text"]
        otherwise when contains(tools, step["name"])     -- solo nombres en la allow-list despachan
            let result be call_tool(tools[step["name"]], step["args"])
            set ctx to ctx + " [" + step["name"] + " => " + text(result) + "]"
        otherwise
            set ctx to ctx + " [" + step["name"] + " => not allowed]"   -- inyectado/alucinado → rechazado
    give "out of steps"
```

El modelo solo devuelve un **nombre** de tool; tu programa decide si la corre. Seguridad = allow-list + gate de capacidad por task + intent congelado + loop acotado. Offline, `llm_step` devuelve `{kind: "final", text: "[no llm provider]", tokens: 0}`.
