AgentCanvas / Pages / Developer Guide / Nodesets / Method / SmartWay
2026-07-05

SmartWay (25.03; Shi et al., IROS 2025) is a zero-shot VLN-CE agent: a TRM waypoint predictor proposes candidate (angle, distance) moves from the panorama, RAM+ object tags enrich each candidate's description, and a backtracking return option lets the LLM undo a mistaken move. This page documents the decomposed port — the 7-node smartway reasoning nodeset plus the smartway_waypoint server sibling and generic FM wrappers (model_dinov2, model_ram ram_plus) driving env_habitat continuous navigation in smartway_ce.json. The validated 3-node monolith (smartway_mono) remains the equivalence ground truth.

At a glance
UpstreamSmartWay (25.03), IROS 2025 — repo sxyxs/SmartWay-Code pinned @ daa2dd8 (MIT). Adelaide Qi Wu group paper — this is a side-experiment port, deliberately excluded from PortBench v1 (author-relationship constraint).
Env consumedenv_habitat — action space hightolow (step_hightolow; (0,0) = canonical STOP), obs space panorama (n_views=12), reset / evaluate (gym-like contract)
FM consumedbuilt-in llmCall — reference graph pins gpt-5-mini · temp 1.0 · max_tokens 2000 · response_format=json_object (frozen by the AAS constraints file; upstream pinned gpt-4o)
Method nodesetssmartway (7 local reasoning nodes) + smartway_waypoint__predict (server: vendored TRM predictor over wired DINOv2 features; ac-smartway env). FMs consumed as generic wrappers since the TODO #56 extraction (2026-07-04): model_dinov2__extract_features (ac-smartway) + model_ram__tag_views (ram_plus @ 384, ac-ram env — cross-env parity verified 12/12)
State9 lastWrite graph_state entries — 7 episode lifetime + 2 step-lifetime scratch (last_picked_*); single-writer-per-key after the decomposition
Graphworkspace/graphs/vln/verified/smartway_ce.json (21 nodes, 40 edges); sibling monolith verified/smartway_mono_ce.json (promoted 2026-07-05 via interface-equivalence audit; equivalence ground truth, test_equivalence.py + test_graph_interface.py)
Fidelityfaithful with justified deviations (audited 2026-06-11, inventory-first): task_description byte-identical; stop/parse/latch semantics source-checked; decomposition ↔ monolith equivalence tests 10/10 passed (re-run green 2026-06-12 post-reorg — §3E); bucket E empty after probes (§3E).
StatusVerified. Locked gpt-5-mini baseline SR 0.29 (run 20260526_163540, perf rand100, 100 ep). Post-migration re-eval 2026-06-13 (run 20260613_115915, gpt-5-mini, rand100 20 ep, step_budget 15): SR 0.20 · SPL 0.199 · nDTW 0.431 — non-zero harvest confirms the TODO #64 fix (7095f196); within 20-ep variance of the 100-ep lock. Graph promoted to graphs/vln/verified/ (§4).

1. Upstream method analysis (Shi et al. 2025)

Upstream SmartWay is a VLN-CE rollout (vlnce_baselines/common/base_il_trainer.py) around the same OneStagePromptManager class name MapGPT uses (vlnce_baselines/GPT/one_stage_prompt_manager.py) — but rewritten for continuous environments with waypoints, tags, and backtracking. Per step:

  1. Predict waypoints. The panorama feeds a trained TRM waypoint predictor (waypoint_predictor/TRM_net.py, BinaryDistPredictor_TRM + DINOv2-small features per Policy_ViewSelection_VLNBERT.py:111) → K candidate (angle, distance) pairs, each with its view RGB.
  2. Tag candidates. RAM+ (ram_plus, swin-L, image_size 384 — base_il_trainer.py:368–371) runs inference_ram on each candidate image (:440–442) → per-candidate object-tag strings.
  3. Topology bookkeeping + synthetic return (make_action_prompt_backtrackv2, one_stage_prompt_manager.py:44–): the current position and each candidate are appended to nodes_list; the index in that list is the episode-global Place ID. last_distance is derived from the previous step's env action (:54–56); when the last step was not itself a backtrack and last_distance ≠ 0, a synthetic return candidate (angle π, distance = last_distance) is appended (:81–90).
  4. Phrase each candidate (:108–112): "<direction> to Place N which is corresponding to Image N, and this image contains objects such as <tags>." — direction from six 60° buckets (get_action_concept_backtrack, :23–41). The trailing return candidate gets the verbatim phrase “Move back to last position in an opposite direction” (:110).
  5. Letter options + stop gate (make_action_options_backtrack, :148–165): from t ≥ 2 (hardcoded) a bare stop is prepended; everything is letter-prefixed A/B/C….
  6. Prompts (make_r2r_json_prompts, :202–245): a static 8-component task_description system prompt — including a dedicated Return Policy paragraph — plus a 4-field user prompt (Instruction / History / Previous Planning / Action options). gpt_infer_back_track (api.py:48–95) packs candidate images with "Image N:" labels before each image.
  7. Parse (parse_json_action, :294–309): letter → index, else 0; at t ≥ 2 the index is decremented to absorb the prepended stop, so −1 means STOP. parse_json_planning (:284–291) extracts New Planning (fallback "No plans currently.") and appends it to the planning list.
  8. Act (make_equiv_action, base_il_trainer.py:167–204): index → the candidate's (angle, distance), executed as rotate + repeated 0.25 m forward primitives.
  9. History + backtrack latch (make_history, :168–184): appends "step t: <phrase>"; if the executed phrase string-equals the return phrase, self.backtrack = True (:183–184) — which suppresses the synthetic return next step.
habitat — R2R-CE rollout episode instruction RGBD panorama ×12 instruction TRM predictorTRM_net + DINOv2 candidates topo + returnbacktrackv2 :44–120 action optionsstop gate t≥2 · :148–165 RAM+ tagsinference_ram :440–442 tags image packapi.py:48–95 prompt assemblymake_r2r_json_prompts candidate RGBs topology state options system + user "Image N:" + images GPT-4o callJSON 3-field reply manager attrs history · planning · backtrack nodes_list · graph · trajectory last_distance (:54–56) Python object state, in-process make_historylatch :183–184 parse_json_*−1 = STOP · :294–309 make_equiv_actiontrainer :167–204 chosen phrase index episode endstop or step budget index = −1 (stop) habitat env steprotate + 0.25 m forwards angle · distance R2R-CE evalSR · SPL · nDTW metrics for t in range(max_steps) — next step re-observes simulator side manager / model fn GPT call rollout guard object-attr state data flow attr read/write rollout loop (next t)

1.1 Upstream invariants worth marking

2. The AgentCanvas port

The port is a Phase-2 decomposition of the validated smartway_mono monolith (3 nodes), split along natural seams: plan_stepupdate_topology / build_action_options / assemble_prompt / build_images; decide_actionparse_response / resolve_action; update_history unchanged. Verbatim prompt constants and pure helpers live in smartway_mono/_prompts.py and are imported by both variants; test_equivalence.py checks the decomposition against the monolith. After the split, every graph_state key has exactly one writer.

2.1 Per-step flow

env_habitat reset → instruction observe_panorama ×12 instruction (iterIn init) waypoint_predictserver · TRM + DINOv2 candidates update_topologymints Place IDs · owns topo build_action_optionsphrases + letters + stop perception_tagserver · RAM+ tags tags build_imagestiles + "Image N:" labels assemble_promptsystem + 4-field user enriched + indices history/planning snaps full_options system + user images + labels llmCallJSON 3-field reply gpt-5-mini · temp 1.0 · max_tokens 2000 · json_object graph_state history · planning · backtrack nodes_list · graph · trajectory last_distance · last_picked_* lastWrite · episode (scratch: step) update_historybacktrack = is_return parse_response−1 = STOP · writes planning resolve_actionpicked → angle/distance picked · is_stop picked_index is_return · distance iterOut · stopends the loop (two-pivot) is_stop step_hightolowenv_habitat · (0,0)=STOP angle · distance evaluateenv_habitat final_stop (post-loop) iterOut → iterIn — step pulse only (agent memory lives in graph_state) env_habitat node smartway node (server-mode noted) LLM call loop control graph_state container data wire state access loop-back (iterOut → iterIn)

Drawn simplified in three places. ① The loop carry is minimal: iter_out collects step_hightolow.info as a pulse (persist=true) plus parse_response.is_stop on its stop port, and iter_in.step re-triggers observe_panoramaall agent memory crosses iterations through graph_state, the opposite of the MapGPT graph (which carries observations through iterOut). ② instruction enters once through iter_in's init_instruction slot (persist=true) and feeds assemble_prompt every step. ③ step_hightolow.terminated → update_history.step_done (an optional gate input) is omitted from the figure. Stop executes on both paths in the final iteration: parse_response.is_stop → iter_out.stop ends the loop (two-pivot model — no separate termination node), while resolve_action emits (0, 0) which step_hightolow treats as the canonical habitat STOP; after the loop, iter_out.final_stop triggers evaluate, harvesting the STOP step's metrics (the §4 rewire).

2.2 Node inventory

NodeInOutRole
smartway_waypoint__predict
(server)
views:ANY, rgb_features:TEXTcandidates:ANY, num_candidatesVendored TRM waypoint predictor (BinaryDistPredictor_TRM + VLN-CE depth encoder) over externally-wired DINOv2 features (base64-npy envelope from model_dinov2__extract_features, remapped clockwise in-engine — byte-identical to the old in-engine forward). rgb_features is required: an optional port would not gate firing and silently races to zeros. → {idx: {angle, distance, rgb_base64}}, integer-indexed 0…K−1.
model_ram__tag_views
(server, generic FM)
views:ANYtags:ANYRAM++ (swin-L @ 384, variant=ram_plus) keyed-dict tags — the predictor's candidates dict wires straight into views (already keyed views), same indices out, no decode mismatch. Replaced smartway_perception__tag in the TODO #56 extraction.
smartway__update_topologycandidates:ANYcandidates_enriched:ANY, candidate_node_indices:ANY, last_backtrack:BOOL, history_snap:TEXT, planning_snap:TEXTSole writer of nodes_list/graph/trajectory; mints UUID place ids → episode-global Place IDs; appends the synthetic return; emits state snapshots so downstream nodes never re-read graph_state mid-iteration.
smartway__build_action_optionscandidates_enriched, candidate_node_indices, last_backtrack, tagsfull_options:ANY, only_options:TEXT, only_actions:TEXT, candidates_dict:TEXTPure: per-candidate phrases (verbatim format incl. tags), letter prefixes, stop at t ≥ 2; emits the manifest resolve_action later consumes.
smartway__assemble_promptinstruction, history_snap, planning_snap, full_optionstask_description:TEXT, prompt:TEXTPure: verbatim static system prompt + 4-field user prompt. Wired into llmCall.system_prompt — unlike the MapGPT graph, no config-borne prompt copy exists.
smartway__build_imagescandidates_enriched, candidate_node_indicesimages:LIST[IMAGE], image_labels:LIST[TEXT]Pure: base64 → RGB arrays + "Image N:" labels aligned to Place IDs (label-before-image, api.py:48–95).
smartway__parse_responseresponse:TEXT, only_options:TEXTpicked_index:TEXT, is_stop:BOOL, thought:TEXT, new_planning:TEXTTolerant JSON parse (fence/prose stripping) → upstream index semantics (−1 = STOP); sole writer of planning (appends, list-valued).
smartway__resolve_actionpicked_index, is_stop, candidates_dictangle:TEXT, distance:TEXT, is_return:BOOLManifest lookup → env action; (0, 0) on stop; writes scratch last_picked_index/last_picked_type.
smartway__update_historypicked_index, is_stop, is_return, only_actions, distance, step_done?history:TEXT, backtrack:BOOLSole writer of history/backtrack/last_distance: appends "step t: <phrase>", sets backtrack = is_return (a wire, not a string match), records last_distance for next step's synthetic return.

2.3 State & memory

graph_state entryReducer · type · lifetimeSole writerUpstream counterpart
historylastWrite · TEXT · episodeupdate_historyself.history
planninglastWrite · ANY (list) · episodeparse_responseself.planning (template reads [-1])
backtracklastWrite · BOOL · episode (initial false)update_historyself.backtrack exact-string latch
nodes_list / graph / trajectorylastWrite · ANY · episodeupdate_topologyself.nodes_list / .graph / .trajectory
last_distancelastWrite · ANY · episodeupdate_historyDerived per-step from env_action (one_stage_prompt_manager.py:54–56)
last_picked_index / last_picked_typelastWrite · TEXT · stepresolve_action— (port-added forensics scratch)

The episode lifetime (vs MapGPT's run) means state clears between batch-eval episodes by container signal, with declared initial_values standing in at step 0. The two last_picked_* scratch keys are step-lifetime — per-step forensics, no cross-step reads.

2.4 Boundary contract

Reasoning is split from models and from the env. smartway (local) is pure reasoning; smartway_waypoint is a task-specific server-mode predictor in the ac-smartway conda env (Python 3.8.20, torch 2.1.1+cu121; SMARTWAY_PYTHON overrides the interpreter), and the foundation models ride generic wrappers: model_dinov2 (RGB features, ac-smartway) and model_ram (ram_plus tags, ac-ram). Env side: env_habitat via reset / observe_panorama (n_views=12) / step_hightolow / evaluate — the hightolow action space executes rotate + ⌊distance/0.25⌋ forward primitives, with (0,0) as the canonical STOP. Boundary-compliant per TODO #56's method/foundation-model split.

2.5 Prompt assets

All verbatim text lives in workspace/nodesets/method/smartway_mono/_prompts.py, shared by the monolith and this decomposition: the 8-component task_description (byte-diffed against upstream :205–232 — identical), the 4-field user-prompt template (:242–245), RETURN_PHRASE (:110), DEFAULT_PLANNING (:20), INIT_HISTORY (:234), and the pure helpers (direction buckets, options, parse). The graph wires assemble_prompt.task_description → llmCall.system_prompt — there is no duplicated prompt copy in the graph JSON.

3. Delta vs upstream — four buckets

A. Preserved verbatim

ElementUpstream anchorHow preserved
8-component task_description (incl. Return Policy paragraph)one_stage_prompt_manager.py:205–232Byte-identical (programmatic diff, 2026-06-11) — build_task_description()
4-field user prompt (Instruction/History/Previous Planning/Action options):242–245Same f-string shape incl. list-repr options and INIT_HISTORY at t=0
RETURN_PHRASE wording:110Character-for-character (the latch semantics depend on it upstream)
Candidate phrase format "… to Place N … contains objects such as <tags>.":108–112Verbatim f-string in make_action_prompts
Six 60° direction bucketsget_action_concept_backtrack :23–41Verbatim port incl. the unreachable-angle assert
Stop gate t ≥ 2 + letter prefixes:148–165Verbatim semantics (prepend_stop_options) — hardcoded, no config knob
Parse index shift (−1 = STOP; fallback 0):294–309Verbatim semantics (parse_json_action)
Planning fallback "No plans currently.":284–291Verbatim (parse_json_planning)
History payload "step t: <phrase>"make_history :168–184Same string; synthetic-return offer conditions (:54–56, :81–90) preserved in update_topology
RAM+ config (ram_plus, swin-L, image_size 384) + per-candidate inferencebase_il_trainer.py:368–371, :440–442Same model/size via model_ram__tag_views (variant=ram_plus; cross-env output parity verified 12/12)
Label-before-image "Image N:" packingapi.py:48–95build_images emits aligned parallel arrays
Waypoint predictor weights/architecturewaypoint_predictor/TRM_net.py + DINOv2 per Policy_ViewSelection_VLNBERT.py:111TRM vendored verbatim under smartway_waypoint/_vendored/; DINOv2 forward extracted to model_dinov2 (features bitwise-equal through the envelope + clockwise remap)

B. Forced by substrate (script → typed graph)

UpstreamPortWhy forced
OneStagePromptManager object attributes9 graph_state entries (lastWrite · episode), single-writer-per-keyCross-iteration memory must live in visible, lifetime-managed containers
One class method per stage, in-process7-node split with explicit snapshot wires (history_snap/planning_snap)Decomposed nodes must not re-read mutable state mid-iteration — snapshots freeze the iter's view at update_topology
TRM predictor + RAM+ called in-processTwo server-mode nodesets in the dedicated ac-smartway conda envtorch 2.1.1/py3.8 model stack can't live in the backend interpreter (ADR-server-001)
Index → action inside the trainer (make_equiv_action)candidates_dict manifest crosses a wire from build_action_options to resolve_actionProducer and consumer are separate nodes; the lookup table must be data
Place identity = simulator waypoint idsUUID4 minted per observation in update_topologyThe continuous env exposes no stable waypoint ids across steps; only index stability matters for the prompt

C. Forced by environment / cost

UpstreamPortReason
Model pinned gpt-4oProfile-driven llmCall; reference graph frozen at gpt-5-mini · temp 1.0 · max_tokens 2000 · json_object (AAS constraints file)The AAS experiments benchmark gpt-5-mini specifically; gpt-5-family needs temp 1.0 + max_tokens ≳ 2000 or llmCall silently returns empty
Sequential rollout scriptJobScheduler profiles (smoke_smartway_ce / perf_smartway_ce, rand100 slice), up to 20 env workersBatch eval throughput; the 20-worker cold start exposed a perception-server race (§4)

D. Intentional divergences

UpstreamPortRationale
Backtrack latch = exact string match on the executed phrase (:183–184)backtrack = is_return — a BOOL carried on a wire from resolve_actionRemoves the string-match fragility and the read-clear-at-iter-top pattern; behaviour equivalence covered by test_equivalence.py
Raw json.loads on the replyMarkdown-fence / embedded-prose stripping before parsingProfile-driven LLMs drift to fenced or prose-wrapped JSON; recover instead of burning the step
Monolithic prompt managerPhase-2 decomposition with the monolith kept as ground truth (smartway_mono_ce.json + test_equivalence.py)Finer AAS search granularity per node; equivalence tests guard the refactor
last_picked_index/last_picked_type scratch statePer-step forensics for eval debugging; no upstream counterpart

E. Unexplained / defects

None found (audit 2026-06-11). What was probed beyond the port's own claims: ① upstream's fuse_close_node=True signature default (one_stage_prompt_manager.py:44) is dead code — both rollout call sites pass False (base_il_trainer.py:446, :448) and the body never reads it, so the port's omission matches actual behaviour; ② the rollout calls make_history v1 (:509) with make_history_v2 commented out (:510) — the port correctly mirrors v1; ③ decomposition ↔ monolith I/O equivalence executed, not just cited: test_equivalence.py 10 passed in 0.17 s (byte-compares every output port and state write under a UUID-determinism fixture). Re-verify 2026-06-12: the reorg commit (119f82da) had left a stale from workspace.nodesets import smartway in the UUID fixture (it migrated the other four import sites); fixed, and the suite re-ran 10 passed in 0.18 s against the post-reorg tree.

4. Evaluation

WhatNumberEvidence
Paper-reportednot recorded in-repoSmartWay is a side-experiment port (author-relationship; dropped from PortBench v1) — no paper-claim closure run exists; the port is benchmarked against its own locked baseline below
Locked baseline (gpt-5-mini, 100 ep, R2R-CE val_unseen rand100)SR 0.29 · SPL 0.229 · nDTW 0.433 · OS 0.40run 20260526_163540 (myloop v0 iter_0 no-patch baseline, perf_smartway_ce, passes=1); independent AFlow v0 baseline on the same graph: median 32%, 95% CI (23%, 41%)
Older reference (gpt-4o · T=0 · budget 20)SR ≈ 0.28Pre-AAS config — not comparable to the gpt-5-mini line (different model/temp/step budget); noted in the gym-migration memory
Post-migration check (gpt-5-mini, 20 ep, step_budget 15)reported SR 0.000 · true ≈ 0.25run 20260610_091657 — the TODO #64 harvest bug, see below

Known gap (roadmap TODO #64 — confirmed on this graph): evaluate was a loop-body node triggered by step_hightolow.info; in the final iteration the loop terminated via parse_response.is_stop → termination before evaluate re-fired, so the harvested output_port__metrics holds the pre-STOP value. Habitat does compute success on the STOP step (visible in step.info.metrics): in run 20260610_091657, 5/20 episodes stopped within the 3 m radius but recorded success=0 → reported SR 0.000 vs true ≈ 0.25. Mechanism, not agent regression (nDTW 0.50 ≥ the 0.40 reference). Fix + re-verify before trusting any SR from the migrated graph. Update (2026-06-11 verify pass; committed 2026-06-11 as 7095f196): is_stop now feeds iter_out.stop and evaluate is triggered post-loop by iter_out.final_stop — the sound final-fire shape (trigger from the pivot, no upstream data-node dep). SR re-verification done 2026-06-13: run 20260613_115915 (rand100 20 ep) harvested SR 0.20 (4/20) — non-zero, every episode logging 3–15 decision steps, zero step_count=0 completions; the mechanism is fixed.

Operational gotcha (engine-level): at worker_count=20, the shared smartway_perception singleton returned HTTP 500 to 11/100 first-episode requests in run 20260516_101057 (cold-start race); GraphExecutor silently completed those episodes with step_count=0 and empty metrics (the silent-episode-completion engine hole). Check per-episode step_count before trusting aggregates from high-worker runs.

Current status: smartway_ce.json was promoted back to workspace/graphs/vln/verified/ on 2026-06-13 — the post-migration re-eval (run 20260613_115915: gpt-5-mini, rand100 20 ep, SR 0.20) confirmed the gym-rewired surface (observe_panorama / terminated / reset) and the two-pivot harvest behave correctly. A full rand100 (100-ep) run remains the headline-number target. The sibling monolith smartway_mono_ce.json was promoted to verified/ on 2026-07-05 via a no-LLM interface-equivalence audit: all 29 edges resolve against the current port declarations, its env-side wiring is handle-identical to the re-verified decomposed graph (so the gym-rewired surface and two-pivot harvest carry over), and the 10/10 mono↔decomp byte-parity suite still passes. The audit is pinned as smartway_mono/test_graph_interface.py.

5. Usage

# load — smartway (local) + the two server-mode model nodesets
POST /api/components/nodesets/smartway/load
POST /api/components/nodesets/smartway_waypoint/load?mode=server   # smartway conda env
POST /api/components/nodesets/smartway_perception/load?mode=server

# graph
workspace/graphs/vln/verified/smartway_ce.json

# batch eval — graph-only form: /experiment:run <profile> <graph_name> [key=value ...]
/experiment:run perf_smartway_ce smartway_ce          # rand100, 100 ep
/experiment:run smoke_smartway_ce smartway_ce episode_count=3

6. What this nodeset is NOT

7. Source files

FilePurpose
workspace/nodesets/method/smartway/__init__.pyThe decomposed nodeset — 7 reasoning nodes (700 lines; docstring carries the decomposition map)
workspace/nodesets/method/smartway/test_equivalence.pyDecomposition ↔ monolith equivalence tests
workspace/nodesets/method/smartway_mono/{__init__.py, _prompts.py}Phase-1 monolith (ground truth) + the shared verbatim constants/helpers module
workspace/nodesets/method/smartway_waypoint/Server-mode TRM waypoint predictor (_vendored/waypoint_predictor/ verbatim upstream sub-tree)
workspace/nodesets/method/smartway_perception/Server-mode RAM+ tagger
workspace/graphs/vln/verified/smartway_ce.json · verified/smartway_mono_ce.jsonDecomposed (this page, verified 2026-06-13) and monolith (ground truth, promoted 2026-07-05) agent graphs
workspace/nodesets/_upstream/smartway-code/fetch_upstream.shClones upstream @ daa2dd8 (MIT) into a gitignored upstream/
outputs/design_runs/{myloop,aflow,adas-subagent}/smartway_ce/AAS search runs on this graph (baseline evidence in §4)
AgentCanvas docs