AgentCanvas / Pages / Developer Guide / Design Docs / Components / NodeSets
2026-07-03 19:40

A node is one function on the canvas; a NodeSet is how related nodes ship together. It groups nodes that share a lifecycle (a loaded model, a simulator handle), registers them under a ${nodeset}__${node} prefix, and loads or unloads atomically. Its one extra power is running in a different process or conda environment than the framework โ€” so a node that needs Python 3.8 + habitat-sim can sit beside one that needs the 3.11 backend. This page owns packaging, lifecycle, and the server modes; the node class itself is Base Canvas Node, and the control panel an env nodeset attaches is Env Panels.


1. What a NodeSet is, and when to make one

A NodeSet is a BaseNodeSet subclass that returns a list of node instances from get_tools(). Reach for one when nodes share expensive initialisation (load a model once, use it from several nodes), when they form a natural group that should load and unload as a unit (an environment's reset/step/observe/evaluate), or when they need process / environment isolation (a different conda env, a crash-prone simulator). A handful of unrelated utility nodes don't need a NodeSet โ€” just drop them in a scanned directory.

2. The smallest NodeSet

from app.components import BaseNodeSet

class GreeterNodeSet(BaseNodeSet):
    name = "greeter"                      # = the workspace file stem; the __ prefix
    description = "A two-node demo set"   #   ({nodeset}__{node}) routes catalog + auto-load

    def get_tools(self):                  # the one required method
        return [HelloNode(), GoodbyeNode()]

Drop the file under workspace/nodesets/, load it in the NodeSet Manager, and greeter__hello / greeter__goodbye appear in the catalog. Open a graph that uses them and the frontend auto-loads the set from the __ prefix.

3. Three server modes at a glance

The same BaseNodeSet runs three ways. Which one is chosen is mostly automatic โ€” it follows server_python and the eval worker count:

Local nodeset in-process same env ยท lowest latency Auto-hosted proxy subprocess server_python โ†’ auto_host ยท HTTP Manual ServerNodeSet proxy your server non-Python / remote machine
ModeWhenHow it's selected
LocalSame conda env as the backend; lowest latency.Default (server_python = None).
Auto-hostedNeeds a different Python / env (habitat-sim 0.1.7, a model's deps).Set server_python to that interpreter โ€” the registry auto-routes to server mode and spawns the subprocess for you.
Manual ServerNodeSetNon-Python server, custom protocol, or a remote machine.Subclass BaseNodeSet + BaseServer and run it yourself.

Independently, an eval run with worker_count > 1 forces server mode so each worker gets its own process (ยง9).

4. The BaseNodeSet contract

One required attribute (name) and one required method (get_tools); the rest are optional ClassVars:

ClassVarDefaultRole
name(required)Unique id; must equal the workspace file stem. Becomes the ${name}__ node prefix.
description""One-line summary for the Manager.
server_pythonNoneInterpreter for server mode; non-None auto-routes to auto-hosted (ยง7).
parallelism"shared"shared (one instance) or replicated (one subprocess per eval worker) โ€” ยง9, ADR-server-003.
env_panelNoneA BaseEnvPanel subclass to register on load (ยง12).
default_per_step_budget_sec30.0Per-episode eval timeout = max_steps ร— this.
replay_parserNonePath to a sibling BaseReplayParser module for log replay.

5. Lifecycle hooks

On load, the registry constructs the NodeSet, calls get_tools(), awaits initialize(**kwargs), and registers the tools; on unload it awaits shutdown(). Both lifecycle hooks default to no-ops โ€” override them only when the set holds a heavyweight resource:

class EnvHabitatNodeSet(BaseNodeSet):
    name = "env_habitat"
    env_panel = HabitatEnvPanel          # control-plane panel (see env-panels)
    parallelism = "replicated"           # stateful sim: one subprocess per eval worker

    async def initialize(self, **kwargs):
        self._sim = load_habitat()       # heavy, held for the nodeset's lifetime

    def get_tools(self):
        return [ResetHabitatTool(), StepDiscreteHabitatTool(),
                ObserveEgocentricHabitatTool(), EvaluateHabitatTool()]

    async def shutdown(self):
        self._sim.close()

Env NodeSets also override get_eval_metadata() to expose splits, episode counts, metric names, and the per-episode step_budget the batch runner reads (Env Panels ยง5).

A NodeSet can also own state containers by overriding get_containers() (default []): these live in the nodeset's own process, are shared by reference by its nodes, and surface only a read-only preview across the boundary โ€” the production “nodeset-owned container” path documented in State Containers ยง9.

6. Local mode

The default. The NodeSet runs in the framework process and conda env; tool calls are plain in-process method calls with no serialisation. Use it for anything whose dependencies coexist with the backend's. Nothing to configure โ€” leave server_python = None.

7. Auto-hosted mode

When a NodeSet needs a different interpreter, set server_python to it. The registry then auto-routes the set to server mode (ADR-server-001): it spawns python -m app.server.auto_host under that interpreter, the child's AutoServerApp introspects get_tools() and serves a /manifest, and the parent generates proxy nodes that forward over HTTP while keeping the original node_type (so graphs are portable across modes).

class ModelSamNodeSet(BaseNodeSet):
    name = "model_sam"
    # different interpreter โ†’ the registry auto-routes this nodeset to SERVER mode:
    # it spawns `python -m app.server.auto_host` under that env, reads /manifest,
    # and generates proxy nodes that keep the original node_type.
    server_python = "python"

    def get_tools(self):
        return [SamSegmentPointTool(), SamSegmentBoxTool(), SamAutoMaskTool()]

Import-boundary gotcha. The framework process must be able to import the NodeSet module to read its server_python โ€” so the heavy, env-specific imports (habitat_sim, torch with CUDA pinned to the other env) must be deferred inside initialize()/tool bodies, not at module top level. The framework imports the class; the subprocess does the heavy lifting. Server-mode proxy node types currently carry an srv_ prefix internally (tracked as TODO #33) โ€” graphs still bind the unprefixed node_type.

8. Manual ServerNodeSet

For a non-Python server, a custom wire protocol, or a model on another machine, subclass BaseNodeSet + BaseServer (ServerNodeSet) and run it yourself. The framework connects to it as a remote and generates the same proxy nodes. Key BaseServer ClassVars: command (str | list[str] โ€” the argv-list form is preferred, no shell), port, startup_timeout (default 1800s), auto_restart. No in-tree NodeSet uses this path today โ€” auto-hosted covers every Python case โ€” so treat it as the escape hatch for genuinely foreign servers.

9. Parallelism (ADR-server-003)

The parallelism ClassVar decides what happens when an eval runs multiple workers. shared (default) keeps a single instance โ€” fine for stateless tools, which may even rendezvous K callers through the batched-inference server. replicated spawns N independent tagged subprocesses, one per worker โ€” required when the NodeSet holds per-worker state (a simulator's scene, an agent pose), which is why every env NodeSet sets it. A worker_count > 1 request forces server mode regardless, since the workers need separate processes.

10. Registry & REST

WorkspaceComponentRegistry.load_nodeset(name, mode, worker_count) drives loading: resolve the class (scan workspace/nodesets/ if needed) โ†’ auto-route to server mode if server_python is set or worker_count > 1 โ†’ for local, get_tools() + initialize() + register tools + register the env panel; for server, spawn the auto_host subprocess(es), fetch /manifest, generate proxies, register the remote env-panel proxy. The REST surface lives under /api/components (file app/api/platform/components.py):

EndpointBehaviour
GET /api/components/nodesetsList discovered + live nodesets.
POST /api/components/nodesets/{name}/load?mode=Load (optionally forcing a mode).
POST /api/components/nodesets/{name}/unloadUnload + shutdown().
GET /api/components/nodesets/{name}/eval-metadataThe get_eval_metadata() dict.
GET/PUT /api/components/nodesets/{name}/sourceRead / write one source file of the NodeSet (path-guarded to its own dir, existing .py only; PUT is 409-conflict- and ast.parse-gated โ€” the nodeset watcher then hot-reloads local NodeSets, server-mode is only flagged stale). Resolution + guard live in app/components/nodeset_source.py.
GET/PUT /api/components/nodesets/{name}/source/scoped?node_type=Per-node slice for the canvas Source tab: module-level globals, the functions the node's class transitively references, and the class itself, each with its line range; PUT splices the edited segments back and syntax-checks the whole file before writing.
GET /api/components/node-schemasAll node specs (the catalog feed โ€” Base Canvas Node ยง9).
GET /api/components/servers + /{name}/start|stop|restartLifecycle of server-mode subprocesses.

11. File layout

NodeSets live under workspace/nodesets/, organised by role: env/ (simulators), method/, policy/, model/ (e.g. model_sam), common/, other/. A simple set is one .py file (env_habitat.py); a complex one is a folder (env/env_libero/, policy/policy_adapter_vla/). The name ClassVar must equal the file (or folder) stem โ€” that's the contract that ties name, the ${name}__ node prefix, and auto-load together. Placement and naming rules are codified in .claude/standard/nodeset-layout.md.

12. env_panel attachment

An env NodeSet exposes its out-of-loop controls (suite, episode, Play/Stop) by setting the env_panel ClassVar to a BaseEnvPanel subclass. On load, _register_env_panel_for instantiates it, stamps a _context ({mode, server_url, nodeset_name}), and registers it โ€” or, in server mode, registers a RemoteEnvPanelProxy over the bridge. That's all that belongs here; the panel's field/action contract, on_load, and signal side-effects are Env Panels, and how its episode/step operations map onto run scopes is Env & Episode Lifecycle.

13. Cookbook

AgentCanvas docs