AgentCanvas / Pages / Developer Guide / Design Docs / Components / Nodeset Env Panels
2026-06-17

Some controls don't belong on the canvas: which dataset split to run, which episode, the Play and Stop buttons. They act around the graph, not inside it. An env panel is the small declarative control surface that holds them โ€” a BaseEnvPanel subclass that ships inside the env nodeset's file and is rendered by one generic frontend panel. This page owns the panel contract; how its episode and step operations map onto the run's scopes is Env & Episode Lifecycle, and the packaging it rides on is NodeSets.


1. What an env panel is, and why it's env-side

A graph models the in-loop agent: observe โ†’ think โ†’ act. It cannot model the out-of-loop interactions an environment needs โ€” pick an episode, reset the scene, press Play. Earlier those lived in a separate controller registry with its own router and frontend bar; the env panel collapses all of that back into the nodeset, so an environment is once again one file that declares both its nodes and its controls. The rule that keeps the boundary clean: a panel carries only env-side runtime knobs (suite, split, episode, parallelism). Anything about the method โ€” model, checkpoint, adapter, per-step reasoning state โ€” belongs on node configs, never on the panel.

2. The smallest panel

A panel is declarative: list the fields, list the actions, implement on_load. The nodeset attaches it with one ClassVar (env_panel = HabitatEnvPanel, see NodeSets ยง12):

from app.components.env_panel import BaseEnvPanel, EnvPanelField, EnvPanelAction

class HabitatEnvPanel(BaseEnvPanel):
    name = "env_habitat"
    display_name = "Habitat"
    fields = [
        EnvPanelField("split", "select", "Split", options=["val_unseen", "val_seen"]),
        EnvPanelField("episode_index", "number", "Episode", min=0),
    ]
    actions = [
        EnvPanelAction("play", "Play", side_effect="run_start", enabled_when="idle"),
        EnvPanelAction("stop", "Stop", side_effect="run_stop", enabled_when="running"),
    ]

    async def on_load(self):                  # status + the values the runner reads
        return {"available": True, "step_budget": 30, "episode_index": 0}

3. The field/action model at a glance

Two dataclasses and four hooks describe everything a panel can do. The fields are inputs, the actions are buttons; the four async hooks respond to load, edits, clicks, and dynamic dropdowns:

PieceShape
EnvPanelFieldname, kind (select / number / text / โ€ฆ), label, optional options / min / max / step / placeholder.
EnvPanelActionname, label, side_effect (ยง6), enabled_when (always / idle / running / paused).
on_load()Returns the panel's current state โ€” available, field values, and the per-episode step_budget (ยง5).
on_field_change(name, value)A committed field edit; may return a side_effect (ยง6).
on_action(name, params)A button click; returns its side_effect.
get_options(field)Dynamic dropdown contents (e.g. episode list for the chosen split).

4. The BaseEnvPanel contract

A subclass declares four ClassVars โ€” name, display_name, fields, actions โ€” and implements the four hooks (on_load is abstract; the other three default to no-ops). info() serialises the field/action lists for the frontend. After construction the registry stamps _context = {mode, server_url, nodeset_name} so a panel can word its status correctly when, in server mode, it can't reach the env directly. One purity rule keeps the layering sound: a panel must not import the loop runner or executor โ€” it returns declarative intents (ยง6), and the framework acts on them.

5. on_load & step_budget resolution

on_load() is the panel's status read: it returns whether the env is available, the current field values, and โ€” importantly for eval โ€” a per-episode step_budget. The batch runner resolves the effective budget through a three-tier cascade, most-specific first:

  1. an explicit step_budget on the eval API request (wins);
  2. else the panel's on_load() step_budget (legacy fallback key max_steps_default);
  3. else the graph's static step_budget.

A resolved budget below 1 is refused (no zero-step episodes). The per-episode wall-clock timeout is then resolved_budget ร— default_per_step_budget_sec (the nodeset ClassVar, NodeSets ยง4). In batch, the runner drives the panel per episode โ€” pushing field values, then on_field_change("episode_index", i) and on_action("play") โ€” to place each episode; the scope/timing side of that is Env Lifecycle.

6. The side_effect channel

Hooks don't do things; they return intents. The side_effect of an action (or a field change) is one of run_start ยท run_pause ยท run_stop ยท run_step ยท signal ยท none. The framework โ€” not the panel โ€” carries it out. The interesting one is signal: it tells the env-panel router to forward a named framework signal into the running executor's state containers. An episode change uses it to clear lifetime="episode" memory:

async def on_field_change(self, name, value):
    if name == "episode_index":
        # tell the running executor to clear lifetime="episode" containers
        return {"ok": True, "side_effect": "signal",
                "signal_name": "episode_reset", "signal_payload": {"index": value}}
    return {"ok": True}

The router's _forward_signal_if_any reads that result and calls executor.broadcast_signal(signal_name, payload) on the live run. The signal's timing and what it actually resets is Env Lifecycle ยง2 and State Containers ยง6.

7. REST surface

The frontend talks to the panel through five endpoints under /api/env-panels (app/api/canvas/env_panel.py); the router has no env knowledge โ€” it just dispatches to the registered panel and applies side-effects:

EndpointHook
GET /api/env-panelslist registered panels
GET /api/env-panels/{name}/stateon_load()
GET /api/env-panels/{name}/options/{field}get_options(field)
POST /api/env-panels/{name}/field/{field}on_field_change() โ€” guarded: refused while a run is active
POST /api/env-panels/{name}/action/{action}on_action() โ€” guarded for run_start / none / signal side-effects when not idle

The frontend (canvas/panels/EnvPanel.tsx) is one generic renderer for all panels; it remembers the active panel under localStorage["agentcanvas:active_env_panel"].

8. Server-bridge proxy & overrides

When the env nodeset runs in server mode (NodeSets ยง7), the panel lives in the subprocess. The framework registers a RemoteEnvPanelProxy that mirrors the real panel's name/display_name/fields/actions and forwards each hook over the bridge's /env-panel/state|field|action|options routes โ€” so the REST surface and the frontend are identical whether the panel is local or remote.

frontend /api/env-panelsframework RemoteEnvPanelProxy /env-panelsubprocess same REST surface ยท same frontend ยท panel is remote

At worker_count > 1 each replicated worker gets its own tagged proxy (name#k), and the per-worker panel + server URL are tracked as env_panel_overrides / server_url_overrides on the worker handle so the batch runner drives the right one. The subprocess spawn, fan-out, and rollback are nodeset mechanics โ€” NodeSets.

9. What an env panel is not

Keep the surfaces distinct: a node is in-loop computation; a nodeset is packaging; an env panel is the out-of-loop env control plane. A panel must not carry model/checkpoint/adapter choices (those are node configs) or per-step reasoning state (that's container state). If a knob changes how the method behaves, it isn't a panel field.

AgentCanvas docs