use crate::agents::config::AgentsConfig;
use crate::agents::error::AgentError;
use crate::agents::records::{AgentEvent, AgentRecord, AgentTransition};
use crate::config::templates::is_segment;
use lys_pass::drafts::Execution;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
#[derive(Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Land {
pub(super) version: u8,
pub(super) name: String,
pub(super) owner: String,
pub(super) transition: AgentTransition,
pub(super) receipt: Option<Receipt>,
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub(super) struct Receipt {
pub(super) draft: String,
pub(super) execution: Execution,
}
#[derive(Default)]
pub(super) struct Index {
pub(super) next: u64,
pub(super) records: BTreeMap<String, AgentRecord>,
pub(super) receipts: BTreeMap<String, Execution>,
pub(super) uncertain: bool,
}
pub(super) const fn action(change: &AgentEvent) -> Option<&'static str> {
match change {
AgentEvent::Create(_) => Some("create_agent"),
AgentEvent::Start => Some("start"),
AgentEvent::Stop => Some("stop"),
AgentEvent::Retire => Some("retire"),
_ => None,
}
}
pub(super) fn apply(index: &mut Index, land: Land) -> Result<(), AgentError> {
if let AgentEvent::Create(spec) = &land.transition.change {
if index.records.contains_key(&land.name) {
return Err(AgentError::Invalid("duplicate stored name"));
}
index.records.insert(
land.name.clone(),
AgentRecord {
name: land.name.clone(),
harness: spec.harness,
purpose: spec.purpose.clone(),
owner: land.owner.clone(),
service: spec.service.clone(),
created_at_ms: land.transition.at_ms,
transitions: Vec::new(),
},
);
}
let record = index
.records
.get_mut(&land.name)
.ok_or(AgentError::Invalid("event precedes creation"))?;
if record.owner != land.owner {
return Err(AgentError::Invalid("stored event changes its owner"));
}
if !record.active() {
return Err(AgentError::Invalid("stored event follows retirement"));
}
record.transitions.push(land.transition);
if let Some(receipt) = land.receipt {
if index
.receipts
.insert(receipt.draft, receipt.execution)
.is_some()
{
return Err(AgentError::Invalid("duplicate stored draft"));
}
}
index.next = index
.next
.checked_add(1)
.ok_or(AgentError::Invalid("history sequence overflow"))?;
Ok(())
}
pub(super) fn validate_act(
bounds: &AgentsConfig,
name: &str,
transition: &AgentTransition,
) -> Result<(), AgentError> {
bounds.field(name, "name")?;
if !is_segment(name) {
return Err(AgentError::Invalid("name is not one segment"));
}
bounds.field(&transition.holder, "holder")?;
match &transition.change {
AgentEvent::Create(spec) => {
bounds.field(&spec.purpose, "purpose")?;
if spec.purpose.lines().count() != 1 {
return Err(AgentError::Invalid("purpose is one sentence"));
}
if let Some(service) = &spec.service {
bounds.field(service, "service")?;
}
}
AgentEvent::Gone { reason, .. } => {
bounds.field(reason, "departure reason")?;
}
_ => {}
}
Ok(())
}
pub(super) const fn check_bytes(bounds: &AgentsConfig, count: usize) -> Result<(), AgentError> {
if count > bounds.max_event_bytes {
return Err(AgentError::Bound {
bound: "max_event_bytes",
limit: bounds.max_event_bytes,
count,
});
}
Ok(())
}
pub(super) fn store_error(error: impl std::fmt::Display) -> AgentError {
AgentError::Store(error.to_string())
}
pub(super) fn check_population(
index: &Index,
bounds: &AgentsConfig,
holder: &str,
) -> Result<(), AgentError> {
let active = index
.records
.values()
.filter(|record| record.active())
.count();
let owned = index
.records
.values()
.filter(|record| record.active() && record.owner == holder)
.count();
for (bound, count, limit) in [
("max_per_owner", owned, bounds.max_per_owner),
("max_per_exchange", active, bounds.max_per_exchange),
] {
if count >= limit {
return Err(AgentError::Bound {
bound,
limit,
count,
});
}
}
Ok(())
}
pub(super) fn refusal_name(refusal: &str) -> String {
if !refusal.is_empty()
&& refusal
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
{
refusal.into()
} else {
"agent_placement_refused".into()
}
}