Skip to main content

rig_harness/
load.rs

1//! What each plugin added, as data in the world: one entity per plugin
2//! `plugins.toml` lists (its `Name` and [`PluginSource`]), and
3//! [`ProvidedBy`] on what that plugin spawned, such as its commands,
4//! tools, prompt sections and observers. "What did plugin X add" is a
5//! query for `ProvidedBy(x)`; which plugin added a schedule's system is in
6//! [`PluginSystems`]; what it keeps with the session is its [`Keeps`].
7//!
8//! Every named entity, observer and schedule system a plugin adds in its
9//! `build`, `finish` and `cleanup` is attributed. What it spawns later, such as a
10//! tool an MCP server lists, it attributes itself, with its entity from
11//! [`PluginEntity<P>`]:
12//!
13//! ```no_run
14//! use rig_harness::prelude::*;
15//!
16//! #[derive(Default)]
17//! struct McpPlugin;
18//!
19//! impl Plugin for McpPlugin {
20//!     fn build(&self, _app: &mut App) {}
21//! }
22//!
23//! fn spawn_listed_tool(me: Option<Res<PluginEntity<McpPlugin>>>, mut commands: Commands) {
24//!     let tool = commands.spawn(Name::new("listed tool")).id();
25//!     if let Some(me) = me {
26//!         commands.entity(tool).insert(ProvidedBy(me.entity()));
27//!     }
28//! }
29//! ```
30
31use std::collections::{HashMap, HashSet};
32use std::marker::PhantomData;
33
34use bevy_app::prelude::*;
35use bevy_ecs::observer::Observer;
36use bevy_ecs::prelude::*;
37use bevy_ecs::schedule::{InternedScheduleLabel, Schedules, SystemKey};
38use bevy_log::error;
39use bevy_reflect::prelude::*;
40use rig_ecs::prelude::Notice;
41use rig_ecs::restore::SavedKeys;
42use rig_ecs::stage::AgentPass;
43
44/// Where a plugin comes from, on the entity [`load`] spawns for it.
45#[derive(Component, Reflect, Clone, Debug)]
46#[reflect(Component, Clone, Debug)]
47pub struct PluginSource {
48    /// Its package, such as `rig-harness`.
49    pub package: String,
50    /// Where the package is built from, as `plugins.toml` names it, such
51    /// as `path /home/me/.rig/plugins/viz` or `version 0.3`.
52    pub source: String,
53}
54
55/// The plugin that spawned this entity.
56#[derive(Component, Reflect, Debug)]
57#[reflect(Component)]
58#[relationship(relationship_target = Provides)]
59pub struct ProvidedBy(pub Entity);
60
61/// What a plugin spawned.
62#[derive(Component, Reflect, Debug)]
63#[reflect(Component)]
64#[relationship_target(relationship = ProvidedBy)]
65pub struct Provides(Vec<Entity>);
66
67/// The entity [`load`] spawned for the plugin `P`, for what `P` spawns
68/// while the app runs.
69#[derive(Resource)]
70pub struct PluginEntity<P: Plugin> {
71    entity: Entity,
72    plugin: PhantomData<fn() -> P>,
73}
74
75impl<P: Plugin> PluginEntity<P> {
76    /// The plugin's entity.
77    pub fn entity(&self) -> Entity {
78        self.entity
79    }
80}
81
82/// Which plugin added each schedule system, by the system's name and its
83/// schedule's label (such as `AgentPass`), as [`load`] saw it. Two plugins
84/// that add a system of the same name to one schedule are told apart by
85/// neither; a system a plugin adds later, while the app runs, is no
86/// plugin's.
87#[derive(Resource, Default, Debug)]
88pub struct PluginSystems(HashMap<(String, String), Option<Entity>>);
89
90impl PluginSystems {
91    /// The plugin that added the system `system` to the schedule labelled
92    /// `schedule`, if one plugin did.
93    pub fn plugin(&self, system: &str, schedule: &str) -> Option<Entity> {
94        let key = (system.to_owned(), schedule.to_owned());
95        self.0.get(&key).copied().flatten()
96    }
97
98    /// Records each system added since `before` as `plugin`'s.
99    fn record(
100        world: &mut World,
101        before: &HashSet<(InternedScheduleLabel, SystemKey)>,
102        plugin: Entity,
103    ) {
104        let mut added = Vec::new();
105        if let Some(schedules) = world.get_resource::<Schedules>() {
106            for (label, schedule) in schedules.iter() {
107                for (key, system, _) in schedule.graph().systems.iter() {
108                    if !before.contains(&(schedule.label(), key)) {
109                        added.push((system.name().to_string(), format!("{label:?}")));
110                    }
111                }
112            }
113        }
114        let mut systems = world.get_resource_or_init::<PluginSystems>();
115        for key in added {
116            let by = systems.0.entry(key).or_insert(Some(plugin));
117            if *by != Some(plugin) {
118                *by = None;
119            }
120        }
121    }
122}
123
124/// Each pair of systems of one [`AgentPass`] stage that use the same data,
125/// one writing it, with no order between them, so which runs first is
126/// chance: one line each, naming their plugins (by type, or `the harness`
127/// for the harness's, the kernel's and Bevy's own), the systems and the
128/// data. It initializes the agent loop, as the loop's first run would; an
129/// app without one has none. `rig plugin check --build` reports these for
130/// the plugins `plugins.toml` lists, and a debug build at its start
131/// ([`check_stages_at_start`]).
132pub fn stage_conflicts(world: &mut World) -> Vec<String> {
133    let label = format!("{AgentPass:?}");
134    let conflicts = world.try_schedule_scope(AgentPass, |world, pass| {
135        // Named before the build, which moves the systems out of the graph
136        // into the schedule a run has.
137        let built = pass.systems().into_iter().flatten();
138        let names: HashMap<SystemKey, String> = (pass.graph().systems.iter())
139            .map(|(key, system, _)| (key, system))
140            .chain(built)
141            .map(|(key, system)| (key, system.name().to_string()))
142            .collect();
143        let failure = pass.initialize(world).err();
144        let graph = pass.graph();
145        let name = |key| {
146            names
147                .get(key)
148                .cloned()
149                .unwrap_or_else(|| "an unnamed system".to_owned())
150        };
151        let conflicts: Vec<(String, String, String)> = (graph.conflicting_systems().iter())
152            .map(|(first, second, data)| {
153                let data = data
154                    .iter()
155                    .filter_map(|id| world.components().get_name(*id));
156                let data = data.map(|name| name.to_string()).collect::<Vec<_>>();
157                (name(first), name(second), data.join(", "))
158            })
159            .collect();
160        match failure {
161            Some(failure) if conflicts.is_empty() => vec![(
162                String::new(),
163                String::new(),
164                failure.to_string(graph, world),
165            )],
166            _ => conflicts,
167        }
168    });
169    let systems = world.get_resource::<PluginSystems>();
170    let plugin = |system: &str| {
171        systems
172            .and_then(|systems| systems.plugin(system, &label))
173            .and_then(|plugin| world.get::<Name>(plugin))
174            .map_or_else(|| "the harness".to_owned(), |name| format!("`{name}`"))
175    };
176    let conflicts = conflicts.unwrap_or_default().into_iter();
177    conflicts
178        .map(|(first, second, data)| {
179            if first.is_empty() {
180                return format!("the agent loop does not build: {data}");
181            }
182            let on = if data.is_empty() {
183                "the whole world".to_owned()
184            } else {
185                data
186            };
187            let plugins = match (plugin(&first), plugin(&second)) {
188                (one, other) if one == other => one,
189                (one, other) => format!("{one} and {other}"),
190            };
191            format!("{plugins}: {first} and {second}, on {on}")
192        })
193        .collect()
194}
195
196/// Shows each of the app's [`stage_conflicts`] as a warning notice at its
197/// start. The generated `main.rs` adds it to a debug build.
198pub fn check_stages_at_start(app: &mut App) {
199    app.add_systems(Startup, warn_stage_conflicts);
200}
201
202fn warn_stage_conflicts(world: &mut World) {
203    for conflict in stage_conflicts(world) {
204        world.write_message(Notice::warn(
205            None,
206            format!(
207                "Agent-loop systems use the same data with no order between them, so which \
208                 runs first is chance: {conflict}"
209            ),
210        ));
211    }
212}
213
214/// Every system of every schedule, by its schedule's label.
215fn systems(world: &World) -> HashSet<(InternedScheduleLabel, SystemKey)> {
216    let Some(schedules) = world.get_resource::<Schedules>() else {
217        return HashSet::new();
218    };
219    let systems = schedules.iter().flat_map(|(_, schedule)| {
220        (schedule.graph().systems.iter()).map(|(key, ..)| (schedule.label(), key))
221    });
222    systems.collect()
223}
224
225/// Adds `P`, unless the app has it already (another plugin added it), as
226/// the generated `main.rs` adds each plugin `plugins.toml` lists: with an
227/// entity of its own, named after it, from `package` at `source`.
228pub fn load<P: Plugin + Default>(app: &mut App, package: &str, source: &str) {
229    if app.is_plugin_added::<P>() {
230        return;
231    }
232    let plugin = P::default();
233    let source = PluginSource {
234        package: package.to_owned(),
235        source: source.to_owned(),
236    };
237    let entity = app
238        .world_mut()
239        .spawn((Name::new(plugin.name().to_owned()), source))
240        .id();
241    app.insert_resource(PluginEntity::<P> {
242        entity,
243        plugin: PhantomData,
244    });
245    let attribute = attribute(app.world_mut(), entity);
246    let before = systems(app.world());
247    let outer = app.world_mut().remove_resource::<Loading>();
248    app.insert_resource(Loading(entity));
249    // `Attribute` before and after it brackets its `finish` and `cleanup`
250    // (and the plugins it adds), which run in the order plugins were
251    // added.
252    app.add_plugins(Attribute::Open(entity));
253    app.add_plugins(plugin);
254    app.add_plugins(Attribute::Close);
255    app.world_mut().remove_resource::<Loading>();
256    if let Some(outer) = outer {
257        app.insert_resource(outer);
258    }
259    PluginSystems::record(app.world_mut(), &before, entity);
260    stop_attributing(app.world_mut(), attribute);
261}
262
263/// What a plugin keeps with the session, on the entity [`load`] spawned for
264/// it: the saved values it owns, by their saved key (the type path, such as
265/// `my_plugin::Counter`, or a stable one such as `rig::model`), and its
266/// folders of session files in
267/// [`PluginData::session`](crate::prelude::PluginData::session), such as
268/// `vim`. A plugin says so in `build`, `finish` or `cleanup`
269/// ([`AppKeepsExt`]); nothing infers it. A saved value no plugin keeps is
270/// the kernel's or the harness's, or one whose plugin did not say.
271#[derive(Component, Reflect, Clone, Debug, Default, PartialEq, Eq)]
272#[reflect(Component, Default, Clone, Debug)]
273pub struct Keeps {
274    /// The keys of the saved values it keeps.
275    pub saved: Vec<String>,
276    /// Its folders in the session's plugin files.
277    pub files: Vec<String>,
278}
279
280impl Keeps {
281    /// Whether the saved value of `key` is this plugin's.
282    pub fn saved(&self, key: &str) -> bool {
283        self.saved.iter().any(|saved| saved == key)
284    }
285
286    /// Whether the session file `name`, as `PluginData::session` names it
287    /// (`vim/registers.json`), is in one of this plugin's folders.
288    pub fn file(&self, name: &str) -> bool {
289        self.files.iter().any(|folder| {
290            name.strip_prefix(folder.as_str())
291                .is_some_and(|rest| rest.starts_with('/'))
292        })
293    }
294}
295
296/// Saying what a plugin keeps with the session, in its `build`, `finish`
297/// or `cleanup`, onto its [`Keeps`]. A plugin added without [`load`] has no
298/// entity to say it on, and nothing is recorded.
299pub trait AppKeepsExt {
300    /// This plugin keeps the saved value `T` (`#[reflect(.., Saved)]`).
301    fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self;
302
303    /// This plugin keeps the session files under `folder` in
304    /// `PluginData::session`, such as `vim` for `vim/registers.json`.
305    fn keeps_files(&mut self, folder: &str) -> &mut Self;
306
307    /// This plugin's saved value `T` (an agent component or a resource) is
308    /// logged and restored under `key`, such as `goals.goal`, rather than its
309    /// type path, so a rename keeps it and a program without Bevy reads it
310    /// by that key (rig-memory's `Session::value`). Keys are namespaced by
311    /// the plugin; a key another type has, or a second key for `T`, is
312    /// refused with an error in the log. [`SavedAs::listed`] also lists it.
313    fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_>;
314}
315
316/// A saved value's key, as [`AppKeepsExt::saved_as`] recorded it.
317#[must_use = "a saved key is recorded already; call `listed` to list its values too"]
318pub struct SavedAs<'a> {
319    app: &'a mut App,
320    /// The key, or `None` when it was refused.
321    key: Option<&'static str>,
322}
323
324impl<'a> SavedAs<'a> {
325    /// The value is listed too: copied into the session's listing
326    /// (rig-memory's `SessionMeta::listed`) whenever it changes, so a listing
327    /// and a session picker show and filter by it without opening a log.
328    /// Keep listed values small.
329    pub fn listed(self) -> &'a mut App {
330        if let Some(key) = self.key {
331            self.app
332                .world_mut()
333                .get_resource_or_init::<SavedKeys>()
334                .list(key);
335        }
336        self.app
337    }
338}
339
340impl AppKeepsExt for App {
341    fn keeps_saved<T: Component + TypePath>(&mut self) -> &mut Self {
342        keep(self.world_mut(), |keeps| &mut keeps.saved, T::type_path());
343        self
344    }
345
346    fn keeps_files(&mut self, folder: &str) -> &mut Self {
347        let folder = folder.trim_end_matches('/');
348        keep(self.world_mut(), |keeps| &mut keeps.files, folder);
349        self
350    }
351
352    fn saved_as<T: TypePath>(&mut self, key: &'static str) -> SavedAs<'_> {
353        let world = self.world_mut();
354        let added = world
355            .get_resource_or_init::<SavedKeys>()
356            .add(T::type_path(), key);
357        let key = match added {
358            Ok(()) => {
359                keep(world, |keeps| &mut keeps.saved, key);
360                Some(key)
361            }
362            Err(failure) => {
363                error!("not saving `{}` under `{key}`: {failure}", T::type_path());
364                None
365            }
366        };
367        SavedAs { app: self, key }
368    }
369}
370
371/// Adds `what` to the list `of` of the [`Keeps`] of the plugin being
372/// built, finished or cleaned up, once.
373fn keep(world: &mut World, of: fn(&mut Keeps) -> &mut Vec<String>, what: &str) {
374    let loading = world.get_resource::<Loading>().map(|loading| loading.0);
375    let plugin = loading.or_else(|| Some(world.get_resource::<Attributing>()?.plugin));
376    let Some(mut plugin) = plugin.and_then(|plugin| world.get_entity_mut(plugin).ok()) else {
377        return;
378    };
379    if !plugin.contains::<Keeps>() {
380        plugin.insert(Keeps::default());
381    }
382    let Some(mut keeps) = plugin.get_mut::<Keeps>() else {
383        return;
384    };
385    let list = of(&mut keeps);
386    if !list.iter().any(|kept| kept == what) {
387        list.push(what.to_owned());
388    }
389}
390
391/// The plugin [`load`] is building.
392#[derive(Resource)]
393struct Loading(Entity);
394
395/// Marks each named entity and observer spawned from now on as
396/// `plugin`'s, until [`stop_attributing`].
397fn attribute(world: &mut World, plugin: Entity) -> Entity {
398    world
399        .add_observer(
400            move |added: On<Add<(Name, Observer)>>, mut commands: Commands| {
401                commands
402                    .entity(added.entity)
403                    .try_insert_if_new(ProvidedBy(plugin));
404            },
405        )
406        .id()
407}
408
409/// Spawns what was queued while still attributed, then stops.
410fn stop_attributing(world: &mut World, attribute: Entity) {
411    world.flush();
412    world.despawn(attribute);
413}
414
415/// Attributes what a plugin spawns in `finish` and `cleanup`, from its
416/// [`Attribute::Open`] to the [`Attribute::Close`] after it.
417enum Attribute {
418    Open(Entity),
419    Close,
420}
421
422/// The plugin being finished or cleaned up, the observer attributing to
423/// it, and the systems before it.
424#[derive(Resource)]
425struct Attributing {
426    plugin: Entity,
427    observer: Entity,
428    before: HashSet<(InternedScheduleLabel, SystemKey)>,
429}
430
431impl Attribute {
432    fn bracket(&self, app: &mut App) {
433        let world = app.world_mut();
434        match self {
435            Self::Open(plugin) => {
436                let observer = attribute(world, *plugin);
437                let before = systems(world);
438                world.insert_resource(Attributing {
439                    plugin: *plugin,
440                    observer,
441                    before,
442                });
443            }
444            Self::Close => {
445                if let Some(attributing) = world.remove_resource::<Attributing>() {
446                    PluginSystems::record(world, &attributing.before, attributing.plugin);
447                    stop_attributing(world, attributing.observer);
448                }
449            }
450        }
451    }
452}
453
454impl Plugin for Attribute {
455    fn build(&self, _app: &mut App) {}
456
457    fn finish(&self, app: &mut App) {
458        self.bracket(app);
459    }
460
461    fn cleanup(&self, app: &mut App) {
462        self.bracket(app);
463    }
464
465    fn is_unique(&self) -> bool {
466        false
467    }
468}
469
470#[cfg(test)]
471mod tests;