use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use time::OffsetDateTime;
use crate::error::DomainError;
use crate::value_objects::{
AgenticSystemDigest, AgenticSystemId, AgenticSystemLifecycle, AgenticSystemRevision,
AttentionPolicy, CeremonyComposition, CollaborationLink, LogicalParticipant, ParticipantId,
RequestedExecutionProfile, SupervisionPolicy, SystemCeremonyId, SystemPin, SystemPurpose,
SystemRole, SystemRoleId,
};
mod agentic_system_content;
mod agentic_system_parts;
mod dependency_order;
use agentic_system_content::AgenticSystemContent;
pub use agentic_system_parts::AgenticSystemParts;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct AgenticSystem {
id: AgenticSystemId,
revision: AgenticSystemRevision,
lifecycle: AgenticSystemLifecycle,
purpose: SystemPurpose,
integrator: SystemRoleId,
roles: BTreeMap<SystemRoleId, SystemRole>,
participants: BTreeMap<ParticipantId, LogicalParticipant>,
topology: Vec<CollaborationLink>,
profiles: BTreeMap<SystemRoleId, RequestedExecutionProfile>,
ceremonies: BTreeMap<SystemCeremonyId, CeremonyComposition>,
#[serde(default)]
supervision: SupervisionPolicy,
attention: AttentionPolicy,
#[serde(with = "time::serde::rfc3339")]
created_at: OffsetDateTime,
#[serde(with = "time::serde::rfc3339")]
updated_at: OffsetDateTime,
}
impl AgenticSystem {
#[allow(clippy::too_many_arguments)]
pub fn draft(
id: AgenticSystemId,
purpose: SystemPurpose,
integrator: SystemRoleId,
roles: impl IntoIterator<Item = SystemRole>,
participants: impl IntoIterator<Item = LogicalParticipant>,
topology: impl IntoIterator<Item = CollaborationLink>,
profiles: impl IntoIterator<Item = (SystemRoleId, RequestedExecutionProfile)>,
ceremonies: impl IntoIterator<Item = CeremonyComposition>,
supervision: SupervisionPolicy,
attention: AttentionPolicy,
now: OffsetDateTime,
) -> Result<Self, DomainError> {
let roles = index(roles, SystemRole::id, "agentic_system.roles")?;
let participants = index(
participants,
LogicalParticipant::id,
"agentic_system.participants",
)?;
let ceremonies = index(
ceremonies,
CeremonyComposition::id,
"agentic_system.ceremonies",
)?;
if roles.is_empty() {
return Err(DomainError::EmptyCollection {
field: "agentic_system.roles",
});
}
if ceremonies.is_empty() {
return Err(DomainError::EmptyCollection {
field: "agentic_system.ceremonies",
});
}
Ok(Self {
id,
revision: AgenticSystemRevision::INITIAL,
lifecycle: AgenticSystemLifecycle::Draft,
purpose,
integrator,
roles,
participants,
topology: topology.into_iter().collect(),
profiles: profiles.into_iter().collect(),
ceremonies,
supervision,
attention,
created_at: now,
updated_at: now,
})
}
#[must_use]
pub const fn id(&self) -> &AgenticSystemId {
&self.id
}
#[must_use]
pub const fn revision(&self) -> AgenticSystemRevision {
self.revision
}
#[must_use]
pub const fn lifecycle(&self) -> AgenticSystemLifecycle {
self.lifecycle
}
#[must_use]
pub const fn purpose(&self) -> &SystemPurpose {
&self.purpose
}
#[must_use]
pub const fn integrator(&self) -> &SystemRoleId {
&self.integrator
}
#[must_use]
pub const fn roles(&self) -> &BTreeMap<SystemRoleId, SystemRole> {
&self.roles
}
#[must_use]
pub const fn participants(&self) -> &BTreeMap<ParticipantId, LogicalParticipant> {
&self.participants
}
#[must_use]
pub fn topology(&self) -> &[CollaborationLink] {
&self.topology
}
#[must_use]
pub const fn profiles(&self) -> &BTreeMap<SystemRoleId, RequestedExecutionProfile> {
&self.profiles
}
#[must_use]
pub const fn ceremonies(&self) -> &BTreeMap<SystemCeremonyId, CeremonyComposition> {
&self.ceremonies
}
#[must_use]
pub const fn supervision(&self) -> &SupervisionPolicy {
&self.supervision
}
#[must_use]
pub const fn attention(&self) -> &AttentionPolicy {
&self.attention
}
#[must_use]
pub const fn created_at(&self) -> OffsetDateTime {
self.created_at
}
#[must_use]
pub const fn updated_at(&self) -> OffsetDateTime {
self.updated_at
}
pub fn digest(&self) -> Result<AgenticSystemDigest, DomainError> {
let canonical = serde_json::to_vec(&AgenticSystemContent::of(self)).map_err(|_| {
DomainError::InvariantViolated {
reason: "agentic system cannot be rendered canonically",
}
})?;
Ok(AgenticSystemDigest::of_canonical_form(&canonical))
}
pub fn pin(&self) -> Result<SystemPin, DomainError> {
Ok(SystemPin::new(
self.id.clone(),
self.revision,
self.digest()?,
))
}
#[must_use]
pub fn edited(&self, now: OffsetDateTime) -> Self {
Self {
revision: self.revision.next(),
lifecycle: AgenticSystemLifecycle::Draft,
updated_at: now,
..self.clone()
}
}
#[must_use]
pub fn at_revision(&self, revision: AgenticSystemRevision) -> Self {
Self {
revision,
..self.clone()
}
}
pub fn published(&self, now: OffsetDateTime) -> Result<Self, DomainError> {
match self.lifecycle {
AgenticSystemLifecycle::Draft => Ok(Self {
lifecycle: AgenticSystemLifecycle::Published,
updated_at: now,
..self.clone()
}),
AgenticSystemLifecycle::Published | AgenticSystemLifecycle::Deprecated => {
Err(DomainError::InvalidTransition {
from: self.lifecycle.as_str(),
to: "published",
})
}
}
}
pub fn deprecated(&self, now: OffsetDateTime) -> Result<Self, DomainError> {
match self.lifecycle {
AgenticSystemLifecycle::Published => Ok(Self {
lifecycle: AgenticSystemLifecycle::Deprecated,
updated_at: now,
..self.clone()
}),
AgenticSystemLifecycle::Draft | AgenticSystemLifecycle::Deprecated => {
Err(DomainError::InvalidTransition {
from: self.lifecycle.as_str(),
to: "deprecated",
})
}
}
}
#[must_use]
pub fn blocking_dependencies(&self) -> BTreeMap<SystemCeremonyId, Vec<SystemCeremonyId>> {
dependency_order::blocking(&self.ceremonies)
}
#[must_use]
pub fn root_ceremonies(&self) -> Vec<&CeremonyComposition> {
let blocking = self.blocking_dependencies();
self.ceremonies
.iter()
.filter(|(id, _)| blocking.get(*id).is_none_or(Vec::is_empty))
.map(|(_, composition)| composition)
.collect()
}
}
fn index<Item, Key>(
items: impl IntoIterator<Item = Item>,
key: impl Fn(&Item) -> &Key,
field: &'static str,
) -> Result<BTreeMap<Key, Item>, DomainError>
where
Key: Ord + Clone,
{
let mut indexed = BTreeMap::new();
for item in items {
if indexed.insert(key(&item).clone(), item).is_some() {
return Err(DomainError::AlreadyExists { what: field });
}
}
Ok(indexed)
}