smolagents
Verified 2026-08-19 — PASS, 5s. Session persistence: wrapper-owned transcript.
smolagents’ idiom is CodeAgent, which calls tools by generating Python that calls them rather
than emitting structured tool calls. That choice is deliberate here and explained below.
This is one of the two frameworks that appeared to fail in an earlier test run. It was never a framework fault — the transport underneath had a poisoned session. See sessions-and-swarm.md.
The runner
Section titled “The runner”import sys, os, json, pathlibsys.path.insert(0, os.path.expanduser("~/agenttests"))import azcfg, hetoolfrom smolagents import CodeAgent, OpenAIServerModel, tool
SESS = pathlib.Path(os.path.expanduser("~/agenttests/smolagents/sessions"))SESS.mkdir(parents=True, exist_ok=True)
@tooldef get_working_folder() -> str: """Return the agent's current working folder (absolute path).""" return hetool.get_working_folder()
@tooldef run_shell(command: str) -> str: """Run a shell command in the working folder and return its output.
Args: command: The shell command to run. """ return hetool.run_shell(command)
def invoke(sid, message): c = azcfg.load() model = OpenAIServerModel(model_id=c["model"], api_base=c["base_url"], api_key=c["api_key"]) agent = CodeAgent(tools=[get_working_folder, run_shell], model=model, max_steps=4, code_block_tags="markdown")
f = SESS / f"{sid}.json" transcript = json.loads(f.read_text()) if f.exists() else [] history = "\n".join(f'{t["role"]}: {t["content"]}' for t in transcript) prompt = (f"Conversation so far:\n{history or '(none)'}\n\nNew message: {message}\n" "Reply concisely. Use your tools for operational questions.")
reply = str(agent.run(prompt)).strip() transcript += [{"role": "user", "content": message}, {"role": "assistant", "content": reply}] f.write_text(json.dumps(transcript, indent=2)) return replyEngine definition
Section titled “Engine definition”{ "type": "process", "file": "/home/you/smolagents/.venv/bin/python3", "nativeSession": true, "argsNew": ["/home/you/he_adapter.py", "/home/you/smolagents_session.py", "", "{message}"], "argsResume": ["/home/you/he_adapter.py", "/home/you/smolagents_session.py", "{sessionId}", "{message}"], "replyField": "result", "sessionIdField": "session_id", "approve": false, "workdir": "/home/you/work", "proxy": { "shape": "openai", "baseUrlEnv": "OPENAI_BASE_URL", "keyEnv": "OPENAI_API_KEY", "modelEnv": "OPENAI_MODEL", "model": "<route>|<provider model>" }}Adoption
Section titled “Adoption”- Keep your tools as they are, decorated with
@tool. - Keep a transcript keyed on
sidand replay it into the prompt. - Build
OpenAIServerModelfromazcfg.load(). - Seal the engine, point
external.engineat it, restart.
Gotchas
Section titled “Gotchas”Prefer CodeAgent over ToolCallingAgent on non-OpenAI models. ToolCallingAgent sets
tool_choice in a way some providers reject — GLM among them. CodeAgent sidesteps it by having the
model write Python that calls the tools, which is smolagents’ native idiom anyway.
Set code_block_tags="markdown". The default <code></code> tags arrive truncated from some
providers (</code) and fail to parse, so the agent errors out mid-step. Markdown ```py
fences are emitted reliably.
Generated code executes locally. That is how CodeAgent works. HBIA’s jail confines the spawn,
but understand what you are enabling before binding a run_shell tool in production, and prefer
narrow, purpose-built tools over a general shell.
Docstrings need an Args: section for any tool taking parameters — smolagents parses it to build
the schema, and omitting it produces a tool the model cannot call correctly.
max_steps bounds the loop. Each step is an LLM call through the proxy, so this is both a cost and
a latency control; 4 was ample for conversational use.