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_stepstrap, 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).

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 viaLiteLLMModel, or local models (TransformersModel, Ollama). - Tool-agnostic. Tools can come from a simple
@toolfunction, 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 & branch —
for,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 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
LocalPythonExecutoris 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
LocalPythonExecutoris 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_stepstrap. With no step cap, a code agent can fall into infinite loops and burn your budget — set a conservativemax_stepsevery 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 managerCodeAgentcalling 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_stepsbefore it touches production.
🧪 Try It Yourself
Reason these through, then check with the code-agent lab:
- A
ToolCallingAgentneeds 6 steps to total the 3 cheapest products; aCodeAgentneeds 1. In one sentence, why? - What does the
@tooldecorator generate from your function, and what two parts does it read? - A teammate runs a
CodeAgentwith the defaultLocalPythonExecutoron user-submitted tasks. What's the risk, and what's the fix? - Your code agent occasionally never finishes and racks up a huge bill. What's the missing parameter?
- 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
CodeAgentwrites Python code as its action (vs JSON tool-calls), enabling composition (loops, conditionals, nested tool calls) in one step. (ToolCallingAgentkeeps 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:
@toolfunctions (schema from signature + docstring) + aCodeAgent+ a model (LiteLLMModel→ Claude) +agent.run(). Tools from MCP / LangChain / Spaces. - The catch — sandbox it: a CodeAgent runs model-written code. The default
LocalPythonExecutoris 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_stepsloop 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).