AgentCanvas / Pages / Developer Guide / Core / Decisions / Platform / ADR-platform-008
2026-06-11 15:37
Date
2026-06-11 15:37
Status
accepted
Field
platform

Context

Pre-2026-06-11 workspace/nodesets/ had exactly one organizing axis: deployment. The root held nodesets that run inside the backend's agentcanvas interpreter; server/ (a scanner "bucket" β€” a directory without __init__.py) held nodesets that run in their own conda env via auto_host. That axis was pure convention: the engine's actual routing reads the server_python ClassVar (ADR-server-001 / auto-routing), and WorkspaceComponentRegistry._scan_subdir assigns no semantic meaning to directory names.

The layout had accreted real problems: (a) server/ mixed four roles β€” environments (habitat.py, libero/…), policies (policy_*), generic model servers (vlm_*, ram_perception.py), and even pure method logic (explore_eqa.py, tooleqa_explore.py) parked there only because it needed the hmeqa interpreter; (b) one method's parts scattered across the tree by interpreter β€” smartway lived in four places (root smartway/, smartway_mono/, server/smartway_perception/, server/smartway_waypoint/); (c) file stems didn't match nodeset names for six envs (habitat.py β†’ name="env_habitat"); (d) loose non-nodeset files sat at scanner-visible levels (hmeqa_renderer.py, test_matterport3d_path_resolution.py) and got imported on every scan; (e) grab-bags existed (others.py β€” "tools that don't belong to a specific system"). The doc-site nodeset catalog was meanwhile organized by role β€” two taxonomies for one corpus. Tracked as TODO #40.

Decision

Directory encodes role; deployment is not a tree concern at all. Codified in .claude/standard/nodeset-layout.md; the full tree migrated the same day.

  1. Seven top-level directories: env/ (gym-contract simulators + EnvPanel), method/ (one paper/system's reasoning core and its owned parts), policy/ (trained-policy inference), model/ (generic foundation/perception servers), common/ (deliberately cross-method tools), other/ (quarantine for the not-yet-classifiable, entry requires a docstring rationale), _upstream/ (frozen vendored reference, ADR-platform-007). All role dirs are scanner buckets β€” never an __init__.py in them.
  2. server/ is gone. A nodeset needing its own interpreter declares server_python and lives wherever its role puts it.
  3. Stem = name ClassVar, full role prefix included (env/env_habitat.py ⇔ name="env_habitat"). The directory+prefix redundancy is deliberate: the registry and node_type namespaces are flat, so the name must be self-identifying outside its directory (editor tabs, grep hits, AAS diffs).
  4. Ownership beats modality for the method-owned vs generic-model boundary (TODO #56 made operational): a second unrelated method could load it unchanged β†’ model/ (vlm_/model_ prefix); tuned to one method β†’ method/ as {method}_{part}. Method families share the {method} prefix, so same-directory alphabetical sorting reunites scattered parts physically.
  5. Sidecars live inside their owner's folder package; a _-prefixed module may sit loose in a role bucket only when shared by 2+ nodesets there (method/_explore_eqa_tsdf.py, imported by explore_eqa and tooleqa_explore).

Alternatives

(a) Keep the deployment axis, tighten naming only (the first draft of nodeset-layout.md, written hours earlier) β€” rejected on review: it codifies the symptom. The dominant queries ("which envs exist", "where are smartway's parts") still require knowing each nodeset's interpreter history, and the doc-site/code taxonomy split persists.

(b) Ownership-first tree (each method directory owns everything it uses, envs duplicated or symlinked) β€” rejected: envs are shared infrastructure with no single owner; the scanner's one-bucket depth can't express it; the auto_host unit is a file, not a directory closure.

(c) Role directory without name prefix (env/habitat.py deriving env_habitat) β€” rejected: reintroduces "open the file to learn the name", and flat node_type strings still need the prefix anyway, so derivation saves nothing downstream.

(d) Role encoded in names only, directories stay deployment-split β€” rejected for the same reason as (a); also keeps method families physically scattered.

Rationale

The tree has two readers and both think in roles. Humans browse by "what envs do we have"; the coding agents of Paper #1 (AAS implementer) and Paper #2 (porting agents) use paths as retrieval keys β€” env/ answers the env-enumeration query in one hop instead of a scan-plus-classify. The doc-site catalog was already role-organized; this collapses two taxonomies into one.

Deployment is a private implementation detail. The engine reads server_python; encoding the same fact in the path put private information in the public interface while crowding out the semantic axis. Relocation is now free (a role reclassification is a git mv that touches no behavior), and the contested classification axis (method-owned vs generic, TODO #56) lives in the cheap medium β€” names and one-line judgments β€” rather than directory structure.

Zero engine cost. _scan_subdir already supports any number of top-level buckets with packages inside (precedent: server/policy_vla/); the migration was git mv + reference sweep. Migration verification: registry-scan diff before/after (36 discovered, set-identical modulo the three intended renames sam→model_sam, ram_perception→model_ram, others→geometry+mock_frontier), 117 backend tests, and a libero end-to-end smoke (run 20260611_152007, 1390 steps — bit-parity with pre-migration baselines). The scan diff caught the one real breakage (smartway's cross-package import of smartway_mono) before any runtime did.

Accepted costs: (i) historical architect design_runs overlays reference pre-migration relative paths and can no longer replay β€” explicitly waived by the user ("ζ—§ηš„ιƒ½δΈη”¨δΊ†"); (ii) ~107 vendored absolute self-imports (policy_vla, policy_vlnce) and nine __file__-relative path chains needed mechanical depth fixes, each programmatically verified to resolve to the repo root.

Affected docs

Changelog