use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActorType {
Human,
Agent,
}
impl ActorType {
pub fn as_str(&self) -> &'static str {
match self {
Self::Human => "human",
Self::Agent => "agent",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"human" => Some(Self::Human),
"agent" => Some(Self::Agent),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ActorStatus {
Active,
Frozen,
}
impl ActorStatus {
pub fn as_str(&self) -> &'static str {
match self {
Self::Active => "active",
Self::Frozen => "frozen",
}
}
pub fn parse(s: &str) -> Option<Self> {
match s {
"active" => Some(Self::Active),
"frozen" => Some(Self::Frozen),
_ => None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WritableTarget {
pub target: String,
pub actions: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ActorRecord {
pub actor_id: String,
pub actor_type: ActorType,
pub creator_id: Option<String>,
pub lineage: Vec<String>,
pub purpose: Option<String>,
pub status: ActorStatus,
pub writable_targets: Vec<WritableTarget>,
pub energy_share: f64,
pub reduction_policy: String,
pub created_at: String,
pub updated_at: String,
}
#[derive(Debug, Clone)]
pub struct CreateActorSpec {
pub actor_id: String,
pub actor_type: ActorType,
pub creator_id: String,
pub lineage: Vec<String>,
pub purpose: Option<String>,
pub writable_targets: Vec<WritableTarget>,
pub energy_balance: i64,
pub energy_share: f64,
pub reduction_policy: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LifecycleOp {
Freeze { reason: Option<String> },
Unfreeze,
Terminate { reason: Option<String> },
UpdateEnergyShare { energy_share: f64 },
}
pub fn derive_agent_id(creator_id: &str, purpose: &str, seq: i64) -> String {
let sanitized = sanitize_purpose(purpose);
format!("{creator_id}/{sanitized}/{seq}")
}
fn sanitize_purpose(purpose: &str) -> String {
let s: String = purpose
.chars()
.map(|c| {
if c.is_ascii_alphanumeric() || c == '-' {
c.to_ascii_lowercase()
} else {
'-'
}
})
.collect();
let mut result = String::with_capacity(s.len());
let mut prev_hyphen = false;
for c in s.chars() {
if c == '-' {
if !prev_hyphen {
result.push(c);
}
prev_hyphen = true;
} else {
result.push(c);
prev_hyphen = false;
}
}
result.trim_matches('-').to_string()
}
pub fn build_lineage(
creator_type: &ActorType,
creator_id: &str,
_creator_lineage: &[String],
) -> Vec<String> {
match creator_type {
ActorType::Human => vec![creator_id.to_string()],
ActorType::Agent => {
vec![creator_id.to_string()]
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn derive_agent_id_basic() {
assert_eq!(
derive_agent_id("root", "doc-organizer", 1),
"root/doc-organizer/1"
);
}
#[test]
fn derive_agent_id_second_agent() {
assert_eq!(
derive_agent_id("root", "tiktok-downloader", 1),
"root/tiktok-downloader/1"
);
}
#[test]
fn derive_agent_id_sanitizes_spaces() {
assert_eq!(
derive_agent_id("root", "My Cool Agent", 1),
"root/my-cool-agent/1"
);
}
#[test]
fn build_lineage_human_creator() {
let lineage = build_lineage(&ActorType::Human, "root", &[]);
assert_eq!(lineage, vec!["root"]);
}
#[test]
fn build_lineage_agent_creator_fallback() {
let creator_lineage = vec!["root".to_string()];
let lineage = build_lineage(&ActorType::Agent, "root/worker/1", &creator_lineage);
assert_eq!(lineage, vec!["root/worker/1"]);
}
#[test]
fn actor_type_roundtrip() {
assert_eq!(ActorType::parse("human"), Some(ActorType::Human));
assert_eq!(ActorType::parse("agent"), Some(ActorType::Agent));
assert_eq!(ActorType::parse("unknown"), None);
}
#[test]
fn actor_status_roundtrip() {
assert_eq!(ActorStatus::parse("active"), Some(ActorStatus::Active));
assert_eq!(ActorStatus::parse("frozen"), Some(ActorStatus::Frozen));
}
}