DEV Community

Mukunda Rao Katta
Mukunda Rao Katta

Posted on

Building LLM message lists by hand is error-prone. There's a better way.

Hermes Agent Challenge Submission: Write About Hermes Agent

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,
)
Enter fullscreen mode Exit fullscreen mode

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()
)
Enter fullscreen mode Exit fullscreen mode

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", ...}]
Enter fullscreen mode Exit fullscreen mode

Tool result as error

builder.user_tool_result("call_1", "connection timed out", is_error=True)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Inspect

builder.count()   # 3
builder.roles()   # ["system", "user", "assistant"]
builder.last()    # {"role": "assistant", "content": "..."}
len(builder)      # 3
Enter fullscreen mode Exit fullscreen mode

Extend from existing messages

# Add pre-built message dicts from another source
builder.extend(prior_conversation_messages)
Enter fullscreen mode Exit fullscreen mode

Zero dependencies

Standard library only: copy, dataclasses. Nothing else.

pip install llm-message-builder
Enter fullscreen mode Exit fullscreen mode

Repo: https://github.com/MukundaKatta/llm-message-builder

Top comments (0)