A newer version of the Gradio SDK is available: 6.20.0
Subscription-Based Event Routing
Why subscriptions instead of schedules
The Phase 0/1 conductor scheduled agents by turn parity (turn % 3, turn % 2).
That's simple and fine for three agents. It breaks when:
- You add more agents and the modulo arithmetic becomes arbitrary
- Some agents need to react immediately to specific events, not on their next tick
- You want to hibernated agents (not scheduled) to wake up when something relevant happens
Subscription routing solves all three: agents declare what they care about, and the conductor routes accordingly. Coupling is to the event schema, not to each other.
The two-track hybrid schedule
The conductor runs two tracks per step in order:
flowchart TD
S(["step() β _tick(): turn += 1"]) --> Gov["governor.check()"]
Gov --> P1["Track 1 β drain trigger queue FIFO<br/>(agents whose subscribes_to matched)"]
P1 --> P2["Track 2 β run tick agents<br/>(schedule.tick_every fires)"]
P2 --> N["appended events β notify_subscribers<br/>queue matching agents"]
N -.->|feeds next step| P1
P2 --> Snap["maybe snapshot"]
Track 1: Event-triggered (subscriptions)
When any event is appended to the ledger, the conductor checks which agents
have that event kind in manifest.subscribes_to. Those agents are queued in
a FIFO trigger queue and execute before the next tick batch.
def _notify_subscribers(self, event: Event) -> None:
for agent in self.scenario.agents:
if event.kind in agent.manifest.subscribes_to:
self._trigger_queue.append((agent, event))
Important properties:
- A trigger fires once per triggering event (not per turn)
- The governor caps how many triggers can fire per turn (no cascade explosions)
- Triggers are consumed FIFO β the agent that subscribed first reacts first
Example: the Echo agent subscribes to user.injected. When a visitor drops
something into the world, Echo is immediately queued and reacts this turn β
before the next scheduled tick.
Track 2: Tick-based (manifest.schedule.tick_every)
After the trigger queue is drained, scheduled agents fire:
schedule:
tick_every: 3 # fire every 3rd turn regardless of subscriptions
tick_every: None = event-driven only (the agent never fires on a clock).
tick_every: 0 = every turn.
Ticks and subscriptions are orthogonal. An agent can have both.
Legacy fallback
Agents without a manifest use the scenario's legacy schedule(turn) method.
This preserves full backward compatibility β Phase 0/1 agents work unchanged.
Preventing cascades
Without limits, subscriptions create runaway chains:
- Agent A emits
X - Agent B subscribes to
X, emitsY - Agent C subscribes to
Y, emitsZ - Agent A subscribes to
Z... loop
The governor prevents this:
max_calls_per_turn: hard cap on model calls per turnmax_consecutive(manifest): max turns an agent acts in a row- The trigger queue is drained at most once per step β agents added to the queue during a step's trigger processing fire on the next step, not immediately
Subscription patterns by scenario type
Divergent world-growth (Thousand Token Wood)
scene-whisperer β subscribes_to: [], tick: 1
pocket-actor β subscribes_to: [], tick: 2
echo β subscribes_to: [user.injected], tick: 3
mischief-critic β subscribes_to: [], tick: 16
The seedkeeper narrates every turn; the pocket-actor wants something every other turn;
the echo answers visitors instantly and otherwise transforms the table every third turn.
The reckoning (mischief-critic) is paced, not reactive: it stays silent until turn 16,
then records what became real β one closing judge.verdict that ends the show. A judge
that instead subscribes_to: [world.observed] would fire on the genesis event and resolve
the run on turn 1, before the cast ever interacts (see scenario-authoring.md pitfalls).
Convergent mystery-solving (Mystery Roots)
clue-gatherer β subscribes_to: [run.started, world.observed], tick: None
hypothesis-former β subscribes_to: [agent.thought], tick: None
devils-advocate β subscribes_to: [agent.spoke], tick: None
mystery-judge β subscribes_to: [], tick: 4 # periodic synthesis
The swarm is fully event-driven. The judge fires periodically to synthesise. Each clue triggers a hypothesis, each hypothesis triggers a challenge. The governor ensures this chain terminates each turn.
Adding a new agent
- Create
src/agents/my_agent.pywith a manifest that declares subscriptions. - Add the agent to the scenario's
agentstuple. - Done β no conductor or scheduler edits.
The routing is automatic: the conductor reads the manifest and wires up subscriptions the first time an event of the subscribed kind appears in the ledger.