Skip to main content

rig_ecs/
effects.rs

1//! The one dispatch path. Every effect entity ([`stage`](super::stage)),
2//! a model call, a tool call or a plugin's effect, runs through
3//! [`Effects`] under its effect id, recorded in the kernel's own
4//! [`RecordBuffer`], and its record is written as a
5//! [`EffectRecorded`](super::stage::EffectRecorded) when it settles. A plugin that
6//! keeps an effect log reads those, and continues the ids of the log it
7//! appends to with [`Effects::continue_after`].
8//!
9//! A streamed model call is recorded as the response it ended with, not
10//! its stream items: views follow the stream on the call ([`Partial`]),
11//! and a replay answers the call with its recorded outcome before it
12//! starts, in Gate, so nothing re-emits a stream.
13//!
14//! [`Partial`]: super::agent::Partial
15
16use std::sync::Arc;
17use std::sync::atomic::{AtomicU64, Ordering};
18
19use bevy_ecs::prelude::*;
20use bevy_tasks::ConditionalSendFuture;
21use rig_core::effect::Answered;
22use rig_core::effect::{EffectId, EffectKind};
23use rig_core::error::ErrorReport;
24use rig_core::serve::{
25    Dispatch, DispatchContext, ErasedHandler, RecordBuffer, Recorder, Reply, catch_panics,
26};
27use rig_core::tool::{PublishedContext, ToolContext, ToolResultContext};
28
29/// What a handler's run left: its answer, and what a tool published beside
30/// it, if anything.
31pub(crate) type Ran = (Result<Answered, ErrorReport>, Option<ToolResultContext>);
32
33/// The effect ids, and the records of the effects running.
34#[derive(Resource)]
35pub struct Effects {
36    /// The last id taken.
37    last: AtomicU64,
38    /// The records of effect entities' dispatches, timed, until they settle;
39    /// without stream items.
40    buffer: RecordBuffer,
41}
42
43impl Default for Effects {
44    fn default() -> Self {
45        Self {
46            last: AtomicU64::new(0),
47            buffer: RecordBuffer::new().timed(),
48        }
49    }
50}
51
52impl Effects {
53    /// Effects with ids after `last`, such as the highest id of the effect
54    /// log a plugin continues, so ids keep increasing across restarts.
55    pub fn continue_after(last: Option<EffectId>) -> Self {
56        Self {
57            last: AtomicU64::new(last.map_or(0, EffectId::as_u64)),
58            ..Self::default()
59        }
60    }
61
62    /// Runs `kind` through `handler` as the effect `id` of an entity, from
63    /// `origin`, recorded in the kernel's buffer; `consume` turns the reply
64    /// into the outcome, such as `Reply::into_outcome`. A panic in the
65    /// handler or its stream becomes an internal error, recorded as its
66    /// outcome. What a tool publishes into its context comes back beside it.
67    pub(crate) fn run<F>(
68        &self,
69        id: EffectId,
70        origin: DispatchContext,
71        handler: ErasedHandler,
72        kind: EffectKind,
73        consume: impl FnOnce(Reply) -> F + Send + 'static,
74    ) -> impl ConditionalSendFuture<Output = Ran> + 'static
75    where
76        F: ConditionalSendFuture<Output = Result<Answered, ErrorReport>> + 'static,
77    {
78        let recorder: Arc<dyn Recorder + Send + Sync> = Arc::new(self.buffer.clone());
79        recorder.begin(id, handler.key().clone(), kind.clone(), origin);
80        let published = PublishedContext::new();
81        let dispatch = Dispatch::new(id, kind.streams())
82            .with_scope(published.clone())
83            .recorded_by(recorder.clone());
84        let work = async move { consume(handler.handle(kind, dispatch).await).await };
85        let work = catch_panics(Some(recorder), id, work);
86        async move {
87            let outcome = work.await;
88            let facts = published.take().map(ToolContext::into_result_context);
89            (
90                outcome,
91                facts.filter(|facts| *facts != ToolResultContext::default()),
92            )
93        }
94    }
95
96    /// The records of effect entities' dispatches.
97    pub(crate) fn buffer(&self) -> &RecordBuffer {
98        &self.buffer
99    }
100
101    /// Takes the next effect id.
102    pub(crate) fn next_id(&self) -> EffectId {
103        EffectId::from_raw(self.last.fetch_add(1, Ordering::Relaxed) + 1)
104    }
105}
106
107/// An effect handler as a component or resource keeps it. rig-core's
108/// [`ErasedHandler`] is `Send` and `Sync` except on browser wasm, where its
109/// provider clients hold JavaScript values, and Bevy asks for both on
110/// every target.
111#[derive(Clone)]
112pub struct Handler(pub ErasedHandler);
113
114// SAFETY: browser wasm without the `atomics` target feature runs one
115// thread, so a `Handler` is never sent to or shared with another one.
116#[cfg(all(
117    target_arch = "wasm32",
118    target_os = "unknown",
119    not(target_feature = "atomics")
120))]
121unsafe impl Send for Handler {}
122// SAFETY: as for `Send`: there is no other thread.
123#[cfg(all(
124    target_arch = "wasm32",
125    target_os = "unknown",
126    not(target_feature = "atomics")
127))]
128unsafe impl Sync for Handler {}