AgentCanvas / Pages / Developer Guide / Core / Architecture

Β§1System Overview

Three-tier system: frontend (React canvas) ↔ backend (FastAPI + GraphExecutor) ↔ workspace (user-authored Python components + simulators).

Frontend β€” React 18 + TypeScript + Vite Canvas Editor Output Panels Node Library useFlowStore + useStore (Zustand) REST Β· WebSocket Backend β€” FastAPI + Python 3.10+ (agentcanvas env) API Routes (REST + WS) Component Registry Loop Runner Graph Executornode firing + iteration workspace/ (user code, auto-discovered) nodesets/ Β· nodes/ Β· graphs/ Β· graph_nodes/ Simulators (GPU) Habitat Β· MatterSim Β· SIMPLER LIBERO Β· … (via env nodesets)

Β§2Data Flow

A single execution cycle (ADR-canvas-002: unified canvas):

  1. User clicks Play in the ExecutionToolbar
  2. Frontend generates execution_id (UUID)
  3. getGraphForExecution() serializes the root canvas as GraphDefinition (filters out the frontend-only stateContainer node type β€” containers travel as graph.containers; output viewers are backend nodes and are sent along)
  4. POST /api/navigate/run { loop_definition, execution_id }
  5. Backend creates GraphExecutor from loop_definition
  6. Nodes fire by dataflow as inputs arrive: env observe β†’ LLM call β†’ parse β†’ env step (domain nodes are nodeset-namespaced <nodeset>__<verb> types; built-ins like llmCall / textParse fill the reasoning slots)
  7. IterOut collects end-of-step data β†’ IterIn provides it next iteration
  8. Each node emits WebSocket events tagged with execution_id
  9. Frontend discovers output nodes by type, routes WS events to them
  10. Loop ends: IterOut's stop port goes true, the step budget is exhausted, or user Stop β€” the verdict stage then runs once on the after-loop band
  11. nav_complete event carries final metrics (SPL, SR, nDTW, SDTW)

Β§3Flat Workspace (ADR-canvas-001)

The canvas is a ComfyUI-style flat workspace. All node types coexist on one surface β€” environment, model, reasoning, control, and output nodes are wired freely without layer boundaries.

CategoryNodesPurpose
Environment<env>__reset / __step / __observe / __metric, …Interact with simulator via gym-like verbs (from activated env NodeSets)
ModelllmCall (built-in) + NodeSet model/policy nodes (e.g. policy_adapter_vlnce__predict, SAM)Run neural policies or language models
ReasoningtextParse, historyLog (built-ins) + method NodeSets' reason/parse nodesBuild prompts, make decisions
ControliterIn, iterOutManage iteration cycles; iterOut's stop port and after-loop band handle loop exit + verdict
OutputimageViewer, textViewer, textScroll, actionLog, metricsDisplay streaming results
Tool(dynamic from NodeSets)SAM, navigation tools, etc.

The workspace graph is a GraphDefinition β€” save/load the entire canvas as JSON via /api/graphs.

3.1 Unified Canvas Paradigm (ADR-canvas-002)

One editor, one node registry, one catalog at every graph level:

3.2 NodeSet Manager

NodeSets are managed on a separate page (not the canvas). When a NodeSet is activated, its tools appear in the canvas sidebar. When deactivated, they disappear.

NodeSet Manager page [Load] SAM [Load] Habitat [Unload] Others Canvas sidebar SAM: Segment Habitat: Step (removed) activate activate deactivate

3.3 Recursive Composite Nodes (ADR-dataflow-001)

Composites remain available for optional grouping β€” any group of nodes can become a reusable composite with defined I/O, nestable to arbitrary depth.

Key concepts:

  1. Author β€” select nodes β†’ Group β†’ a composite with auto-generated graphIn/graphOut boundaries.
  2. Save β€” written to workspace/graphs/*.json β†’ appears in the sidebar catalog, draggable onto any canvas.
  3. Execute β€” flatten_graph() expands every composite β†’ the GraphExecutor runs one flat graph.

Β§4Execution Engine

One engine β€” the Graph Executor β€” handles every graph regardless of topology. DAG-shaped workflows are just cyclic graphs that fire each node exactly once; iterative agent loops are the same engine with iterIn/iterOut gates added. There is no separate "DAG executor" path (see Glossary Β§6).

4.1 Graph Executor

  • Nodes fire when all required inputs arrive β€” data-driven, not topo-sort-driven
  • Two-sided iteration model (ADR-dataflow-008, folding ADR-dataflow-006's three pivots to two): iterIn is two-sided β€” its left input ports (declared in initPorts) capture run-start seeds once into init_ slots, its right output ports broadcast step boundaries + expose the loop-carry bundle; iterOut collects end-of-iter state and transfers it back to iterIn via pairedWith for the next iteration. The former Initialize pivot is folded into iterIn's left side; the initialize node type was removed entirely on 2026-06-10 β€” graphs still carrying one are rejected at validation with a migration hint.
  • Per-node state via _NodeStateProxy (tracks inputs, outputs, fire count)
  • Supports pause/resume/step via LoopRunner
  • Two-sided iterOut (ADR-dataflow-009): the former Termination node folded into iterOut's stop input port (checked once per iteration by the Decide phase); on termination iterOut emits a final side (final_<name> + final_stop) exactly once, feeding the after-loop band (evaluate β†’ graphOut) that holds the terminal iteration's values
  • Multi-scope (ADR-dataflow-007): N coexisting (iterIn, iterOut) pairs in one flat graph, each with its own iteration cadence; scope forest computed by analyze_scopes()
  • Flatten step: at the top of run(), flatten_graph() recursively expands any composite nodes before processing
  • Startup seed-discovery rule: a node is queued at run-start iff its type is not iterIn AND it has no incoming edges AND it has no required input ports. Required-but-unwired ports are rejected at the API boundary by validate_graph_connectivity() in graph_def.py β€” no silent never-fires.

4.2 Headless Eval Path (JobScheduler)

The same engine, driven without the canvas β€” the platform's primary path for producing numbers. Β§2's interactive Play runs one episode; this runs hundreds.

  1. POST /api/eval/v2/start enqueues a job on the backend-owned JobScheduler (services/job_scheduler.py)
  2. A per-second tick() admits queued jobs when their calibrated per-resource estimates (VRAM + RAM) fit measured free minus reservations (declared marginal_vram_mb as fallback) and canvas Play isn't holding the ExecutionGuard lock (state.py β€” canvas/eval mutual exclusion)
  3. Each admitted run is its own subprocess β€” python -m app.eval_subprocess_main --run-dir …, kernel-tied to the backend by PR_SET_PDEATHSIG. It re-scans workspace/ from disk (fresh code per launch) and attaches to parent-owned shared singletons via register_remote_nodeset β€” no duplicate VRAM
  4. Inside the subprocess, BatchEvalRunner (agent_loop/eval_batch.py) iterates the episode list through fresh LoopRunner instances; at worker_count>1 an EnvWorkerPool leases tagged env subprocesses per episode
  5. Results land self-contained under outputs/eval_runs/{run_id}/episodes/ep{NNNN}/ (log.jsonl, assets, episode.json); spec.json / shared_urls.json / _DONE form the parent↔subprocess file protocol (services/run_state_io.py)

Governed by ADR-eval-001…004 + ADR-server-003 β€” see the Batch Eval design-doc.

Β§5Component System

workspace/ nodesets/*.py nodes/*.py graphs/*.json graph_nodes/*.json startup scan auto-discover WorkspaceComponentRegistry scan by base class instantiate bridge to runtime register handlers

Scan order: nodesets/ β†’ nodes/; graphs/ + graph_nodes/ are pure data. Hot reload re-runs the scan via POST /api/components/reload.

5.1 Auto-Hosted Deployment (ADR-server-001)

Any BaseNodeSet can be loaded in server mode without writing a ServerApp. The registry launches auto_host in a subprocess, which uses AutoServerApp to introspect the nodeset's tools and auto-generate server functions from their port definitions. The result is identical to YAML-based servers β€” proxy nodes are registered via generate_proxy_nodes().

  1. POST /api/components/nodesets/sam/load?mode=server
  2. spawns python -m app.server.auto_host --file …/sam.py --class SamNodeSet --port 9200
  3. fetches /manifest, generates proxy nodes via generate_proxy_nodes()
  4. canvas sees identical nodes to YAML-declared servers
Endpoint / pathRole
workspace/graphs/Saved graph templates (kind="graph")
workspace/graph_nodes/Archived composite nodes (kind="node")
GET /api/graphsList all (both dirs)
POST /api/graphsSave β€” routes by kind field
DELETE /api/graphs/{id}Delete (searches both dirs)

5.2 Python-Driven Node UI (ADR-components-004)

Every BaseCanvasNode carries a ui_config ClassVar (NodeUIConfig) that declares how the node renders on the canvas β€” color, layout, dimensions, and inline config controls. The frontend's GenericBlockRenderer reads ui_config at render time, eliminating the need for per-node .tsx components.

Two layouts: Block (default β€” standard rectangle with ports, label, optional config fields) and Strip (narrow vertical gate, used by iterIn, iterOut, graphIn, graphOut).

ConfigField declares inline widgets on the node face: label, slider, text, select, toggle, textarea. Example: LLMCall shows a temperature slider; Decision shows an action-space dropdown.

This reduced unifiedNodeTypes.ts to 3 explicit entries β€” compositeNode, stateContainer, and a _generic Proxy fallback that routes every other node type (built-ins + NodeSet tools) to GenericBlockRenderer. The former per-viewer custom .tsx components are gone: viewers now render as display-field layouts inside GenericBlockRenderer (ImageViewerField, LogListField, MetricTableField, TextViewerField under nodes/agentloop/inner/layouts/fields/). The remaining custom components handle interactions that exceed port+config rendering:

Custom .tsxWhy
CompositeCanvasNode / CompositeNodeViewSubgraph preview, port derivation, drill-in
StateContainerNodeCanvas-visible state container element (dual-wire system Β§6) β€” frontend-only type, filtered from execution

The /node-schemas API endpoint includes ui_config in its response, so the frontend catalog can enrich sidebar entries with backend-defined styling at drag time.

2 base classes define the extension points:

Base ClassWhat It Provides
BaseCanvasNodeCustom graph node handler (forward method, NodeUIConfig for Python-driven UI, typed port declarations) β€” see ADR-components-004
BaseNodeSetAtomic group of tools (load/unload together, optional server mode)

5.3 Runtime Services Around the Engine

Β§6State Container System (ADR-dataflow-002)

A dual-wire architecture adds visible shared state alongside the existing dataflow edges:

Data edges β€” typed values, per firing VLMCall Prompt LLM Decision rgb prompt response State edges β€” access grants, no data, no firing Nav State container Decision Prompt

6.1 Container Model

A StateContainer is a dict of named BaseState entries. Each state has two axes:

6.2 Two Wire Systems

Data EdgesState Edges
CarryTyped data per-firingAccess grants (no data)
VisualSolid animated linesDashed violet, no arrowhead
Created viaPort handles (left/right)__state__ handle (bottom)
VisibilityAlwaysToggle on/off

6.3 graph_state

Optional graph-level container auto-accessible by all nodes β€” no state edges needed. Displayed in the State tab of the bottom OutputDrawer panel. Each composite can have its own scoped graph_state.

6.4 Executor Integration

See State Containers (design-doc) for the full design.

Β§7Tech Stack

LayerChoiceWhy
Backend runtimePython 3.10+ (agentcanvas env), FastAPIModern ML stack + async web framework (ADR-platform-004)
ML frameworkPyTorch 2.5 + CUDA 12.1 (agentcanvas)Modern torch for SAM, HF, LLM nodesets
SimulatorHabitat-Sim 0.1.7 (ac-vlnce env, Python 3.8)Binary locked to 3.8; accessed via server mode (ADR-server-001)
Frontend frameworkReact 18 + TypeScriptComponent model, strict typing
Canvas library@xyflow/react (React Flow)Mature node-graph editor with customization
State managementZustand (dual store)Lightweight, no boilerplate, works with React Flow
StylingTailwind CSSUtility-first, fast iteration
Build toolViteFast HMR for frontend dev
Config (eval)Hydra + OmegaConfComposable config groups for eval harness
DocsHand-authored HTML + stdlib dev server (docs/run_dev.sh)Full HTML expressivity; live reload via SSE on mtime change. MkDocs retired 2026-05-18.
AgentCanvas docs