Motivation
After PR #85 lands, the simulation backend supports policy rollout, evaluation, dataset recording, and physics introspection — but the benchmarking surface is a single eval_policy(success_fn=...) where success_fn is typed str | Callable and the only registered string value is "contact". That works for ad-hoc sparse-success experiments but does not generalize to established benchmarks (LIBERO, Meta-World, RoboSuite, ManiSkill, user-authored tasks), each of which has a richer notion of "what constitutes a task."
A LIBERO-shaped register_task(scene, goal_predicate, init_sampler) API would fix LIBERO specifically but wouldn't fit Meta-World's dense reward or ManiSkill's procedural scenes. The correct abstraction is the protocol the eval loop calls into, not a benchmark-specific schema.
Proposal
Introduce a BenchmarkProtocol ABC and a string-keyed registry, mirroring the existing register_urdf shape. Each adapter is kinematically tied to one or a small set of robots (LIBERO's BDDL and scene files reference Panda body names, Meta-World hardcodes Sawyer, RoboSuite parameterizes per-task over a fixed robot list), so robot compatibility is first-class metadata on the protocol — without it, agents will silently evaluate with the wrong robot.
# strands_robots/simulation/benchmark.py
class BenchmarkProtocol(ABC):
"""Protocol every benchmark (LIBERO, Meta-World, custom) implements."""
max_steps: int
# --- Robot compatibility (first-class metadata) ---
@property
@abstractmethod
def supported_robots(self) -> list[str]:
"""Registry config names this benchmark accepts.
Empty list means any robot (unusual; dense-reward benchmarks rarely generalize)."""
@property
@abstractmethod
def default_robot(self) -> str:
"""Robot on_episode_start loads when the sim is empty."""
# --- Lifecycle hooks ---
def on_episode_start(self, sim: SimEngine, rng: random.Random) -> None:
"""Per-episode init. Called after sim.reset() and before the first obs.
Default impl ensures a supported robot exists: if the sim has no robots,
add default_robot; otherwise validate the loaded robot's data_config is
in supported_robots. Override to layer on per-episode randomization."""
@abstractmethod
def on_step(self, sim: SimEngine, obs: dict, action: dict) -> StepInfo:
"""Return dense reward + done flag + info dict for this step."""
@abstractmethod
def is_success(self, sim: SimEngine) -> bool:
"""Terminal success predicate."""
def is_failure(self, sim: SimEngine) -> bool:
"""Optional early-termination failure condition. Default: False."""
PolicyRunner.evaluate grows a spec: BenchmarkProtocol | None parameter. Before episode 1 it must validate the sim's current robot against spec.supported_robots and return a structured error dict (not an exception) on mismatch, surfacing the allowed list. The loop itself becomes:
for ep in range(n_episodes):
sim.reset()
spec.on_episode_start(sim, rng)
cumulative_reward = 0.0
for step in range(spec.max_steps):
obs = sim.get_observation(robot_name=robot_name)
action = policy.get_actions(obs, instruction)[0]
sim.send_action(action, robot_name=robot_name)
info = spec.on_step(sim, obs, action)
cumulative_reward += info.reward
if info.done or spec.is_success(sim) or spec.is_failure(sim):
break
The existing success_fn: str | Callable path subsumes cleanly: "contact" and user callables both become trivial BenchmarkProtocol adapters under the hood, kept for backwards compatibility.
Registry + tool-spec surface
Three new agent-tool actions (same pattern as register_urdf / list_urdfs):
list_benchmarks — enumerate registered benchmarks.
evaluate_benchmark(name, policy_provider, policy_config, n_episodes, ...) — run any registered benchmark.
register_benchmark_from_file(name, spec_path) — load a user-authored YAML/JSON spec (declarative named-predicate DSL, no Python eval) so agents can author benchmarks without writing code.
Declarative spec format sketch:
name: drawer-open
scene: /path/to/drawer_scene.xml
max_steps: 300
init_state:
- randomize: { type: uniform, joint: drawer_slide, low: -0.05, high: 0.0 }
success:
all:
- joint_above: [drawer_slide, 0.15]
failure:
any:
- body_below_z: [gripper, -0.1]
dense_reward:
- term: { type: distance_neg, body_a: gripper, body_b: drawer_handle, weight: 1.0 }
- term: { type: joint_progress, joint: drawer_slide, weight: 5.0 }
This is LLM-authorable and safely sandboxed.
Adapters (separate follow-up PRs)
With the protocol in place, each real benchmark becomes a thin adapter, not a core change. Adapters live in
optional extras (strands-robots[benchmark-libero] etc.); the core package stays dependency-free.
Tracked as sub-tasks:
Scope for this issue
In scope:
BenchmarkProtocol ABC + StepInfo dataclass in strands_robots/simulation/benchmark.py.
- Widen
PolicyRunner.evaluate to accept spec: BenchmarkProtocol alongside the existing success_fn (kept for backcompat).
register_benchmark / list_benchmarks / evaluate_benchmark tool actions + tool_spec.json entries.
- Named-predicate helper library (
on_table, grasped, joint_above, inside_region, body_above_z, …) as a foundation for declarative specs.
- Declarative YAML/JSON spec loader (
register_benchmark_from_file), restricted to the named-predicate DSL.
- One reference adapter (suggest:
MetaWorldAdapter, since it's Python-native and doesn't need BDDL parsing).
- Tests: protocol contract, cumulative-reward accounting, per-episode randomization reproducibility by seed, declarative-spec loader.
Out of scope (follow-up issues):
- BDDL parser / LIBERO adapter.
- RoboSuite / ManiSkill adapters.
- Dense-reward curriculum tooling, RL-style training harness.
- Replacing the current
success_fn path (kept working for the whole backcompat window).
Why this belongs on the roadmap
The PR #85 architecture (SimEngine ABC + backend-agnostic PolicyRunner + string-keyed register_urdf) already does 80% of the work. Adding BenchmarkProtocol uses the same patterns and unlocks every standard benchmark through adapters — without committing the core to any single benchmark's conventions.
Relevant PR: #85. Related labels: simulation, enhancement, roadmap.
Motivation
After PR #85 lands, the simulation backend supports policy rollout, evaluation, dataset recording, and physics introspection — but the benchmarking surface is a single
eval_policy(success_fn=...)wheresuccess_fnis typedstr | Callableand the only registered string value is"contact". That works for ad-hoc sparse-success experiments but does not generalize to established benchmarks (LIBERO, Meta-World, RoboSuite, ManiSkill, user-authored tasks), each of which has a richer notion of "what constitutes a task."A LIBERO-shaped
register_task(scene, goal_predicate, init_sampler)API would fix LIBERO specifically but wouldn't fit Meta-World's dense reward or ManiSkill's procedural scenes. The correct abstraction is the protocol the eval loop calls into, not a benchmark-specific schema.Proposal
Introduce a
BenchmarkProtocolABC and a string-keyed registry, mirroring the existingregister_urdfshape. Each adapter is kinematically tied to one or a small set of robots (LIBERO's BDDL and scene files reference Panda body names, Meta-World hardcodes Sawyer, RoboSuite parameterizes per-task over a fixed robot list), so robot compatibility is first-class metadata on the protocol — without it, agents will silently evaluate with the wrong robot.PolicyRunner.evaluategrows aspec: BenchmarkProtocol | Noneparameter. Before episode 1 it must validate the sim's current robot againstspec.supported_robotsand return a structured error dict (not an exception) on mismatch, surfacing the allowed list. The loop itself becomes:The existing
success_fn: str | Callablepath subsumes cleanly:"contact"and user callables both become trivialBenchmarkProtocoladapters under the hood, kept for backwards compatibility.Registry + tool-spec surface
Three new agent-tool actions (same pattern as
register_urdf/list_urdfs):list_benchmarks— enumerate registered benchmarks.evaluate_benchmark(name, policy_provider, policy_config, n_episodes, ...)— run any registered benchmark.register_benchmark_from_file(name, spec_path)— load a user-authored YAML/JSON spec (declarative named-predicate DSL, no Python eval) so agents can author benchmarks without writing code.Declarative spec format sketch:
This is LLM-authorable and safely sandboxed.
Adapters (separate follow-up PRs)
With the protocol in place, each real benchmark becomes a thin adapter, not a core change. Adapters live in
optional extras (
strands-robots[benchmark-libero]etc.); the core package stays dependency-free.Tracked as sub-tasks:
Scope for this issue
In scope:
BenchmarkProtocolABC +StepInfodataclass instrands_robots/simulation/benchmark.py.PolicyRunner.evaluateto acceptspec: BenchmarkProtocolalongside the existingsuccess_fn(kept for backcompat).register_benchmark/list_benchmarks/evaluate_benchmarktool actions +tool_spec.jsonentries.on_table,grasped,joint_above,inside_region,body_above_z, …) as a foundation for declarative specs.register_benchmark_from_file), restricted to the named-predicate DSL.MetaWorldAdapter, since it's Python-native and doesn't need BDDL parsing).Out of scope (follow-up issues):
success_fnpath (kept working for the whole backcompat window).Why this belongs on the roadmap
The PR #85 architecture (
SimEngineABC + backend-agnosticPolicyRunner+ string-keyedregister_urdf) already does 80% of the work. AddingBenchmarkProtocoluses the same patterns and unlocks every standard benchmark through adapters — without committing the core to any single benchmark's conventions.Relevant PR: #85. Related labels:
simulation,enhancement,roadmap.