Skip to main content

Overview

Synthetic Worlds let you develop and test agents that use tools — without calling the real tools. Instead of hitting live APIs, databases, or services, your tool calls get realistic responses generated on the fly. This is useful when:
  • Real tools are slow or expensive — external APIs, database writes, third-party services
  • You need deterministic behavior — reproducible runs for debugging and CI
  • The real service doesn’t exist yet — build your agent against the contract, not the implementation
  • You want to test failure handling — simulate timeouts, errors, and rate limits

How it works

You create a world — an isolated session that intercepts your tool calls and returns generated responses. Define your tools as normal Python functions with type hints. The SDK extracts the schema automatically and sends it to the backend, which generates a realistic response that matches the return type.
from rdk.synthetic import create_synthetic_world

with create_synthetic_world(project_id="my-project") as world:
    @world.tool
    def get_user(user_id: str) -> dict:
        """Fetch a user by ID."""
        ...

    @world.tool
    def create_order(product_id: str, quantity: int) -> dict:
        """Create a new order."""
        ...

    user = get_user("u1")
    print(user)  # {"id": "u1", "name": "Alice", "email": "alice@acme.com"}

    order = create_order("prod_42", 3)
    print(order)  # {"id": "ord_1", "product_id": "prod_42", "quantity": 3, "status": "created"}
When the with block exits, the world is destroyed and resources are freed.

Setup

1. Set environment variables

export RDK_API_KEY="your-api-key"
export RDK_PLATFORM_URL="https://your-platform.example.com"  # defaults to http://localhost:7878

2. Create a world and register tools

from rdk.synthetic import create_synthetic_world

world = create_synthetic_world(project_id="my-project")
Or use the context manager (recommended):
with create_synthetic_world(project_id="my-project") as world:
    # register tools and make calls here
    ...

Registering tools

Using the @world.tool decorator

The simplest way. Your function’s type hints and docstring become the tool schema automatically:
@world.tool
def search_products(query: str, limit: int = 10) -> dict:
    """Search the product catalog."""
    ...
The SDK extracts:
  • name from the function name (search_products)
  • description from the docstring (Search the product catalog.)
  • parameters from the type hints (query: str required, limit: int optional with default)
  • return type from the return annotation (dict)
Calling search_products("shoes", limit=5) routes through the synthetic backend and returns a generated response.

Using register_tool

For cases where you need manual control over the schema:
from rdk.synthetic import ToolSpec

spec = ToolSpec(
    name="get_weather",
    description="Get current weather for a city",
    parameters={
        "type": "object",
        "properties": {
            "city": {"type": "string"},
            "units": {"type": "string"},
        },
        "required": ["city"],
    },
    returns={
        "type": "object",
        "properties": {
            "temperature": {"type": "number"},
            "condition": {"type": "string"},
        },
    },
)

world.register_tool(spec)
result = world.call("get_weather", {"city": "Tokyo"})

Generation modes

Control how responses are generated by setting the mode parameter:
ModeDescriptionUse case
schema_onlyGenerates responses from the tool schema aloneFast iteration, no trace history needed
examplesGrounds generation in real tool call examples from your tracesMore realistic responses based on actual production data
statefulRemembers prior calls within the sessionMulti-step workflows where consistency matters
# Schema-only (default, fastest)
with create_synthetic_world(project_id="proj", mode="schema_only") as world:
    ...

# Grounded in real examples from your traces
with create_synthetic_world(project_id="proj", mode="examples") as world:
    ...

# Stateful — "create user" in step 1 is reflected in "get user" in step 5
with create_synthetic_world(project_id="proj", mode="stateful") as world:
    ...

Deterministic output

Pass a seed for reproducible responses. The same seed, tool, and input produce the same output every time:
with create_synthetic_world(project_id="proj", seed=42) as world:
    @world.tool
    def get_user(user_id: str) -> dict:
        """Fetch a user."""
        ...

    # Same result every run
    user = get_user("u1")

Simulating failures

Test your agent’s error handling by injecting failures:
with create_synthetic_world(
    project_id="proj",
    seed=42,
    failure_profile={"rate": 0.3, "codes": ["timeout", "internal_error"]},
) as world:
    @world.tool
    def call_api(endpoint: str) -> dict:
        """Call an external API."""
        ...

    try:
        result = call_api("/users")
    except Exception as e:
        print(f"Handled error: {e}")  # 30% chance of timeout or 500
Failures are deterministic when a seed is set — the same step always produces the same failure or success. Available error codes: timeout, internal_error, rate_limit, not_found, bad_request.

Idempotency

Pass an idempotency_key to cache responses. Repeated calls with the same key return the cached result without hitting the backend:
result1 = world.call("get_user", {"id": "u1"}, idempotency_key="req-1")
result2 = world.call("get_user", {"id": "u1"}, idempotency_key="req-1")
# result1 == result2, second call is a cache hit

Resetting a world

Reset the step counter and optionally clear all cached and stateful data:
world.reset()              # Reset step counter
world.reset(hard=True)     # Reset counter + clear cache + clear stateful memory

Choosing a model

By default, the backend picks the generation model. You can override it:
with create_synthetic_world(
    project_id="proj",
    model="gpt-4o",
) as world:
    ...

Full example

from rdk.synthetic import create_synthetic_world

with create_synthetic_world(
    project_id="crm-agent",
    mode="stateful",
    seed=42,
) as world:
    @world.tool
    def create_customer(name: str, email: str) -> dict:
        """Create a new customer record."""
        ...

    @world.tool
    def get_customer(customer_id: str) -> dict:
        """Look up a customer by ID."""
        ...

    @world.tool
    def create_ticket(customer_id: str, subject: str) -> dict:
        """Open a support ticket for a customer."""
        ...

    # Simulate a multi-step agent workflow
    customer = create_customer("Alice", "alice@example.com")
    print(f"Created: {customer}")

    looked_up = get_customer(customer["id"])
    print(f"Found: {looked_up}")

    ticket = create_ticket(customer["id"], "Login issue")
    print(f"Ticket: {ticket}")

    # Reset and replay
    world.reset(hard=True)

See Also