Skip to main content

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

Introduction

Most environments serve their prompt with get_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

Implement get_messages() instead of get_prompt() — one of the two is required, and each defaults to delegating to the other:
A Message has a role ("system", "user", or "assistant") and content blocksTextBlock / 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:
The format argument mirrors list_tools(format=...):

Rich conversations: reasoning and tool history

Requires openreward>=0.1.157.
A conversation the policy must continue often contains more than text: past reasoning (interleaved-thinking models carry reasoning across turns, and OpenReward reasoning is plain text meant to be passed back in) and tool interactions (an agent transcript is a conversation). Three further block types cover this, following Anthropic message semantics — everything is a content block, and no 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.
The example above builds blocks by hand; if you’d rather keep conversations in your task data, store them in the canonical 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 than 0.1.157 (which has no /messages route) the client falls back the same way. A harness can adopt get_messages() unconditionally.
  • Old harnesses still work against conversation environments. /prompt is 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_types in the tool list (absent on servers before 0.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