Tools and images
Give the math environment a calculator, then extend the same pattern to image observations. A rollout can alternate between model turns and your environment while River preserves the token sequence used for training.
4 of 5 · Previous: Policy versions and trajectory control · Next: Checkpoints and evaluation
Add a calculator
Add this tool and environment to the math example. The decorator derives the model-facing schema from Python type hints and the docstring:
import operator
from typing import Literal
@rl.tool
async def calculator(a: float, operation: Literal["+", "-", "*", "/"], b: float) -> str:
"""Calculate an arithmetic operation on two numbers."""
operations = {
"+": operator.add,
"-": operator.sub,
"*": operator.mul,
"/": operator.truediv,
}
return str(operations[operation](a, b))
class ToolMathEnv(MathEnv):
tools = [calculator]
async def reset(self, row):
messages = await super().reset(row)
messages[0]["content"] += " Use the calculator when it helps."
return messagesIn the engine, use env=ToolMathEnv and allow more turns:
budget = rl.Budget(
max_turns=4,
max_generated_tokens=8192,
max_context_tokens=16_384,
max_turn_tokens=2048,
segment_tokens=1024,
tool_output_tokens=1024,
)Pass this as budget=budget when creating the engine. The final numeric answer
uses the same reward function; calling the tool does not itself earn reward.
The default Env.on_turn executes declared tool calls and returns their results
to the model. When the model replies without tool calls, the rollout finishes
and reward runs. Tool execution happens in your Python process, so tools can
call your databases, simulators, or services.
Default tool dispatch can execute several calls concurrently. For a browser or
other environment where action order matters, override on_turn and execute
the actions in the required order.
Give each trajectory its own environment
Pass an environment class or factory when it holds mutable state:
# Each rollout gets a fresh instance and private browser state.
engine = rl.RolloutEngine(
model,
env=lambda: BrowserEnv(session),
renderer=renderer,
budget=budget,
)Here BrowserEnv is your environment implementation. Open its browser in
reset, handle actions in on_turn, and close it in close. Passing a single
instance shares that object across trajectories; use that only when the
environment is stateless or explicitly manages state by trajectory ID.
Return only new environment messages from on_turn, or None when the
episode is finished. The engine appends them to the sampled conversation.
Do not return the whole conversation after every turn.
Images
For a vision-capable model, upload an image and include its handle alongside
text in a message. This example returns a browser screenshot from reset or
on_turn:
from river_client.renderers import image_part
async def screenshot_observation(session, png_bytes):
image = await session.upload_image_async(png_bytes)
return {
"role": "user",
"content": [
{"type": "text", "text": "The page after your last action."},
image_part(image),
],
}The upload can run while other trajectories sample. Later requests refer to the handle instead of repeatedly sending the image bytes. The renderer expands image positions into the model's input format, and the RL library checks their alignment before building training data.
For an image-bearing tool reply, use role="tool", the matching
tool_call_id, and a list of text and image content parts. Produce this message
in your custom on_turn; decorated tool functions return strings.
Handles belong to the session that uploaded them. Upload evaluation images through the evaluation session. The default image TTL is six hours without use; each accepted request containing a handle refreshes its TTL. Keep the bytes needed to restore an expired handle. RL checkpoints persist referenced image bytes for recovery into a new session.
Budget.max_images defaults to no additional image-count cap. Token/context
limits and the model's image limits still apply. Release handles once no future
rollout, training request, or recovery state needs them.
Bound long conversations
Turn, generated-token, and context budgets bound each trajectory independently
of wall-clock load. Use tool_output_tokens to keep large text tool results
from consuming the entire context. A watchdog such as environment_timeout
detects a stuck environment; it is not a task score.
For deliberate context compaction, traj.rewrite(messages, chunks=...) replaces
the conditioning context while retaining earlier training spans. It requires a
new prefill. Keep the original context whenever it still fits and is useful.
Raise rl.InfrastructureError for an unavailable browser or external service.
That stops the run instead of teaching the model that an infrastructure outage
was an unsuccessful action. Choose how that environment recovers in
the next guide.