Tool Catalog#
The open-robot-skills registry ships seven tool bundles
under tools/. Each bundle is a directory with a SKILL.md (frontmatter
declares the tool list and a gap.requires capability block) and a
tools.py of @tool-decorated functions. Heavy model code lives in
private modules and is imported lazily — importing a bundle never pulls
torch. Tool I/O uses numpy arrays plus the gap.types types
(Se3Pose, JointState, Trajectory, WorldConfig, Mask,
BoundingBox2D, OrientedBoundingBox, PointCloud, …); failures
raise gap.errors.ToolError, PlanningFailed, or PerceptionFailed.
Each bundle declares a pip extra named after itself, so installs are one resolver run:
uv sync --extra sam3 --extra grounding-dino --extra geometry # = --extra quickstart
Note
Requirements
Requirements vary per bundle: sam3 and grounding-dino need a CUDA
GPU and downloadable weights, curobo needs a CUDA toolkit at install
time, gemini-er and vlm need API keys, molmo needs a self-hosted
vLLM endpoint, and geometry needs nothing. gap check reports which
bundles are runnable from each bundle’s gap.requires declaration.
Bundle |
Category |
Tools |
|
Env vars (default) |
|---|---|---|---|---|
perception |
6 |
|
|
|
perception |
1 |
|
|
|
perception |
1 |
|
|
|
perception |
3 |
|
|
|
perception |
2 |
|
|
|
planning |
10 |
|
— ( |
|
perception |
18 |
none (CPU, no weights) |
— |
sam3#
Segment Anything 3: text-, point-, and box-prompted instance segmentation plus a stateful streaming video tracker. Source: tools/sam3.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Requirements. gap.requires: {gpu, weights}. Extra sam3 installs
torch, torchvision, the upstream sam3 package pinned to b26a5f3,
plus decord2 and pycocotools. Weights download on the first model
build. GAP_SAM3_DEVICE selects the device (default cuda); the image
model also runs on cpu (slowly), the video tracker is CUDA-only in
practice. The upstream package’s metadata pins numpy==1.26 — the
repo’s uv.lock relaxes it, but plain pip may downgrade numpy.
Behavior.
Tracker prompt precedence is box > point > text; a point prompt is converted to a small box (10% of image). No prompt at all raises
ToolError; a prompt that finds nothing returnsobject_present=Falsewith an emptytracker_id(no exception).Sessions and TTL: tracker sessions idle longer than 120 s are evicted lazily on the next tracker call; an unknown or evicted
tracker_idraisesToolError.Drift handling in
tracker_update: a mask-area jump > 1.5× the running median, or confidence < 0.30, holds the last good mask withconfidence=0.0andobject_present=True(“drifting — skip this frame”); after 5 consecutive drift hitsobject_present=Falseand the caller should re-init.The video tracker JIT-compiles Triton NMS kernels via the
CCenv var; the bundle forcesCCto a real compiler before tracker use (a staleCCotherwise surfaces asFileNotFoundErrorintracker_init).
grounding-dino#
Zero-shot 2D detection (IDEA-Research/grounding-dino-base via
transformers). Source: tools/grounding-dino.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
Requirements. gap.requires: {gpu, weights}. Extra
grounding-dino installs torch>=2.7 and transformers>=4.41; weights
download from HuggingFace on first call. GAP_DINO_DEVICE (default
cuda; CPU works but is slow) and GAP_DINO_MODEL (default
IDEA-Research/grounding-dino-base). The module also exposes a
weights_cached() probe so gap check can report weight presence
without downloading.
Behavior. The text encoder expects period-terminated phrases
("red cube. green cube."); a missing final period is appended
automatically, but separate multiple objects yourself. The 0.20/0.20
defaults are deliberately low for recall; zero or negative thresholds
fall back to the defaults. label strings are the matched text spans,
not your full query. Empty detections means not-found — don’t retry
blindly with the same prompt.
gemini-er#
Hosted open-vocabulary 2D detection on Gemini Robotics-ER. Zero GPU. Source: tools/gemini-er.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
Requirements. gap.requires: {env_any: [GOOGLE_API_KEY, GEMINI_API_KEY]}. Extra gemini-er installs google-genai.
GAP_GEMINI_ER_MODEL overrides the model (default
gemini-robotics-er-1.5-preview).
Behavior. The model emits Gemini’s box_2d convention
([ymin, xmin, ymax, xmax] normalized 0–1000); the tool converts to
pixel-space {x1, y1, x2, y2} and clamps to image bounds. score
defaults to 1.0 when the model reports none. No match (or unparseable
output) returns an empty list, never an error. This is the hosted,
GPU-free alternative to grounding-dino.detect and
molmo.point_prompt.
molmo#
Visual pointing and Q&A against a self-hosted Molmo vLLM endpoint (OpenAI-compatible). The bundle itself is zero-GPU (httpx client only), but Molmo has no hosted API. Source: tools/molmo.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
|
|
|
|
|
|
Requirements. gap.requires: {env: [GAP_MOLMO_BASE_URL]}; no local
deps. GAP_MOLMO_MODEL selects the served model (default
allenai/Molmo2-8B). The SKILL.md ships a vLLM hosting recipe:
CUDA_VISIBLE_DEVICES=0 PYTHONNOUSERSITE=1 \
python -m vllm.entrypoints.openai.api_server \
--model allenai/Molmo2-8B --trust-remote-code --dtype bfloat16 \
--port 8122 --gpu-memory-utilization 0.5 \
--max-model-len 4096 --max-num-batched-tokens 4096
export GAP_MOLMO_BASE_URL=http://127.0.0.1:8122/v1
On a dedicated GPU, --gpu-memory-utilization 0.85 --max-num-batched-tokens 8192 gives roughly 2× perception throughput.
Behavior. point_prompt sends the canonical "Point at <query>"
prompt and parses all four Molmo point output formats (Molmo2
<points coords=...>, Molmo1 <point x= y=>, legacy <points x1= y1=>, plain x, y), converting normalized coordinates to pixels.
query_yes_no coerces with the verbatim substring rule: answer is
true iff "yes" appears in the lowercased reply. A backend unreachable
after 3 retries (exponential backoff) raises ToolError.
vlm#
Free-form and yes/no visual Q&A against a hosted VLM, behind a two-provider switch. Zero GPU. Source: tools/vlm.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
|
|
|
Requirements. gap.requires: {env_any: [OPENROUTER_API_KEY, GAP_VLM_API_KEY, GAP_VLM_PROJECT_ID]} — one provider must be
configured. Six env vars:
Env var |
Meaning |
Default |
|---|---|---|
|
|
|
|
Model name (OpenRouter accepts Gemini slugs with or without the |
|
|
OpenAI-compatible endpoint (openrouter provider) |
|
|
API key for the openrouter provider (or |
— |
|
GCP project (vertex provider) |
— |
|
Vertex region |
|
The openrouter provider talks to OpenRouter’s OpenAI-compatible
chat-completions API (OPENROUTER_API_KEY, or GAP_VLM_API_KEY), and
points at any other OpenAI-compatible server (e.g. local vLLM) via
GAP_VLM_BASE_URL; the vertex provider routes GEMINI models through
google-genai and needs the engine’s vertex extra
(pip install "graph-as-policy[vertex]"). An unknown provider name
raises ToolError listing the valid choices.
Behavior. All providers pin temperature 0.0 and max_tokens 1024;
there is no system-prompt knob by design. images= accepts multiple
context images. query_yes_no appends an explicit “Answer with the
single word YES or NO first” instruction and coerces via the first
standalone yes/no word (word-boundary regex), falling back to the
legacy substring check — without the instruction, affirmative prose
containing no literal “yes” gets silently mislabeled as False (the
documented cream-cheese failure mode that molmo.query_yes_no, which
keeps the verbatim substring rule, is still subject to).
curobo#
NVIDIA cuRobo motion planning: collision-free trajectories, attached-
object transport, constrained linear moves, IK, batch feasibility, and
trajectory validation. Source: tools/curobo.
Trajectories in/out are GaP Trajectory dicts; worlds are WorldConfig
dicts built with geometry.build_world_config.
Requirements. gap.requires: {gpu: true}. cuRobo JIT-compiles CUDA
extensions at install time — build isolation must be off and CUDA_HOME
must match your torch build:
export CUDA_HOME=/usr/local/cuda
uv sync --extra curobo # (pip: pip install -e ".[curobo]" --no-build-isolation)
The extra pins nvidia-curobo to NVlabs SHA 4ea7736 (the “cuRoboV2
research release”) plus cuda-core[cu12]>=0.7, torch>=2.7, and
trimesh. The first planner call per process pays JIT/warmup latency;
planner instances are cached (recreating them per call corrupts CUDA
graph state), and GPU access is serialized by a module lock.
Signatures (planning failure returns success=False; infrastructure
errors raise PlanningFailed):
curobo.plan_to_grasp_poses(
world_config, start_joint_position, grasp_poses: list[Se3Pose],
robot_file="franka.yml", max_attempts=8, use_cuda_graph=False,
position_threshold=0.01, rotation_threshold=0.05, position_threshold_z=0.01,
grasp_pose_is_fingertip=True, grasp_z_clearance=0.005, num_ik_seeds=128,
relax_orientation=False, use_grasp_approach=False, grasp_approach_offset=0.03,
grasp_approach_linear_axis=2, grasp_approach_tstep_fraction=0.7,
use_world_collision=True, robot_collision_sphere_buffer=-0.01,
collision_activation_distance=0.001, ignore_obstacle_names=None,
debug_out_dir=None,
) -> {"success", "trajectory", "goalset_index"}
curobo.plan_with_grasped_object(
world_config, start_joint_position, target_pose, object_name,
robot_file="franka.yml", max_attempts=8, use_cuda_graph=False,
position_threshold=0.05, rotation_threshold=0.1, position_threshold_z=0.05,
num_ik_seeds=128, use_world_collision=True,
robot_collision_sphere_buffer=-0.01, collision_activation_distance=0.01,
surface_sphere_radius=0.001, link_name="attached_object",
remove_obstacles_from_world=False, debug_out_dir=None,
) -> {"success", "trajectory"}
curobo.plan_linear(
start_pose, end_pose, start_joint_position,
robot_file="franka.yml", hold_vec_weight=None,
) -> {"success", "trajectory", "failure_reason"}
curobo.plan_directed_linear(
start_joint_position, start_pose=None, target_pose=None,
allowed_axes=None, explicit_direction=None, distance=None,
endpoint_mode="PROJECT_TO_TARGET", orientation_mode="LOCK",
orientation_target=None, robot_file="franka.yml",
) -> {"success", "trajectory", "failure_reason"}
curobo.plan_grasp_motion(
start_joint_position, grasp_pose, approach_axis="y",
approach_distance=0.12, approach_in_tool_frame=False,
lift_axis="y", lift_distance=0.20, lift_in_tool_frame=False,
robot_file="franka.yml",
) -> {"success", "approach_trajectory", "grasp_trajectory",
"lift_trajectory", "failure_reason"}
curobo.plan_to_pose(
target_pose, start_joint_position, robot_file="franka.yml",
tcp_offset=None, world_config=None, position_threshold=0.005,
rotation_threshold=0.05, max_attempts=4,
) -> {"success", "trajectory"}
curobo.solve_ik(
target_pose, seed_config=None, robot_file="franka.yml",
tcp_offset=None, num_seeds=32, position_threshold=0.005,
rotation_threshold=0.05,
) -> {"success", "joint_config"}
curobo.batch_grasp_feasibility(
world_config, start_state, grasp_poses, grasp_pose_is_fingertip=True,
approach_offset_m=0.10, num_corridor_samples=5, num_ik_seeds=32,
position_threshold=0.005, rotation_threshold=0.05,
collision_activation_distance=0.01, robot_collision_sphere_buffer=None,
robot_file="franka.yml", ignore_obstacle_names=None,
) -> {"feasible", "grasp_ik_ok", "approach_ik_ok",
"corridor_collision_fraction"} # all aligned with input order
curobo.validate_joint_trajectory_robot(
world_config, trajectory, robot_file="franka.yml", use_cuda_graph=False,
robot_collision_sphere_buffer=-0.01, collision_activation_distance=0.01,
ignore_obstacle_names=None,
) -> {"success", "failure_reason", "first_collision_waypoint", # -1 if none
"collision_status_detail"}
curobo.validate_joint_trajectory_grasped(
world_config, trajectory, object_name, robot_file="franka.yml",
use_cuda_graph=False, robot_collision_sphere_buffer=-0.01,
collision_activation_distance=0.01, surface_sphere_radius=0.001,
link_name="attached_object", remove_obstacles_from_world=False,
) -> {"success", "failure_reason", "first_collision_waypoint",
"collision_status_detail"}
Behavior.
Version split. The planners (
plan_to_grasp_poses,plan_with_grasped_object,plan_linear,plan_directed_linear,plan_to_pose) target the cuRobo v0.8 MotionPlanner API, andplan_grasp_motionrequires v0.8.solve_ikandbatch_grasp_feasibilityare built on the v0.7 IKSolver API that v0.8 removed — on a v0.8-only install they raisePlanningFailed; plan via the goalset tools instead. The validators use the v0.7 MotionGen path.Frames and the fingertip offset. Grasp/target poses are in the robot-base frame. With
grasp_pose_is_fingertip=True(the default) grasp positions are fingertip-pad centers, converted solver-side to thepanda_handframe with the 0.1029 m offset along hand Z.Always pass the grasp target’s mesh name in
ignore_obstacle_namesforplan_to_grasp_poses/batch_grasp_feasibility— closing on the target is not a collision.robot_collision_sphere_buffer=-0.01is intentionally negative: it shrinks the robot collision spheres 1 cm to reduce IK failures against dense perception meshes.use_cuda_graphmust stayFalsefor the validators, andvalidate_joint_trajectory_graspedalways invalidates the planner cache afterward so the attachment cannot leak — expect the next planning call to re-create the planner.Set
debug_out_diron the grasp/transport planners to dump world and robot-sphere OBJ/PLY artifacts on failure; plan results (success and failure) are appended to anevents.jsonllog in the same place. The value is treated as a trace directory — artifacts land under<debug_out_dir>/data/gap/skills/curobo/; with no value the fallback is./curobo_debug*in the working directory.
geometry#
Pure-math CPU geometry: back-projection, transforms, OBB fitting, grasp
derivation, and collision-world reconstruction. No GPU, no weights.
Source: tools/geometry. Extra geometry
installs open3d>=0.18 and scikit-learn>=1.3 (cv2/scipy come with GaP
core); the module imports lazily, so light tools work without the extra.
Tool |
Signature |
Returns |
|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Behavior.
OBB
extentholds HALF-extents (gap.types convention).compute_obbis upright-only — rotation only around world Z — and extents use the 2nd/98th percentile of points, not strict min/max. Single-camera 2.5D clouds bias OBB centers a few cm toward the camera.top_down_grasp_candidatesdefaults toz_offset=-0.04(fingertip 4 cm below the OBB top); with0.0the fingers close above the object — a silent empty grip. Grasp Z is clamped to −0.05 m (the table-clearance floor; the LIBERO table top is at world z = 0).poses[0]is the canonical primary,poses[1]its 90°-rotated alternate, followed by the enriched fan for a planner goalset.top_down_grasp_from_obbyaw is not derived from the OBB — fingers may close across the wide axis; use the candidate fan when orientation matters.build_world_config: table removal only runs whentable_z_threshold != 0(typical −0.01); robot-point exclusion is Franka-only and skips non-7-DOF joint states; prefer explicitobject_masksover thetarget_obbprojection fallback (the projection is a corner-AABB approximation inflated by 2 cm). The excluded target is returned inmesh_namesso planners canignore_obstacle_namesit.Output dicts are wrappers: bind
Ref("node.obb"),Ref("node.points"),Ref("node.pose"),Ref("node.position")— never a bareRef("node"). Cloud-consuming tools takepoints=, notpoint_cloud=.
The canonical perception-to-grasp chain#
The tool bundles compose into one recipe that every grasp workflow in the skill catalog instantiates:
grounding-dino.detect / gemini-er.detect / molmo.point_prompt # find the object in 2D
│
▼
sam3.segment_box # pixel-accurate mask inside the detection box
│
▼
geometry.mask_to_world_points # back-project mask + depth to a world cloud
│
▼
geometry.filter_and_compute_obb # DBSCAN filter + upright OBB fit
│
├──▶ geometry.top_down_grasp_candidates # grasp goalset from the OBB
│
└──▶ geometry.build_world_config # collision world (target excluded by mask)
│
▼
curobo.plan_to_grasp_poses # goalset plan; pass the target mesh name
# in ignore_obstacle_names
Pick the detector for your platform — grounding-dino (local GPU),
gemini-er (hosted API, zero GPU), or molmo.point_prompt
(self-hosted pointing) — and the rest of the chain is identical. The
vlm tools slot in wherever semantic judgment is needed: target
disambiguation among detections, checkpoint verification, and policy
termination.
See also#
Skill catalog — the skill bundles that orchestrate these tools into subgraphs.
Skill registries — discovery,
gap check, and thegap toolscommands.Authoring bundles — writing your own tool bundle.
Connector tools — the
robot.*/sim.*tools the connector provides alongside these.Environment variables — the full env var reference.