smolagents Integration
Pair contextweaver's bounded-choice routing and context firewall with Hugging Face's smolagents so
CodeAgent/ToolCallingAgentruns see a focused tool shortlist instead of everyToolin their registry, and large observations never blow up the token budget.
Why
smolagents is built around the same problem space as contextweaver ("execute tools, keep the context small") but at a different layer: smolagents owns the agent loop and the model call, contextweaver owns the prompt that loop hands to the model. Three problems contextweaver addresses for a multi-step smolagents run:
- Tool overload.
CodeAgent.tools_to_call_codeputs every tool's signature + docstring into the system prompt on every step. A 25-tool agent burns 3-4 K tokens before the first observation. - Unbounded observations. A
web_fetchreturning 30 KB of HTML ends up verbatim in the next step'sActionStep.observations, poisoning every subsequent step. - No phase awareness. The same prompt drives reasoning, tool selection, and final-answer synthesis — each phase has different needs.
contextweaver fixes all three without forking smolagents. The adapter
is a thin stateless converter (adapters/); no smolagents internals
are wrapped.
Prerequisites
pip install 'contextweaver[smolagents]'
export HF_TOKEN=hf_... # smolagents typically defaults to a HF Inference model
The plain-dict conversion paths
(smolagents_tool_to_selectable, smolagents_tools_to_catalog,
from_smolagents_agent) work without the [smolagents] extra —
useful for CI fixtures and unit tests that exercise routing without
spinning up a real model.
Architecture
User goal
│
▼
contextweaver Router ← all smolagents tools registered as SelectableItems
│ (top-k shortlist for this step)
▼
smolagents Agent ← receives only the shortlist as tools
│ (Tool.forward)
▼
contextweaver Firewall ← intercepts large observations
│ (summary + artifact handle)
▼
contextweaver ContextManager ← phase-specific prompt for the next step
│ (budgeted ContextPack)
▼
smolagents Model client → LLM
You hook contextweaver in at two points:
- Before each
agent.run— narrow the available tools to a shortlist. - After each step — ingest
agent.memory.stepsviafrom_smolagents_agentso the prior step's tool calls and observations flow through contextweaver's budget-aware pipeline instead of accumulating in raw form.
Minimal wiring
from smolagents import CodeAgent, HfApiModel, Tool
from contextweaver.adapters.smolagents import (
load_smolagents_catalog,
from_smolagents_agent,
)
from contextweaver.context.manager import ContextManager
from contextweaver.routing.router import Router
from contextweaver.routing.tree import TreeBuilder
from contextweaver.types import Phase
class WebSearch(Tool):
name = "web_search"
description = "Search the public web for a query and return top results."
inputs = {"query": {"type": "string", "description": "User search query."}}
output_type = "string"
def forward(self, query: str) -> str:
...
class ImageGenerator(Tool):
name = "image_generator"
description = "Generate an image from a text description."
inputs = {"prompt": {"type": "string", "description": "Image description."}}
output_type = "image"
def forward(self, prompt: str) -> bytes:
...
all_tools = [WebSearch(), ImageGenerator(), ...]
# 1. Build a contextweaver Catalog from the full tool set.
catalog = load_smolagents_catalog(all_tools)
# 2. Compile the routing graph once.
graph = TreeBuilder(max_children=8).build(catalog.all())
router = Router(graph, items=catalog.all(), top_k=3)
# 3. Per-task: narrow tools before constructing the agent.
def run_agent(task: str) -> str:
result = router.route(task)
short_ids = {it.id.removeprefix("smolagents:") for it in result.candidate_items}
short_tools = [t for t in all_tools if t.name in short_ids]
agent = CodeAgent(model=HfApiModel(), tools=short_tools)
return agent.run(task)
Step-log ingestion
Use from_smolagents_agent to pull a finished agent's memory.steps
into contextweaver's event log so a follow-up turn (or a different
agent in the same session) sees only the budget-aware projection:
ctx_mgr = ContextManager()
agent = CodeAgent(model=..., tools=short_tools)
answer = agent.run(task)
from_smolagents_agent(agent, into=ctx_mgr)
pack = ctx_mgr.build_sync(phase=Phase.answer, query="summarise the last run")
The mapping is:
| smolagents step | contextweaver ItemKind |
|---|---|
TaskStep |
user_turn |
ActionStep.model_output |
agent_msg (free-text reasoning) |
ActionStep.tool_calls[*] |
tool_call (one per call) |
ActionStep.observations |
tool_result (linked via parent_id) |
PlanningStep |
plan_state |
FinalAnswerStep |
agent_msg (with metadata["final_answer"]=True) |
Note on CodeAgent code blocks. CodeAgent runs emit Python code
that the runtime executes locally. The adapter translates only the
executed tool calls into ContextItems — the raw code blocks are
intentionally not ingested. Per #274's acceptance criterion, agents
should reason about the same artefacts the LLM acted on, not the
generated code surface.
Firewalling observations
Wrap each tool's forward so its return value flows through
ContextManager.ingest_tool_result before being saved as an
observation:
from contextweaver.context.manager import ContextManager
ctx_mgr = ContextManager()
def firewalled(tool: Tool) -> Tool:
original = tool.forward
def _forward(*args, **kwargs):
raw = original(*args, **kwargs)
item, _envelope = ctx_mgr.ingest_tool_result(
tool_call_id=f"{tool.name}:{id(raw)}",
raw_output=str(raw),
tool_name=tool.name,
)
return item.text # compact summary; raw addressable in artifact_store
tool.forward = _forward # type: ignore[method-assign]
return tool
short_tools = [firewalled(t) for t in short_tools]
Namespace inference
The adapter infers a namespace from the tool name's prefix (separator =
., /, or _). A tool named web_search lands as:
id:smolagents:web_searchname:search(namespace prefix stripped)namespace:web
Force a uniform namespace with the namespace= argument on
smolagents_tools_to_catalog:
catalog = load_smolagents_catalog(all_tools, namespace="hf")
Inputs → JSON Schema
smolagents' Tool.inputs is a mapping
{arg: {"type": "string", "description": "...", "nullable": ?}}. The
adapter coerces this to a JSON-Schema properties + required block
on SelectableItem.args_schema:
nullable=True→ the arg is not added torequired.nullable=False(or unset) → the arg is added torequired(sorted alphabetically).- The
output_typestring is preserved both onmetadata["output_type"]and as a customx-smolagents-output-typefield on the schema for downstream consumers that care.
Troubleshooting
Q: My CodeAgent reasoning is missing from the ContextManager —
By design. The adapter ingests ActionStep.model_output (free-text
reasoning) but not the generated code blocks. If you want the code
inline, register a custom _decode_action_step shim or copy the body
of from_smolagents_agent into your own ingestion routine.
Q: from_smolagents_agent raised 'could not locate ... steps' —
You passed an object that exposes neither memory.steps nor a top-level
steps attribute. Pass either a real MultiStepAgent, or a plain list
of step dicts.
Q: Routing returns the wrong tool for a code-flavoured query —
smolagents agents often pick tools by function-name lexical match. If
your tool name and docstring don't share keywords with the query, score
will be near zero. Enrich the description with synonyms or set
SelectableItem.examples after conversion.
See also
examples/smolagents_adapter_demo.py— Runnable demo: 4 tools → catalog → routing + step-log ingestion.- How contextweaver Fits — Positioning page.
- smolagents docs
- smolagents on GitHub
docs/cookbook.md— The bring-your-own-tools recipe is the canonical fallback if yourCodeAgentsetup deviates from the standardToolshape.