Agents for robot physical reasoning.
Work in progress.
git clone https://github.com/tomsilver/robocode.git
cd robocode
bash install.shThis installs everything except the optional LIBERO-PRO extra, which is opt-in.
Use Python 3.11.
It is pinned in .python-version, so uv picks it up automatically and install.sh needs no flags.
If uv has no 3.11 on hand it downloads one.
3.11 is the only version actually exercised: the CI matrix is ["3.11"], and the Docker image installs python3.11 and syncs against it.
3.12 resolves and would probably work, but nothing tests it, and a local/sandbox version split is a bad trade in a project where agent code runs in the container.
Moving to 3.12 means updating the CI matrix and the Dockerfile first.
3.13 and newer cannot work at all, because the kindergarden submodule declares requires-python = ">=3.10,<3.13".
robocode mirrors that ceiling in its own requires-python so uv rejects a too-new interpreter up front instead of failing deep in a build.
The following tools are not installed by install.sh / uv sync and must be set up separately:
| Tool | Required for | Install |
|---|---|---|
| Ollama | Local model serving (Claude + Ollama, OpenCode + Ollama) | curl -fsSL https://ollama.com/install.sh | sh |
| Claude Code CLI | claude backend (default) |
curl -fsSL https://claude.ai/install.sh | bash |
| OpenCode CLI | opencode backend (multi-provider) |
curl -fsSL https://opencode.ai/install | bash |
| vLLM | Serving models via OpenAI-compatible API | pip install vllm (in a separate env) |
| Docker | Docker sandbox (recommended for isolation) | See Docker docs |
For local model serving with Ollama, pull a model after installing:
ollama pull gemma4:31bThe agentic approach supports two backends: Claude Code CLI (default) and OpenCode (for GPT, Gemini, open-source models via vLLM/Ollama, etc.).
The Claude Code CLI (claude) is the default backend. Authenticate via one of:
- Subscription (free usage):
claude auth login - API key: set
ANTHROPIC_API_KEYin your environment
Optionally set ROBOCODE_CLAUDE_CMD to point to a specific claude binary (defaults to claude on PATH).
The model parameter in agentic.yaml takes a full model ID. Override per-run with e.g. approach/backend=claude_sonnet5.
| Backend preset | Model ID |
|---|---|
claude_opus5 (default) |
claude-opus-5 |
claude_sonnet5 |
claude-sonnet-5 |
claude_opus48 |
claude-opus-4-8 |
claude_sonnet46 |
claude-sonnet-4-6 |
claude_haiku45 |
claude-haiku-4-5-20251001 |
See Anthropic models overview for the full list.
OpenCode supports 75+ providers including OpenAI, Google, Anthropic, and local models served via Ollama or vLLM.
Install: curl -fsSL https://opencode.ai/install | bash (also pre-installed in the Docker image).
Authenticate with your provider:
# API key (set the appropriate env var for your provider)
export OPENAI_API_KEY=sk-...
export GOOGLE_API_KEY=...
# Or use OpenCode's interactive auth
opencode providers loginOptionally set ROBOCODE_OPENCODE_CMD to point to a specific opencode binary.
Models use the provider/model format:
| Model | Provider |
|---|---|
openai/gpt-4o |
OpenAI |
google/gemini-2.5-pro |
|
anthropic/claude-sonnet-5 |
Anthropic |
ollama/qwen3.5:latest |
Ollama (local) |
For local models (Ollama, vLLM), create an opencode.json config with your provider:
{
"provider": {
"ollama": {
"npm": "@ai-sdk/openai-compatible",
"options": { "baseURL": "http://localhost:11434/v1" },
"models": { "qwen3.5:latest": { "name": "Qwen 3.5" } }
}
}
}All environments are available as Hydra configs via environment=<config_name>.
| Config | Description |
|---|---|
small_maze |
Small grid maze |
large_maze |
Large grid maze |
| Config | Kinder ID | Difficulty |
|---|---|---|
motion2d_easy |
kinder/Motion2D-p0-v0 |
Easy (0 passages) |
motion2d_medium |
kinder/Motion2D-p1-v0 |
Medium (1 passage) |
motion2d_hard |
kinder/Motion2D-p3-v0 |
Hard (3 passages) |
obstruction2d_easy |
kinder/Obstruction2D-o0-v0 |
Easy (0 obstructions) |
obstruction2d_medium |
kinder/Obstruction2D-o2-v0 |
Medium (2 obstructions) |
obstruction2d_hard |
kinder/Obstruction2D-o4-v0 |
Hard (4 obstructions) |
clutteredretrieval2d_easy |
kinder/ClutteredRetrieval2D-o1-v0 |
Easy (1 obstruction) |
clutteredretrieval2d_medium |
kinder/ClutteredRetrieval2D-o10-v0 |
Medium (10 obstructions) |
clutteredretrieval2d_hard |
kinder/ClutteredRetrieval2D-o25-v0 |
Hard (25 obstructions) |
clutteredstorage2d_easy |
kinder/ClutteredStorage2D-b1-v0 |
Easy (1 block) |
clutteredstorage2d_medium |
kinder/ClutteredStorage2D-b3-v0 |
Medium (3 blocks) |
clutteredstorage2d_hard |
kinder/ClutteredStorage2D-b7-v0 |
Hard (7 blocks) |
stickbutton2d_easy |
kinder/StickButton2D-b1-v0 |
Easy (1 button) |
stickbutton2d_medium |
kinder/StickButton2D-b3-v0 |
Medium (3 buttons) |
stickbutton2d_hard |
kinder/StickButton2D-b5-v0 |
Hard (5 buttons) |
pushpullhook2d |
kinder/PushPullHook2D-v0 |
Single variant |
| Config | Kinder ID | Difficulty |
|---|---|---|
obstruction3d_easy |
kinder/Obstruction3D-o0-v0 |
Easy (0 obstructions) |
obstruction3d_medium |
kinder/Obstruction3D-o2-v0 |
Medium (2 obstructions) |
obstruction3d_hard |
kinder/Obstruction3D-o4-v0 |
Hard (4 obstructions) |
shelf3d_easy |
kinder/KinematicShelf3D-o1-v0 |
Easy (1 cube) |
shelf3d_medium |
kinder/KinematicShelf3D-o3-v0 |
Medium (3 cubes) |
shelf3d_hard |
kinder/KinematicShelf3D-o5-v0 |
Hard (5 cubes) |
transport3d_easy |
kinder/Transport3D-o1-v0 |
Easy (1 cube) |
transport3d_hard |
kinder/Transport3D-o2-v0 |
Hard (2 cubes) |
packing3d_easy |
kinder/Packing3D-p1-v0 |
Easy (1 part) |
packing3d_medium |
kinder/Packing3D-p2-v0 |
Medium (2 parts) |
packing3d_hard |
kinder/Packing3D-p3-v0 |
Hard (3 parts) |
Realistic 3D backgrounds (optional): dynamic3d / TidyBot envs (e.g.
ConstrainedCupboard3D) render on a plain white background by default. For the realistic room scene (floor/wall textures), download the MimicLabs assets once (~1 GB, gitignored):python third-party/kindergarden/scripts/download_mimiclabs_assets.py, then setscene_bg: mimiclabs-lab2in the env config (already set forconstrainedcupboard3d_easy).
PDDLStream's packed benchmark, re-exposed as a
closed-loop gymnasium environment: a PR2 must pick every block off the table and place it
on a green plate. The scene and goal are carried over verbatim so instances line up with
the packed -n conditions in the LLM-PDDLStream
paper, but the PDDL domain, the stream samplers, and the planner are deliberately left
behind: robocode's approaches synthesize their own task and motion planning.
| Config | Blocks | Observation |
|---|---|---|
pr2packed_easy |
3 | Box(72,) |
pr2packed_medium |
4 | Box(83,) |
pr2packed_hard |
5 | Box(94,) |
pr2packed_generalized |
1-5 (varies per reset) | ObjectCentricState |
pr2packed_generalized is the generalization axis: it keeps one backend per count and
returns object-centric observations, so a single frozen program spans every instance
size. It implements VariableCountEnv (src/robocode/environments/variable_count.py),
the shared contract the runner's count-sweep lifecycle keys off — design_counts are
what an approach is built against, eval_counts adds held-out larger instances, and
the results carry by_count and a design/held-out count_regimes split. The kinder
VariableObjectCountEnv implements the same contract.
The action space matches kinder's 3D mobile-manipulation envs exactly — Box(11,) of
delta base pose, delta arm joints, and gripper open/close — so approaches and prompts
written against obstruction3d and friends transfer without a new interface to learn.
Dynamics are kinematic: joints are set rather than servoed, and a motion that would put
the robot or a held block in collision is rejected. Closing the gripper attaches a block
rigidly to the tool frame, but only when the fingers are actually around it — hovering
above a block does not grasp it. Opening drops the held block straight down, and the drop
is refused if it would land overlapping something, leaving the block held. Reward is -1
per step and the episode terminates once every block rests on the plate and no two
blocks overlap: the plate is small enough to force a packing, so dropping them all at
one spot does not count.
Verifying against stock PDDLStream. scripts/compare_pddlstream_rollout.py rolls
episodes in our environment and replays every state visited into a scene built from
stock examples.pybullet.tamp.problems.packed, comparing the stock library's own
collision / placement / kinematics results against ours, and optionally rendering the
two side by side. The stock tree is pinned as a submodule but not cloned by
default — its nested submodule is a second copy of ss-pybullet at the commit we
already vendor (~590MB). Opt in with:
git submodule update --init --recursive third-party/pddlstream
python scripts/compare_pddlstream_rollout.py --policy oracle --render /tmp/cmpExit code 0 means every recorded value agreed to 1e-9; 1 prints a per-value diff.
Oracle. Run it with approach=oracle:
python experiments/run_experiment.py approach=oracle environment=pr2packed_generalized \
eval_seed="$EVAL_SEED"approach=oracle dispatches on the environment choice name
(robocode.approaches.oracle_approach.ORACLE_TARGETS), so it also runs the existing
kinder oracles (obstruction2d_medium, clutteredstorage2d_medium,
stickbutton2d_medium, pushpullhook2d). An oracle is a solvability check and a
reference row to compare synthesized approaches against, not a scored method.
robocode.oracles.pr2packed is a reference TAMP policy: for each block it
samples a base pose that can reach both the block and a free plate cell (inverse
reachability), solves arm IK there, plans collision-free joint paths, and tracks them
with the environment's bounded delta actions, resampling a block's plan when a stage
fails. It exists to confirm the task is solvable — it solves 60/60 episodes across
3/4/5 blocks (means of 136/185/237 steps), comfortably inside max_steps_for_count.
tests/oracles/pr2packed/ is that check.
Every candidate configuration and path is validated against the environment's own collision test rather than the planner's, because ss-pybullet configures the two separately and a path the planner accepts is not automatically one the environment will execute.
Geometry, grasping, and collision checking come from
ss-pybullet, vendored as a submodule under
third-party/ss-pybullet/ along with its own nested
motion-planners submodule. Neither ships
packaging metadata, so they are imported off sys.path by
robocode/environments/ss_pybullet.py rather than installed by uv; install.sh already
runs git submodule update --init --recursive, which is all the setup they need. Nothing
here requires a FastDownward build, because nothing here runs the PDDLStream planner.
LIBERO-PRO is a Franka tabletop manipulation benchmark (~80 task suites covering goal / spatial / object / 10-task mixes plus OOD and perturbation variants) built on MuJoCo via robosuite. It is vendored as a submodule under third-party/LIBERO-PRO/ and gated behind the optional libero extra — it is not installed by default because it pins old upstreams (robosuite==1.4.0, gym==0.25.2, robomimic==0.2.0, bddl==1.0.1) and drags in a CUDA-enabled torch.
install.sh passes --no-extra libero, so the default install skips it entirely.
Linux only.
The extra cannot be installed on macOS: robomimic 0.2.0 depends on egl-probe, which compiles an EGL loader, and EGL has no macOS implementation.
Use the Docker sandbox to run LIBERO from a Mac.
Install (into the same venv as the rest of robocode):
sudo apt-get install -y libegl1 libgl1 cmake # EGL/GL runtime for MuJoCo; cmake builds egl-probe
uv sync --all-extras --dev # ~60 extra Python packages, several GBFirst use of the libero package runs an interactive input() prompt asking where to store datasets; the test harness writes ~/.libero/config.yaml automatically. If you hit the prompt manually, answer N — the default paths are fine for env rollouts (pre-recorded demos are not required).
List available benchmark suites:
from libero import benchmark
print(list(benchmark.get_benchmark_dict().keys())) # ~80 suitesMinimal rollout on libero_goal task 0:
from libero import benchmark
from libero.envs import OffScreenRenderEnv
task_suite = benchmark.get_benchmark_dict()["libero_goal"]()
bddl = task_suite.get_task_bddl_file_path(0)
env = OffScreenRenderEnv(bddl_file_name=bddl, camera_heights=128, camera_widths=128)
env.seed(0)
obs = env.reset() # dict with agentview_image, robot state, per-object poses, ...
obs, reward, done, info = env.step([0.0] * 7)
env.close()Smoke tests live at tests/environments/test_libero.py (benchmark dict + rollout); they skip cleanly if the extra isn't installed.
Note on OpenGL: LIBERO's MuJoCo needs to coexist in-process with kinder's pybullet. src/robocode/environments/kinder_geom2d_env.py and kinder_geom3d_env.py pin MUJOCO_GL=egl / PYOPENGL_PLATFORM=egl before kinder loads so PyOpenGL latches to the EGL platform — without this, later robosuite imports in the same process fail with 'NoneType' object has no attribute 'glGetError'. If you see that error, confirm libegl1 is installed.
The agent runs inside a Docker container (robocode-sandbox) that provides full filesystem isolation, a restricted network, and a pre-built Python environment.
| Layer | Mechanism |
|---|---|
| Filesystem | Docker bind-mount: agent can only write to /sandbox (the run's output dir) |
| Network | init-firewall.sh whitelists API endpoints for the configured provider (Anthropic, OpenAI, Google, etc.), GitHub IPs, and telemetry; blocks everything else via iptables. Extra domains are passed via ROBOCODE_FIREWALL_EXTRA_DOMAINS. |
| Write hook | Claude backend: PreToolUse hook in .claude/settings.json double-checks Write/Edit paths stay inside /sandbox. OpenCode backend: "permission": "allow" in opencode.json (Docker provides the isolation). |
The Apptainer backend (container_backend=apptainer, for HPC clusters with no Docker daemon) keeps the same filesystem isolation but has no network firewall: unprivileged Apptainer cannot grant CAP_NET_ADMIN, so init-firewall.sh is skipped and generated code runs with unrestricted network egress. Use Docker where the iptables allowlist matters.
| Path | Contents |
|---|---|
/sandbox/ |
Working directory — agent writes approach.py, test scripts, etc. here |
/sandbox/primitives/ |
Source files from src/robocode/primitives/ (read reference) |
/robocode/.venv/bin/python |
Python 3.11 with all robocode dependencies pre-installed |
/robocode/third-party/kindergarden/ |
The kinder env package, bind-mounted read-only from the host submodule |
Simply open the Docker Desktop application. Look for the status indicator in the bottom-left corner of the GUI; it should say "Docker Engine Running".
sudo systemctl start docker
sudo systemctl enable docker
Build once from the repo root (rebuild when pyproject.toml / uv.lock change; not needed for third-party/kindergarden code changes):
bash docker/build.shThe original macOS Seatbelt / Linux bubblewrap sandbox is still available (container_backend: local in agentic.yaml) but has a known limitation: it restricts filesystem writes but allows reads of the entire host filesystem.
Red team the sandbox:
python integration_tests/red_team_sandbox.py # OS-level
python integration_tests/red_team_sandbox.py --docker # DockerSet the private evaluation-suite seed in your shell before using any experiment command or checked-in launcher. The prompt avoids saving it in shell history:
read -rsp "Private evaluation seed: " EVAL_SEED
echo
export EVAL_SEEDRun an experiment:
python experiments/run_experiment.py approach=random environment=small_maze replicate_seed=0 eval_seed="$EVAL_SEED"Run a sweep over multiple seeds and environments:
python experiments/run_experiment.py -m replicate_seed=0,1,2 eval_seed="$EVAL_SEED" environment=small_maze,large_maze approach=randomAnalyze results from one or more runs:
python experiments/analyze_results.py multirun/Browse runs in the browser (metrics, per-episode GIFs, and the sandbox git history of the generated approach.py), then open http://localhost:8000. The history view charts replay solve rate and per-commit effort, and can replay the same failed seed across versions to show where it was fixed:
python -m experiments.results_viewer --root . --port 8000The viewer can also read ZIP result archives recursively from a Google Drive folder. The recommended backend is rclone, which provides browser login without requiring every collaborator to create a Google Cloud project. Install rclone from its official downloads on Linux or with Homebrew on macOS:
# Linux: https://rclone.org/downloads/
# macOS:
brew install rclone
rclone configIn rclone config, create a remote named robocode-drive, choose Google
Drive, leave the client ID and secret blank, choose read-only access, and allow
browser authentication. The resulting token stays in rclone's user config
outside the repository. Then launch:
python -m experiments.results_viewer --drive-folder "<Google Drive folder URL>"Only .zip files are downloaded. Name each archive <Experiment ID>.zip so
the tracker ID, Drive result, and local cache directory match without manual
renaming. Archives are extracted under the user's cache directory, and the
viewer scans that local copy. The Refresh button checks Drive again, downloads
changed archives, removes archives deleted remotely, and rescans the cache.
Unchanged extracted archives are left in place, so GIFs rendered by the viewer
stay local and survive refreshes. The Drive folder URL and rclone configuration
are runtime configuration and must not be committed. Their locations can be
overridden with
ROBOCODE_RESULTS_DRIVE_FOLDER, ROBOCODE_RESULTS_CACHE,
ROBOCODE_RCLONE_REMOTE, and RCLONE_CONFIG. The same viewer command works on
Linux and macOS as long as rclone is on PATH.
Rclone currently warns that its shared Google Drive OAuth client is scheduled for retirement during 2026. The blank-client-ID setup is therefore a convenient prototype path, not a permanent team dependency. Configure a team-owned OAuth client on the same rclone remote before Google disables the shared client; the viewer command and archive layout do not change.
The agentic approach launches a coding agent during train(). The agent reads the environment source code, figures out the state/action space and dynamics, and writes a GeneratedApproach class that is used at evaluation time. The agent can also write and run test scripts against the real environment to verify its solution before committing.
By default the agent uses the Claude Code CLI backend and runs in the Docker sandbox (requires bash docker/build.sh once):
python experiments/run_experiment.py approach=agentic environment=motion2d_easy eval_seed="$EVAL_SEED"Set approach.blackbox=true to hide the environment source and force the agent to discover the dynamics empirically through a host-side env server instead of reading code. See docs/blackbox.md for the architecture.
To use a different backend/model, override the approach/backend config:
# GPT-5.4 via OpenCode
python experiments/run_experiment.py approach=agentic approach/backend=opencode_gpt54 eval_seed="$EVAL_SEED"
# Local Ollama model
python experiments/run_experiment.py approach=agentic approach/backend=opencode_qwen eval_seed="$EVAL_SEED"
# Or override individual fields
python experiments/run_experiment.py approach=agentic approach.backend.backend=opencode approach.backend.model=google/gemini-2.5-pro eval_seed="$EVAL_SEED"Available backend presets: claude_opus5 (default), claude_sonnet5, claude_opus48, claude_sonnet46, claude_haiku45, claude_ollama_qwen, opencode_gpt54, opencode_gpt4omini, opencode_gpt5nano, opencode_qwen.
The experiment runner rejects the legacy local sandbox for generated-code methods because that sandbox permits host filesystem reads. Use Docker or Apptainer so experimenter-only evaluation configuration is outside the synthesis sandbox.
To skip re-generation and load a previously generated approach:
python experiments/run_experiment.py approach=agentic environment=small_maze \
eval_seed="$EVAL_SEED" \
approach.load_dir=outputs/2026-02-16/16-00-41Parallel sweeps each get their own container (named robocode-sandbox-<uuid>), so multiple runs never interfere:
python experiments/run_experiment.py -m replicate_seed=0,1,2 eval_seed="$EVAL_SEED" environment=small_maze,large_maze approach=agenticUse the joblib launcher to run jobs in parallel locally:
python experiments/run_experiment.py -m \
approach=agentic \
approach.container_backend=docker \
replicate_seed=42,24,424,444,222 \
eval_seed="$EVAL_SEED" \
primitive_level=none \
environment=motion2d_easy,obstruction2d_easy,clutteredretrieval2d_easy,clutteredstorage2d_easy,stickbutton2d_easy,pushpullhook2d \
'hydra.sweep.dir=multirun/2026-02-23/no_primitives_5d_s42_24_424_444_222' \
'hydra.sweep.subdir=r${replicate_seed}/${hydra:runtime.choices.environment}' \
hydra/launcher=joblib hydra.launcher.n_jobs=4Hydra defines executable choices, while small campaign files select the conditions intended for one study. Keep exploratory or smoke-test campaigns local, and commit only study definitions that should be shared. Generate a local CSV without running experiments:
python -m experiments.tracker.generate \
path/to/campaign.yaml \
--eval-seed "$EVAL_SEED" --dry-run
python -m experiments.tracker.generate \
path/to/campaign.yaml \
--eval-seed "$EVAL_SEED" \
--output experiments/generated/my_campaign.csvCampaign files call the repeated runs replicate_seeds; they never contain the
private evaluation seed. The generator requires that fixed seed explicitly and places
it in the ignored local CSV and shared Sheet. Each condition becomes one row whose
Hydra command sweeps every replicate while holding the evaluation suite fixed.
The generated Experiment ID is passed into Hydra, recorded in every results.json,
and used as the exact parent directory under multirun/. Each invocation creates a
timestamped run beneath that parent with one replicate_<replicate_seed> directory
per replicate, so the condition folder can be uploaded to Drive without renaming it.
The ID fingerprints the complete Hydra-composed condition and both seed fields. Hydra
defaults such as access mode, model/backend, and the 60-second evaluation timeout are
materialized in every generated command. Editing any executable setting or seed
protocol therefore appends a distinct run instead of relabeling earlier results.
Study campaigns use the named primitive_level=none|low_level|bilevel config choices,
which resolve to the primitive list consumed by build_primitives(). Explicit
constraints exclude invalid cells such as primitive_level=bilevel with
approach.blackbox=true, and Hydra composition catches missing config choices.
Install the optional Google client and synchronize the generated CSV:
uv sync --extra tracker
python -m experiments.tracker.sync_google_sheet \
experiments/generated/my_campaign.csv --sheet-id SPREADSHEET_IDThe sync uses Experiment ID as its key. It updates generated columns only for the exact same canonical run, appends changed conditions or seed protocols, marks removed conditions from the synchronized campaign inactive, and never writes existing Owner, Status, Progress, Priority, Notes, Results, or Git SHA cells. It also rejects a same-ID seed change as malformed input. New Sheets receive a native table with People, file, and dropdown column types. Generated categorical columns such as Campaign, Environment, Method, Primitive Level, Access, Model / Backend, and Active are dropdown chips whose choices refresh from all rows in the tracker, including inactive experiments from older campaigns. Priority is placed immediately after Replicate Seeds and Evaluation Seed. Dropdown-chip colors can be customized directly in the Google Sheets UI without changing the cells' backgrounds; an unchanged sync preserves that native chip styling. Status and Owner are the first two columns so the Sheet reads as a work queue at a glance.
Authentication uses gspread's desktop OAuth flow. By default it reads
~/.config/gspread/credentials.json and stores the authorized-user token outside the
repository. Override those paths with --credentials / --authorized-user or the
GOOGLE_OAUTH_CLIENT_SECRET / GOOGLE_AUTHORIZED_USER environment variables. Never
commit either credential file.
The generated approach.py and full agent log are saved under sandbox/ in the run's output directory (e.g. outputs/2026-02-16/16-00-41/sandbox/).
llm_genplan and best_of_k call a model directly (messages in, code out) with no tools and no agent loop. They take their model from the approach/completion config group, which is separate from the approach/backend group the agentic approach uses:
# Default: Opus 5 through the Claude CLI
python experiments/run_experiment.py approach=llm_genplan environment=small_maze eval_seed="$EVAL_SEED"
# Same approach on a different model
python experiments/run_experiment.py approach=best_of_k approach/completion=cli_sonnet5 eval_seed="$EVAL_SEED"The cli_* presets drive the same authenticated Claude CLI as the agentic backend, so these runs need claude auth login (or ANTHROPIC_API_KEY) and no separate setup. The CLI applies an irreducible ~2k-token system prompt that cannot be stripped, so these baselines are prompted with that preamble present; the anthropic_* presets call the Messages API directly when a prompt with nothing else in it is required.
| Completion preset | Provider | Model |
|---|---|---|
cli_opus5 (default) |
Claude CLI | claude-opus-5 |
cli_sonnet5 |
Claude CLI | claude-sonnet-5 |
cli_opus48 |
Claude CLI | claude-opus-4-8 |
cli_sonnet46 |
Claude CLI | claude-sonnet-4-6 |
cli_claude |
Claude CLI | sonnet (alias; the CLI picks the generation) |
anthropic_opus |
Messages API | claude-opus-4-8 |
anthropic_sonnet |
Messages API | claude-sonnet-4-6 |
ollama_qwen |
OpenAI-compatible | qwen3.6 (local Ollama) |
vllm |
OpenAI-compatible | Qwen/Qwen3.6-35B-A3B (local vLLM) |
The anthropic_* presets bill the Messages API and need ANTHROPIC_API_KEY; their input_cost_per_mtok / output_cost_per_mtok fields turn reported token usage into an estimated cost_usd, which bounds approach.max_budget_usd. The CLI reports its own cost, and the local presets report none.
replicate_seed and eval_seed have deliberately different roles:
replicate_seedidentifies one independent replicate and seeds randomness controlled by Robocode, such as an approach's NumPy generator and action-space sampling. It does not seed Claude or make an agentic run reproducible.eval_seedis a fixed team value supplied explicitly in final-run commands. Robocode uses it to derive the same ordered evaluation episode suite for every method and replicate, so score differences are not caused by different sampled test suites. The checked-in default isnull, and the runner fails if a command omits the value. Keep it out of public configs and agent-visible inputs.
The generalized synthesis agent does not receive eval_seed or the derived
episode seeds. Hydra's full configuration remains on the experimenter side, and
only the child sandbox/ directory plus filtered source are mounted into Docker
or Apptainer. Per-instance methods are a separate protocol: they receive the one
derived episode seed they are solving, but not the master eval_seed used to
construct the suite.
For variable-object-count environments, evaluation sweeps the configured design
and held-out counts on that fixed episode schedule. results.json retains the
per-count curve and also reports design_count_solve_rate and
held_out_count_solve_rate separately.
The Claude Code CLI
and Anthropic Messages API
do not expose a sampling-seed control. Consequently, replicates capture
uncontrolled model-generation variation even when replicate_seed is held
fixed; session IDs and model names are not random seeds.
This isolation boundary protects the synthesis process and its tool calls. The generated policy is currently loaded by the trusted experiment runner for rollout; it is reviewed as an experiment artifact rather than treated as hostile code. Protecting the host from a deliberately malicious generated policy would require running policy inference behind a separate process or container boundary as well.
On small_maze, the agent independently discovered A* pathfinding and achieved a 100% solve rate with optimal path lengths (mean 2.3 steps across 10 episodes):
{
"mean_eval_reward": -2.3,
"mean_eval_steps": 2.3,
"solve_rate": 1.0,
"num_eval_tasks": 10
}Generated approach.py (A* pathfinding)
"""Optimal approach for MazeEnv using A* pathfinding algorithm."""
import heapq
from typing import Optional
class GeneratedApproach:
"""Optimal maze solver using A* pathfinding."""
def __init__(self, action_space, observation_space):
self.action_space = action_space
self.observation_space = observation_space
self.planned_path: Optional[list[tuple[int, int]]] = None
self.path_index = 0
self.UP = 0
self.DOWN = 1
self.LEFT = 2
self.RIGHT = 3
self.action_to_delta = {
self.UP: (-1, 0),
self.DOWN: (1, 0),
self.LEFT: (0, -1),
self.RIGHT: (0, 1)
}
def reset(self, state, info):
self.planned_path = self._astar_search(state)
self.path_index = 0
def get_action(self, state):
if self.planned_path and self.path_index < len(self.planned_path) - 1:
next_pos = self.planned_path[self.path_index + 1]
dr = next_pos[0] - state.agent[0]
dc = next_pos[1] - state.agent[1]
for action, (delta_r, delta_c) in self.action_to_delta.items():
if (dr, dc) == (delta_r, delta_c):
self.path_index += 1
return action
return self._greedy_action(state)
def _astar_search(self, state) -> Optional[list[tuple[int, int]]]:
start, goal = state.agent, state.goal
heap = [(self._heuristic(start, goal), start, 0, [start])]
visited = set()
while heap:
_, current, g_score, path = heapq.heappop(heap)
if current in visited:
continue
visited.add(current)
if current == goal:
return path
r, c = current
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
nr, nc = r + dr, c + dc
neighbor = (nr, nc)
if (0 <= nr < state.height and 0 <= nc < state.width
and neighbor not in state.obstacles
and neighbor not in visited):
new_g = g_score + 1
heapq.heappush(heap, (
new_g + self._heuristic(neighbor, goal),
neighbor, new_g, path + [neighbor]
))
return None
def _heuristic(self, pos1, pos2) -> int:
return abs(pos1[0] - pos2[0]) + abs(pos1[1] - pos2[1])
def _greedy_action(self, state) -> int:
dr = state.goal[0] - state.agent[0]
dc = state.goal[1] - state.agent[1]
if abs(dr) >= abs(dc):
return self.DOWN if dr > 0 else self.UP
return self.RIGHT if dc > 0 else self.LEFTAgent log (19 turns, $0.36)
The agent's workflow:
- Explored the sandbox directory and read
maze_env.pyandbase_env.py - Analyzed the state type (
_MazeStatewithagent,goal,obstacles,height,width), action space (4 directional moves), and reward structure (-1 per step) - Identified this as a shortest-path problem and implemented A* with Manhattan distance heuristic
- Wrote and ran three test scripts verifying correctness on simple mazes, complex mazes with obstacles, impossible mazes (graceful fallback), and optimality of path lengths
- Cleaned up test files and produced the final
approach.py
18:32:05 Agent: I'll analyze the environment source files and create an optimal approach.py.
18:32:06 Tool call: Glob({'pattern': '**/*.py'})
18:32:08 Agent: Let me explore the main environment files to understand the structure:
18:32:09 Tool call: Read({'.../maze_env.py'})
18:32:09 Tool call: Read({'.../base_env.py'})
18:32:11 Agent: [Analyzed state/action space, identified A* as optimal strategy]
18:32:40 Tool call: Write({'.../approach.py'})
18:32:43 Agent: Now let me create a simple test script to verify the approach works:
18:33:00 Tool call: Write({'.../test_approach.py'})
18:33:03 Tool call: Bash({'command': 'python test_approach.py'})
18:33:07 Agent: Excellent! Testing on a more complex maze...
18:33:22 Tool call: Write({'.../test_complex_maze.py'})
18:33:26 Tool call: Bash({'command': 'python test_complex_maze.py'})
18:33:31 Agent: A* correctly found optimal 12-step path around all obstacles.
18:33:39 Tool call: Write({'.../test_real_state.py'})
18:33:54 Tool call: Bash({'command': 'python test_real_state.py'})
18:33:58 Agent: Works with actual _MazeState class.
18:34:09 Tool call: Write({'.../test_optimality.py'})
18:34:11 Tool call: Bash({'command': 'python test_optimality.py'})
18:34:13 Agent: All test cases produce optimal solutions.
18:34:14 Tool call: Bash({'command': 'rm test_*.py'})
18:34:25 Session done: turns=19, cost=$0.3628055, error=False