Goals
- Learn how to serve a task’s prompt as a role-tagged conversation instead of a single block of text
- Understand how harnesses consume conversation-shaped prompts, and how the feature degrades gracefully everywhere else
Prerequisites
openreward>=0.1.157- Completing the Your First Environment tutorial
Introduction
Most environments serve their prompt withget_prompt(): a list of content blocks that every harness wraps in a single {"role": "user"} message. That remains the default — and for most tasks, the right — way to serve a prompt.
Some tasks, though, are conversations. A dialogue dataset where the model must write the next reply. A preference task built on chat transcripts. A support conversation the policy must continue. For those, the task carries the conversation so far — user turns, assistant turns, maybe a system prompt — and squeezing it into one user message loses the structure the model was trained on.
get_messages() is an additional, opt-in prompt surface for exactly these tasks. The environment returns role-tagged messages, the harness feeds them to the model as a real conversation, and the model writes the next assistant turn. Everything else about the episode — the tool loop, the terminal answer, rewards — proceeds exactly as it always has.
This is not user simulation. The conversation is task data, served once at the start of the episode. The environment does not generate new user turns mid-episode. (If you need a simulated user, see the pattern in Chat Backends — a simulated user is a model role the environment owns, called from inside a tool.)
Serving a conversation
Implementget_messages() instead of get_prompt() — one of the two is required, and each defaults to delegating to the other:
Message has a role ("system", "user", or "assistant") and content blocks — TextBlock / ImageBlock, plus the rich block types below, so multimodal and agentic conversations work unchanged.
Note that the {"role": ..., "text": ...} dicts in this task spec are just this example’s own convention — task specs are plain JSON, and the SDK doesn’t prescribe how a conversation is stored in them. get_messages() is the translation point where your task data becomes Message objects; store whatever shape suits your data and build the blocks there.
Rewards are unaffected: grade the model’s next turn however you would grade any response — a submit tool, a terminal tool, an LLM grader or rubrics.
Consuming a conversation
On the client,session.get_messages() returns the conversation, with optional provider formatting:
format argument mirrors list_tools(format=...):
Rich conversations: reasoning and tool history
Requires
openreward>=0.1.157.role: "tool" exists:
Placement is validated on the
Message model itself — a tool_result in an assistant turn fails at construction with a clear error, not at the provider.
Message wire shape and let pydantic parse them — all block types, placement validation included:
format conversion handles each provider’s shape wherever its schema can represent the block:
Where a schema can’t represent a block, conversion raises a clear
ValueError rather than silently dropping content; format=None always returns the full canonical messages.
Compatibility
Both prompt surfaces coexist, and/prompt remains the primary one:
- Existing environments and harnesses change nothing. A prompt-based environment behaves byte-identically to before.
get_messages()works against every environment. Against a prompt-based environment it returns the prompt wrapped as a single user message; against a server older than0.1.157(which has no/messagesroute) the client falls back the same way. A harness can adoptget_messages()unconditionally.- Old harnesses still work against conversation environments.
/promptis served for every environment; a conversation flattens to[role]-tagged text blocks (rich blocks render to text: reasoning as<think>…</think>, tool calls as[tool_call name] {args}lines). A conversation consisting of exactly one plain user message flattens to its blocks verbatim. - Rich blocks are versioned. A server’s supported block set is advertised as
message_block_typesin the tool list (absent on servers before0.1.157, meaning text/image only). Clients skip block types they don’t know when reading/messages(lossy but safe), so future block types degrade the same way.
Next Steps
Preference Rewards
Conversation-shaped tasks pair naturally with preference-based (A ≻ B) rewards

