Repository navigation
Conversation
A model may return a list of content blocks for the <end_plan> call. The plan text is a string, so interpolating that list directly put its Python repr into PlanningStep.plan. Extract the text blocks (used by both the initial-plan and update-plan branches) so the plan stays readable.
VANDRANKI
left a comment
There was a problem hiding this comment.
Community review, does not clear the merge gate.
Traced this in src/smolagents/agents.py's _generate_planning_step. plan_message_content = plan_message.content comes straight from ChatMessage.content, and I confirmed elsewhere in the same file (the plan_update_pre SYSTEM message a few lines below) that ChatMessage.content is genuinely used as either a plain string or a list of {"type": ..., "text": ...} blocks in this codebase, so a provider returning structured content for the plan response is a real possibility, not a hypothetical. Before this fix, that list got embedded directly into an f-string, so plan (a markdown string meant for display and for later reuse as conversation history) would contain the Python repr of the list of dicts instead of readable text.
The new _plan_message_content_to_text helper passes strings through unchanged, and for a list keeps only blocks with type == "text", joining their text values. I checked the change is scoped to only the two non-streaming generate() branches (initial plan and update plan); the streaming branch above builds plan_message_content by concatenating event.content as a string already, so it was never affected by this bug and this diff correctly leaves it untouched.
The new test uses a fake model that returns content=[{"type": "text", "text": "Step 1: do the thing."}] for the planning call and asserts the resulting plan text contains "Step 1: do the thing." and does not contain "[{" (i.e. no repr leakage). That's a direct regression test for the described bug (issue #2720).
Traced end to end, comfortable approving.
What this PR does
When a provider returns structured content (a list of content blocks) for the
<end_plan>call,_generate_planning_stepembedded that list directly into the plan f-string. Theplanfield is a string, so the result was the list's Python repr rather than readable text — e.g.:instead of:
The plan text is what gets logged and read back by the next model call, so this corrupted the plan in the structured-content case. It affected both the initial-plan branch and the update-plan branch.
How it's fixed
Added
_plan_message_content_to_text, which returns the text blocks of aChatMessage.content. It is applied when building the plan in both branches.strcontent is unchanged; alistof blocks keeps onlytype == "text"blocks (images and other block types have no place in the markdown plan text); everything else yields"".Test
test_planning_step_with_structured_plan_contentuses a mock model that returns real structured content for the plan call and asserts the plan contains the text block and not a repr. It fails onmainand passes with this fix.Validation
pytest tests/test_agents.py -k plan→ 4 passedpytest tests/test_memory.py→ 12 passedruff check+ruff format --checkon the two changed files → cleanI attempted this contribution using an AI coding agent. I have read the diff, run the tests above, and can explain each line and answer review questions.
Fixes #2720