AgentCanvas / Pages / Developer Guide / Nodesets / Method / MapGPT
2026-06-13

MapGPT (24.01; Chen et al., ACL 2024) is a zero-shot VLN agent: it embeds a linguistic-form topological map of explored/observed places directly in the LLM prompt and carries adaptive multi-step planning text across steps. Section 1 dissects how the upstream OneStagePromptManager pipeline actually works; Section 2 documents the AgentCanvas port — the 8-node mapgpt nodeset driving env_mp3d discrete panoramic navigation; Section 3 files every difference into the four delta buckets.

At a glance
UpstreamMapGPT (24.01), ACL 2024 — repo chen-judge/MapGPT pinned @ 7c642f4 (no upstream LICENSE; see fetch script)
Env consumedenv_mp3d — action space waypoint (step_waypoint), obs spaces navigable + panorama (gym-like contract)
FM consumedbuilt-in llmCall (multimodal, profile-driven; reference graph pins gpt-5-mini · temp 1.0 · max_tokens 6000 — upstream pinned gpt-4o JSON mode)
Nodes / state8 canvas nodes · graph_state entries topo_map / history / planning
Graphworkspace/graphs/vln/verified/mapgpt_mp3d.json (22 nodes, 49 edges)
Fidelityfaithful with justified deviations + 3 unexplained minor divergences (inventory-first audit 2026-06-11 against 7c642f4): system prompt byte-identical; map-fold / options / parse / budget semantics source-checked; bucket E earned — headline finding: upstream pins image detail:"low", our llmCall sends no detail param (§3E).
StatusVerified. Port closure 2026-05-08 (run 675821f9, gpt-4o, 100 ep: SR 0.55 vs paper 0.46). Post-migration re-eval 2026-06-13 (run 20260613_120854, gpt-5-mini, MapGPT72 20 ep): SR 0.55 · SPL 0.394 · nDTW 0.436 — reproduces the closure number on the rewired two-pivot topology; graph promoted to graphs/vln/verified/ (§4).

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

Upstream MapGPT is a rollout script (vln/gpt_agent.py) around one stateful prompt manager (GPT/one_stage_prompt_manager.py). Per step:

  1. Observe. The simulator returns the current viewpoint, heading, and the navigable candidate viewpoints, each with a per-view panorama image keyed by pointId.
  2. Grow the map. The manager folds the current viewpoint and candidates into four parallel structures — nodes_list (every place ever seen; index = Place ID), graph (adjacency, recorded on first visit only), trajectory (visited sequence), node_imgs (one cached image per place).
  3. Serialize the map into language (make_map_prompt, lines 124–165): a Trajectory line of Place IDs, a Map block of “Place i is connected with Places j, k” lines, and Supplementary Info — places seen but never visited (the backtracking menu). Returns "Nothing yet." when empty (line 163).
  4. Build action options (make_action_prompt / make_action_options): each candidate becomes a phrase "<direction> to Place i which is corresponding to Image i", where the direction word (“turn left”, “go forward”, “turn around”, “go up”…) is computed from the heading delta by get_action_concept (lines 16–39). Options are letter-prefixed A./B./C.… (make_action_options, lines 95–112); from t ≥ stop_after a bare stop option is prepended (lines 103–105). The canonical action list always carries stop at index 0 (make_history, line 117), so while stop is not yet offered parse_json_action compensates by adding +1 to the parsed index (lines 349–351) — the “index-shift trick”.
  5. Assemble the 7-field user prompt (make_r2r_json_prompts, lines 218–236): Instruction / History / Trajectory / Map / Supplementary Info / Previous Planning / Action options, plus every cached place image packed as "Image i:"-labelled inputs aligned to Place IDs (GPT/api.py:22–44).
  6. Call GPT in JSON mode. The system prompt demands a 3-field JSON response: {"Thought", "New Planning", "Action": "<letter>"}. parse_json_action (lines 336–352) reads the letter; New Planning is stored and echoed next step as Previous Planning — this is the “adaptive planning” carry.
  7. Act. Letter → candidate viewpoint; the executed action phrase is appended to History as "step t: <phrase>" (make_history, lines 114–122). A hard budget aborts the episode with STOP when the packed image list exceeds 20 (vln/gpt_agent.py:133–136).
MatterSim — R2R rollout viewpoint · heading candidates + pointId views fold mapmake_action_prompt map fold :54–87 · directions :16–39 map → textmake_map_prompt :124–165 action optionsletters + stop gate A. stop, B. turn left to Place 3 … image packapi.py:22–44 7-field promptmake_r2r_json_prompts prompt + "Image i:" GPT-4o callJSON 3-field reply manager attrs nodes_list · graph · trajectory node_imgs (Place i ↔ Image i) history · planning Python object state, in-process make_historyhistory += step t parse_json_actionletter → candidate makeActionMatterSim teleport chosen phrase candidate vp len(image_list) image budgetgpt_agent.py:133–136 stop episode endSTOP or budget a_t = STOP trajectory R2R evalSR · SPL · nDTW for t in range(max_steps) — next step re-observes simulator side prompt-manager fn GPT call rollout guard object-attr state data flow attr read/write rollout loop (next t)

The row layout deliberately mirrors the port diagram in §2.1 — each upstream function sits where its corresponding canvas node sits, so the two figures can be compared box-by-box.

1.1 Upstream invariants worth marking

2. The AgentCanvas port

workspace/nodesets/method/mapgpt.py splits the monolithic prompt manager into a per-step pipeline of single-responsibility nodes. The split point that matters: direction phrases need both the current heading and each candidate's heading, which only coexist at the env boundary — so observe computes them immediately and freezes them into candidates_json; downstream nodes never need heading access. (An earlier split that deferred direction computation to a later stage lost the current heading and was abandoned — see the module docstring.)

2.1 Per-step flow

env_mp3d — obs spaces observe_navigable observe_panorama observeenv → features update_mapowns topo_map build_optionsletters + manifest options_text · options_json system promptllmCall config (verbatim) render_prompt7-field + images prompt · images llmCallJSON 3-field reply graph_state topo_map · lastWrite · run history · lastWrite · run planning · lastWrite · run declared in graph JSON update_historyhistory += step t parse_actionletter → manifest step_waypointenv_mp3d action_phrase viewpoint_id image_count image_budgetdone when > max terminated iter_out — stopstop · final_stop done final_stop evaluateenv_mp3d → metrics graphOut iterOut → iterIn — next iteration (loop_nav / loop_pano re-observe) env_mp3d node mapgpt node LLM call loop control / guard graph_state container data wire state access (read/write graph_state) loop-back (iterOut → iterIn)

The loop is a standard iterIn/iterOut scope, drawn simplified: observations enter observe 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. Stop: parse_action emits viewpoint_id="STOP", which step_waypoint answers with terminated=true, wired into iter_out.stop (since 2026-06-11; env_mp3d treats STOP as episode-done without moving). image_budget.done is the second writer to iter_out.stop — either signal ends the loop. evaluate now sits in the after-loop band, triggered once by iter_out.final_stop at termination.

2.2 Node inventory

NodeInOutRole
mapgpt__system_prompttext:TEXTStatic emitter — verbatim JSON-mode system prompt. Not instantiated by the reference graph, which carries the byte-identical text in planner_llm.config.system_prompt instead.
mapgpt__observeviewpoint_id:TEXT, heading:TEXT, navigable_json:TEXT, views:LIST[IMAGE], view_meta:TEXTcurrent_vp:TEXT, candidates_json:TEXT, candidate_tiles:LIST[IMAGE]Pure env→features transform: per-candidate direction phrase (heading delta) + candidate image picked as views[view_index] (nearest heading+elevation when the env didn't attach an index).
mapgpt__update_mapcurrent_vp, candidates_json, candidate_tilestopo_snapshot:ANYSole writer of topo_map — appends trajectory, folds candidates into nodes_list/node_imgs (tile refresh on re-sight), first-visit adjacency; emits merged snapshot for same-iter consumers.
mapgpt__build_optionstopo_snapshot, candidates_jsonoptions_text:TEXT, options_json:TEXTLetter-prefixed options + machine-readable manifest [{letter, vp, phrase}]; prepends stop when ctx.step ≥ stop_after (slider, default 3).
mapgpt__render_promptinstruction, topo_snapshot, options_textprompt:TEXT, image_list:LIST[IMAGE], image_labels:LIST[TEXT], image_count:TEXT7-field user prompt; reads history/planning state; packs cached tiles with "Image i:" labels aligned to Place IDs.
mapgpt__parse_actionresponse:TEXT, options_json:TEXTviewpoint_id:TEXT, action_phrase:TEXT, thought:TEXT, new_planning:TEXT, is_stop:BOOLTwo-tier parse (JSON → regex fallback); letter → manifest record; writes planning state; fallback = record 0 (upstream index-shift semantics); is_stop is unwired in the reference graph — stop reaches the env as viewpoint_id="STOP".
mapgpt__update_historyaction_phrase, thought?history:TEXTAppends "step t: <phrase>" to history state — upstream make_history payload shape.
mapgpt__image_budgetimage_count:TEXTdone:BOOLGuard — emits done=True when count exceeds max_images (slider, default 20); omits the key otherwise so the done-scan doesn't fire early.

2.3 State & memory

graph_state entryReducer · type · lifetimeWriter(s)Upstream counterpart
topo_maplastWrite · ANY · runupdate_map (sole)self.nodes_list / .graph / .trajectory / .node_imgs object attrs
historylastWrite · TEXT · runupdate_historyself.history
planninglastWrite · TEXT · run (initial = upstream default-planning string)parse_actionself.planning

2.4 Boundary contract

Reasoning-only nodeset — no simulator calls, no model weights. Env side: env_mp3d via reset / observe_navigable / observe_panorama / step_waypoint / evaluate (waypoint action space, navigable + panorama obs spaces per the env contract). FM side: the built-in multimodal llmCall, model selected by LLM profile — no vendor lock-in in the nodeset. This page is boundary-compliant per TODO #56's method/foundation-model split.

2.5 Prompt assets

All prompt text lives as constants in mapgpt.py: the system prompt (verbatim from one_stage_prompt_manager.py:218–236), init-history / default-planning / no-supplementary strings (lines 189, 14, 163). The 7-field user prompt is assembled at runtime by render_prompt with upstream's exact field order and labels. The reference graph additionally duplicates the system prompt byte-identically into planner_llm.config.system_prompt (verified 2026-06-11) — when editing one copy, update the other.

3. Delta vs upstream — four buckets

A. Preserved verbatim

ElementUpstream anchorHow preserved
JSON-mode system promptone_stage_prompt_manager.py:218–236Character-for-character (_SYSTEM_PROMPT)
Direction-phrase function:16–39 (get_action_concept)Verbatim port (_get_action_concept), radians in/out
Map / trajectory / supplementary serialization:124–165 (make_map_prompt)Verbatim port (_make_map_text), incl. "Nothing yet."
History payload shape "step t: <phrase>":114–122 (make_history)Same string; lookup unnecessary because parse_action forwards the exact phrase
Init constants (history / planning / no-supp):189, :14, :163Verbatim strings; planning initial value also declared in the graph's state container
Stop gating + index-shift fallbackoptions assembly + parse_json_action defaultSame semantics: stop prepended after threshold; parse fallback resolves to record 0
First-visit adjacency, tile cache, Place-ID indexingmap-update logic (lines ~54–87)Same fold order in update_map
"Image i:" label ↔ Place-ID alignmentGPT/api.py:22–44render_prompt emits aligned image_labels
Image-budget abort semanticsvln/gpt_agent.py:133–136 (>20 ⇒ stop)image_budget node, default 20

B. Forced by substrate (script → typed graph)

UpstreamPortWhy forced
One OneStagePromptManager object4-node pipeline observe → update_map → build_options → render_promptCanvas nodes are single-responsibility; the split is dictated by data availability — heading pairs only coexist at the env boundary, so directions are frozen into candidates_json in observe
Python object attributes carry stategraph_state entries (topo_map / history / planning), declared in graph JSONCross-iteration state must live in visible, checkpointable containers, not node instances
Letter→viewpoint resolved inside the manageroptions_json manifest wire from build_options to parse_action (STOP rides as sentinel vp="STOP")Producer and consumer are separate nodes; the mapping must cross a wire as data
Per-candidate image keyed by pointId cropviews[view_index] from the env's per-view panorama primitive (nearest heading+elevation fallback)env_mp3d exposes the panorama as an indexed view list; no cropping primitive on the wire surface

C. Forced by environment / cost

UpstreamPortReason
Model pinned to gpt-4o (JSON mode)Profile-driven llmCall; the reference graph pins gpt-5-mini @ temperature 1.0, max_tokens 6000 (gpt-5-family floor: temp 1.0 + max_tokens ≳ 2000, else silently empty replies)Model availability + search-loop cost; gpt-5-family silently returns empty below these params
Headline eval = authors' 72-scene R2R sampleSame cut available as split MapGPT72 (216 ep); loop-progress signal uses val_unseen eps 0–49val_unseen front-loads easy scans (a 50-ep prefix overstates SR by 18–22 pp) — slices must be disclosed (§4)
Simulator batched upstreamenv_mp3d wrapper pinned batch=1, subprocess-per-worker (ADR-server-003)Our MatterSim wrapper parallelises at the worker level, not the sim-batch level

D. Intentional divergences

UpstreamPortRationale
JSON-only response parsing (parse_json_action, :336–352)Two-tier parse: JSON (with markdown-fence / embedded-JSON tolerance) → regex letter fallbackProfile-driven LLMs drift to free-form answers once the JSON instruction leaves recent context; tier 2 recovers the letter instead of burning the step on fallback
Free-text prompt variant (make_r2r_prompts, :169–187) for gpt-4-visionOmitted — JSON path onlyWe never target gpt-4-vision-preview; one path is simpler and the variant is recoverable from upstream
Stop threshold + image budget hard-codedstop_after / max_images exposed as ConfigField sliders (defaults 3 / 20)Search-loop knobs; defaults preserve upstream behaviour exactly

E. Unexplained / defects

Inventory-first audit, 2026-06-11 — three findings, none with recorded intent (hence E, not D):

ElementUpstreamObserved in portSeverityTracking
Image fidelity sent to the LLM"detail": "low" pinned on every image (api.py:40)llmCall has no detail parameter — provider default (auto, typically high)Moderate — the port's model sees higher-fidelity images than the paper's; changes token cost and is an inflationary confound candidate for the +9 pp closure margin (§4)Decide: record as intent (→ D) or add a detail knob to llmCall and pin low — candidate TODO
Place-tile refresh on re-sightUnconditional overwrite node_imgs[idx] = cc['image'] (one_stage_prompt_manager.py:76)update_map refreshes only when the resolved tile is not None — a failed view resolution retains the stale tileLow — divergent only when observe fails to resolve a candidate viewNote in code review of update_map
History write on the stop stepRollout breaks before make_history on stop (gpt_agent.py:171–174) — history never logs the stopupdate_history appends "step t: stop" in the final iterationNone — post-terminal state write, never read by a subsequent prompt

Probed and cleared (adjudicated non-issues): ① previous_angle is the agent's heading at prompt time (set from fresh obs at gpt_agent.py:93/:167) — the port's cand_heading − cur_heading is equivalent despite the misleading upstream name; ② upstream stores plannings as a list but every prompt reads only [-1] (:200/:202, :285) — the port's lastWrite string is behaviour-equivalent; ③ make_history is live on the json path (:174); ④ packed image_list = all cached place images with None skipped — matches render_prompt; ⑤ upstream loop budget max_action_len=15 (parser.py:16) maps to the eval-side step budget, not a nodeset constant. No mapgpt equivalence test exists to run (unlike SmartWay) — eval-parity is the only executable check.

4. Evaluation

WhatNumberEvidence
Paper-reported (R2R val_unseen, the paper's 72-scene subset)SR 0.46Chen et al. 2024, as recorded in the PortBench method checklist
Our port, closure run (gpt-4o, 100 ep, R2R val_unseen)SR 0.55run 675821f9, 2026-05-08 — archived as outputs/archive/mapgpt_r2r_100ep_2026-05-08/ (pruned from disk 2026-05-21 — the run_id is the surviving citation); stop_after=3, step_budget=30, 10 workers. Checklist's own caveat: +9 pp sits within 100-ep sample variance plus the tier-2 regex-parser advantage; the run also pre-dates the 2026-05-11 stratification finding, so a val_unseen 100-ep prefix likely runs high (see callout).
MapGPT72 full 216-ep (AAS iter_3 evolved graph, gpt-5-mini era)SR 0.500 (95% CI 0.435–0.565)2026-05-11 stratification study; same study: val_unseen eps 0–199 = 0.455

Slice discipline. R2R val_unseen is ordered easy-first: eps 0–49 run ~18–22 pp above the full split. Quote headline numbers on MapGPT72 (216 ep, the paper's own random sample — split auto-discovered from R2R_MapGPT72.json); never quote a val_unseen prefix without naming the ep range.

Comparability caveat (from §3E): upstream sends every image at detail:"low"; our llmCall sends no detail parameter, so the port's model sees higher-fidelity images than the paper's. Treat the +9 pp closure margin as partially confounded until the detail divergence is resolved.

Known gap — closed structurally (2026-06-11): the loop-body evaluate pattern that under-counted SR after the terminal STOP step (roadmap TODO #64) was eliminated by the two-sided-iterOut refactor: evaluate is now after-loop, fed exactly once at termination via final_stop, so the harvested output_port__metrics reflects the terminal step by construction. The 2026-06-13 re-eval confirms it: run 20260613_120854 harvested SR 0.55 (11/20) with every episode logging real decision steps and zero step_count=0 completions.

Current status: mapgpt_mp3d.json was promoted to workspace/graphs/vln/verified/ on 2026-06-13 after the post-migration re-eval (run 20260613_120854: gpt-5-mini, MapGPT72 20 ep, SR 0.55 reproducing the closure number) confirmed the gym-rewired surface (observe_navigable / observe_panorama / step_waypoint) and the two-pivot harvest behave correctly. A full 216-ep MapGPT72 run remains the headline-number target.

5. Usage

# load (canvas: NodeSet Manager page, or REST)
POST /api/components/nodesets/mapgpt/load          # local mode; env_mp3d loads in server mode

# graph
workspace/graphs/vln/verified/mapgpt_mp3d.json     # open in canvas, or submit via eval

# batch eval — graph-only form: /experiment:run <profile> <graph_name> [key=value ...]
# (the legacy "profile -- command" form was removed 2026-05-07)
/experiment:run perf_mapgpt_mp3d mapgpt_mp3d split=MapGPT72 episode_count=216 worker_count=4

6. What this nodeset is NOT

7. Source files

FilePurpose
workspace/nodesets/method/mapgpt.pyThe nodeset — 8 nodes, verbatim constants, helpers (983 lines; module docstring is the design note)
workspace/graphs/vln/verified/mapgpt_mp3d.jsonThe agent graph (22 nodes / 49 edges / 1 state container)
workspace/nodesets/_upstream/mapgpt/fetch_upstream.shClones upstream @ 7c642f4 into a gitignored upstream/ for diffing (no LICENSE upstream — fair-use attribution)
workspace/nodesets/env/env_mp3d/__init__.pyThe env nodeset this method drives (doc)
AgentCanvas docs