AgentCanvas / Pages / Developer Guide / Core / Decisions / Components / ADR-components-007
2026-04-17 15:55
Date
2026-04-17 15:55
Status
accepted
Field
components
Old ID
ADR-029

Context

ObservationViewerSink was a fixed 2-port (rgb, depth) RGB+depth viewer — every instance rendered identically, and scenes that wanted a different sensor set (front+back cameras, multi-view RGB-D, depth-only, RGB-only) had to fork the class. The rest of the configurable-port infrastructure already existed (BaseCanvasNode._resolve_ports(config), frontend/src/canvas/portResolution.ts, PortListEditor, ports_mode="sink") and IterIn/IterOut (ADR-dataflow-003) were already using it — the viewer was the odd-one-out.

Decision

Replace ObservationViewerSink with a new ImageViewerSink (node_type imageViewer) whose ports and grid layout are instance-derived from per-graph-instance config. Config shape at graph root: { rows: int, cols: int, ports: [{name, wire_type}, ...] } with wire_type ∈ {IMAGE, DEPTH}. _resolve_ports(config) derives one required-in-intent, optional=True-at-class-level port per ports[] entry (matching existing viewer firing semantics and the _SinkBase invariant that class-level ports stay optional so _is_ready() doesn't deadlock). A new frontend layout imageGrid in GenericBlockRenderer dispatches to ImageGridViewerLayout.tsx, which reads data.rows/cols/ports directly (ports materialise on both sides from the same config key). WS wire unchanged: viewer_data {node_id, step, fields: {<port.name>: <b64>}}. Step-sync is provided by the env emitting all outputs in one step; frontend bag-merges fields across multiple firings per step. The 4 workspace graphs using observationViewer (straightforward.json, navgpt_ce.json, text_analysis_demo.json, opennav_habitat.json) are migrated in-place to imageViewer with {rows:1, cols:2, ports: [rgb/IMAGE, depth/DEPTH]} — existing edges already target handles rgb/depth so no edge changes were needed.

Alternatives

(a) Keep observationViewer as a thin preset around imageViewer — rejected, adds a second type that does nothing the new one doesn't. (b) Nest grid config as {grid: {rows, cols}} — rejected, ConfigFieldRenderer has flat-key addressing (data[field.name]), so flat rows/cols at config root costs nothing and saves a nesting hop. (c) Declare dynamic ports required (optional=False) to enforce strict same-step — rejected; standard/node_io.py's REQUIRED_INPUTS reads class-level ports only, so required instance ports would be invisible to _is_ready() and cause premature firing. Env-side same-step emission + frontend bag-merging already gives the same user-visible guarantee. (d) Add a label field on port config — deferred; port.name doubles as cell label for now, a richer field is a follow-up.

Rationale

One configurable type replaces a family of hardcoded forks. The design reuses every piece of the existing configurable-port contract (ports_mode="sink", _resolve_ports, portResolution.ts, PortListEditor) so the new node is mostly a thin subclass + one new layout component — no new frontend plumbing. Choosing config.ports (the canonical key already consumed by portResolution.ts) over a semantic config.images means the frontend's existing port-derivation logic works unchanged. The optional=True-at-class-level decision preserves the _SinkBase firing invariant (ADR-components-006 family) so no executor changes were needed. Wire types restricted to IMAGE/DEPTH in _resolve_ports — enforced backend-side for v1; a frontend allowed_wire_types filter on PortListEditor is a small follow-up. Note that backend/app/standard/node_io.py:45-60 still builds REQUIRED_INPUTS from class-level input_ports only; this is safe today because imageViewer (like all viewers) uses optional ports, but is worth a general fix when a configurable-port node needs true required semantics (follow-up).

Affected docs

core/architecture.md, core/blueprint.md, design-docs/canvas-system.md, design-docs/graph-system.md, tutorials/execution-logs.md, capabilities/real-time-observability.md, demos/navgpt-second-try.md, core/codebase-map.md, api-reference/base-canvas-node.md, core/roadmap.md (changelog)