By default, all methods decorated with @tool are shared — they are returned by environment.list_tools() and are available to any client before a session even starts. This works well for tools like bash or read_file that are the same regardless of the task.But some environments need to present different tool interfaces depending on the task. For example, an environment that tests an agent’s ability across multiple different configurations might give the agent a bash tool for some tasks, a select_option tool for multiple-choice tasks, and a completely different set of tools for other task types. The tool interface itself is part of what varies across tasks.Task-specific tools solve this by making tools available only through session.list_tools(), which requires an active session with a task loaded. This lets you vary the set of tools an agent sees on a per-task basis.
The simplest way to create a task-specific tool is to pass shared=False to the @tool decorator. These tools are not returned by environment.list_tools() but are returned by session.list_tools().
from openreward.environments import Environment, tool, ToolOutput, TextBlockfrom pydantic import BaseModel, Fieldclass SearchParams(BaseModel): query: str = Field(..., description="Search query")class SubmitParams(BaseModel): answer: str = Field(..., description="Your answer")class ToolVariantEnvironment(Environment): # Shared tool — returned by environment.list_tools() @tool async def submit_answer(self, params: SubmitParams) -> ToolOutput: """Submit the final answer""" correct = params.answer.strip() == self.task_spec["answer"] return ToolOutput( blocks=[TextBlock(text="Correct!" if correct else "Incorrect.")], reward=1.0 if correct else 0.0, finished=True, ) # Task-specific tool — only returned by session.list_tools() # Some tasks give the agent a search tool, others don't @tool(shared=False) async def search(self, params: SearchParams) -> ToolOutput: """Search a knowledge base""" results = search_kb(params.query) # your own search implementation return ToolOutput( blocks=[TextBlock(text=results)], ) @classmethod def list_splits(cls): return ["train"] @classmethod def list_tasks(cls, split): return [ {"question": "What is the capital of France?", "answer": "Paris", "tools": ["search"]}, {"question": "What is 2+2?", "answer": "4", "tools": []}, ] def get_prompt(self): return [TextBlock(text=self.task_spec["question"])]
In this example, submit_answer is shared and always visible. The search tool is task-specific — it only appears when a session is active. Combined with list_task_tools() (below), you can control exactly which task-specific tools appear for each task.
For fully dynamic tools whose definitions depend on the task, override the list_task_tools() instance method. This lets you generate tool specifications at runtime based on self.task_spec — controlling exactly which tools the agent sees for each task.
from openreward.environments import Environment, tool, ToolOutput, TextBlockfrom openreward.environments.types import ListToolsOutput, ToolSpecfrom pydantic import BaseModel, Fieldclass SearchParams(BaseModel): query: str = Field(..., description="Search query")class SelectOptionParams(BaseModel): option: str = Field(..., description="Selected option (A, B, C, or D)")class SubmitParams(BaseModel): answer: str = Field(..., description="Your answer")class MultiInterfaceEnvironment(Environment): """An environment that presents different tool interfaces per task.""" @tool async def submit_answer(self, params: SubmitParams) -> ToolOutput: """Submit the final answer""" correct = params.answer.strip() == self.task_spec["answer"] return ToolOutput( blocks=[TextBlock(text="Correct!" if correct else "Incorrect.")], reward=1.0 if correct else 0.0, finished=True, ) def list_task_tools(self) -> ListToolsOutput: """Present different tools depending on the task type.""" tools = [] task_type = self.task_spec.get("type") if task_type == "multiple_choice": tools.append(ToolSpec( name="select_option", description="Select an answer option (A, B, C, or D)", input_schema=SelectOptionParams.model_json_schema(), )) elif task_type == "open_book": tools.append(ToolSpec( name="search", description="Search a knowledge base for relevant information", input_schema=SearchParams.model_json_schema(), )) return ListToolsOutput(tools=tools) @classmethod def list_splits(cls): return ["train"] @classmethod def list_tasks(cls, split): return [ {"type": "multiple_choice", "question": "Capital of France?", "answer": "B"}, {"type": "open_book", "question": "Explain the photoelectric effect.", "answer": "..."}, {"type": "closed_book", "question": "What is 2+2?", "answer": "4"}, ] def get_prompt(self): return [TextBlock(text=self.task_spec["question"])]
In this example, the agent sees different tool interfaces depending on the task:
Note that list_task_tools() is an instance method (not a classmethod) because it needs access to self.task_spec to determine which tools to generate.When list_task_tools() is used, the returned tools are combined with any @tool(shared=False) tools and all shared tools when a client calls session.list_tools().For a real-world example of this pattern, see GeneralReasoning/toolathon-gym — an environment that presents different tool configurations per task to test agents across varied interfaces.
The SDK provides two different list_tools() methods depending on whether you have a session:
from openreward import OpenRewardclient = OpenReward(api_key="your-api-key")environment = client.environments.get(name="username/my-environment")# Environment-level: returns shared tools onlyshared_tools = environment.list_tools()print(f"Shared tools: {[t.name for t in shared_tools]}")# → Shared tools: ['submit_answer']# Session-level: returns shared tools + task-specific toolswith environment.session(split="train", index=0) as session: all_tools = session.list_tools() print(f"All tools: {[t.name for t in all_tools]}") # → All tools: ['submit_answer', 'select_option']
Under the hood:
environment.list_tools() calls the /tools endpoint, which returns only shared tools
session.list_tools() calls the /task_tools endpoint, which returns shared tools combined with task-specific tools (both @tool(shared=False) and those from list_task_tools())