This is a submission for the Hermes Agent Challenge.
Every Hermes agent I wrote had a different helper function for building the messages list. Some used append, some built dicts inline, some forgot to deep copy before passing to the API. After accidentally sharing state between two conversation forks I decided to make this its own library.
That's llm-message-builder.
Basic conversation
from llm_message_builder import MessageBuilder
messages = (
MessageBuilder()
.system("You are a research assistant.")
.user("What is the boiling point of water?")
.assistant("100°C at standard pressure.")
.user("What about at altitude?")
.build()
)
response = client.messages.create(
model="claude-sonnet-4-5",
messages=messages,
)
Tool use (the hard part)
Building tool_use/tool_result pairs by hand is where bugs hide. The builder does it right:
messages = (
MessageBuilder()
.system("You are helpful.")
.user("Search for the latest Python release.")
.assistant_tool_use("call_1", "web_search", {"q": "Python latest release"})
.user_tool_result("call_1", "Python 3.14 was released in 2025.")
.assistant("The latest Python release is 3.14.")
.build()
)
The tool_use_id in the result block always matches the id in the tool_use block. No manual string tracking.
Optional text before tool_use
builder.assistant_tool_use(
"call_1", "web_search", {"q": "query"},
text="Let me look that up for you.",
)
# content: [{"type": "text", "text": "..."}, {"type": "tool_use", ...}]
Tool result as error
builder.user_tool_result("call_1", "connection timed out", is_error=True)
Fork conversations without sharing state
base = MessageBuilder().system("You are a research assistant.")
# Fork — each gets its own deep copy
thread_a = base.copy().user("Question A").assistant("Answer A")
thread_b = base.copy().user("Question B").assistant("Answer B")
# base is unchanged
Inspect
builder.count() # 3
builder.roles() # ["system", "user", "assistant"]
builder.last() # {"role": "assistant", "content": "..."}
len(builder) # 3
Extend from existing messages
# Add pre-built message dicts from another source
builder.extend(prior_conversation_messages)
Zero dependencies
Standard library only: copy, dataclasses. Nothing else.
pip install llm-message-builder
Top comments (0)