Nested Menu Protocol — Tile, Unit, Node, Routing
Status: ACTIVE (reference) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T21:55:00Z Claim: synthesis | 2026-05-11T21:50:00Z Session: Nested menu hierarchy for RG — every tile interaction drills down through unit, node, output, routing, and context curation
Menu Tree
TILE (any hex on the grid)
│
├── UNIT (if a unit occupies this tile)
│ │
│ ├── [Status] — what it's doing, phase, position
│ ├── [Config] — model, harness, tools (can edit)
│ ├── [Trail] — context accumulated across prior nodes
│ ├── [Access] — what other nodes/outputs it can see
│ ├── [History] — where it's been, what it produced
│ │
│ └── [Move] → (select destination tile)
│ ├── [Config for this move] — override model/harness for this move
│ ├── [Context to bring] — select what trail context travels
│ └── [Context to leave] — explicit block for this destination
│
├── NODE (the tile's own node — always present if not empty terrain)
│ │
│ ├── [Properties] — phase, status, output locale pointer
│ ├── [Output] — summary sections (if completed)
│ │ ├── Section 1 → [Expand] → raw output from wiki
│ │ └── Section N → [Expand] → raw output from wiki
│ ├── [Config] — prompt template, default model, harness for this node
│ ├── [Edges] — what tiles are reachable from here
│ │ └── Edge to target_node
│ │ ├── [Curate Context] — agent reads source output, produces targeted subset
│ │ └── [Point Wholesale] — target node gets full pointer to source output
│ └── [History] — past units that visited, past outputs produced here
│
└── EMPTY (no node, no unit — just terrain)
└── [Terrain info] — nothing actionable
How LangGraph Configs Carry Each Menu Level
Tile → Unit submenu
All from state. The unit's entire backpack is state keys.
state["unit"] = {
"name": "Rif",
"role": "researcher",
"position": "node_14", # which tile
"config": { # config #4
"model": "deepseek-v4",
"harness": "opencode",
"tools": ["read", "write"]
},
"trail_context": [ # accumulated across nodes
"node_01/curated",
"node_08/curated"
],
"context_access": [ # config #5 — what it can see
"node_01/full",
"node_08/full",
"wiki/research/x.md"
],
"history": [ # traversal record
{"node": "node_01", "output": "wiki/planning/scope.md"},
{"node": "node_08", "output": "wiki/research/configs.md"}
]
}
RG reads this state key and populates the Unit submenu. Every field maps to a menu item.
Move → Config for this move
When the player selects a destination, the RG fires invoke() with the move details. The node function receives the move config as part of the input state or resume payload:
# RG sends:
graph.invoke(Command(resume={
"action": "move",
"target": "node_15",
"move_config": {
"model": "kimi-k2.6", # override for this move
"context_to_bring": [ # what trail context travels
"node_01/curated"
],
"context_to_leave": [ # explicit block at destination
"node_08/curated"
]
}
}), config)
Tile → Node properties submenu
All from state. Each node has its own state entry.
state["nodes"]["node_14"] = {
"phase": "design", # config #3
"status": "in_progress", # config #7
"output_locale": "wiki/research/langgraph-configs.md", # config #6
"config": { # per-node defaults
"prompt_template": "Design a solution for: {task}. Consider: {context}.",
"default_model": "deepseek-v4",
"default_harness": "opencode"
}
}
Tile → Output submenu (completed nodes only)
Reads output_summary from state. Sections with header/body/status. A on section expands inline. Y points to full wiki page.
state["nodes"]["node_14"]["output_summary"] = {
"title": "LangGraph Config Research",
"sections": [
{"header": "Recommendation", "body": "...", "status": "final"},
...
]
}
Tile → Edges submenu
Reads edges from state. Shows which tiles are reachable. Player selects a target tile, then gets the [Curate Context] / [Point Wholesale] choice.
state["nodes"]["node_14"]["edges"] = ["node_15", "node_18", "node_22"]
Edges → Curate Context
This is the cross-node curation mechanic. When the player selects [Curate Context] for edge node_14 → node_18:
- RG sends
invoke()with{"action": "curate_for", "source": "node_14", "target": "node_18"} - A synth/curation node activates (or inline logic in the current node)
- It reads
node_14.output_summaryandnode_18.config.prompt_template - It produces a curated subset of node_14's output that matches node_18's prompt thrust
- It writes to state:
routed_context[node_18][from_node_14] = curated_subset - Node_18's access list now includes
routed_context/node_18/from_node_14
def curate_context_node(state):
source = state["action"]["source"] # "node_14"
target = state["action"]["target"] # "node_18"
source_output = state["nodes"][source]["output_summary"]
target_prompt = state["nodes"][target]["config"]["prompt_template"]
# Agent reads source output, filters for what matches target's prompt thrust
curated = agent.curate(
source=source_output,
target_prompt=target_prompt
)
return {
"routed_context": {
target: {
f"from_{source}": curated
}
},
# Also add to target's access list
"unit": {
"context_access": [f"routed_context/{target}/from_{source}"]
}
}
Edges → Point Wholesale
Simpler. Just adds a pointer so the target node knows it can check the source output.
# Player selects [Point Wholesale] for edge node_14 → node_18
# Writes a pointer without curation
return {
"routed_context": {
"node_18": {
"from_node_14": {
"type": "pointer",
"source": "node_14",
"output_locale": state["nodes"]["node_14"]["output_locale"],
"curated": False # not curated — full pointer
}
}
}
}
What the Player Sees (RG Flow)
Step 1: Grid view — D-pad moves cursor across tiles
┌──────────────────────────────────────────────────────────┐
│ [01] [02] [03] [04] PLANNING │
│ [05] [06] [07] [08] RESEARCH │
│ ▶[14] [15] [16] [17] DESIGN ← cursor here │
│ [19] [20] [21] [22] DEVELOP │
│──────────────────────────────────────────────────────────│
│ TILE: node_14 | UNIT: Rif | PHASE: Design │
│ [A] Inspect [X] Unit Menu [Y] Node Menu │
└──────────────────────────────────────────────────────────┘
Step 2: Unit menu (X pressed)
┌──────────────────────────────────────────────────────────┐
│ UNIT: Rif — Researcher │
│──────────────────────────────────────────────────────────│
│ ▶ Status: Active at node_14 (Design) │
│ ▶ Config: deepseek-v4 / opencode │
│ ▶ Trail: 2 items (node_01, node_08) │
│ ▶ Access: 3 sources available │
│ ▶ History: 2 nodes visited │
│──────────────────────────────────────────────────────────│
│ [A] Select [X] Move Unit [B] Back │
└──────────────────────────────────────────────────────────┘
Step 3: Move → select destination, then move config
┌──────────────────────────────────────────────────────────┐
│ MOVE: Rif → node_15 (Design) │
│──────────────────────────────────────────────────────────│
│ Config: deepseek-v4 / opencode [Change] │
│ Bring: 2 items from trail [Select] │
│ Block: none [Add block] │
│──────────────────────────────────────────────────────────│
│ [A] Confirm Move [B] Cancel │
└──────────────────────────────────────────────────────────┘
Step 4: Node menu (Y pressed from grid)
┌──────────────────────────────────────────────────────────┐
│ NODE: node_14 — Design │
│──────────────────────────────────────────────────────────│
│ ▶ Properties: in_progress, design phase │
│ ▶ Config: prompt template set, default deepseek-v4 │
│ ▶ Edges: → node_15, → node_18, → node_22 │
│──────────────────────────────────────────────────────────│
│ [A] Select [Y] Output [B] Back │
└──────────────────────────────────────────────────────────┘
Step 5: Edges → select target, then routing action
┌──────────────────────────────────────────────────────────┐
│ ROUTE: node_14 → node_22 (Develop) │
│──────────────────────────────────────────────────────────│
│ Node_14 output: 4 sections, status: in_progress │
│ Node_22 prompt: "Develop implementation for: {task}" │
│──────────────────────────────────────────────────────────│
│ [A] Curate Context │
│ Agent filters node_14 output for node_22's needs │
│ [X] Point Wholesale │
│ Node_22 gets full pointer to node_14 output │
│ [B] Cancel │
└──────────────────────────────────────────────────────────┘
LangGraph Configs Summary
Every menu level is a state read. Every action is an invoke().
| Menu Level | State Keys Read | Action on A |
|---|---|---|
| Grid tile | nodes[node].phase, nodes[node].status, unit.position |
Open tile menu |
| Unit status | unit.role, unit.config, unit.trail_context |
Drill into sub-item |
| Unit config | unit.config.model, unit.config.harness |
Edit (prompt input → invoke) |
| Unit trail | unit.trail_context[], unit.history[] |
Select/deselect items |
| Move config | unit.config, unit.trail_context |
Confirm → invoke with move payload |
| Node properties | nodes[node].phase, nodes[node].status, nodes[node].config |
Drill into sub-item |
| Node output | nodes[node].output_summary.sections[] |
Expand section / open wiki |
| Node edges | nodes[node].edges[] |
Select target → curate or point |
| Curate context | nodes[source].output_summary, nodes[target].config.prompt_template |
Confirm → invoke curation |
| Point wholesale | nodes[source].output_locale |
Confirm → write pointer |
All menus are built from state. All actions are invoke() or Command(resume=...). Nothing needs to be pre-staged in the graph topology beyond the nodes and edges existing. The menus are a view layer concern — the state carries the data, the RG renders the options, the player's selection flows back through invoke.