1use std::collections::BTreeMap;
20
21use serde::{Deserialize, Serialize};
22use time::OffsetDateTime;
23
24use crate::error::DomainError;
25use crate::value_objects::{
26 AgenticSystemDigest, AgenticSystemId, AgenticSystemLifecycle, AgenticSystemRevision,
27 AttentionPolicy, CeremonyComposition, CollaborationLink, LogicalParticipant, ParticipantId,
28 RequestedExecutionProfile, SupervisionPolicy, SystemCeremonyId, SystemPin, SystemPurpose,
29 SystemRole, SystemRoleId,
30};
31
32mod agentic_system_content;
33mod agentic_system_parts;
34mod dependency_order;
35
36use agentic_system_content::AgenticSystemContent;
37
38pub use agentic_system_parts::AgenticSystemParts;
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
42pub struct AgenticSystem {
43 id: AgenticSystemId,
44 revision: AgenticSystemRevision,
45 lifecycle: AgenticSystemLifecycle,
46 purpose: SystemPurpose,
47 integrator: SystemRoleId,
48 roles: BTreeMap<SystemRoleId, SystemRole>,
49 participants: BTreeMap<ParticipantId, LogicalParticipant>,
50 topology: Vec<CollaborationLink>,
51 profiles: BTreeMap<SystemRoleId, RequestedExecutionProfile>,
52 ceremonies: BTreeMap<SystemCeremonyId, CeremonyComposition>,
53 #[serde(default)]
54 supervision: SupervisionPolicy,
55 attention: AttentionPolicy,
56 #[serde(with = "time::serde::rfc3339")]
57 created_at: OffsetDateTime,
58 #[serde(with = "time::serde::rfc3339")]
59 updated_at: OffsetDateTime,
60}
61
62impl AgenticSystem {
63 #[allow(clippy::too_many_arguments)]
72 pub fn draft(
73 id: AgenticSystemId,
74 purpose: SystemPurpose,
75 integrator: SystemRoleId,
76 roles: impl IntoIterator<Item = SystemRole>,
77 participants: impl IntoIterator<Item = LogicalParticipant>,
78 topology: impl IntoIterator<Item = CollaborationLink>,
79 profiles: impl IntoIterator<Item = (SystemRoleId, RequestedExecutionProfile)>,
80 ceremonies: impl IntoIterator<Item = CeremonyComposition>,
81 supervision: SupervisionPolicy,
82 attention: AttentionPolicy,
83 now: OffsetDateTime,
84 ) -> Result<Self, DomainError> {
85 let roles = index(roles, SystemRole::id, "agentic_system.roles")?;
86 let participants = index(
87 participants,
88 LogicalParticipant::id,
89 "agentic_system.participants",
90 )?;
91 let ceremonies = index(
92 ceremonies,
93 CeremonyComposition::id,
94 "agentic_system.ceremonies",
95 )?;
96 if roles.is_empty() {
97 return Err(DomainError::EmptyCollection {
98 field: "agentic_system.roles",
99 });
100 }
101 if ceremonies.is_empty() {
102 return Err(DomainError::EmptyCollection {
103 field: "agentic_system.ceremonies",
104 });
105 }
106 Ok(Self {
107 id,
108 revision: AgenticSystemRevision::INITIAL,
109 lifecycle: AgenticSystemLifecycle::Draft,
110 purpose,
111 integrator,
112 roles,
113 participants,
114 topology: topology.into_iter().collect(),
115 profiles: profiles.into_iter().collect(),
116 ceremonies,
117 supervision,
118 attention,
119 created_at: now,
120 updated_at: now,
121 })
122 }
123
124 #[must_use]
125 pub const fn id(&self) -> &AgenticSystemId {
126 &self.id
127 }
128
129 #[must_use]
130 pub const fn revision(&self) -> AgenticSystemRevision {
131 self.revision
132 }
133
134 #[must_use]
135 pub const fn lifecycle(&self) -> AgenticSystemLifecycle {
136 self.lifecycle
137 }
138
139 #[must_use]
140 pub const fn purpose(&self) -> &SystemPurpose {
141 &self.purpose
142 }
143
144 #[must_use]
146 pub const fn integrator(&self) -> &SystemRoleId {
147 &self.integrator
148 }
149
150 #[must_use]
151 pub const fn roles(&self) -> &BTreeMap<SystemRoleId, SystemRole> {
152 &self.roles
153 }
154
155 #[must_use]
156 pub const fn participants(&self) -> &BTreeMap<ParticipantId, LogicalParticipant> {
157 &self.participants
158 }
159
160 #[must_use]
161 pub fn topology(&self) -> &[CollaborationLink] {
162 &self.topology
163 }
164
165 #[must_use]
166 pub const fn profiles(&self) -> &BTreeMap<SystemRoleId, RequestedExecutionProfile> {
167 &self.profiles
168 }
169
170 #[must_use]
171 pub const fn ceremonies(&self) -> &BTreeMap<SystemCeremonyId, CeremonyComposition> {
172 &self.ceremonies
173 }
174
175 #[must_use]
176 pub const fn supervision(&self) -> &SupervisionPolicy {
177 &self.supervision
178 }
179
180 #[must_use]
182 pub const fn attention(&self) -> &AttentionPolicy {
183 &self.attention
184 }
185
186 #[must_use]
187 pub const fn created_at(&self) -> OffsetDateTime {
188 self.created_at
189 }
190
191 #[must_use]
192 pub const fn updated_at(&self) -> OffsetDateTime {
193 self.updated_at
194 }
195
196 pub fn digest(&self) -> Result<AgenticSystemDigest, DomainError> {
198 let canonical = serde_json::to_vec(&AgenticSystemContent::of(self)).map_err(|_| {
199 DomainError::InvariantViolated {
200 reason: "agentic system cannot be rendered canonically",
201 }
202 })?;
203 Ok(AgenticSystemDigest::of_canonical_form(&canonical))
204 }
205
206 pub fn pin(&self) -> Result<SystemPin, DomainError> {
208 Ok(SystemPin::new(
209 self.id.clone(),
210 self.revision,
211 self.digest()?,
212 ))
213 }
214
215 #[must_use]
222 pub fn edited(&self, now: OffsetDateTime) -> Self {
223 Self {
224 revision: self.revision.next(),
225 lifecycle: AgenticSystemLifecycle::Draft,
226 updated_at: now,
227 ..self.clone()
228 }
229 }
230
231 #[must_use]
233 pub fn at_revision(&self, revision: AgenticSystemRevision) -> Self {
234 Self {
235 revision,
236 ..self.clone()
237 }
238 }
239
240 pub fn published(&self, now: OffsetDateTime) -> Result<Self, DomainError> {
246 match self.lifecycle {
247 AgenticSystemLifecycle::Draft => Ok(Self {
248 lifecycle: AgenticSystemLifecycle::Published,
249 updated_at: now,
250 ..self.clone()
251 }),
252 AgenticSystemLifecycle::Published | AgenticSystemLifecycle::Deprecated => {
253 Err(DomainError::InvalidTransition {
254 from: self.lifecycle.as_str(),
255 to: "published",
256 })
257 }
258 }
259 }
260
261 pub fn deprecated(&self, now: OffsetDateTime) -> Result<Self, DomainError> {
264 match self.lifecycle {
265 AgenticSystemLifecycle::Published => Ok(Self {
266 lifecycle: AgenticSystemLifecycle::Deprecated,
267 updated_at: now,
268 ..self.clone()
269 }),
270 AgenticSystemLifecycle::Draft | AgenticSystemLifecycle::Deprecated => {
271 Err(DomainError::InvalidTransition {
272 from: self.lifecycle.as_str(),
273 to: "deprecated",
274 })
275 }
276 }
277 }
278
279 #[must_use]
285 pub fn blocking_dependencies(&self) -> BTreeMap<SystemCeremonyId, Vec<SystemCeremonyId>> {
286 dependency_order::blocking(&self.ceremonies)
287 }
288
289 #[must_use]
295 pub fn root_ceremonies(&self) -> Vec<&CeremonyComposition> {
296 let blocking = self.blocking_dependencies();
297 self.ceremonies
298 .iter()
299 .filter(|(id, _)| blocking.get(*id).is_none_or(Vec::is_empty))
300 .map(|(_, composition)| composition)
301 .collect()
302 }
303}
304
305fn index<Item, Key>(
306 items: impl IntoIterator<Item = Item>,
307 key: impl Fn(&Item) -> &Key,
308 field: &'static str,
309) -> Result<BTreeMap<Key, Item>, DomainError>
310where
311 Key: Ord + Clone,
312{
313 let mut indexed = BTreeMap::new();
314 for item in items {
315 if indexed.insert(key(&item).clone(), item).is_some() {
316 return Err(DomainError::AlreadyExists { what: field });
317 }
318 }
319 Ok(indexed)
320}