warming up your workspace

How LLM function calling actually works, and why the model never runs your code

Agents are the story of 2026. Every product now has an assistant that can check your calendar, query the database, hit an API, and act. It looks like the language model reached out and ran something. It did not. A language model produces text and nothing else. It cannot execute a function, open a socket, or read a file. The entire illusion of an agent is a loop you write around the model, and once you build that loop, function calling stops being magic and becomes an obvious protocol.

The one idea

The model does not call functions. It emits a message that says which function it wants and with what arguments, as structured text. Your code reads that message, runs the real function, and hands the result back to the model as more text. The model then writes the final answer using that result. There are two round trips: "I want to call get_weather('Lagos')", then, after you run it, "the weather is 31 degrees". The model is the planner. Your loop is the hands.

Build the loop

You need three parts: real functions, a way to describe them to the model, and the loop that runs them. Here are the functions and their descriptions, auto-built from the signatures.

import json, inspect

def get_weather(city: str):
    fake = {"Paris": 18, "Lagos": 31, "Oslo": 7}
    return {"city": city, "celsius": fake.get(city, "unknown")}

def add(a: float, b: float):
    return {"sum": a + b}

TOOLS = {"get_weather": get_weather, "add": add}

def schema(fn):
    return {"name": fn.__name__, "parameters": list(inspect.signature(fn).parameters)}

That schema list is what gets sent to the model alongside the user's message: here are the tools, here are their argument names, pick one if you need it. Now the loop that turns the model's choice into a real result.

def run(user_msg):
    step1 = model(user_msg)                       # model reads tools + message
    if "tool_call" not in step1:
        return step1["content"]                   # it just answered directly
    call = step1["tool_call"]
    fn = TOOLS[call["name"]]                       # dispatch by name
    result = fn(**call["arguments"])              # YOUR code runs the function
    step2 = model(user_msg, tool_results=result)  # feed the result back in
    return step2["content"]                        # model answers using it

Three details that matter:

  • model() here stands in for the API call. A real model returns exactly this shape, a JSON object that is either {"content": "..."} when it wants to talk, or {"tool_call": {...}} when it wants a function run. The provider trained it to produce well-formed calls; your job is only to honor them.
  • The fn(**call["arguments"]) line is the security boundary of every agent. The model chose the name and the arguments, but you decided which functions live in TOOLS. Anything not in that dictionary cannot be called, no matter what the model asks for. That whitelist is the difference between an agent and a liability.
  • There are two calls to the model, not one. The first plans the tool use; the second, after seeing the result, writes the answer. Multi-step agents just repeat this: the loop keeps going as long as the model keeps asking for tools.

Proof: two questions, two tools, no model-run code

print(run("what is the weather in Lagos"))
print(run("what is 40 plus 2"))

Prints:

Done. Result was {'city': 'Lagos', 'celsius': 31}.
Done. Result was {'sum': 42}.

The weather question routed to get_weather, the arithmetic to add, each with arguments the model chose from the plain-English request. Every actual computation, the dictionary lookup, the addition, happened in ordinary Python that you can read, test, and lock down. The model never touched it. It only decided what to run.

Where this shows up

This exact loop is what sits under every agent framework and under the Model Context Protocol that standardized how tools get described and called in 2026. The production versions add real JSON-schema validation of the arguments, parallel tool calls, retries when the model asks for a tool that failed, and permission prompts before anything with side effects runs. But the skeleton does not change: the model proposes, your code disposes, and the whitelist is where safety lives.

If you want to build the model side too, the attention, the sampling, and then wrap it in an agent loop that plans across many tool calls, that is the arc the ai track on IWTLP builds from the ground up.

Sources