1use std::sync::Arc;
16
17use bevy_app::prelude::*;
18use bevy_ecs::lifecycle::HookContext;
19use bevy_ecs::prelude::*;
20use bevy_ecs::reflect::AppTypeRegistry;
21use bevy_ecs::resource::IsResource;
22use bevy_ecs::schedule::ScheduleLabel;
23use bevy_ecs::system::SystemParam;
24use bevy_ecs::world::DeferredWorld;
25use bevy_log::error;
26use bevy_reflect::serde::TypedReflectSerializer;
27use bevy_reflect::{CreateTypeData, Reflect, TypePath};
28use rig_core::conversation::ChangeRecord;
29use rig_memory::{Repair, SESSION, Session, SessionError, Store, Writer};
30
31use super::agent::{Agent, AgentId, Conversation, Notice, SpawnedBy};
32use super::restore::SavedKeys;
33use super::{SaveSystems, WriteLogs};
34
35#[derive(Clone)]
55pub struct ReflectSaved {
56 watch: fn(&mut App),
57}
58
59impl<T: Component + Reflect + TypePath> CreateTypeData<T> for ReflectSaved {
60 fn create_type_data(_: ()) -> Self {
61 let watch = |app: &mut App| {
64 app.add_systems(Last, log_changed::<T>.in_set(SaveSystems));
65 };
66 Self { watch }
67 }
68}
69
70fn log_changed<T: Component + Reflect + TypePath>(
76 changed: Query<
77 (Entity, Option<&AgentId>, &T),
78 (Or<(With<Agent>, With<IsResource>)>, Changed<T>),
79 >,
80 mut removed: RemovedComponents<T>,
81 agents: Query<&AgentId, With<Agent>>,
82 spawned: Query<(), Added<Agent>>,
83 log: Res<SessionLog>,
84 registry: Res<AppTypeRegistry>,
85 keys: Res<SavedKeys>,
86) {
87 if !log.0.is_live() {
88 return;
89 }
90 let path = T::type_path();
91 let name = keys.key(path).unwrap_or(path);
92 let registry = registry.read();
93 for (entity, id, component) in &changed {
94 let value = TypedReflectSerializer::new(component.as_partial_reflect(), ®istry);
95 let log_name = id.map_or(SESSION, |id| id.0.as_str());
96 let starts = id.is_some() && !spawned.contains(entity);
97 match serde_json::to_value(value) {
98 Ok(value) => log.0.save_value(log_name, name, Some(value), starts),
99 Err(failure) => error!("not logging {name}: {failure}"),
100 }
101 }
102 for entity in removed.read() {
103 if let Ok(id) = agents.get(entity)
104 && !changed.contains(entity)
105 {
106 log.0
107 .save_value(&id.0, name, None, !spawned.contains(entity));
108 }
109 }
110}
111
112#[derive(Resource, Clone)]
116pub struct SessionStore(pub Arc<dyn Store>);
117
118impl SessionStore {
119 pub fn new(store: impl Store) -> Self {
121 Self(Arc::new(store))
122 }
123}
124
125#[derive(Resource, Clone, Default)]
129pub struct SessionLog(pub(crate) Writer);
130
131impl SessionLog {
132 pub(crate) fn flush(&self) -> Result<(), SessionError> {
135 self.0.flush()
136 }
137
138 pub fn has_written(&self) -> bool {
141 self.0.has_written()
142 }
143
144 pub fn failure(&self) -> Option<String> {
148 self.0.failure()
149 }
150}
151
152#[derive(Resource, Clone, Debug, Default)]
160pub struct SessionRestored {
161 pub repairs: Vec<Repair>,
163}
164
165#[derive(Resource)]
167pub(crate) struct Opened(pub(crate) Session);
168
169pub fn open_session(world: &mut World, store: Arc<dyn Store>) -> Result<(), SessionError> {
184 let (writer, session) = Writer::open(store.clone())?;
185 world.insert_resource(SessionStore(store));
186 world.insert_resource(SessionLog(writer));
187 world.insert_resource(Opened(session));
188 Ok(())
189}
190
191#[derive(Message, Clone, Debug)]
195pub struct ConversationChanged {
196 pub agent: Entity,
198 pub change: ChangeRecord,
200}
201
202#[derive(EntityEvent, Clone, Debug)]
206pub struct Retire {
207 pub entity: Entity,
209}
210
211pub(crate) fn save_conversation(
214 log: &SessionLog,
215 agent: Entity,
216 id: &AgentId,
217 conversation: &mut Conversation,
218) -> Vec<ConversationChanged> {
219 let saved = log.0.save(&id.0, conversation);
220 let committed = |change| ConversationChanged { agent, change };
221 saved.into_iter().map(committed).collect()
222}
223
224#[derive(SystemParam)]
227pub(crate) struct Drain<'w, 's> {
228 agents:
229 Query<'w, 's, (Entity, &'static AgentId, &'static mut Conversation), Changed<Conversation>>,
230 log: Res<'w, SessionLog>,
231 committed: MessageWriter<'w, ConversationChanged>,
232}
233
234impl Drain<'_, '_> {
235 pub(crate) fn save(&mut self) -> Option<Entity> {
237 let mut changed = None;
238 for (agent, id, mut conversation) in &mut self.agents {
239 let conversation = conversation.bypass_change_detection();
240 let saved = save_conversation(&self.log, agent, id, conversation);
241 if !saved.is_empty() {
242 changed = Some(agent);
243 self.committed.write_batch(saved);
244 }
245 }
246 changed
247 }
248}
249
250#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
254pub(crate) struct SaveConversations;
255
256#[derive(Resource, Default)]
259pub(crate) struct SavedOne(Option<Entity>);
260
261pub(crate) fn save_conversations(mut drain: Drain, mut saved: ResMut<SavedOne>) {
264 if let Some(agent) = drain.save() {
265 saved.0 = Some(agent);
266 }
267}
268
269pub(crate) fn save_now(world: &mut World) -> Option<Entity> {
272 world.get_resource_mut::<SavedOne>()?.0 = None;
273 world.try_run_schedule(SaveConversations).ok()?;
274 world.get_resource_mut::<SavedOne>()?.0.take()
275}
276
277pub(crate) fn save_discarded(mut world: DeferredWorld, context: HookContext) {
281 let agent = context.entity;
282 let (Some(log), Some(id)) = (
283 world.get_resource::<SessionLog>().cloned(),
284 world.get::<AgentId>(agent).cloned(),
285 ) else {
286 return;
287 };
288 let Some(mut conversation) = world.get_mut::<Conversation>(agent) else {
289 return;
290 };
291 if conversation.unsaved().is_empty() {
292 return;
293 }
294 let conversation = conversation.bypass_change_detection();
295 for committed in save_conversation(&log, agent, &id, conversation) {
296 world.write_message(committed);
297 }
298}
299
300pub(crate) fn retire(
302 retire: On<Retire>,
303 mut agents: Query<(&AgentId, &mut Conversation)>,
304 log: Res<SessionLog>,
305 mut committed: MessageWriter<ConversationChanged>,
306 mut commands: Commands,
307) {
308 let agent = retire.entity;
309 let Ok((id, mut conversation)) = agents.get_mut(agent) else {
310 return;
311 };
312 let conversation = conversation.bypass_change_detection();
313 committed.write_batch(save_conversation(&log, agent, id, conversation));
314 log.0.retire(&id.0);
315 commands.entity(agent).try_despawn();
316}
317
318pub struct SessionLogPlugin;
321
322impl Plugin for SessionLogPlugin {
323 fn build(&self, app: &mut App) {
324 app.add_systems(PreStartup, super::restore::restore_session)
325 .add_systems(Startup, super::restore::reconcile)
326 .add_systems(Last, write_logs.in_set(WriteLogs))
327 .add_observer(open_child_log)
328 .init_resource::<SavedKeys>();
329 }
330
331 fn finish(&self, app: &mut App) {
334 let watches: Vec<fn(&mut App)> = app
335 .world()
336 .get_resource::<AppTypeRegistry>()
337 .map(|registry| {
338 let registry = registry.read();
339 let saved = registry.iter_with_data::<ReflectSaved>();
340 saved.map(|(_, saved)| saved.watch).collect()
341 })
342 .unwrap_or_default();
343 for watch in watches {
344 watch(app);
345 }
346 }
347}
348
349fn open_child_log(
352 spawned: On<Add<SpawnedBy>>,
353 agents: Query<(&AgentId, &SpawnedBy)>,
354 ids: Query<&AgentId>,
355 log: Res<SessionLog>,
356) {
357 if let Ok((child, parent)) = agents.get(spawned.entity)
358 && let Ok(parent) = ids.get(parent.0)
359 {
360 log.0.open_log(&child.0, Some(&parent.0));
361 }
362}
363
364fn write_logs(log: Res<SessionLog>, mut reported: Local<bool>, mut notices: MessageWriter<Notice>) {
366 if let Err(failure) = log.flush()
367 && !*reported
368 {
369 *reported = true;
370 error!("the session log stopped: {failure}");
371 notices.write(Notice::error(
372 None,
373 format!("The session is no longer saved: {failure}"),
374 ));
375 }
376}
377
378#[cfg(test)]
379pub(crate) mod tests;