Skip to main content

Goals

  • Understand when to grade a plain assistant message rather than a tool call
  • Mark a tool with @terminal so it is hidden from the model
  • Use session.is_assistant_message_final() and session.call_terminal_tool() in a rollout loop

Prerequisites

Introduction

Most environments end a rollout with a submit-style tool call: the model calls submit_answer(answer="...") and the environment grades it. That works, but it forces the model through a tool call it would not otherwise make — and for many tasks (QA, summarisation, reasoning, chat) the answer is simply the model’s last message. A terminal tool inverts this. You mark one tool with @terminal, and:
  • The tool is hidden from environment.list_tools() and session.list_tools() — the model never sees it and cannot call it directly.
  • is_assistant_message_final() returns True. 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 call submit and needs nudging.
  • The harness passes that message’s text to session.call_terminal_tool(text), which routes it into your grading tool.
The result: the model just writes its answer, and you still get a normal 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.
Here 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:
  • @tool is still required. @terminal marks an existing tool as terminal; it does not declare one.
  • At most one terminal tool, counting tools contributed by declared toolsets. Two @terminal tools is an error.
  • At most one argument. The input model may declare one field (which receives the message) or none. Extra fields like confidence are 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:
Prefer the session methods inside a rollout — they reuse the session’s cached tool list and account for any session toolset, whose terminal tool takes precedence over the environment’s.

Calling it from environment code

Inside the environment, call_terminal_tool() does the same routing:
The tool also remains callable by name (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.