LangGraph Gameplay: Auto-Run vs Control Mode + Per-Unit Toggles

Status: ACTIVE (synthesis from MjF + sandshrew conversation) Agent: opencode/ext-agent (sandshrew) Timestamp UTC: 2026-05-11T18:25:00Z Claim: synthesis | 2026-05-11T18:20:00Z Session: MjF gameplay design exploration β€” LangGraph game surface

Prior Context

Short Answer

Three execution layers, all natively expressible in LangGraph: 1. Control mode: Human decides every move. interrupt() pauses at each decision point. 2. Auto-run mode: AI decides every move. Graph loops without interrupt(). 3. Per-unit toggles: Each unit/lane has an auto_pilot flag. Conditional edges check the flag β€” true routes to AI decision node, false routes to interrupt() for human input.

The toggle is live: flip a unit's flag mid-game, and the next turn cycle picks it up. No mode switch β€” it's per-unit granularity by default.

The Turn Cycle

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                  TURN CYCLE                       β”‚
β”‚                                                   β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”  β”‚
β”‚  β”‚ PLANNING │───▢│EXECUTION │───▢│ RESOLUTION β”‚  β”‚
β”‚  β”‚ per-unit β”‚    β”‚  Send    β”‚    β”‚  apply     β”‚  β”‚
β”‚  β”‚ decisionsβ”‚    β”‚ parallel β”‚    β”‚  effects   β”‚  β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜  β”‚
β”‚       β”‚                                β”‚          β”‚
β”‚       └──── if auto_run ── loop β”€β”€β”€β”€β”€β”€β”˜          β”‚
β”‚       └──── if control ── interrupt() ── pause β”€β”€β”˜
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Phase 1: Planning (Per-Unit Decisions)

For each unit that needs orders this turn:

def planning_router(state: GameState) -> list[Send]:
    """Fan out: one decision cycle per active unit"""
    return [
        Send("unit_decision", {"unit": unit})
        for unit in state["units"]
        if unit_needs_orders(unit)
    ]

Phase 2: Unit Decision (Human or AI)

def unit_decision(state: UnitDecisionState):
    unit = state["unit"]

    if unit["auto_pilot"]:
        # AI decides β€” no interrupt, instant
        return ai_decide_move(unit)
    else:
        # Human decides β€” interrupt, pause, wait for resume
        choice = interrupt(f"Order for {unit['name']} at {unit['position']}?")
        return apply_human_order(unit, choice)

Phase 3: Execution & Resolution

All moves fire in parallel via Send, results merge via state reducers, turn counter increments.

Phase 4: End-of-Turn Gate

def end_of_turn_router(state: GameState) -> Literal["planning", "end"]:
    if state["auto_run_global"]:
        return "planning"  # loop without pause
    else:
        interrupt(f"Turn {state['turn']} complete. Continue?")
        return "planning"  # resume after human acknowledges

State Schema

class UnitState(TypedDict):
    unit_id: str
    name: str
    position: str          # node name (hex_17, etc.)
    auto_pilot: bool       # THE toggle β€” AI or human?
    health: int
    inventory: list[str]
    role: str              # "scout", "builder", "hero", "grunt"

class LaneState(TypedDict):
    lane_id: str           # "northern_front", "supply_line", etc.
    unit_ids: list[str]    # units assigned to this lane
    auto_pilot: bool       # lane-level override β€” all units in lane

class GameState(TypedDict):
    units: list[UnitState]
    lanes: list[LaneState]
    turn: int
    phase: str
    auto_run_global: bool  # master toggle
    history: list[dict]    # replay log

Toggle Mechanics

Global Toggle (Master Switch)

auto_run_global = true  β†’ no interrupts, AI runs everything, game loops
auto_run_global = false β†’ interrupts fire where auto_pilot=false on units

Toggled via Command(update={"auto_run_global": True/False}) at any interrupt point. Or via a dedicated "command node" that runs before planning.

Per-Unit Toggle

Each unit's auto_pilot flag can be flipped: - At interrupt points: Human can modify any unit's flag before resuming - During auto-run: An "overseer" conditional edge can check if a unit should be reclaimed by human control - Hotkey/command mode: "Take manual control of Rif" β†’ sets auto_pilot: false on that unit

def reclaim_unit(state: GameState):
    """Human interrupts auto-run to seize a unit"""
    unit_id = interrupt("Which unit to reclaim?")
    # Toggle the flag
    for unit in state["units"]:
        if unit["unit_id"] == unit_id:
            unit["auto_pilot"] = False
    return {"units": state["units"]}

Lane Toggle

A lane groups units under one toggle:

def unit_auto_pilot_resolver(state, unit_id: str) -> bool:
    """Unit obeys lane override if set, otherwise its own flag"""
    lane = get_lane_for_unit(state, unit_id)
    if lane and lane.get("auto_pilot_override") is True:
        return True  # lane overrides unit flag β€” AI runs it
    return get_unit(state, unit_id)["auto_pilot"]

UX Flow: What the Player Sees

Control Mode (auto_run_global = false)

Turn 3 β€” your move.
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  [AUTO] Scout Alpha      patrolling hex_12   β”‚  ← AI unit, no prompt
β”‚  [AUTO] Builder Beta     constructing        β”‚  ← AI unit, no prompt
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”    β”‚
β”‚  β”‚ β–Ά HERO Rif at hex_17                β”‚    β”‚  ← YOUR unit, interrupt fires
β”‚  β”‚   [Move] [Attack] [Wait] [Inventory]β”‚    β”‚
β”‚  β”‚   [Auto-pilot this unit]            β”‚    β”‚  ← one-click: set auto=true
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜    β”‚
β”‚  [AUTO] Grunt Gamma      idle at hex_09      β”‚  ← AI unit
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Human only sees prompts for auto_pilot=false units. AI units resolve silently.

Auto-Run Mode (auto_run_global = true)

Turn 12 β€” AI running...
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Scout Alpha    hex_12 β†’ hex_14  [patrol]     β”‚
β”‚  Builder Beta   hex_08  constructing 67%      β”‚
β”‚  Rif            hex_17 β†’ hex_20  [advance]    β”‚  ← AI commanding your hero
β”‚  Grunt Gamma    hex_09 β†’ hex_10  [follow]     β”‚
β”‚                                               β”‚
β”‚  [STOP] [Reclaim Rif] [Reclaim All Heroes]    β”‚  ← interrupt toggles
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Player watches. At any end-of-turn interrupt, they can: - Hit STOP β†’ sets auto_run_global = false, next turn is full control - Hit "Reclaim Rif" β†’ sets auto_pilot = false on Rif only, continues auto-run for others - Let it run β†’ next turn auto-cycles

LangGraph Primitive Mapping

Gameplay Feature LangGraph Primitive How
Human decision per unit interrupt() Pauses graph, returns human choice via Command(resume=...)
AI decision per unit Node function with LLM call No interrupt, immediate return
Route to human vs AI Conditional edge on auto_pilot graph.add_conditional_edges("planning", route_by_autopilot)
Parallel unit execution Send fan-out [Send("unit_decision", {"unit": u}) for u in units]
End-of-turn pause interrupt() gated by auto_run_global Only fires when global flag is false
Toggle flip mid-game Command(update=...) or state mutation in node Change flag, next cycle picks it up
Save/load game Checkpointer Persists all state including toggle flags
Replay/history Append-only list in state history: Annotated[list, operator.add]

Edge Cases & Failure Modes

  1. All units auto-pilot, global auto-run = true: Graph runs forever. Needs a max-turn guard (recursion_limit or explicit check).
  2. Unit dies while human is deciding: Interrupt prompt for a dead unit. Mitigation: validate unit is alive before showing prompt, auto-skip dead units.
  3. Toggle thrash: Human rapidly toggles auto_pilot on/off. Harmless β€” state is read fresh each cycle.
  4. AI makes bad move, human wants to undo: Checkpointer enables time-travel. graph.get_state(config, checkpoint_id=previous) β†’ rewind.
  5. Concurrent human input: Two devices (RG40XXV + Wii) try to command the same unit. Need a turn-lock: only one client can hold the interrupt at a time.
  6. Lane override conflicts: Lane says auto=true, unit says auto=false. Resolution order must be explicit. Recommendation: lane override wins unless unit has lockout: true (hero units override lane).

Decision Needed From Mehdi

Next Probe

Implement the dual-mode planning node in a minimal 3-node graph: one hero unit (auto_pilot=false), one scout unit (auto_pilot=true). Prove that hitting invoke() cycles the scout silently and pauses for the hero. Then add the toggle-flip Command to let the human seize the scout mid-auto-run.