yoagent-state 0.2.0

Durable state and lineage for long-running agents.
Documentation
use crate::{Node, PackId, Relation, StateError};
use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use std::collections::{BTreeMap, BTreeSet};

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Pack {
    pub id: PackId,
    pub name: String,
    pub version: String,
    pub object_types: BTreeMap<String, ObjectType>,
    pub relation_types: BTreeMap<String, RelationType>,
    pub policies: Vec<String>,
    pub prompts: Vec<String>,
    pub settings: JsonValue,
}

impl Pack {
    pub fn new(id: PackId, name: impl Into<String>, version: impl Into<String>) -> Self {
        Self {
            id,
            name: name.into(),
            version: version.into(),
            object_types: BTreeMap::new(),
            relation_types: BTreeMap::new(),
            policies: Vec::new(),
            prompts: Vec::new(),
            settings: JsonValue::Object(Default::default()),
        }
    }

    pub fn add_object_type(mut self, object_type: ObjectType) -> Self {
        self.object_types
            .insert(object_type.kind.clone(), object_type);
        self
    }

    pub fn add_relation_type(mut self, relation_type: RelationType) -> Self {
        self.relation_types
            .insert(relation_type.rel.clone(), relation_type);
        self
    }

    pub fn validate_node(&self, node: &Node) -> Result<(), StateError> {
        let Some(object_type) = self.object_types.get(&node.kind) else {
            return Ok(());
        };

        for required in &object_type.required_props {
            if node.props.get(required).is_none_or(|value| value.is_null()) {
                return Err(StateError::Validation(format!(
                    "node {} of kind {} is missing required prop {}",
                    node.id, node.kind, required
                )));
            }
        }

        Ok(())
    }

    pub fn validate_relation(
        &self,
        relation: &Relation,
        from: &Node,
        to: &Node,
    ) -> Result<(), StateError> {
        let Some(relation_type) = self.relation_types.get(&relation.rel) else {
            return Ok(());
        };

        if !relation_type.from_kinds.is_empty() && !relation_type.from_kinds.contains(&from.kind) {
            return Err(StateError::Validation(format!(
                "relation {} cannot start from kind {}",
                relation.rel, from.kind
            )));
        }

        if !relation_type.to_kinds.is_empty() && !relation_type.to_kinds.contains(&to.kind) {
            return Err(StateError::Validation(format!(
                "relation {} cannot point to kind {}",
                relation.rel, to.kind
            )));
        }

        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ObjectType {
    pub kind: String,
    pub required_props: BTreeSet<String>,
    pub description: Option<String>,
}

impl ObjectType {
    pub fn new(kind: impl Into<String>) -> Self {
        Self {
            kind: kind.into(),
            required_props: BTreeSet::new(),
            description: None,
        }
    }

    pub fn require(mut self, prop: impl Into<String>) -> Self {
        self.required_props.insert(prop.into());
        self
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RelationType {
    pub rel: String,
    pub from_kinds: BTreeSet<String>,
    pub to_kinds: BTreeSet<String>,
    pub description: Option<String>,
}

impl RelationType {
    pub fn new(rel: impl Into<String>) -> Self {
        Self {
            rel: rel.into(),
            from_kinds: BTreeSet::new(),
            to_kinds: BTreeSet::new(),
            description: None,
        }
    }

    pub fn from_kind(mut self, kind: impl Into<String>) -> Self {
        self.from_kinds.insert(kind.into());
        self
    }

    pub fn to_kind(mut self, kind: impl Into<String>) -> Self {
        self.to_kinds.insert(kind.into());
        self
    }
}