Skip to main content

rig_ecs/bus/
dispatch.rs

1//! Ordered dispatch of pending effects with bounded intake and serial-key checks.
2//!
3//! ```
4//! use rig_ecs::bus::dispatch::handler_unavailable;
5//! let report = handler_unavailable(&"missing".into());
6//! assert_eq!(report.kind, rig_core::error::ErrorKind::HandlerUnavailable);
7//! ```
8
9use std::{
10    collections::{HashMap, HashSet},
11    sync::Arc,
12};
13
14use bevy_ecs::prelude::*;
15
16use rig_core::serve::Reply;
17use rig_core::{
18    effect::{EffectId, HandlerKey},
19    error::{ErrorKind, ErrorReport},
20    serve::{Dispatch, Origin},
21};
22
23use super::{
24    effect::{
25        EffectOutcome, Held, IdCounter, InFlight, Issued, PendingEffect, Publishing, Reserved,
26        Scope, Seq, Streamed, Tasks, ToolInputs,
27    },
28    handlers::{HandlerIndex, Registry, Served, ServedBy, Serves},
29    plugin::{Policy, Wake},
30    record::Recording,
31    witness::{DispatchWitness, Refused, bus_emitter},
32};
33use rig_core::observe::{Action, Reason, Stage};
34
35/// A pending effect `Dispatch` may take: not held, not answered, not yet
36/// issued.
37pub type Candidate = (
38    Without<InFlight>,
39    Without<EffectOutcome>,
40    Without<Held>,
41    Without<Issued>,
42);
43
44/// What `Dispatch` reads of a candidate: its order, its intent, a reserved
45/// id, and a tool call's inputs.
46pub type CandidateView = (
47    Entity,
48    &'static Seq,
49    &'static PendingEffect,
50    Option<&'static Reserved>,
51    Option<&'static ToolInputs>,
52    Option<&'static super::AdapterOperation>,
53    Option<&'static ServedBy>,
54);
55
56/// Dispatch eligible effects in [`Seq`] order up to the serving policy's intake
57/// bound. Busy serial keys wait; reentrant effects, missing handlers, and exhausted
58/// IDs receive errors without opening records.
59///
60/// Accepted effects use reserved or fresh IDs and inherit the nearest issued
61/// parent and scope. Task-served effects own their execution; world-served effects
62/// await system answers. Tool inputs and publication slots travel beside requests.
63pub fn dispatch(
64    mut commands: Commands,
65    policy: Res<Policy>,
66    wake: Res<Wake>,
67    index: Res<HandlerIndex>,
68    registry: Registry,
69    mut tasks: Tasks,
70    serves: Query<&Serves>,
71    served_by: Query<&ServedBy>,
72    pending: Query<CandidateView, Candidate>,
73    in_flight: Query<&InFlight>,
74    parents: Query<&ChildOf>,
75    issued: Query<&Issued>,
76    scopes: Query<&Scope>,
77    recording: Option<Res<Recording>>,
78    witnessing: DispatchWitness,
79    mut ids: ResMut<IdCounter>,
80) {
81    let mut candidates: Vec<_> = pending.iter().collect();
82    candidates.sort_by_key(|(_, seq, ..)| **seq);
83    let mut intake = 0usize;
84    let DispatchWitness { witness, subjects } = witnessing;
85
86    let policy = policy.0;
87    let serial = policy.serial_per_handler;
88    // Ids issued in this pass: `Issued` lands when the commands apply, and a
89    // child taken in the same pass as its parent must still name it.
90    let mut issued_now: HashMap<Entity, EffectId> = HashMap::new();
91    // Handler entities taken this pass: their `Serves` cannot see it yet.
92    let mut busy_now: HashSet<Entity> = HashSet::new();
93
94    for (entity, _, effect, reserved, inputs, operation, resolved) in candidates {
95        // Clamped at the read as well as at install: a host may replace the
96        // resource, and a tick still takes at least one effect.
97        if intake >= policy.command_capacity.max(1) {
98            return;
99        }
100        let key = &effect.key;
101        let handler = resolved
102            .map(|ServedBy(handler)| *handler)
103            .or_else(|| index.entity(key));
104        let served = handler.and_then(|handler| registry.served(handler));
105        if let Some(handler) = handler
106            && serial
107            && (busy_now.contains(&handler)
108                || serves
109                    .get(handler)
110                    .is_ok_and(|serves| serves.iter().any(|effect| in_flight.contains(effect))))
111        {
112            if ancestor_served_by(entity, handler, &parents, &in_flight, &served_by) {
113                if let Some(witness) = &witness {
114                    let mut subject = subjects.of(entity);
115                    subject.parent = nearest_issued(entity, &parents, &issued, &issued_now);
116                    witness.emit(
117                        subject,
118                        Stage::Dispatch,
119                        bus_emitter(),
120                        Action::Refused {
121                            reason: Reason::with_detail("reentrant", reentrant(key).message),
122                        },
123                    );
124                }
125                commands
126                    .entity(entity)
127                    .insert((Refused, EffectOutcome(Err(reentrant(key)))));
128            }
129            continue;
130        }
131        let Some(served) = served else {
132            if let Some(witness) = &witness {
133                let mut subject = subjects.of(entity);
134                subject.parent = nearest_issued(entity, &parents, &issued, &issued_now);
135                witness.emit(
136                    subject,
137                    Stage::Dispatch,
138                    bus_emitter(),
139                    Action::Refused {
140                        reason: Reason::with_detail(
141                            "handler_unavailable",
142                            handler_unavailable(key).message,
143                        ),
144                    },
145                );
146            }
147            commands
148                .entity(entity)
149                .insert((Refused, EffectOutcome(Err(handler_unavailable(key)))));
150            continue;
151        };
152
153        let raw_id = reserved.map_or(ids.0, |Reserved(id)| id.as_u64());
154        let Some(next_id) = raw_id.checked_add(1) else {
155            let report = ErrorReport::new(ErrorKind::Request, "effect ID allocator exhausted");
156            if let Some(witness) = &witness {
157                let mut subject = subjects.of(entity);
158                subject.parent = nearest_issued(entity, &parents, &issued, &issued_now);
159                witness.emit(
160                    subject,
161                    Stage::Dispatch,
162                    bus_emitter(),
163                    Action::Refused {
164                        reason: Reason::with_detail("ids_exhausted", report.message.clone()),
165                    },
166                );
167            }
168            commands
169                .entity(entity)
170                .insert((Refused, EffectOutcome(Err(report))));
171            continue;
172        };
173        ids.0 = ids.0.max(next_id);
174        let id = EffectId::from_raw(raw_id);
175        let origin = Origin {
176            parent: nearest_issued(entity, &parents, &issued, &issued_now),
177            scope: nearest_scope(entity, &parents, &scopes)
178                .map(|scope| std::sync::Arc::from(scope.as_str())),
179        };
180        // Parent Issued writes can still be deferred in this dispatch pass.
181        // Resolve once from issued_now for every observer/context surface.
182        let mut subject = subjects.of(entity);
183        subject.effect = Some(id);
184        subject.parent = origin.parent;
185        let adapter = operation.map(|operation| {
186            operation
187                .context
188                .for_host_attempt(subject.clone(), operation.host_attempt)
189        });
190
191        if let Some(witness) = &witness {
192            witness.emit(
193                subject.clone(),
194                Stage::Dispatch,
195                bus_emitter(),
196                Action::Issued,
197            );
198        }
199        let mut entity_commands = commands.entity(entity);
200        match served {
201            Served::Task(handler) => {
202                if let Some(recording) = &recording {
203                    recording.begin(id, key.clone(), effect.kind.clone(), origin);
204                }
205                let handler = handler.clone();
206                let kind = effect.kind.clone();
207                let observed = Arc::new(super::record::ObservedState::default());
208                entity_commands.insert(super::record::Observed(observed.clone()));
209                let mut dispatch = Dispatch::new(id, effect.is_stream());
210                if let rig_core::effect::EffectKind::ToolCall { .. } = &kind {
211                    let inbound = inputs.map(|inputs| inputs.0.clone()).unwrap_or_default();
212                    let published = rig_core::tool::PublishedContext::new();
213                    dispatch = dispatch
214                        .with_scope(Arc::new(inbound))
215                        .with_scope(published.clone());
216                    entity_commands.insert(Publishing(published));
217                }
218                let published = dispatch.scope::<rig_core::tool::PublishedContext>();
219                entity_commands.insert(super::record::ReplacedBy(dispatch.replaced_by()));
220                let dispatch = dispatch.with_observer(Box::new(super::record::WorldObserver {
221                    adapter,
222                    published,
223                    id,
224                    recording: recording.as_ref().map(|r| (**r).clone()),
225                    observed,
226                    witness: witness
227                        .as_ref()
228                        .map(|witness| ((**witness).clone(), subject.clone())),
229                }));
230                let streaming = effect.is_stream();
231                let wake = wake.clone();
232                let task = bevy_tasks::IoTaskPool::get().spawn(async move {
233                    let reply = handler.handle(kind, dispatch).await;
234                    let reply = if streaming {
235                        reply
236                    } else {
237                        Reply::Outcome(reply.into_outcome().await)
238                    };
239                    wake.signal();
240                    reply
241                });
242                entity_commands.insert(tasks.serving(entity, task));
243                if effect.is_stream() {
244                    entity_commands.insert(Streamed::default());
245                }
246            }
247            Served::World(world) => {
248                if let Err(report) = (world.ask)(&mut entity_commands, &effect.kind) {
249                    entity_commands.insert(EffectOutcome(Err(report)));
250                    continue;
251                }
252                if let Some(recording) = &recording {
253                    recording.begin(id, key.clone(), effect.kind.clone(), origin);
254                }
255            }
256        }
257        entity_commands
258            .insert((Issued(id), InFlight { key: key.clone() }))
259            .remove::<Reserved>();
260        if let Some(handler) = handler {
261            if resolved.is_none() {
262                entity_commands.insert(ServedBy(handler));
263            }
264            busy_now.insert(handler);
265        }
266        issued_now.insert(entity, id);
267        intake += 1;
268    }
269}
270
271/// Whether an ancestor of `entity` is in flight on `handler`.
272fn ancestor_served_by(
273    entity: Entity,
274    handler: Entity,
275    parents: &Query<&ChildOf>,
276    in_flight: &Query<&InFlight>,
277    served_by: &Query<&ServedBy>,
278) -> bool {
279    let mut current = entity;
280    while let Ok(parent) = parents.get(current) {
281        current = parent.parent();
282        if in_flight.contains(current)
283            && served_by
284                .get(current)
285                .is_ok_and(|ServedBy(serving)| *serving == handler)
286        {
287            return true;
288        }
289    }
290    false
291}
292
293/// The nearest issued ancestor's id: the record's `parent`.
294fn nearest_issued(
295    entity: Entity,
296    parents: &Query<&ChildOf>,
297    issued: &Query<&Issued>,
298    issued_now: &HashMap<Entity, EffectId>,
299) -> Option<EffectId> {
300    let mut current = entity;
301    while let Ok(parent) = parents.get(current) {
302        current = parent.parent();
303        if let Some(id) = issued_now.get(&current) {
304            return Some(*id);
305        }
306        if let Ok(Issued(id)) = issued.get(current) {
307            return Some(*id);
308        }
309    }
310    None
311}
312
313/// The nearest [`Scope`], the entity's own first: the record's `scope`.
314fn nearest_scope(
315    entity: Entity,
316    parents: &Query<&ChildOf>,
317    scopes: &Query<&Scope>,
318) -> Option<String> {
319    let mut current = entity;
320    loop {
321        if let Ok(Scope(scope)) = scopes.get(current) {
322            return Some(scope.clone());
323        }
324        match parents.get(current) {
325            Ok(parent) => current = parent.parent(),
326            Err(_) => return None,
327        }
328    }
329}
330
331/// The report for a dispatch to a key with no bound handler.
332pub fn handler_unavailable(key: &HandlerKey) -> ErrorReport {
333    ErrorReport::new(
334        ErrorKind::HandlerUnavailable,
335        format!("no handler is bound to `{key}`"),
336    )
337    .with_retryable(false)
338}
339
340/// The report for an effect whose ancestor is in flight on its own serial
341/// key: served, it would wait for itself.
342pub fn reentrant(key: &HandlerKey) -> ErrorReport {
343    ErrorReport::new(
344        ErrorKind::Request,
345        format!(
346            "`{key}` is served one at a time and an ancestor of this effect is in flight on it: served, it would wait for itself"
347        ),
348    )
349    .with_retryable(false)
350}