       self.port = port
       self.connectionQueueCapacity = connectionQueueCapacity
       self.workerCount = workerCount
+    }
+  }
+}
+
+/// Simulation agent. Agents can interact with the Jelly Bean World by being added to simulators
+/// that manage them.
+public protocol Agent {
+  /// Returns the agent's desired next action. This function is called automatically by all
+  /// simulators managing this agent, whenever the `Simulator.step()` function is invoked.
+  ///
+  /// - Parameter state: Current state of the agent.
+  /// - Returns: Action requested by the agent.
+  ///
+  /// - Note: The action is not actually performed until the simulator advances by a time step and
+  ///   issues a notification about that event. The simulator only advances by a time step only
+  ///   once all agents have requested some action.
+  mutating func act(using state: AgentState) -> Action
+}
+
+/// State of an agent in the Jelly Bean World.
+public struct AgentState {
+  /// Position of the agent.
+  public let position: Position
+
+  /// Direction in which the agent is facing.
+  public let direction: Direction
+
+  /// Scent that the agent smells. This is a vector with size `S`, where `S` is the scent vector
+  /// size (i.e., the scent dimensionality).
+  public let scent: ShapedArray<Float>
+
+  /// Visual field of the agent. This is a matrix with shape `[V + 1, V + 1, C]`, where `V` is the
+  /// visual range of the agent and `C` is the color vector size (i.e., the color dimensionality).
+  public let vision: ShapedArray<Float>
+
+  /// Items that collected by the agent so far represented as a dictionary mapping items to counts.
+  public let items: [Item: Int]
+}
+
+/// Action that can be taken by agents in the jelly bean world.
+public enum Action {
+  /// No action.
+  case none
+
+  /// Move action, along the specified direction and for the provided number of steps.
+  case move(direction: Direction, stepCount: Int = 1)
+
+  /// Turn action (without any movement to a different cell).
+  case turn(direction: TurnDirection)
+
+  /// Invokes this action in the simulator.
+  ///
+  /// - Parameters:
+  ///   - simulatorHandle: Pointer to the C simulator instance.
+  ///   - clientHandle: Pointer to the C simulator client instance (if a client is being used).
+  ///   - agentID: Identifier of the agent for which to invoke this action.
+  @inlinable
+  internal func invoke(
+    simulatorHandle: UnsafeMutableRawPointer?,
+    clientHandle: UnsafeMutableRawPointer?,
+    agentID: UInt64
+  ) {
+    switch self {
+    case .none:
+      simulatorNoOpAgent(simulatorHandle, clientHandle, agentID)
+    case let .move(direction, stepCount):
+      simulatorMoveAgent(
+        simulatorHandle,
+        clientHandle,
+        agentID,
+        direction.toC(),
+        UInt32(stepCount))
+    case let .turn(direction):
+      simulatorTurnAgent(simulatorHandle, clientHandle, agentID, direction.toC())
     }
   }
 }

  /// - Parameters:
  ///   - file: File in which the simulator is saved.
  ///   - agents: Agents that this simulator manages.
  ///   - serverConfiguration: Optional server configuration. If this is provided, then the loaded
  ///     simulator will also act as a simulation server.
  /// - Precondition: The number of agents provided must match the number of agents the simulator
  ///   managed before its state was saved.
  public init(
    fromFile file: URL,
    agents: [Agent],
    serverConfiguration: ServerConfiguration? = nil
  ) {
    let cSimulatorInfo = simulatorLoad(file.absoluteString, nativeOnStepCallback)
    defer { simulatorDeleteSimulatorInfo(cSimulatorInfo) }
    self.handle = cSimulatorInfo.handle
    self.configuration = Configuration(fromC: cSimulatorInfo.config)
    self.serverConfiguration = serverConfiguration
    self.time = cSimulatorInfo.time
    self.agentStates = [UInt64: AgentState](
      uniqueKeysWithValues: UnsafeBufferPointer(

      """)
    self.agents = [UInt64: Agent](uniqueKeysWithValues: zip(agentStates.keys, agents))
    simulatorSetStepCallbackData(handle, Unmanaged.passUnretained(self).toOpaque())
  }

  deinit {
    if let h = handle { simulatorDelete(h) }
  }

  /// Adds a new agent to this simulator, and updates
