deepset-ai/haystack · error · ValueError
Tool message must contain only one tool call result.
Error message
Tool message must contain only one tool call result.
What it means
A tool-role message built from a template must contain exactly one ToolCallResult part and nothing else. This error fires when the tool message has zero tool call results, more than one, or contains additional parts.
Solutions
- Insert exactly one ToolCallResult message per tool message block via {% insert %}.
- Split multiple tool results into separate tool messages, one per result.
- Remove extra text or other parts from tool messages; put explanations in user/assistant messages.
- Verify the variable inserted holds a single ChatMessage with role 'tool', not a list.
Example fix
// before
{% insert %}{{ tool_results }}{% endinsert %} // list of 2 results
// after
{% for r in tool_results %}tool message with {% insert %}{{ r }}{% endinsert %}{% endfor %} // one per message Defensive patterns
Strategy: validation
Validate before calling
from haystack.dataclasses import ChatMessage, ToolCallResult
def validate_tool_message(msg: ChatMessage):
results = [p for p in msg.content_parts if isinstance(p, ToolCallResult)]
if len(results) != 1 or len(msg.content_parts) != 1:
raise TypeError("Tool message must contain exactly one ToolCallResult part") Type guard
def is_single_tool_result(msg: ChatMessage) -> bool:
parts = msg.content_parts
return len(parts) == 1 and isinstance(parts[0], ToolCallResult) Try / catch
try:
messages = renderer.run(template=tpl, variables=vars)["messages"]
except ValueError as e:
if "Tool message must contain only one tool call result" in str(e):
log.error("Tool block invalid; ensure one ToolCallResult per tool message")
raise Prevention
- Emit one tool message per tool call result; never batch results.
- Always insert a ToolCallResult-carrying ChatMessage into tool blocks.
- Keep tool messages free of extra text or other parts.
- Test agent templates with multi-tool-call scenarios.
When it happens
Trigger: A tool message block with no {% insert %} of a ToolCallResult; inserting a list of multiple tool results into one tool message; mixing tool results with text parts in the same tool message.
Common situations: Batching several tool outputs into a single tool turn; forgetting to insert the ToolCallResult and only writing text; converting agent frameworks where one tool turn carries multiple results.
Related errors
- Assistant message must contain at least one text or tool…
- Assistant message must contain one text part at most.
- Assistant message must contain only text, tool call or…
- System message must contain a text part.
- System message must contain only one text part.
AI-assisted analysis of deepset-ai/haystack@e318778c9b (2026-08-30).
Data as JSON: /api/errors/404f497043b7383e.
Report an issue: GitHub.
Appendix: source
Thrown at haystack/utils/jinja2_chat_extension.py:397
reasoning = [part for part in parts if isinstance(part, ReasoningContent)]
if len(texts) > 1:
raise ValueError("Assistant message must contain one text part at most.")
if len(texts) == 0 and len(tool_calls) == 0:
raise ValueError("Assistant message must contain at least one text or tool call part.")
if len(parts) > len(texts) + len(tool_calls) + len(reasoning):
raise ValueError("Assistant message must contain only text, tool call or reasoning parts.")
return ChatMessage.from_assistant(
meta=meta,
name=name,
text=texts[0] if texts else None,
tool_calls=tool_calls or None,
reasoning=reasoning[0] if reasoning else None,
)
if role == "tool":
tool_call_results = [part for part in parts if isinstance(part, ToolCallResult)]
if len(tool_call_results) == 0 or len(tool_call_results) > 1 or len(parts) > len(tool_call_results):
raise ValueError("Tool message must contain only one tool call result.")
tool_result = tool_call_results[0].result
origin = tool_call_results[0].origin
error = tool_call_results[0].error
return ChatMessage.from_tool(meta=meta, tool_result=tool_result, origin=origin, error=error)
raise ValueError(f"Unsupported role: {role}")
@pass_environment
def templatize_part(environment: Any, value: ChatMessageContentT) -> "_TemplatizedPart":
"""
Jinja filter to convert a ChatMessageContentT object into a JSON string wrapped in sentinel content tags.
:param environment: The Jinja2 environment
:param value: The ChatMessageContentT object to convert
:return: A `_TemplatizedPart` holding a JSON string wrapped in special XML content tagsView on GitHub (pinned to e318778c9b)