← All Problems

6. Simulate a Conversational Agent's Turn-Taking State Machine

General Medium Anthropic-Style Coding Rounds
Grounding: General pattern common across ML-research-lab technical interviews, not tied to one specific reported example. Finite-state-machine simulation driven by a transition table is a standard category in general software engineering interviews; no source in this batch's research names a specific reported instance of this exact question at Anthropic.

Problem

A conversational agent's runtime moves through a small set of states while handling a turn: it starts IDLE, begins THINKING once a user message arrives, may need to make a TOOL_CALL and return to THINKING with the result, and eventually starts RESPONDING before returning to IDLE.

Only certain events are valid in each state, and the runtime needs to replay a sequence of events and report where the agent ends up, or that the sequence was invalid for the current state at some point.

Source: src/6_simulate_agent_state_machine.py

def run_state_machine(events: list[str]) -> str:
    ...

Examples:
>>> run_state_machine(['user_message', 'tool_needed', 'tool_result', 'ready_to_respond', 'response_sent'])
'IDLE'

>>> run_state_machine(['tool_needed'])
'INVALID'

Step-by-Step Approach

  1. Encode the state machine as data, not as a chain of if/elif statements: a dict mapping (current_state, event) -> next_state covers every valid transition compactly.
  2. Start a running state variable at 'IDLE'.
  3. For each event in order, look up (state, event) in the transition table.
  4. If the lookup misses (there's no entry for that state/event pair), the sequence is invalid — return 'INVALID' immediately, without processing any remaining events.
  5. If the lookup hits, update state to the returned next_state and continue to the next event.
  6. After all events are processed without hitting an invalid transition, return the final state.

The key insight is that a transition table turns FSM simulation into pure data lookup — the loop logic never needs to change no matter how many states or events the machine has, which is also why table-driven FSMs are easy to extend and to unit-test exhaustively.

Reference solution

# transition table doubles as the spec: (state, event) -> next_state
_TRANSITIONS = {
    ("IDLE", "user_message"): "THINKING",
    ("THINKING", "tool_needed"): "TOOL_CALL",
    ("THINKING", "ready_to_respond"): "RESPONDING",
    ("TOOL_CALL", "tool_result"): "THINKING",
    ("RESPONDING", "response_sent"): "IDLE",
}


def run_state_machine(events: list[str]) -> str:
    state = "IDLE"
    for event in events:
        next_state = _TRANSITIONS.get((state, event))
        if next_state is None:
            # no valid transition for this (state, event) pair
            return "INVALID"
        state = next_state
    return state

Key Functions & Tricks

  • dict keyed on (state, event) tuples — compact table encodes the entire transition function as data
  • _TRANSITIONS.get((state, event)) — O(1) lookup that returns None for any undefined transition
  • early return on None — invalid transitions short-circuit the remaining events instead of silently continuing

How to Recognize This Pattern

The signal is "simulate an entity moving through a fixed set of states in response to a sequence of events/inputs," with a small, enumerable set of legal transitions. Encoding the transition function as a dict keyed on (state, input) is almost always cleaner and less error-prone than nested conditionals, and it also makes the set of legal transitions trivially testable on its own. Common variations add transition side effects (e.g. an action or callback fired on each transition), guard conditions that make a transition valid only under extra state, or ask you to report *how far* through the event sequence processing got before failing rather than just a boolean. A common pitfall is forgetting that an empty event list is a valid input that should return the start state unchanged, not be treated as an error.