alfworldpythonverified

alfworld household skill v4

Six household skills, v4: hardened to start already holding a wrong object and to find a target already moved; doc passed the composer gate.

rec_7ee3f427aa5a49a48ddfa4d0c5e44790 · banked 2026-09-04 · by neruva
SKILL.md
Not yet certified

This skill has no published checker, so its evidence cannot be reproduced from here.

Scorecard

Six household skills for a text-adventure benchmark: pick, clean, heat, cool, examine, pick two.

Score on the benchmark
134/134
Without it
70/134 without it
Model turns per task
1.00 model turns per game
Cost
0.033 cents per game
Smallest model measured
Qwen3-1.7B, on a laptop, no API
Cost to build it once
$0.22
Checked by
the benchmark's own win signal
Where it does not apply
  • The task sentences in this benchmark are templated.
  • Built from 24 training games; the 134 evaluation games were never touched by the forge.

Evidence

  • forge run 7 ($0.22): ratchet 24/24 (12 plain + 12 perturbed train games), generality gate 24/24, composer gate 12/12
  • ALFWorld valid_unseen 134 games: deepseek-chat + v4 134/134 at 1.00 turns, 0.033c/game; parser rung alone 134/134
  • supersedes alfworld_household_rungs (v1) for executors that may call with a wrong first action

Use it

# in Claude Code (MCP tools from neruva-mcp)
rung_search(q="alfworld_household_rungs_v4")
rung_install(id="rec_7ee3f427aa5a49a48ddfa4d0c5e44790", dir="~/.claude/skills")
# the skill folder is now loaded like any other skill

Usage guide

What the model reads to call the skill. This is the whole interface.

Show the guide
Skill library usage guide
==========================

This module exposes six task functions. Each takes a World instance `w` as
the first argument, plus one or two TYPE names (never instance names like
"apple 2" -- just "apple"). Each returns True/False for task success.

Mapping a task sentence to a call:
  - Find the two/three key nouns in the sentence. The "obj" is the thing
    being manipulated, the "dest" is the receptacle it ends up in/on, and
    for lamp tasks the "lamp" is the light source (desklamp/floorlamp).
  - Type names are lowercase, singular-ish, no spaces (e.g. "saltshaker",
    "creditcard", "coffeetable", "diningtable", "winebottle"), matching
    exactly how the game names them (drop leading articles and numbers).

1. pick_and_place(w, obj, dest)
   "put a <obj> in/on <dest>"
   Example: pick_and_place(w, "pillow", "sofa")
     -- task: "put a pillow on the sofa"

2. clean_and_place(w, obj, dest)
   "put a clean <obj> in <dest>" / "clean some <obj> and put it in <dest>"
   Example: clean_and_place(w, "lettuce", "diningtable")
     -- task: "put a clean lettuce in diningtable"

3. heat_and_place(w, obj, dest)
   "heat some <obj> and put it in <dest>" / "put a hot <obj> in <dest>"
   Example: heat_and_place(w, "egg", "countertop")
     -- task: "heat some egg and put it in countertop"

4. cool_and_place(w, obj, dest)
   "cool some <obj> and put it in <dest>" / "put a cold <obj> in <dest>"
   Example: cool_and_place(w, "potato", "microwave")
     -- task: "cool some potato and put it in microwave"

5. examine_in_light(w, obj, lamp)
   "examine the <obj> with the <lamp>" / "look at <obj> under the <lamp>"
   Example: examine_in_light(w, "book", "desklamp")
     -- task: "examine the book with the desklamp"
   (lamp is usually "desklamp" or "floorlamp")

6. pick_two_and_place(w, obj, dest)
   "put two <obj> in/on <dest>" / "find two <obj> and put them in <dest>"
   Example: pick_two_and_place(w, "cd", "safe")
     -- task: "put two cds in safe"
   Example: pick_two_and_place(w, "soapbottle", "toilet")
     -- task: "find two soapbottle and put them in toilet"

Notes:
  - obj/dest/lamp must be singular type names as they appear in the game's
    object list (e.g. "soapbar" not "soap bar", "cellphone" not "cell phone").
    Strip leading articles ("a", "an", "the") and any trailing numbers.
  - If a sentence mentions two actions (e.g. "clean" + "put"), pick the
    matching combined function (clean_and_place / heat_and_place /
    cool_and_place) rather than calling pick_and_place twice.
  - Every function is safe to call even if the agent is already holding an
    unrelated object (it will be set down first), or if the target object
    was already moved/placed by an earlier partial attempt (search checks
    the current spot, previously visited receptacles, and the destination
    itself before falling back to the full room search order).
  - pick_two_and_place in particular never gets stuck holding an object: if
    a placement attempt fails it puts the item down again before continuing
    to search for the next instance, since only one object can be held at a
    time.
  - Every function already handles opening closed receptacles/appliances
    and stays within the environment's ~40 step budget.

Code

342 lines of python, hashed and signed below.

Show the code
"""Task-level skills built on top of primitives.World.

Each function below performs one ALFWorld task type end-to-end using only
World's verbs (go/open/close/take/put/clean/heat/cool/use/examine) and
queries (objects_here/receptacles_of/instances_of/receptacles), plus the
externally supplied `search_order(obj_type, receptacles)` prior that orders
receptacle instances by how likely they are to hold a given object type.

No task logic (search order, plans) lives in primitives.py; it all lives here.

Robustness notes:
  - Every entry point may be called while the agent is already holding a
    (possibly wrong) object, and/or after earlier partial progress (target
    already at destination, receptacles already opened/visited). Each
    function starts by getting rid of a wrong held item and by searching
    broadly enough (current location, previously visited receptacles, the
    destination, then the full prior order) to find the target wherever it
    ended up.
  - pick_two_and_place additionally must never get "stuck" holding an item:
    if a placement attempt fails, the held item is set down again before
    continuing the search, since the game only allows holding one object at
    a time and a stuck-holding bug would silently stall all further takes.
"""

from __future__ import annotations

# Soft step budget: we try to leave a few steps of slack under the ~40 step
# limit so a task never gets cut off mid-action.
MAX_STEPS = 40
SAFETY_MARGIN = 4


def _steps_left(w) -> int:
    try:
        return MAX_STEPS - w.steps
    except Exception:
        return MAX_STEPS


def _budget_ok(w, cost: int = 2) -> bool:
    return _steps_left(w) > SAFETY_MARGIN + cost


# ---------------------------------------------------------------------------
# type / instance helpers
# ---------------------------------------------------------------------------

def _norm_type(t: str) -> str:
    """Normalize a caller-supplied type name for comparison purposes."""
    return t.strip().lower().replace(" ", "")


def _inst_type(instance_name: str) -> str:
    """'soap bar 1' -> 'soapbar', 'apple' -> 'apple'."""
    if not instance_name:
        return ""
    parts = instance_name.strip().split()
    if parts and parts[-1].isdigit():
        parts = parts[:-1]
    return "".join(parts).lower()


# ---------------------------------------------------------------------------
# internal helpers
# ---------------------------------------------------------------------------

def _refresh(w, recep):
    """Go to a receptacle, opening it if it's closed. Returns the obs text."""
    obs = w.go(recep)
    if w.is_closed_here(obs):
        obs = w.open(recep)
    return obs


def _put_down_somewhere(w):
    """Drop whatever is currently held onto the current or any receptacle."""
    if not w.holding:
        return
    if w.at:
        w.put(w.holding, w.at)
        if not w.holding:
            return
    for r in w.receptacles:
        if not _budget_ok(w):
            break
        obs = w.go(r)
        if w.is_closed_here(obs):
            w.open(r)
        w.put(w.holding, r)
        if not w.holding:
            return


def _ensure_not_holding_wrong(w, obj_type):
    """If holding something that isn't obj_type, put it down first."""
    if not w.holding:
        return
    if _inst_type(w.holding) == _norm_type(obj_type):
        return
    _put_down_somewhere(w)


def _locate_next(w, obj_type, visited, taken=()):
    """Find one instance of obj_type not already in `taken`.

    Order of search:
      1. wherever we currently stand (cheap, no extra step),
      2. receptacles we've already visited this task (from cache, in case
         they held more than one instance of obj_type -- important for
         pick_two_and_place when both copies sit on the same receptacle),
      3. the remaining receptacles in search_order(obj_type, ...), each
         visited (go + open-if-closed) at most once.

    Mutates `visited` in place. Returns (instance_name, receptacle_name) or
    (None, None). Backs off early if the step budget is getting tight.
    """
    # 1. current location, no extra step needed
    if w.at:
        items = w.objects_here()
        insts = [i for i in w.instances_of(obj_type, items) if i not in taken]
        if insts:
            visited.add(w.at)
            return insts[0], w.at

    # 2. receptacles already visited (cached), re-check for leftovers
    for recep in list(visited):
        cached = w.seen.get(recep, [])
        insts = [i for i in w.instances_of(obj_type, cached) if i not in taken]
        if insts and _budget_ok(w):
            _refresh(w, recep)
            items = w.objects_here()
            insts2 = [i for i in w.instances_of(obj_type, items) if i not in taken]
            if insts2:
                return insts2[0], recep

    # 3. fresh receptacles via the prior
    for recep in search_order(obj_type, w.receptacles):
        if recep in visited:
            continue
        if not _budget_ok(w):
            break
        visited.add(recep)
        _refresh(w, recep)
        items = w.objects_here()
        insts = [i for i in w.instances_of(obj_type, items) if i not in taken]
        if insts:
            return insts[0], recep
    return None, None


def _take(w, inst, recep):
    """Take inst from recep; returns True if now holding it."""
    w.take(inst, recep)
    return w.holding == inst


def _place(w, inst, dest_type):
    """Try to put the held instance onto/into some instance of dest_type."""
    for d in w.receptacles_of(dest_type):
        if not _budget_ok(w):
            break
        obs = w.go(d)
        if w.is_closed_here(obs):
            obs = w.open(d)
        obs = w.put(inst, d)
        if obs and ("you put" in obs.lower() or "you move" in obs.lower()):
            return True
    return False


def _find_and_take(w, obj_type, visited, taken=()):
    """Locate an instance of obj_type and pick it up, retrying on failure.

    If already holding a matching instance (not in taken), use it directly
    without spending any extra steps -- this covers resumed/retried calls.
    """
    _ensure_not_holding_wrong(w, obj_type)
    if w.holding and _inst_type(w.holding) == _norm_type(obj_type) and w.holding not in taken:
        return w.holding

    failed = set()
    for _ in range(5):
        if not _budget_ok(w):
            return None
        inst, recep = _locate_next(w, obj_type, visited, tuple(taken) + tuple(failed))
        if not inst:
            return None
        if _take(w, inst, recep):
            return inst
        failed.add(inst)
    return None


def _go_appliance(w, appliance_type):
    """Go to (and open if needed) the first instance of an appliance type."""
    insts = w.receptacles_of(appliance_type)
    if not insts:
        return None
    inst = insts[0]
    obs = w.go(inst)
    if w.is_closed_here(obs):
        w.open(inst)
    return inst


# ---------------------------------------------------------------------------
# entry points
# ---------------------------------------------------------------------------

def pick_and_place(w, obj, dest):
    visited = set()
    inst = _find_and_take(w, obj, visited)
    if not inst:
        return False
    return _place(w, inst, dest)


def clean_and_place(w, obj, dest):
    visited = set()
    inst = _find_and_take(w, obj, visited)
    if not inst:
        return False

    sinks = w.receptacles_of("sinkbasin")
    if not sinks:
        return False
    sink = sinks[0]
    obs = w.go(sink)
    if w.is_closed_here(obs):
        w.open(sink)
    w.clean(inst, sink)

    return _place(w, inst, dest)


def heat_and_place(w, obj, dest):
    visited = set()
    inst = _find_and_take(w, obj, visited)
    if not inst:
        return False

    m = _go_appliance(w, "microwave")
    if not m:
        return False
    w.heat(inst, m)

    return _place(w, inst, dest)


def cool_and_place(w, obj, dest):
    visited = set()
    inst = _find_and_take(w, obj, visited)
    if not inst:
        return False

    f = _go_appliance(w, "fridge")
    if not f:
        return False
    w.cool(inst, f)

    return _place(w, inst, dest)


def examine_in_light(w, obj, lamp):
    visited = set()
    inst = _find_and_take(w, obj, visited)
    if not inst:
        return False

    linst, lrecep = _locate_next(w, lamp, visited)
    if not linst:
        return False

    if w.at != lrecep:
        w.go(lrecep)
        items = w.objects_here()
        fresh = w.instances_of(lamp, items)
        if fresh:
            linst = fresh[0]

    w.use(linst)
    return w.won


def pick_two_and_place(w, obj, dest):
    visited = set()
    taken: list[str] = []
    failed: set[str] = set()

    # Drop any wrong-type held object first so it never blocks picking up
    # instances of obj (the game only lets you hold one item at a time).
    _ensure_not_holding_wrong(w, obj)

    # Check whether one (or both) instances already sit at dest from an
    # earlier/partial attempt -- count them without moving them.
    for d in w.receptacles_of(dest):
        if not _budget_ok(w):
            break
        _refresh(w, d)
        visited.add(d)
        items = w.objects_here()
        for inst in w.instances_of(obj, items):
            if inst not in taken:
                taken.append(inst)
        if len(taken) >= 2:
            break

    # If we're currently holding a matching instance not yet counted (e.g.
    # resumed after a prior partial attempt), try to place it right away.
    if (
        len(taken) < 2
        and w.holding
        and _inst_type(w.holding) == _norm_type(obj)
        and w.holding not in taken
    ):
        held = w.holding
        if _place(w, held, dest):
            taken.append(held)
        else:
            failed.add(held)
            _put_down_somewhere(w)

    attempts = 0
    while len(taken) < 2 and attempts < 6 and _budget_ok(w, 4):
        attempts += 1
        inst, recep = _locate_next(w, obj, visited, tuple(taken) + tuple(failed))
        if not inst:
            break
        if not _take(w, inst, recep):
            failed.add(inst)
            continue
        if _place(w, inst, dest):
            taken.append(inst)
        else:
            # Placement failed -- do NOT stay stuck holding it, or every
            # future take() call will silently fail for the rest of the
            # task. Put it down somewhere and keep searching.
            failed.add(inst)
            _put_down_somewhere(w)

    return len(taken) >= 2

Certificate

Code sha256 22c60cf120db1531b9c9909eda61cd131b9f9b1c7b08a845e8403c76834eccb2
Signature ed25519, present
Last re-checked
passing

Re-checked today. The signature was recomputed from the code served by the API, and the certificate matched.

A certificate proves the code has not changed. Re-running the check proves it still works today, on today's libraries.