Two ways an agent can act

Every LLM agent needs a way to do things โ€” call an API, run a calculation, search a database. Hugging Face's smolagents library is a deliberately small framework built around a sharp question: how should the model express its actions? It offers two answers. A CodeAgent writes and executes Python code as its actions. A ToolCallingAgent emits structured JSON tool calls that your runtime dispatches. That single design choice cascades into everything โ€” expressiveness, security, debuggability โ€” so getting it right is the most important decision you make.

This guide covers both agent types, defining tools, model backends, MCP integration, and the failure modes that bite in production. If you are wiring smolagents into a real product and something is looping or throwing, our Hugging Face proxy job support team debugs agents live, and the smolagents job support page is the dedicated reference.


What smolagents is

smolagents is a minimalist agent framework from Hugging Face. Its philosophy is small surface area: an agent is a loop that, on each step, asks the model what to do next, executes that action, feeds the observation back, and repeats until the model calls a special final_answer. There is no sprawling abstraction layer โ€” the whole point is that you can read the loop and understand exactly what your agent does.

Because it is model-agnostic, smolagents runs on Hugging Face Inference Providers, local Transformers models, or a self-hosted vLLM endpoint. It ships first-class support for tools, multi-step reasoning, and the Model Context Protocol (MCP). What it does not do is hide the mechanics โ€” which is exactly why it is a good framework to learn agents on and a common topic in Hugging Face proxy interview support sessions.


CodeAgent: actions as executable Python

A CodeAgent expresses each action by writing a snippet of Python and running it. Instead of emitting "call tool search with args X", the model writes results = search("X") and the agent executes it. This is surprisingly powerful: the model can compose tools, loop, branch, do arithmetic, and store intermediate variables โ€” all in one step โ€” because code is a more expressive action space than a single JSON call.

from smolagents import CodeAgent, InferenceClientModel, WebSearchTool

model = InferenceClientModel()   # uses HF Inference Providers by default

agent = CodeAgent(
    tools=[WebSearchTool()],
    model=model,
)

agent.run("How many years passed between the first and last Apollo Moon landing?")

Under the hood the model might write code that searches, parses two dates, subtracts them, and calls final_answer(...) โ€” several logical steps expressed as one program. The cost of this power is obvious: you are executing model-generated code. Read on to sandboxing, because that is not optional.


Sandboxing and security for CodeAgent

Running LLM-written Python on your host is a remote-code-execution vulnerability by design. smolagents mitigates this in layers, and you must choose the right one for your risk level:

  • Restricted local interpreter. By default CodeAgent uses a constrained Python executor that blocks dangerous builtins and only allows imports you explicitly authorize via additional_authorized_imports. Good for trusted internal use, not a hard security boundary.
  • Sandboxed executors. For untrusted input or production, run code in an isolated sandbox โ€” a container or a remote sandboxed executor โ€” so generated code cannot touch your host filesystem, secrets, or network beyond what you allow.
agent = CodeAgent(
    tools=[WebSearchTool()],
    model=model,
    additional_authorized_imports=["math", "datetime"],  # nothing else importable
    # executor_type="e2b" / "docker"  # isolate execution off-host for real safety
)

The rule of thumb: if any part of the task input comes from an untrusted user, the local interpreter is not enough โ€” use a real sandbox. This is the single most important operational point for CodeAgent, and getting it wrong is the failure that turns an agent into an incident. Deeper patterns live on the code agents job support page.


ToolCallingAgent: actions as JSON tool calls

A ToolCallingAgent takes the more conventional route: on each step the model emits a structured JSON tool call โ€” a tool name plus arguments โ€” which the runtime validates against the tool's schema and dispatches. This is the same paradigm as the tool-calling APIs you may know from hosted model providers.

from smolagents import ToolCallingAgent, InferenceClientModel, WebSearchTool

agent = ToolCallingAgent(
    tools=[WebSearchTool()],
    model=InferenceClientModel(),
)

agent.run("What is the current population of Tokyo?")
# The model emits: {"name": "web_search", "arguments": {"query": "Tokyo population"}}

The advantage is safety and predictability: no arbitrary code runs, every action is a schema-validated call, and the action space is exactly the tools you exposed โ€” nothing more. The cost is expressiveness: composing three tools or doing a calculation takes multiple round-trips instead of one program. For a side-by-side comparison of the two paradigms, see tool-calling agents job support.


Defining tools with @tool

Both agent types share the same tool abstraction. The fastest way to define one is the @tool decorator: a plain Python function with a clear docstring and type hints becomes a tool the model can use. The docstring and argument descriptions are the schema the model reads โ€” vague docstrings cause the model to call tools wrong.

from smolagents import tool

@tool
def convert_currency(amount: float, from_ccy: str, to_ccy: str) -> float:
    """Convert an amount between two currencies using live rates.

    Args:
        amount: The numeric amount to convert.
        from_ccy: Source currency code, e.g. "USD".
        to_ccy: Target currency code, e.g. "EUR".
    """
    rate = fetch_rate(from_ccy, to_ccy)
    return amount * rate

For a CodeAgent the model calls this as convert_currency(100, "USD", "EUR") inside its Python; for a ToolCallingAgent it produces the equivalent JSON. Same tool, two invocation styles โ€” which is why smolagents lets you swap agent types without rewriting tools.


Model backends: Inference Providers, local, and vLLM

smolagents does not ship a model; you plug one in. Three common backends cover most needs:

  • Inference Providers via InferenceClientModel โ€” the zero-infra default; requests route through Hugging Face to a hosted model. Best for prototyping and low-to-moderate volume.
  • Local Transformers via TransformersModel โ€” runs a model in-process on your own GPU. Full control, no per-token cost, but you own the serving.
  • Self-hosted vLLM (or any OpenAI-compatible endpoint) via OpenAIServerModel pointed at your vLLM server โ€” the production path for high throughput.
from smolagents import OpenAIServerModel, CodeAgent

model = OpenAIServerModel(
    model_id="meta-llama/Llama-3.1-8B-Instruct",
    api_base="http://localhost:8000/v1",   # your vLLM server
    api_key="EMPTY",
)
agent = CodeAgent(tools=[], model=model)

One practical caveat: CodeAgent leans on the model's ability to write correct Python, so a capable instruct model matters more here than for a ToolCallingAgent. If you serve the backend yourself, tune it with vLLM inference job support.


Multi-step reasoning and MCP

Both agents run a multi-step loop: think, act, observe, repeat. You cap it with max_steps so a confused agent cannot run forever. The observation from each action (a return value, or code stdout for CodeAgent) is fed back into the next prompt, letting the agent correct course โ€” retry a failed search, reparse a bad response, refine a query.

For connecting to external tools, smolagents supports the Model Context Protocol (MCP). MCP is an open standard for exposing tools and data sources to agents over a uniform interface, so you can plug in an MCP server โ€” a filesystem server, a database server, a company-internal tool server โ€” and its tools become available to your agent without bespoke glue code.

from smolagents import CodeAgent, InferenceClientModel, ToolCollection

# Load tools exposed by an MCP server and hand them to the agent
with ToolCollection.from_mcp({"url": "http://localhost:8080/sse"}) as tc:
    agent = CodeAgent(tools=[*tc.tools], model=InferenceClientModel())
    agent.run("Summarize today's new rows in the orders table.")

MCP is how you scale an agent from a handful of hand-written tools to a whole ecosystem of shared, reusable ones. The MCP agents job support page covers wiring and debugging MCP servers end to end.


Common failures

The agents that break in production usually break in one of these ways:

  • Unsafe code execution. A CodeAgent given untrusted input without a sandbox can be prompt-injected into destructive or exfiltrating code. Always sandbox untrusted workloads; treat generated code as hostile.
  • Tool schema errors. Missing type hints or a thin docstring means the model guesses argument shapes and calls tools with wrong or malformed arguments. Rich docstrings and precise types are your schema โ€” invest in them.
  • Infinite / wasteful loops. An agent that never emits final_answer keeps stepping until it burns budget. Set a sane max_steps and log each step so you can see where it got stuck.
  • Weak model, code path. A small model that cannot reliably write valid Python will fail as a CodeAgent while doing fine as a ToolCallingAgent. Match the paradigm to the model's real capability.
  • Silent tool exceptions. A tool that raises returns an error observation the model may ignore or misread. Return clear, structured error strings so the agent can recover instead of looping.

When to use code vs tool-calling agents

The decision comes down to expressiveness versus control:

  • Use CodeAgent when tasks require composing multiple tools, doing computation, or multi-step logic in a single action โ€” data analysis, math-heavy workflows, orchestration โ€” and you have a capable model plus a proper sandbox. It typically solves complex tasks in fewer steps.
  • Use ToolCallingAgent when safety and auditability dominate, when the model is smaller or you cannot sandbox, or when actions are simple discrete API calls. Every action is schema-validated and no arbitrary code runs, which is easier to lock down and reason about.

A useful default: prototype with a CodeAgent to see how far expressiveness gets you, then move to a ToolCallingAgent for the parts that must be tightly controlled โ€” or keep the CodeAgent behind a real sandbox. Either way, smolagents lets you switch with almost no code change because the tools are shared. This same reasoning powers agentic RAG systems, where a retrieve-then-rerank step becomes just another tool the agent calls.

If you are building on smolagents and hitting sandbox questions, schema bugs, runaway loops, or backend choices, our Hugging Face proxy job support engineers can pair with you on a call, review your agent loop, and get it reliable and safe before it ships.