ADR 0004: Async Event-Driven Engine Mode¶
Status¶
Proposed.
Context¶
The runtime engine today is round-based but already compute-parallel. The
FixedStepsLoopStrategy advances discrete steps (global ticks); within a step the
step strategy groups agents into flow batches, runs flows sequentially, and runs
agents within a flow concurrently through a thread pool. Each turn observes a
snapshot of world state at batch start, calls act() (a blocking language-model
call that overlaps with other agents), and resolves under a per-game-master lock.
The language-model layer is synchronous; concurrency comes from threads.
This gives simultaneous-move, lockstep rounds. A conversation thread advances at most one exchange per global step, so causal chains (a post, then a reaction, then a reply to that reaction) take several steps to play out. "Async agents" is often requested, but it splits into two very different things, and only one is a real change.
Decision¶
Add an event-driven engine mode as a new LoopStrategy plus StepStrategy
(selected via sim.engine.step.built_in: async_event) and a scheduler component
that replaces next_acting. We do not port the language-model layer to
asyncio: thread-pool parallelism already covers concurrent model calls, so an
asyncio rewrite changes implementation, not behavior, and is deferred.
The mode is a deterministic discrete-event simulation:
- A virtual clock and a priority queue ordered by
(sim_timestamp, seeded_tiebreak)drive execution. Agents self-schedule wake-ups and resolved actions enqueue reactive triggers (for example, a mention wakes the mentioned agent). - Each agent observes live state at its own wake time (just-in-time observation), not a per-tick snapshot, so causal chains resolve within one logical window.
- Model latency is modeled as a simulated duration, not wall-clock time, so runs remain reproducible under a fixed seed.
- Events apply in timestamp order on a single apply loop, preserving the existing write-serialization guarantees (per-GM lock, single-writer backend).
The Agent interface is unchanged: observe(event) / act() is already
event-shaped, and backends already event-source their actions in
action_events.jsonl.
Consequences¶
- Behavior is genuinely different: reactive, just-in-time agents instead of lockstep simultaneous moves, a closer model of a live social system.
- Reproducibility, checkpointing, and per-episode evaluation are preserved only because the scheduler is deterministic. A checkpoint must additionally persist the pending event queue and the virtual clock; probes and telemetry move from per-episode to time-windowed.
- The round-based engine remains the default. The async mode is opt-in and slots into existing extension points, so agents and backends need no changes.
- Interleaved reads can race (an agent acting on slightly stale state); event ordering by virtual timestamp plus idempotent resolution contains this.