use bevy_reflect::Reflect;
use std::collections::BTreeMap;
use bevy_ecs::prelude::*;
use serde::{Deserialize, Serialize};
use super::{Failed, Run, Settled};
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Reflect)]
#[reflect(Component)]
pub struct ToolTurnCommit {
pub turn: usize,
}
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
#[relationship(relationship_target = AssistantForTurns)]
#[reflect(Component)]
pub struct TurnAssistant(pub Entity);
#[derive(Component, Debug, Default, Reflect)]
#[relationship_target(relationship = TurnAssistant)]
#[reflect(Component)]
pub struct AssistantForTurns(Vec<Entity>);
#[derive(Component, Debug, Clone, Copy, PartialEq, Eq, Reflect)]
#[relationship(relationship_target = ResultsForTurns)]
#[reflect(Component)]
pub struct TurnResults(pub Entity);
#[derive(Component, Debug, Default, Reflect)]
#[relationship_target(relationship = TurnResults)]
#[reflect(Component)]
pub struct ResultsForTurns(Vec<Entity>);
#[derive(Component, Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize, Reflect)]
#[reflect(Component)]
pub struct ToolTurnHolds(BTreeMap<String, usize>);
impl ToolTurnHolds {
pub fn owners(&self) -> impl Iterator<Item = (&str, usize)> {
self.0.iter().map(|(owner, turn)| (owner.as_str(), *turn))
}
pub fn blocks(&self, committed_turn: usize) -> bool {
self.0.values().any(|turn| *turn <= committed_turn)
}
}
#[derive(Event, Debug, Clone, Copy)]
pub struct ToolTurnCommitted {
pub run: Entity,
pub turn: Entity,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum TurnHoldError {
#[error("checkpoint hold needs a live run")]
NotLiveRun,
#[error("checkpoint hold owner must not be empty")]
EmptyOwner,
#[error("checkpoint turn must be at least one")]
ZeroTurn,
}
pub fn hold_after_tool_turn(
world: &mut World,
run: Entity,
owner: impl Into<String>,
turn: usize,
) -> Result<bool, TurnHoldError> {
if world.get::<Run>(run).is_none()
|| world.get::<Failed>(run).is_some()
|| world.get::<Settled>(run).is_some()
{
return Err(TurnHoldError::NotLiveRun);
}
let owner = owner.into();
if owner.is_empty() {
return Err(TurnHoldError::EmptyOwner);
}
if turn == 0 {
return Err(TurnHoldError::ZeroTurn);
}
let mut holds = world.get::<ToolTurnHolds>(run).cloned().unwrap_or_default();
let previous = holds.0.get(&owner).copied();
if previous.is_some_and(|previous| previous <= turn) {
return Ok(false);
}
holds.0.insert(owner, turn);
world.entity_mut(run).insert(holds);
Ok(true)
}
pub fn release_tool_turn_hold(
world: &mut World,
run: Entity,
owner: &str,
) -> Result<bool, TurnHoldError> {
if world.get::<Run>(run).is_none() {
return Err(TurnHoldError::NotLiveRun);
}
if owner.is_empty() {
return Err(TurnHoldError::EmptyOwner);
}
let Some(mut holds) = world.get_mut::<ToolTurnHolds>(run) else {
return Ok(false);
};
let removed = holds.0.remove(owner).is_some();
if holds.0.is_empty() {
world.entity_mut(run).remove::<ToolTurnHolds>();
}
Ok(removed)
}
pub(crate) fn validate(
world: &World,
entity: Entity,
) -> Result<(), crate::checkpoint::CheckpointError> {
use super::{Batch, Cursor, Role, Turn, Utterance};
let refused = crate::checkpoint::CheckpointError::InvalidGraph;
if let Some(holds) = world.get::<ToolTurnHolds>(entity)
&& (world.get::<Run>(entity).is_none()
|| holds
.owners()
.any(|(owner, turn)| owner.is_empty() || turn == 0))
{
return Err(refused("invalid tool-turn hold"));
}
let assistant = world.get::<TurnAssistant>(entity);
let results = world.get::<TurnResults>(entity);
let commit = world.get::<ToolTurnCommit>(entity);
if assistant.is_none() && results.is_none() && commit.is_none() {
return Ok(());
}
let run = world
.get::<ChildOf>(entity)
.map(ChildOf::parent)
.filter(|run| world.get::<Run>(*run).is_some())
.ok_or(refused("tool-turn link has no run"))?;
if world.get::<Turn>(entity).is_none() {
return Err(refused("tool-turn link is not on a turn"));
}
for (target, role) in [
(assistant.map(|link| link.0), Role::Assistant),
(results.map(|link| link.0), Role::User),
] {
if let Some(target) = target
&& (world.get::<ChildOf>(target).map(ChildOf::parent) != Some(run)
|| world.get::<Utterance>(target).is_none()
|| world.get::<Role>(target) != Some(&role))
{
return Err(refused("tool-turn utterance has invalid ownership or role"));
}
}
if let Some(commit) = commit {
if commit.turn == 0
|| assistant.is_none()
|| results.is_none()
|| world.get::<Batch>(entity).is_some()
|| world
.get::<Cursor>(run)
.is_none_or(|cursor| commit.turn > cursor.turn)
{
return Err(refused("invalid committed tool turn"));
}
} else if results.is_some() {
return Err(refused("tool results have no commit"));
}
Ok(())
}