Skip to main content

smolagents & Code Agents

Introduction

You've seen control (LangGraph, L150 — LangGraph) and convenience (CrewAI, L151 — CrewAI). smolagents is the third pole: radical simplicity. Built by Hugging Face, the entire library is "a barebones library for agents that think in code" — the core logic fits in about 1,000 lines you can read in one sitting.

But its real claim to fame is a different way for an agent to act. Most frameworks have the model emit a JSON tool-call ({"tool": "get_price", "args": {...}}) per step. smolagents' headline agent, the CodeAgent, instead writes Python code as its action:

Instead of one JSON blob per step, the agent writes a snippet that calls tools, loops, filters, and computes — in code. It turns out LLMs, trained on mountains of Python, are better at acting through code than through JSON — fewer steps, more natural composition.

In this lesson:

  • The philosophy — ~1,000 lines, minimal abstractions, the anti-framework framework
  • Code agents — actions as code, not JSON (hands-on), and why it wins (the CodeAct result)
  • Building one@tool, a model, agent.run() — in a handful of lines
  • The catch — you're running model-written code, so sandboxing is non-negotiable
  • Limits & fit — no persistence, the max_steps trap, and when to reach for it vs. LangGraph

Scope: the smolagents deep dive (L149 map, L150 — LangGraph LangGraph, L151 — CrewAI CrewAI). The OpenAI Agents SDK is next (L153 — OpenAI Agents SDK). Builds directly on tools (L121) and code execution & sandboxing (L124).

An infographic titled 'smolagents & Code Agents' showing Hugging Face's radically simple agent library and its defining idea. The centerpiece contrasts two ways an agent can act on the same multi-step task. A ToolCallingAgent emits one JSON tool-call per step — search, then get_price, get_price, get_price — making many round-trips while the model tracks and sorts the values in its head. A CodeAgent instead writes a single Python snippet as its action: it calls search, filters and sorts the results, sums the cheapest three, and returns the total, all in one executed block, so the loop and math happen in code rather than across turns. The key point: actions as code, not JSON, which the CodeAct research shows cuts steps and LLM calls by about thirty percent and enables natural composition with loops, conditionals, and nested tool calls, because LLMs are fluent in Python. A panel notes the framework's philosophy: the whole library is about a thousand lines, minimal abstractions over raw code, and it is model-agnostic, tool-agnostic, and modality-agnostic, with tools coming from a tool decorator, MCP, LangChain, or a Hub Space. A security panel warns that a CodeAgent runs model-written code, so the default local executor is AST-checked with imports denied unless allowlisted but is not a real sandbox, and untrusted code must run in Docker, E2B, or Modal isolation. Cards summarize when to use it: reach for smolagents for minimal, code-first, computation-heavy tasks and quick prototypes; cap max_steps to avoid runaway loops; and know its limits — no native persistence or checkpointing, so use LangGraph when you need durable state. The takeaway: smolagents trades features for radical simplicity, and its code-first CodeAgent makes agents write Python as their actions, which is fewer steps and more composable than JSON tool-calling but must be sandboxed.

The Philosophy — Radical Simplicity

smolagents is a deliberate reaction against framework bloat. Its whole pitch is minimalism:

  • ~1,000 lines of core logic. You can read the entire framework in an afternoon — no hidden control flow, no deep abstraction stack. "Abstractions kept to their minimal shape above raw code."
  • Model-agnostic. Power it with any LLM: HF Inference (InferenceClientModel), APIs like Anthropic/OpenAI via LiteLLMModel, or local models (TransformersModel, Ollama).
  • Tool-agnostic. Tools can come from a simple @tool function, an MCP server, LangChain, or even a Hugging Face Space.
  • Modality-agnostic (text, vision, audio) and Hub-integrated (share/load agents as Spaces), with handy CLI tools (smolagent, webagent).

The bet: most agents don't need a state-machine graph or a role-orchestration engine — they need a tight loop and good tools. smolagents gives you exactly that and gets out of the way. A simple ReAct agent is ~40 lines here vs. ~120 in LangGraph. The cost of that minimalism (no built-in persistence, limited observability) is the subject of the last section — but first, the idea that makes smolagents distinctive, not just small.

Code Agents — Actions as Code

Here's the core innovation. When an agent decides to act, it has to express that action somehow. The standard way (Section 3, L121 — Tools) is a JSON tool-call — a structured blob naming a tool and its arguments, which the framework parses and runs. smolagents' CodeAgent does it differently: it writes a Python code snippet as its action, and the framework executes it.

Why does that matter? Because a single code action can do what many JSON calls would:

  • Compose — call a tool, feed its result to another, all in one action.
  • Loop & branchfor, if, list comprehensions — control flow JSON can't express.
  • Compute — filter, sort, sum, transform — in code, not in the model's head across turns.

(smolagents also ships a ToolCallingAgent for the classic JSON style when you prefer it — but the CodeAgent is the headline.) See the same task done both ways:

The same multi-step task — total the 3 cheapest products under $50 — done two ways. ToolCallingAgent: one JSON tool-call per step (search, then get_price repeatedly), ~6 LLM round-trips, with the model tracking and sorting the prices in its head. CodeAgent: a single Python snippet that calls search, filters and sorts the results, sums the cheapest three, and returns the total — one LLM call, with the loop and math running in code. Toggle to feel the difference: code-as-action means ~30% fewer steps and LLM calls plus natural composition (the CodeAct result), at the cost of running model-written code (which must be sandboxed).

The contrast is the whole lesson in one screen: the JSON agent makes the model the orchestrator (juggling intermediate values over many turns); the code agent makes code the orchestrator (one composable snippet). That is what "agents that think in code" means.

Why Code-as-Action Wins

This isn't just an aesthetic preference — there's evidence behind it. The paper "Executable Code Actions Elicit Better LLM Agents" (the CodeAct work you met in L124 — Code Execution & Sandboxing) showed that letting an LLM act through executable code rather than JSON measurably improves agent performance. smolagents reports the practical payoff: code agents use ~30% fewer steps and LLM calls than JSON tool-calling, with better accuracy on complex benchmarks.

The reasons are intuitive once you see them:

  • LLMs are fluent in code. They've trained on enormous amounts of Python — generating a correct snippet with a loop is more natural to them than emitting a precise nested-JSON schema.
  • Fewer round-trips. One snippet that composes three tools replaces three JSON call/parse/observe cycles — fewer LLM calls means lower cost and latency (L147).
  • Real control flow. Loops, conditionals, and intermediate variables live in the code, where they belong — instead of the model re-deriving them in natural language each turn (error-prone).

The deep point: the action language matters. JSON tool-calling forces a reasoning engine to express plans in a format it's clumsy with; code lets it express them in its native idiom. smolagents made that the default — which is why a tiny, ~1,000-line library can hold its own against far bigger frameworks on agentic tasks.

Building One — @tool, a Model, run()

In practice, smolagents is almost startlingly compact. A tool is a decorated function; an agent is a CodeAgent with some tools and a model; you call agent.run(). Note we point it at Claude (via LiteLLM), not the HF default — and we cap max_steps:

from smolagents import CodeAgent, tool, LiteLLMModel

# A TOOL is just a typed, documented function — @tool generates the schema from it:
@tool
def get_price(product_id: str) -> float:
    """Look up a product's price.
    Args:
        product_id: the product's id.
    """
    return DB[product_id]["price"]

# CODE-FIRST: the agent writes Python that CALLS your tools. Point it at Claude via LiteLLM:
model = LiteLLMModel(model_id="anthropic/claude-opus-4-8")        # not the HF default
agent = CodeAgent(tools=[get_price, web_search], model=model, max_steps=6)  # cap the loop!

result = agent.run("Total the 3 cheapest products under $50.")
# The agent writes ONE snippet — search → filter → sort → sum, calling get_price in a loop —
# instead of N JSON round-trips. Tools can also come from MCP, LangChain, or a HF Space.

That's a complete, capable agent. The @tool decorator turns the function's signature and docstring into the tool schema (just like the principles from L122 — Designing Robust Tool Interfaces); the CodeAgent runs the think → write code → execute → observe loop; and because smolagents is tool-agnostic, you can drop in tools from MCP (L141), LangChain, or a Hub Space without ceremony. The one knob you must not skip is max_steps — without it, a confused agent can loop forever (and bill forever). And there's a second thing the snippet quietly does that deserves real attention: it executes code the model wrote.

The Catch — Executing Model Code Safely

A CodeAgent's superpower is also its danger: it runs Python that an LLM generated. If that model was steered by a prompt injection (L124, L142 — The MCP Ecosystem & Security Considerations), the code it writes could read your secrets, hit your network, or wipe files. So smolagents takes execution security seriously — and so must you:

  • The default LocalPythonExecutor is not naive — it parses the AST and executes operation-by-operation, and imports are denied by default unless you allowlist them (additional_authorized_imports). But the docs are blunt: it is not a security sandbox — its restrictions can be bypassed, so it must not be your security boundary.
  • For anything untrusted, isolate the execution. smolagents supports running the code in Docker, E2B, or Modal — real sandboxes that contain the blast radius (exactly the isolation ladder from L124 — Code Execution & Sandboxing).
# THE CATCH — a CodeAgent RUNS model-written Python. The default LocalPythonExecutor is
# AST-checked (imports are DENIED unless you allowlist them) — but it is NOT a sandbox:
agent = CodeAgent(tools=[...], model=model,
    additional_authorized_imports=["numpy", "pandas"])     # imports off by default

# For anything untrusted, run in a REAL sandbox — the only safe way (L124):
agent = CodeAgent(tools=[...], model=model, executor_type="docker")   # or "e2b" / "modal"
# Local = quick dev only. Docker / E2B / Modal = production isolation.
# NEVER run model-generated code on your host — it's the lethal trifecta waiting to happen.

The rule, straight from L124: never run model-generated code on your host. The LocalPythonExecutor is fine for quick local dev with trusted inputs; the moment real users or untrusted content are involved, switch to Docker / E2B / Modal. Code-as-action is powerful precisely because it's executable — which is exactly why where it executes is a first-class decision.

Limits & When to Use It

smolagents is brilliant at what it does, and honest about what it doesn't. Know the edges:

  • No native persistence. It doesn't checkpoint long-running tasks across sessions — there's no built-in durable state or crash-resume. For that you checkpoint to a DB yourself, or reach for LangGraph (L150).
  • The max_steps trap. With no step cap, a code agent can fall into infinite loops and burn your budget — set a conservative max_steps every time.
  • Limited observability. Minimal by design means fewer built-in tracing/debugging affordances than the big frameworks (though it integrates with tracers).
  • Multi-agent is basic. You can nest agents via managed_agents (a manager CodeAgent calling sub-agents), but it's lightweight — not a full orchestration engine.

Reach for smolagents when: you want a minimal, auditable agent; the task is code-first or computation-heavy (data analysis, calling Python libraries, quick tool composition); or you're prototyping and value reading every line over framework features. Reach for LangGraph instead when you need durable state, checkpointing, human-in-the-loop, or audit trails; CrewAI when the job is a clean role decomposition.

Placed on L149 (The Framework Landscape)'s spectrum: smolagents is the radical-simplicity lane — "a small, self-contained agent that runs code," not a DAG or a role engine. Use it where its tiny surface and code-first power are exactly what the task wants — and bolt on sandboxing and max_steps before it touches production.

🧪 Try It Yourself

Reason these through, then check with the code-agent lab:

  1. A ToolCallingAgent needs 6 steps to total the 3 cheapest products; a CodeAgent needs 1. In one sentence, why?
  2. What does the @tool decorator generate from your function, and what two parts does it read?
  3. A teammate runs a CodeAgent with the default LocalPythonExecutor on user-submitted tasks. What's the risk, and what's the fix?
  4. Your code agent occasionally never finishes and racks up a huge bill. What's the missing parameter?
  5. You need an agent that resumes a multi-day workflow after a crash. Is smolagents the right tool? If not, what is?

(1) The CodeAgent writes one Python snippet that searches, filters, sorts, and sums in code — the JSON agent makes a separate round-trip per tool call and juggles the values in its head (~30% fewer steps for code, per CodeAct). (2) It generates the tool schema (name, description, typed args) from the function's signature/type-hints and its docstring (L122). (3) It runs model-written code on the host — a prompt injection could exfiltrate or destroy data; the LocalPythonExecutor is not a sandbox. Fix: run in Docker / E2B / Modal (and allowlist imports). (4) max_steps — without a cap the loop can run forever (and bill forever). (5) No — smolagents has no native persistence/checkpointing. Use LangGraph (durable state + resume), or checkpoint smolagents' state to a DB yourself.

Mental-Model Corrections

  • "A CodeAgent is an agent that writes code for you." No — it's an agent that acts BY writing code. The Python it generates is its action (calling tools, looping, computing), not the deliverable.
  • "Code-as-action is just a style choice." It's evidence-backed (CodeAct): ~30% fewer steps/LLM calls and better accuracy, because LLMs are fluent in code and can compose in one action.
  • "smolagents is a toy because it's only ~1,000 lines." Small is the point — minimal abstractions, fully auditable, model/tool-agnostic. It's production-capable with sandboxing + max_steps.
  • "The LocalPythonExecutor sandboxes the code." It's AST-checked with import allowlists, but explicitly not a security boundary. Untrusted code needs Docker / E2B / Modal.
  • "It can do everything LangGraph can." It has no native persistence/checkpointing and only basic multi-agent. For durable, stateful, audited systems, that's LangGraph's job.
  • "Code agents are dangerous, so avoid them." They're dangerous unsandboxed — the power (executable composition) is real; just run it in isolation (L124).

Key Takeaways

  • smolagents = radical simplicity: Hugging Face's ~1,000-line "agents that think in code" library — minimal abstractions, model/tool/modality-agnostic, ~40 lines to a working agent.
  • The big idea — code agents: a CodeAgent writes Python code as its action (vs JSON tool-calls), enabling composition (loops, conditionals, nested tool calls) in one step. (ToolCallingAgent keeps the JSON style when you want it.)
  • Why it wins (CodeAct): acting through executable code beats JSON — ~30% fewer steps & LLM calls and better accuracy, because LLMs are fluent in code and round-trip less (L124, L147 — Coordination & Cost).
  • Build it in a few lines: @tool functions (schema from signature + docstring) + a CodeAgent + a model (LiteLLMModelClaude) + agent.run(). Tools from MCP / LangChain / Spaces.
  • The catch — sandbox it: a CodeAgent runs model-written code. The default LocalPythonExecutor is AST-checked (additional_authorized_imports) but not a sandbox — use Docker / E2B / Modal for anything untrusted, and never run on the host (L124).
  • Limits: no native persistence/checkpointing (use LangGraph for durable state), the max_steps loop trap, limited observability, basic multi-agent (managed_agents).
  • Use it for minimal, code-first, computation-heavy, quick-prototype agents — the radical-simplicity lane of L149 (The Framework Landscape).
  • Next — The OpenAI Agents SDK — the provider-SDK lane: a lightweight, handoff-based SDK (the production successor to Swarm).