Skip to content

Interactions

When an agent performs PickupDrop or Toggle, the step pipeline runs a list of interaction functions — called branches — to determine what happens. Each branch independently decides whether it should fire and what state changes to apply.

Step Pipeline Context

Interactions run after tick functions and movement:

tick → movement → interactions → observations → rewards

PickupDrop ("pick up / put down") and Toggle ("activate / use") are semantically different actions. Built-in branches for pickup, drop, and place only fire on PickupDrop. Custom branches can check ctx.action against ctx.action_id to distinguish which action the agent chose.

Branch Function Signature

Every interaction function has the same interface:

def my_branch(ctx: InteractionContext) -> tuple[bool, dict]:
    should_apply = ...  # bool: should this interaction fire?
    changes = {...}     # dict: array name -> updated array
    return should_apply, changes
  • should_apply — boolean. If false, changes is ignored.
  • changes — dict mapping state array names (e.g. "object_type_map", "agent_inv") to their new values.

The pipeline applies changes via xp.where: for each key in changes, the new value replaces the old one where should_apply is true. When multiple branches fire for the same agent, later branches overwrite earlier ones for overlapping keys.

InteractionContext

The pipeline builds a frozen InteractionContext dataclass before calling branches. Each agent gets its own context per step.

InteractionContext

InteractionContext dataclass

InteractionContext(can_interact: object, action: object, action_id: object, facing_row: object, facing_col: object, facing_type: object, agent_index: object, held_item: object, agent_inv: object, object_type_map: object, object_state_map: object, type_ids: dict = dict(), extra: dict = dict(), _tables: dict = dict())

What the agent sees when it interacts.

Always available (auto-populated by the pipeline):

can_interact      bool -- agent performed PickupDrop or Toggle and cell ahead is clear
action            int  -- raw action index this agent chose this step
action_id         ActionID -- indices for all actions in this env
facing_row        int  -- row of the cell this agent faces
facing_col        int  -- col of the cell this agent faces
facing_type       int  -- type ID of object in faced cell (0 = empty)
agent_index       int  -- which agent (0, 1, ...)
held_item         int  -- type ID of item the agent holds (-1 = empty)
agent_inv         (n_agents, 1) int32 -- all inventories
object_type_map   (H, W) int32 -- the grid
object_state_map  (H, W) int32 -- per-cell state
type_ids          dict -- {"goal": 5, "wall": 1, ...}

Extra state (auto-populated from EnvState.extra_state):

Any extra_state key is accessible by name with the scope prefix
stripped.  ``state.extra_state["global.goals_collected"]`` becomes
``ctx.goals_collected``.

Standard fields

Field Type Description
can_interact bool True if this agent performed PickupDrop or Toggle and no other agent blocks the cell ahead.
action int Raw action index this agent chose this step.
action_id ActionID Named indices for all actions. Use ctx.action_id.pickup_drop, ctx.action_id.toggle, etc. Actions not in the action set have index -1.
facing_row int Row of the cell the agent is facing.
facing_col int Column of the cell the agent is facing.
facing_type int Type ID of the object in the faced cell (0 = empty).
agent_index int Which agent (0, 1, ...) is acting.
held_item int Type ID of item the agent holds. -1 = empty hands.
type_ids dict Maps object names to integer type IDs. ctx.type_ids["goal"] returns the goal's type ID.
object_type_map (H, W) int array Grid of object type IDs.
object_state_map (H, W) int array Grid of per-cell state values.
agent_inv (n_agents, 1) int array All agents' inventories.

Extra-state fields

Arrays declared by components via extra_state_schema are accessible directly as attributes on ctx. The scope prefix is stripped automatically — if a component declares goals_collected, access it as ctx.goals_collected.

Lookup tables

Built-in branches use guard tables to decide whether an action is allowed for a given object type and held item. These are available on the context:

  • ctx.CAN_PICKUP — which object types can be picked up
  • ctx.PICKUP_FROM_GUARD — which (object, held-item) pairs allow pickup-from
  • ctx.PLACE_ON_GUARD — which (object, held-item) pairs allow place-on

Built-in Branches

The autowire system assembles the branch list automatically from registered components. These are the built-in branches, in the order they run:

Branch Fires on Description
branch_pickup PickupDrop Pick up a loose object from the faced cell into the agent's inventory.
branch_pickup_from_container PickupDrop Pick up the cooked result from a container (requires holding the right item, e.g. a plate).
branch_pickup_from PickupDrop Pick up from stacks or counters. Stacks dispense infinitely; counters yield their stored item.
branch_drop_on_empty PickupDrop Drop the held item onto an empty floor cell.
branch_place_on_container PickupDrop Place an ingredient into a container. Validates against recipe prefixes and container capacity.
branch_place_on_consume PickupDrop Place an item on a consume surface (e.g. delivery zone). The item is removed from play.
branch_place_on PickupDrop Place the held item on a surface like a counter.

Container and consume branches are only included when the environment has container or consume objects. Autowire handles this automatically.

Helper Functions

Import from cogrid.core.pipeline.context:

Helper functions

clear_facing_cell

clear_facing_cell(ctx: InteractionContext) -> ArrayLike

Return object_type_map with the faced cell set to empty (0).

set_facing_cell

set_facing_cell(ctx: InteractionContext, type_id: ArrayLike) -> ArrayLike

Return object_type_map with the faced cell set to type_id.

pickup_from_facing_cell

pickup_from_facing_cell(ctx: InteractionContext) -> tuple[ArrayLike, ArrayLike]

Pick up the faced object.

Returns (object_type_map, agent_inv) with the cell cleared and the object in the agent's inventory.

place_in_facing_cell

place_in_facing_cell(ctx: InteractionContext) -> tuple[ArrayLike, ArrayLike]

Place the held item in the faced cell.

Returns (object_type_map, agent_inv) with the item on the grid and the agent's hands empty.

give_item

give_item(ctx: InteractionContext, type_id: ArrayLike) -> ArrayLike

Return agent_inv with this agent holding type_id.

empty_hands

empty_hands(ctx: InteractionContext) -> ArrayLike

Return agent_inv with this agent holding nothing.

increment

increment(array: ArrayLike, index: int) -> ArrayLike

Return array with array[index] += 1.

Works on both JAX arrays (.at[].add) and numpy arrays.

find_facing_instance

find_facing_instance(positions: ArrayLike, facing_row: int, facing_col: int) -> tuple[ArrayLike, ArrayLike]

Find which instance of a multi-position object the agent faces.

Parameters:

  • positions (ArrayLike) –

    (n_instances, 2) int32 array of positions.

  • facing_row (int) –

    Row the agent faces.

  • facing_col (int) –

    Column the agent faces.

Returns:

  • tuple[ArrayLike, ArrayLike]

    (index, is_match) -- index into positions, and whether any instance matched.

Writing a Custom Interaction

Define a function with the branch signature and pass it in the config. Here is an example that removes a goal when an agent picks it up:

from cogrid.core.pipeline.context import clear_facing_cell, increment


def collect_goal(ctx):
    """Remove a goal when the agent picks it up."""
    goal_id = ctx.type_ids["goal"]
    is_pickup = ctx.action == ctx.action_id.pickup_drop
    should_apply = ctx.can_interact & is_pickup & (ctx.facing_type == goal_id)
    changes = {
        "object_type_map": clear_facing_cell(ctx),
        "goals_collected": increment(ctx.goals_collected, ctx.agent_index),
    }
    return should_apply, changes

Wire it into the environment config:

goal_config = {
    ...
    "interactions": [collect_goal],
}

User-provided interactions run before auto-discovered ones, so they take priority for overlapping keys.

Composition

The autowire system reads container and consume metadata from registered objects and calls compose_interactions() internally. This returns:

  • An extras function that pre-computes container context (contents, timers, recipe matching) and attaches it to the interaction context.
  • A branch list assembled conditionally — container branches are included only when containers exist, consume branches only when consume surfaces exist.

User-provided "interactions" from the config are prepended to the auto-discovered list, so they run first.