Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion src/smolagents/agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,20 @@ def from_dict(cls, agent_dict: dict[str, Any], **kwargs) -> "MultiStepAgent":
for tool_info in agent_dict["tools"]:
tools.append(Tool.from_code(tool_info["code"]))
# Load managed agents
# Do not propagate CodeAgent-specific kwargs (authorized_imports, executor
# config, etc.) to managed agents: each agent carries its own configuration
# in its serialized dict. Generic overrides (model=, max_steps=, ...) are
# still forwarded so callers can inject a new model tree-wide. Without this
# filter the parent's additional_authorized_imports leaks into every managed
# agent and overwrites the child's own import allowlist. See issue #1849.
_MANAGED_AGENT_EXCLUDED_KWARGS = frozenset({
"additional_authorized_imports",
"executor_type",
"executor_kwargs",
"max_print_outputs_length",
"code_block_tags",
})
managed_agent_kwargs = {k: v for k, v in kwargs.items() if k not in _MANAGED_AGENT_EXCLUDED_KWARGS}
managed_agents = []
for managed_agent_dict in agent_dict["managed_agents"]:
agent_class = AGENT_REGISTRY.get(managed_agent_dict["class"])
Expand All @@ -1040,7 +1054,7 @@ def from_dict(cls, agent_dict: dict[str, Any], **kwargs) -> "MultiStepAgent":
f"Unknown agent class '{managed_agent_dict['class']}'. "
f"Supported agents: {', '.join(sorted(AGENT_REGISTRY.keys()))}"
)
managed_agent = agent_class.from_dict(managed_agent_dict, **kwargs)
managed_agent = agent_class.from_dict(managed_agent_dict, **managed_agent_kwargs)
managed_agents.append(managed_agent)
# Extract base agent parameters
agent_args = {
Expand Down
60 changes: 60 additions & 0 deletions tests/test_agents.py
Original file line number Diff line number Diff line change
Expand Up @@ -2343,6 +2343,66 @@ def test_from_dict(self):
assert agent.additional_authorized_imports == ["requests"]
assert agent.executor_kwargs == {"max_print_outputs_length": 5_000}

def test_from_dict_managed_agent_authorized_imports_not_leaked(self):
"""Managed agents must reconstruct with their own authorized_imports, not the parent's.

Regression test for https://github.com/huggingface/smolagents/issues/1849.

When a CodeAgent with managed sub-agents is deserialized, CodeAgent.from_dict()
builds code_agent_kwargs that includes the parent's additional_authorized_imports
and passes them via super().from_dict(). Without the fix, MultiStepAgent.from_dict()
forwards those kwargs unchanged to every managed agent, overwriting the child's
own import allowlist.
"""
mock_model_class = MagicMock()
mock_model_instance = MagicMock()
mock_model_class.from_dict.return_value = mock_model_instance

child_agent_dict = {
"class": "CodeAgent",
"model": {"class": "InferenceClientModel", "data": {"model_id": "child/model"}},
"tools": [],
"managed_agents": {},
"authorized_imports": ["sympy", "math"], # child's own imports
"executor_type": "local",
"executor_kwargs": {},
}
parent_agent_dict = {
"class": "CodeAgent",
"model": {"class": "InferenceClientModel", "data": {"model_id": "parent/model"}},
"tools": [],
"managed_agents": [child_agent_dict],
"authorized_imports": ["numpy", "pandas"], # parent's own imports
"executor_type": "local",
"executor_kwargs": {},
}

with patch.dict(
"smolagents.models.MODEL_REGISTRY",
{"InferenceClientModel": mock_model_class},
):
parent = CodeAgent.from_dict(parent_agent_dict)

# Parent should have its own imports
assert "numpy" in parent.authorized_imports
assert "pandas" in parent.authorized_imports

assert len(parent.managed_agents) == 1
child = list(parent.managed_agents.values())[0]

# Child must retain its own imports, not the parent's
assert "sympy" in child.authorized_imports, (
"Child agent lost its 'sympy' import after from_dict(). "
"Parent's authorized_imports were incorrectly propagated to the child."
)
assert "math" in child.authorized_imports
assert "numpy" not in child.authorized_imports, (
"Parent's 'numpy' import leaked into the child agent's authorized_imports."
)
assert "pandas" not in child.authorized_imports, (
"Parent's 'pandas' import leaked into the child agent's authorized_imports."
)

def test_custom_final_answer_with_custom_inputs(self):
class CustomFinalAnswerToolWithCustomInputs(FinalAnswerTool):
inputs = {
Expand Down