AgentCanvas / Pages / Developer Guide / Nodesets / Method / DiscussNav
2026-06-16

DiscussNav (23.09; Long et al., ICRA 2024 — "Discuss Before Moving: Visual Language Navigation via Multi-expert Discussions") is a zero-shot R2R agent that, every step, runs a panel of role-specialised GPT-4 experts — instruction analysis, vision perception, completion estimation, and a decision-testing pair — over a sampled set of candidate moves before committing to a viewpoint. Section 1 dissects the upstream rollout (DiscussNav.py, the single-file reference). Section 2 documents the AgentCanvas port — the 9-node discussnav nodeset on env_mp3d discrete panoramic nav. Section 3 files every difference into the five delta buckets. Bottom line up front (re-audit 2026-06-16): the per-step reasoning, prompts, and — after a control-flow repair pass — the loop control now match upstream: the adaptive 5/7 horizon drives a live horizon_stop, pred_vp samples n=5 with retry-until-5-valid and an early break_flag, and vision is navigable-gated like upstream's observe_view. Verdict: faithful with justified deviations; verified (smoke).

At a glance
UpstreamDiscussNav (23.09), ICRA 2024 — Long et al. Reference copy at third_party/zz_just_for_refer/DiscussNav/DiscussNav.py (533 lines). Not pinned — no _upstream/ fetch script, no recorded commit (audit fact, §7).
Env consumedenv_mp3d — action space waypoint (step_waypoint), obs spaces navigable + panorama (gym-like contract)
FM consumedbuilt-in llmCall (3 instances + in-node litellm calls; run pins gpt-5-mini) · model_ram (RAM tagger, server-mode) · model_instructblip (InstructBLIP flan-t5-xl, dedicated server-mode FM nodeset)
Nodes / state9 canvas nodes · graph_state entry nav_history (accumulator-on-the-wire)
Graphworkspace/graphs/vln/unverified/discussnav_mp3d.json (29 nodes, 69 edges, adaptive step_budget 5/7)
FidelityFaithful with justified deviations — inventory-first audit 2026-06-15, re-audit after the control-flow + L2 vision repair 2026-06-16, against the local reference copy. Per-step reasoning + 6 system prompts byte-identical (§3A); control flow (adaptive horizon, retry/break, live stop) now matches (§3A); remaining deviations are recorded (§3D). The 2026-06-15 high-severity defects (§3E) are resolved.
StatusVerified (smoke). Graph still in graphs/vln/unverified/ pending a paper-comparable slice. Faithful run 20260616_172215 (LAVIS-aligned beam-5 decoding): all 5 eps R2R val_unseen / gpt-5-mini navigate end-to-end (4–7 steps, trajectory ≈ 16.8 m). The 5-ep SR is noise-dominated — 0/5 here vs 1/5 in the earlier greedy run, a single-episode swing (§4); not paper-comparable (tiny slice).

1. Upstream method analysis (Long et al. 2024)

Upstream DiscussNav is a single-file MatterSim rollout (DiscussNav.py) over four "expert" classes plus a DiscussNav_Agent. The "discussion" is not eight independent agents — it is GPT-4 sampled n=5 times from one prediction agent, then those samples grouped, fused, and adjudicated. Per episode (main, :480–528):

  1. Instruction analysis (once per episode). detect_actions (:103–110) first translates the instruction to English (:104), then GPT-4 decomposes it into a newline-separated action list; detect_landmarks (:112–119) extracts landmarks from those actions. Results are cached per instruction (actions_cache).
  2. Adaptive horizon. step_length = 5 if len(actions.split("\n")) <= 5 else 7 (:506) — the loop runs exactly step_length steps (for current_step in range(step_length), :507). There is no STOP action; the episode ends at the budget, and the final viewpoint is wherever the agent stands.
  3. Vision perception (per step). observe_environment (:206–232) turns to each of 12 directions; per navigable direction observe_view (:161–204) runs RAM tagging + InstructBLIP captioning and emits "Direction d … Navigable Viewpoint ID: vp [(Passed Area)] Elevation: Eye Level Scene Description: {caption} Scene Objects: {tags}; " (:170, :185–187). Crucially it captions only the navigable directions, caching by view (view_record). A "stair"-in-instruction branch additionally looks down −30° and appends an Elevation: Look Down block (:189–200).
  4. Completion estimation (per step). review_history (:267–271) joins prior steps as "Step N Observation: … Thought: …" with " -> "; estimate_completion (:273–290) calls GPT-4 and returns only the slice after "Executed Actions" (:286–288).
  5. Prediction — the discussion (per step). DiscussNav_Agent.pred_vp (:297–347) issues one GPT-4 call with n=num_predictions=5. Each sample must contain Prediction:, parse to a 32-char hex vp, and be a substring of the observation (:327–338). It retries the whole n=5 call up to num_retry=5 times until 5 valid predictions land (:322, :343–345); if it never does, break_flag=True and the episode loop breaks (:516–517).
  6. Decision testing (per step). thought_fusion (:364–382) groups predictions by unique vp and fuses each group's thoughts with a GPT-4 call; test_decisions (:384–405) returns the single vp directly when there is only one, else asks a final GPT-4 (temperature 0, ≤3 retries) to pick.
  7. Act + memory (per step). make_action (:349–361) teleports to the chosen vp and returns that direction's observation string; save_history (:256–265) runs two sequential GPT-4 summarisations (observation, then thought) and appends {viewpoint, observation, thought} to nav_history.
  8. Evaluate. After the loop, final_vp = sim.getState()…viewpointId (:525) → eval_pred (:407–445) computes SR / SPL / OSR / nDTW against the GT path (ERROR_MARGIN = 3).
MatterSim — R2R rollout 12-dir navigable vps per-view RGB observe_environmentRAM + InstructBLIP :161–232 observation text12 directions :185–187 review_history:267–271 estimate_completionGPT-4 · :273–290 manager state nav_history (list) actions · landmarks view_record cache Python object state, in-process pred_vpGPT-4 n=5 · retry≤5 :297–347 thought_fusionGPT-4 per-vp · :364–382 test_decisionsGPT-4 retry≤3 · :384–405 make_actionsim teleport · :349–361 chosen vp + dir obs save_history2× GPT-4 summarise break_flag / horizonrange(5 or 7) · :506,:516 episode endbudget or break final_vp eval_predSR/SPL · :407–445 for current_step in range(step_length 5/7) — next step MatterSim / eval expert fn GPT-4 call horizon / guard object-attr state rollout loop

The row layout mirrors the port diagram in §2.1 box-for-box, so the two figures compare directly.

1.1 Upstream invariants worth marking

2. The AgentCanvas port

workspace/nodesets/method/discussnav.py splits the rollout into 9 single-responsibility nodes, wired in discussnav_mp3d.json against env_mp3d and two foundation-model nodesets. The per-step reasoning maps cleanly, and after the 2026-06-16 repair pass the loop scaffolding matches upstream too: pred_vp carries the n=5 retry/break, horizon_stop drives the adaptive 5/7 termination, and panorama_to_views is navigable-gated (§2.1).

2.1 Per-step flow

env_mp3d — obs spaces observe_navigable observe_panorama panorama_to_viewsnavigable-gated +RAM + instructblip (cached) observation_aggregatorobs + manifest history_readerreads nav_history estimate_completion_llmgpt-5-mini graph_state nav_history · accumulator 3 access grants declared in graph JSON pred_vpgpt-5-mini n=5 · retry≤5 break_flag → horizon_stop thought_fusiongpt-5-mini per-vp decision_testgpt-5-mini retry≤3 step_waypointenv_mp3d teleport next_vp + thought history_writer2× gpt-5-mini · chosen dir horizon_stop → iter_out.stopstep ≥ adaptive 5/7 − 1 OR break break_flag + step_budget episode endadaptive budget or break iter_out.final_stop evaluateafter-loop band iterOut → iterIn — next iteration (loop_nav / loop_pano re-observe) env_mp3d node discussnav / FM node LLM call loop control graph_state

The loop is a standard iterIn/iterOut scope: observations enter through iterIn's dual slots (init_* from seed_nav/seed_pano at step 0, iterout_* thereafter), and step.info triggers the loop_nav/loop_pano re-observations that iterOut collects for the next iteration. evaluate sits in the after-loop band, fed once by iter_out.final_stop (so TODO #64's loop-body-evaluate under-count does not apply here). Termination now matches upstream: horizon_stop is the single writer of iter_out.stop — it fires when ctx.step ≥ step_budget − 1 (the adaptive 5/7 carried from init_decompose through iterIn) or when pred_vp raises break_flag (fewer than 5 valid predictions after 5 retries). The dead step.terminated → iter_out.stop edge from the first cut is gone.

2.2 Node inventory

NodeInOutRole
discussnav__init_decomposeactions_response:TEXT, landmarks_response:TEXTactions:TEXT, landmarks:TEXT, step_budget:TEXTFolds the two init-LLM responses; computes the adaptive 5/7 budget (:506 formula). step_budget is now wired through iterIn.init_step_budget to horizon_stop (§3A).
discussnav__panorama_to_viewsviews:LIST[IMAGE], view_meta:TEXT, navigable_json:TEXT, agent_heading_deg:TEXTview_tiles:ANY, dir_ids:LIST[TEXT]Navigable-gated (L2, 2026-06-16): for each distinct 12-direction bucket holding a navigable vp, selects the single eye-level (elevation≈0) tile whose absolute heading matches, encodes it to base64, tags dir_id = bucket. ~2–5 tiles, not 36 — mirrors observe_view (§3A). Absolute view headings vs relative navigable headings are bridged by agent_heading_deg.
discussnav__observation_aggregatortags_per_dir:LIST[TEXT], captions_per_dir:LIST[TEXT], dir_ids:LIST[TEXT], navigable_json:TEXT, instruction:TEXTobservation:TEXT, manifest:TEXTBuckets navigable vps into 12 directions (reading navigable_json's heading/elevation keys — the radian fields env_mp3d actually emits); maps each caption/tag to its bucket via dir_ids; builds the verbatim per-direction observation + a JSON manifest of valid (direction, vp) pairs. Reads nav_history for (Passed Area). Stairs look-down skipped (§3D).
discussnav__history_readertrigger:ANY?history_traj:TEXTReads nav_history state; formats as review_history's "Step N Observation: … Thought: …" join; "Step 0 start position." when empty (:512).
discussnav__pred_vpinstruction, actions, landmarks, history_traj, estimation, observation, manifest (all TEXT)predictions:LIST[TEXT], thoughts:LIST[TEXT], break_flag:BOOL, thinking:TEXTThe discussion. Slices estimation to the "Executed Actions" part (:286–288); builds the verbatim Step {step} … input (:314–315); fires num_predictions=5 concurrent llm_complete calls, retrying up to num_retry=5 until 5 valid (32-hex + manifest-member) predictions land; break_flag = len(valid) < 5 (:322, :343–345, :516–517).
discussnav__thought_fusionpredictions:LIST[TEXT], thoughts:LIST[TEXT]fused_json:TEXT, unique_count:TEXTGroups by unique vp; fuses each group's thoughts with an in-node gpt-5-mini call — no single-thought short-circuit (matches :371–378).
discussnav__decision_testfused_json:TEXT, observation:TEXT, instruction:TEXTnext_vp:TEXT, final_thought:TEXTSingle-vp passthrough; else gpt-5-mini (configurable temperature, ≤3 retries) with a fallback to the first vp when no valid pick (more robust than upstream — §3D).
discussnav__history_writernext_vp:TEXT, observation:TEXT, final_thought:TEXT, manifest:TEXThistory_text:TEXTTwo sequential gpt-5-mini summarisations; appends {viewpoint, observation, thought} to nav_history (copy-on-write). Summarises the chosen direction's line, found via the manifest (matches :256–257, :349–361).
discussnav__horizon_stopstep_budget:TEXT, break_flag:BOOLstop:BOOLThe loop-control node. stop = (ctx.step ≥ step_budget − 1) OR break_flag — the adaptive horizon + early break, the single writer of iter_out.stop (§3A).

The instruction-analysis and completion-estimation GPT calls are not in this nodeset — they are built-in llmCall nodes in the graph (decompose_actions_llm, detect_landmarks_llm, estimate_completion_llm), carrying their system prompts in llmCall.config. The prediction call moved into pred_vp (it needs the n=5 retry/break loop); the nodeset's own GPT calls (prediction / fusion / decision / summarise) use the _SYS_* constants in discussnav.py.

2.3 State & memory

graph_state entryReducer · type · lifetimeWriter(s)Upstream counterpart
nav_historyaccumulator (via copy-on-write list) · ANY · runhistory_writer (write); observation_aggregator + history_reader (read)nav_history Python list passed through the rollout

Three access grants are declared (one per reader/writer above). Upstream's view_record caption cache is now ported as a content-hash cache inside model_ram / model_instructblip (§3A); the actions_cache / eval_cache on-disk caches are not (§3C).

2.4 Boundary contract

Reasoning-only nodeset — no simulator calls, no model weights (verified: discussnav.py imports only app.components.bases, app.llm, and stdlib/PIL). Env side: env_mp3d via reset / observe_navigable / observe_panorama / step_waypoint / evaluate. FM side: built-in llmCall (profile-driven), model_ram (RAM tagger, server-mode), and model_instructblip (InstructBLIP flan-t5-xl, dedicated server-mode FM nodeset extracted from navgpt_mp3d_tools per TODO #56's method/foundation-model split). The InstructBLIP server runs in the agentcanvas conda env (transformers 4.45.2) — not ac-ram, whose tokenizers 0.15.2 cannot load the flan-t5 processor (§4 — this 500-ed an entire run).

2.5 Prompt assets

All six system prompts + the 12 direction labels live as verbatim constants in discussnav.py (with DiscussNav.py:line citations, including the preserved "Font Right" typo at :152). The decompose / landmark / estimate system prompts are duplicated into the graph's llmCall.config.system_prompt fields — two copies of the same string, a drift risk (§7). User templates are byte-identical to upstream (see §3A).

3. Delta vs upstream — five buckets

Each row carries an equivalence icon (orthogonal to the bucket letter — the letter says why it differs, the mark says whether behaviour changed): 🟢 equivalent (identical or semantically identical behaviour) · 🟡 near-equivalent (same intent; mechanism, library, or an edge case differs — output usually matches) · 🟠 divergent (behaviour can differ — by design or, rarely, a defect).

A. Preserved verbatim / faithful

Equiv.ElementUpstream anchorHow preserved
🟢6 expert system prompts:106, :115, :275–282, :298–313, :374–375, :391–392Byte-for-byte constants (_SYS_DETECT_ACTIONS_SYS_TEST_DECISIONS); upstream's run-of-spaces indentation preserved literally
🟢12 direction labels (incl. "Font Right" typo):237–238_DIRECTION_LABELS_12, verbatim
🟢Decompose / landmark / estimate / pred_vp / fusion / decision / summarise user templates:107, :116, :283, :314–315, :376, :393, :243/:251Same strings (graph llmCall templates + in-node f-strings)
🟢pred_vp parse: strip " ' \n . * + 32-hex check + manifest membership:331–335pred_vp, same cleanup + length gate against the typed manifest
🟢Adaptive horizon 5 if ≤5 actions else 7 drives the loop:506–507Computed in init_decompose, carried via iterIn.init_step_budget to horizon_stop, which stops at step ≥ budget−1 (resolved §3E-1/2, 2026-06-16)
🟢Five-valid-or-retry + break:322, :343–345, :516–517pred_vp retries the n=5 batch up to 5× until 5 valid; break_flaghorizon_stop ends the episode (resolved §3E-3)
🟢Navigable-gated, cached vision (observe_view):161–204panorama_to_views captions only the eye-level tile per navigable direction (~2–5/step); model_ram/model_instructblip share an sha1-of-image cache = view_record (L2, 2026-06-16)
🟢Estimation "Executed Actions" slice:286–288pred_vp slices estimation_raw.split("Executed Actions",1)[1] before building the prediction prompt (resolved §3E-4)
🟢History summarises the chosen direction:256–257, :349–361history_writer looks the chosen vp up in the manifest and summarises that direction's line (resolved §3E-5)
🟢Per-direction observation string + (Passed Area); single-vp passthrough; review_history join:185–187, :385–387, :268observation_aggregator / decision_test / history_reader, same format

B. Forced by substrate (script → typed graph)

Equiv.UpstreamPortWhy forced
🟢One DiscussNav.py rollout over expert classes9 nodeset nodes + 3 llmCall nodes + env/FM nodesCanvas nodes are single-responsibility; calls must cross typed wires
🟢Per-turn sim.getState() reads navigable vpsnavigable_json bucketed by relative heading into 12 directionsThe env exposes a navigable dict, not a turn-by-turn sim handle
🟢nav_history Python listgraph_state nav_history container (copy-on-write append)Cross-iteration state must live in a visible, checkpointable container
🟢RAM tagging via in-process inference_ram (swin_large_14m)model_ram (server-mode, base64 IPC via panorama_to_views)Separate conda env / server mode; same checkpoint + get_transform(384) + inference_ram → bit-identical
🟢InstructBLIP captioning via in-process LAVIS (blip2_t5_instruct flant5xl)model_instructblip (server-mode, base64 IPC)LAVIS not installed → HF transformers: same weights, 1:1 preprocessing (224 BICUBIC + same CLIP mean/std), decoding aligned to LAVIS defaults (beam-5 / max_length 256). LAVIS calls the same HF T5 .generate → equivalent up to checkpoint-conversion float noise
🟢Absolute MatterSim view headings vs sim.getState() relative navigable headings, both in one processpanorama_to_views takes agent_heading_deg to convert; dir_ids carry the bucket alignment to the aggregatorThe two heading frames arrive on separate typed wires, so the bridge is explicit
🟡GPT-4 n=5 batch inside one functionpred_vp fires 5 concurrent llm_complete calls and parses in-nodeThe n=5 sampling + retry/break is one logical unit, kept in a single node

C. Forced by environment / cost

Equiv.UpstreamPortReason
🟠All 6 call sites pinned to gpt-4Profile-driven; this run pins gpt-5-mini (temp 1.0 + max_tokens 2000 per gpt-5 llmCall rules)Model availability + cost; gpt-5-mini is the maintained, cheaper successor used for the smoke run
🟢On-disk actions_cache / eval_cacheRecomputed each episode; not ported (the view_record caption cache is ported — §3A)Eval harness owns persistence; per-episode runs are independent
🟢Simulator batched in MatterSimenv_mp3d pinned batch=1, subprocess-per-worker (ADR-server-003)Parallelism is at the worker level, not the sim-batch level

D. Intentional divergences

Equiv.UpstreamPortRationale (recorded)
🟠Stairs look-down −30° block (:189–200)Skipped — eye-level onlyCode comment discussnav.py observation_aggregator: "stairs look-down … SKIPPED in v1 — covered as out-of-scope in the implement-graph plan"
🟡test_decisions returns the last (possibly invalid) candidate on retry exhaustionFalls back to the first vp keydecision_test.forward "fallback-first" path — deliberate robustness over upstream's undefined behaviour
🟡detect_actions translates the instruction to English first (:104)No translation node; decompose_actions_llm consumes the raw instructionHarmless for English R2R (the only split run); a translation node would be needed for RxR-style multilingual splits

E. Unexplained / defects

The three high-severity control-flow defects from the 2026-06-15 audit are resolved (2026-06-16). §3E-1 (adaptive horizon dropped) and §3E-2 (dead stop edge) → horizon_stop now reads the adaptive budget and is the single live writer of iter_out.stop; §3E-3 (no retry/break) → pred_vp carries the n=5 retry + break_flag. The medium items §3E-4 (estimation slice) and §3E-5 (chosen-direction history) are also fixed, and §3E-8 (fusion single-thought short-circuit) removed — all now in §3A. §3E-6 was a false positive: the built-in llmCall substitutes {step} from ctx.step (builtin_nodes.py:229), and pred_vp now injects the step itself — there was never a literal {step} in the prompt.

#Equiv.UpstreamObserved in portSeverity
9🟡detect_landmarks strips newlines from actions first (:113)Port passes the raw decompose response (with newlines) into the landmark call — cosmetic; the landmark prompt tolerates newlinesTrivial

Probed and cleared: the RAM + InstructBLIP "Scene Description: {caption} Scene Objects: {tags};" observation format is faithful (upstream :170 uses the same two models — InstructBLIP is not a port-only addition); the 6 system prompts and 12 direction labels are byte-identical; evaluate is correctly on the after-loop band (TODO #64 does not apply). No upstream equivalence test exists to run; the smoke run (§4) is the behavioural check.

4. Evaluation

WhatNumberEvidence
Paper-reported (R2R val_unseen)Not recorded in-repoLit-review (vln-methods §2.5.4) carries venue + citation count only, no SR/SPL. Quote the paper directly when adding a number.
Our port — smoke (gpt-5-mini, faithful beam-5)SR 0/5 · nDTW 0.214 · CLS 0.264 · nav_error 11.36 m · oracle_error 5.78 mRun 20260616_172215, 5 eps R2R val_unseen, 5 workers, step_budget=7, per_step_budget_sec=300, 834 s. All 5 eps navigate (4–7 steps; trajectory ≈ 16.8 m / 5.8 steps avg). InstructBLIP decoding = LAVIS defaults (beam-5 / 256).
Our port — earlier smoke (greedy/64 decoding)SR 1/5 · nDTW 0.380 · nav_error 6.36 mRun 20260616_154036, same slice / per_step_budget_sec=150. The 1/5↔0/5 gap vs the faithful run is one episode — 5-ep noise, not a decoding effect (see callout).

5-episode smoke slice — the SR is noise-dominated, not a paper-comparable number. With n=5, 0/5 and 1/5 are statistically indistinguishable (the 95% CI for a true SR≈0.2 spans roughly [0.5%, 72%]), so the faithful beam-5 run (0/5) and the earlier greedy run (1/5) do not differ meaningfully — both just prove the repaired port navigates end-to-end with correct loop control. Per-episode summary rows report the metric as None (populated only in the aggregate); read the aggregate. For a real number, run a full R2R val_unseen split (or a named prefix — mind the stratification easy-first ordering) with the paper's GPT-4-class model.

Audit note — the earlier "crashes" were not crashes. Before this slice landed, several runs showed status=aborted with a python: … epoxy_get_proc_address: Assertion … Couldn't find current GLX or EGL context line. That assertion is harmless MatterSim-teardown noise (a completed mapgpt_mp3d run emits it 20×). The real cause was rc=-15 (external SIGTERM): the /experiment:run wrapper posts /cancel on interrupt, so killing or replacing the launch command cancels the in-flight eval. Run discussnav uninterrupted. Two genuine issues found and fixed along the way: the InstructBLIP processor failing to load in ac-ram (§2.4), and the panorama over-captioning that timed out 5-worker runs (§3A L2 navigable-gating).

5. Usage

Load the method nodeset plus its two foundation-model dependencies:

POST /api/components/nodesets/discussnav/load
POST /api/components/nodesets/model_ram/load?mode=server
POST /api/components/nodesets/model_instructblip/load?mode=server

Graph: workspace/graphs/vln/unverified/discussnav_mp3d.json. Smoke eval (graph-only /experiment:run, since 2026-05-07) — the actual command behind run 20260616_172215:

/experiment:run discussnav-mp3d discussnav_mp3d split=val_unseen episode_count=5 worker_count=5 step_budget=7 per_step_budget_sec=300

(discussnav sets default_per_step_budget_sec = 90.0; pass per_step_budget_sec explicitly since the episode timeout is step_budget × per_step_budget_sec, resolved from the env nodeset. The shared model_instructblip server loads InstructBLIP once and content-hash-caches captions, so step 0 is slow (~75 s incl. model load + beam-5 captions) and later steps are fast on revisited views. Beam-5 decoding (LAVIS-faithful) is several× slower than greedy, hence per_step_budget_sec=300.)

6. What this nodeset is NOT

7. Source files

KindPath
Nodesetworkspace/nodesets/method/discussnav.py (9 nodes)
Graphworkspace/graphs/vln/unverified/discussnav_mp3d.json (29 nodes, 69 edges, kind="graph")
Upstream (unpinned reference)third_party/zz_just_for_refer/DiscussNav/DiscussNav.py — raw read-only copy; no fetch script, no commit pin
FM depsworkspace/nodesets/model/model_ram.py · workspace/nodesets/model/model_instructblip.py

Side findings (doc/code drift, candidate fixes — not delta buckets):

AgentCanvas docs