Goals
- Understand when to grade a plain assistant message rather than a tool call
- Mark a tool with
@terminalso it is hidden from the model - Use
session.is_assistant_message_final()andsession.call_terminal_tool()in a rollout loop
Prerequisites
- An OpenReward account
- An OpenReward API key
- Completion of the Your First Environment tutorial
Introduction
Most OpenReward environments end an episode on a tool call: the model callssubmit_answer(answer="..."), the environment grades it, and the rollout stops. The environment decides when the episode is over, because it is the environment that sees the final action.
But in many cases the last thing an agent should do is write a message, not call a tool — reporting to the user that the changes have been made to the code, answering a question, summarising what it found. Forcing a submit call there is an artificial extra step, and it means the environment never gets to see the thing you actually want to grade.
When the final action is an assistant message, the termination event happens on the harness side, not the environment side: the environment can’t observe a message with no tool calls, so only the harness knows the episode has ended. Terminal tools exist to accommodate this. You mark one tool with @terminal, the harness recognises that a message with no tool calls ends the rollout, and it hands that message back to the environment for grading.
Concretely, marking a tool with @terminal means:
- The tool is hidden from
environment.list_tools()andsession.list_tools()— the model never sees it and cannot call it directly. is_assistant_message_final()returnsTrue. A harness checks this once, up front, to learn that in this environment a message with no tool calls is the finished answer — rather than a model that forgot to callsubmitand needs nudging.- The harness passes that message’s text to
session.call_terminal_tool(text), which routes it into your grading tool.
ToolOutput with a reward.
Declaring a terminal tool
@terminal sits on top of @tool — it marks an existing tool as terminal rather than declaring one, so @tool is still required. (The two are order-independent, but reading @terminal first says what the tool is before how it’s wired.) The tool accepts one argument: a Pydantic model with a single field, which receives the assistant’s message. The field can be named whatever reads best.
session.list_tools() returns only search. An environment whose only tool is the terminal tool is valid too — that is the pure “answer the question” case, with no tools at all shown to the model.
Terminal tools with no arguments
Sometimes the message itself doesn’t matter: the model works in a sandbox, says “done”, and grading runs against environment state. Give the terminal tool no arguments and the message is simply dropped.call_terminal_tool(text) still works — the text is ignored — and call_terminal_tool() is valid too.
Rules
An environment fails at server startup if any rule is broken:@toolis still required.@terminalmarks an existing tool as terminal; it does not declare one.- At most one terminal tool, counting tools contributed by declared toolsets. Two
@terminaltools is an error. - At most one argument. The input model may declare one field (which receives the message) or none. Extra fields like
confidenceare an error — there is only one thing to pass in.
Using it in a rollout
The client reports the environment’s choice, so a harness can support both styles with the same loop:is_assistant_message_final() returns False for environments without a terminal tool, in which case a message with no tool calls means whatever your harness already does with it. Environments served by an SDK version older than 0.1.139 also report False.
Both methods are available on the sync client (Session) and the async client (AsyncSession), and on the environment itself:
Calling it from environment code
Inside the environment,call_terminal_tool() does the same routing:
session.call_tool("grade", {"answer": "Paris"})) — hiding it from the tool list stops the model from discovering it, not your own code from using it.
