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:

  1. RG sends invoke() with {"action": "curate_for", "source": "node_14", "target": "node_18"}
  2. A synth/curation node activates (or inline logic in the current node)
  3. It reads node_14.output_summary and node_18.config.prompt_template
  4. It produces a curated subset of node_14's output that matches node_18's prompt thrust
  5. It writes to state: routed_context[node_18][from_node_14] = curated_subset
  6. 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.