use std::sync::Arc;
use bevy_app::prelude::*;
use bevy_ecs::lifecycle::HookContext;
use bevy_ecs::prelude::*;
use bevy_ecs::reflect::AppTypeRegistry;
use bevy_ecs::resource::IsResource;
use bevy_ecs::schedule::ScheduleLabel;
use bevy_ecs::system::SystemParam;
use bevy_ecs::world::DeferredWorld;
use bevy_log::error;
use bevy_reflect::serde::TypedReflectSerializer;
use bevy_reflect::{CreateTypeData, Reflect, TypePath};
use rig_core::conversation::ChangeRecord;
use rig_memory::{Repair, SESSION, Session, SessionError, Store, Writer};
use super::agent::{Agent, AgentId, Conversation, Notice, SpawnedBy};
use super::restore::SavedKeys;
use super::{SaveSystems, WriteLogs};
#[derive(Clone)]
pub struct ReflectSaved {
watch: fn(&mut App),
}
impl<T: Component + Reflect + TypePath> CreateTypeData<T> for ReflectSaved {
fn create_type_data(_: ()) -> Self {
let watch = |app: &mut App| {
app.add_systems(Last, log_changed::<T>.in_set(SaveSystems));
};
Self { watch }
}
}
fn log_changed<T: Component + Reflect + TypePath>(
changed: Query<
(Entity, Option<&AgentId>, &T),
(Or<(With<Agent>, With<IsResource>)>, Changed<T>),
>,
mut removed: RemovedComponents<T>,
agents: Query<&AgentId, With<Agent>>,
spawned: Query<(), Added<Agent>>,
log: Res<SessionLog>,
registry: Res<AppTypeRegistry>,
keys: Res<SavedKeys>,
) {
if !log.0.is_live() {
return;
}
let path = T::type_path();
let name = keys.key(path).unwrap_or(path);
let registry = registry.read();
for (entity, id, component) in &changed {
let value = TypedReflectSerializer::new(component.as_partial_reflect(), ®istry);
let log_name = id.map_or(SESSION, |id| id.0.as_str());
let starts = id.is_some() && !spawned.contains(entity);
match serde_json::to_value(value) {
Ok(value) => log.0.save_value(log_name, name, Some(value), starts),
Err(failure) => error!("not logging {name}: {failure}"),
}
}
for entity in removed.read() {
if let Ok(id) = agents.get(entity)
&& !changed.contains(entity)
{
log.0
.save_value(&id.0, name, None, !spawned.contains(entity));
}
}
}
#[derive(Resource, Clone)]
pub struct SessionStore(pub Arc<dyn Store>);
impl SessionStore {
pub fn new(store: impl Store) -> Self {
Self(Arc::new(store))
}
}
#[derive(Resource, Clone, Default)]
pub struct SessionLog(pub(crate) Writer);
impl SessionLog {
pub(crate) fn flush(&self) -> Result<(), SessionError> {
self.0.flush()
}
pub fn has_written(&self) -> bool {
self.0.has_written()
}
pub fn failure(&self) -> Option<String> {
self.0.failure()
}
}
#[derive(Resource, Clone, Debug, Default)]
pub struct SessionRestored {
pub repairs: Vec<Repair>,
}
#[derive(Resource)]
pub(crate) struct Opened(pub(crate) Session);
pub fn open_session(world: &mut World, store: Arc<dyn Store>) -> Result<(), SessionError> {
let (writer, session) = Writer::open(store.clone())?;
world.insert_resource(SessionStore(store));
world.insert_resource(SessionLog(writer));
world.insert_resource(Opened(session));
Ok(())
}
#[derive(Message, Clone, Debug)]
pub struct ConversationChanged {
pub agent: Entity,
pub change: ChangeRecord,
}
#[derive(EntityEvent, Clone, Debug)]
pub struct Retire {
pub entity: Entity,
}
pub(crate) fn save_conversation(
log: &SessionLog,
agent: Entity,
id: &AgentId,
conversation: &mut Conversation,
) -> Vec<ConversationChanged> {
let saved = log.0.save(&id.0, conversation);
let committed = |change| ConversationChanged { agent, change };
saved.into_iter().map(committed).collect()
}
#[derive(SystemParam)]
pub(crate) struct Drain<'w, 's> {
agents:
Query<'w, 's, (Entity, &'static AgentId, &'static mut Conversation), Changed<Conversation>>,
log: Res<'w, SessionLog>,
committed: MessageWriter<'w, ConversationChanged>,
}
impl Drain<'_, '_> {
pub(crate) fn save(&mut self) -> Option<Entity> {
let mut changed = None;
for (agent, id, mut conversation) in &mut self.agents {
let conversation = conversation.bypass_change_detection();
let saved = save_conversation(&self.log, agent, id, conversation);
if !saved.is_empty() {
changed = Some(agent);
self.committed.write_batch(saved);
}
}
changed
}
}
#[derive(ScheduleLabel, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct SaveConversations;
#[derive(Resource, Default)]
pub(crate) struct SavedOne(Option<Entity>);
pub(crate) fn save_conversations(mut drain: Drain, mut saved: ResMut<SavedOne>) {
if let Some(agent) = drain.save() {
saved.0 = Some(agent);
}
}
pub(crate) fn save_now(world: &mut World) -> Option<Entity> {
world.get_resource_mut::<SavedOne>()?.0 = None;
world.try_run_schedule(SaveConversations).ok()?;
world.get_resource_mut::<SavedOne>()?.0.take()
}
pub(crate) fn save_discarded(mut world: DeferredWorld, context: HookContext) {
let agent = context.entity;
let (Some(log), Some(id)) = (
world.get_resource::<SessionLog>().cloned(),
world.get::<AgentId>(agent).cloned(),
) else {
return;
};
let Some(mut conversation) = world.get_mut::<Conversation>(agent) else {
return;
};
if conversation.unsaved().is_empty() {
return;
}
let conversation = conversation.bypass_change_detection();
for committed in save_conversation(&log, agent, &id, conversation) {
world.write_message(committed);
}
}
pub(crate) fn retire(
retire: On<Retire>,
mut agents: Query<(&AgentId, &mut Conversation)>,
log: Res<SessionLog>,
mut committed: MessageWriter<ConversationChanged>,
mut commands: Commands,
) {
let agent = retire.entity;
let Ok((id, mut conversation)) = agents.get_mut(agent) else {
return;
};
let conversation = conversation.bypass_change_detection();
committed.write_batch(save_conversation(&log, agent, id, conversation));
log.0.retire(&id.0);
commands.entity(agent).try_despawn();
}
pub struct SessionLogPlugin;
impl Plugin for SessionLogPlugin {
fn build(&self, app: &mut App) {
app.add_systems(PreStartup, super::restore::restore_session)
.add_systems(Startup, super::restore::reconcile)
.add_systems(Last, write_logs.in_set(WriteLogs))
.add_observer(open_child_log)
.init_resource::<SavedKeys>();
}
fn finish(&self, app: &mut App) {
let watches: Vec<fn(&mut App)> = app
.world()
.get_resource::<AppTypeRegistry>()
.map(|registry| {
let registry = registry.read();
let saved = registry.iter_with_data::<ReflectSaved>();
saved.map(|(_, saved)| saved.watch).collect()
})
.unwrap_or_default();
for watch in watches {
watch(app);
}
}
}
fn open_child_log(
spawned: On<Add<SpawnedBy>>,
agents: Query<(&AgentId, &SpawnedBy)>,
ids: Query<&AgentId>,
log: Res<SessionLog>,
) {
if let Ok((child, parent)) = agents.get(spawned.entity)
&& let Ok(parent) = ids.get(parent.0)
{
log.0.open_log(&child.0, Some(&parent.0));
}
}
fn write_logs(log: Res<SessionLog>, mut reported: Local<bool>, mut notices: MessageWriter<Notice>) {
if let Err(failure) = log.flush()
&& !*reported
{
*reported = true;
error!("the session log stopped: {failure}");
notices.write(Notice::error(
None,
format!("The session is no longer saved: {failure}"),
));
}
}
#[cfg(test)]
pub(crate) mod tests;