Skip to main content

Goals

  • Understand the difference between shared tools and task-specific tools
  • Use @tool(shared=False) for tools that should only be available within a session
  • Use list_task_tools() for fully dynamic tools generated based on the current task

Prerequisites

Introduction

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.

Using @tool(shared=False)

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, TextBlock
from pydantic import BaseModel, Field

class 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)
        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.

Using list_task_tools()

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, TextBlock
from openreward.environments.types import ListToolsOutput, ToolSpec
from pydantic import BaseModel, Field

class 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:
  • Multiple-choice tasks: submit_answer + select_option
  • Open-book tasks: submit_answer + search
  • Closed-book tasks: submit_answer only
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.

How Clients Access Task-Specific Tools

The SDK provides two different list_tools() methods depending on whether you have a session:
from openreward import OpenReward

client = OpenReward(api_key="your-api-key")
environment = client.environments.get(name="username/my-environment")

# Environment-level: returns shared tools only
shared_tools = await 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 tools
async with environment.session(split="train", index=0) as session:
    all_tools = await 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())

Next Steps

Using Toolsets

Create reusable tool collections and compose them into environments