liminal-server 0.25.4

Standalone server for the liminal messaging bus
Documentation
//! Durable agent metadata. Debug deliberately excludes all free text.

use serde::{Deserialize, Serialize};

/// A harness label, never a selector of server behaviour.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum AgentHarness {
    /// Codex with channel wake.
    #[serde(rename = "cdx")]
    Cdx,
    /// Claude Code.
    #[serde(rename = "claude-code")]
    ClaudeCode,
    /// Codex through the ACP host.
    #[serde(rename = "codex")]
    Codex,
    /// Gemini CLI through the ACP host.
    #[serde(rename = "gemini-cli")]
    GeminiCli,
    /// `OpenCode` through the ACP host.
    #[serde(rename = "opencode")]
    Opencode,
    /// Pi through its separately launched host.
    #[serde(rename = "pi")]
    Pi,
}

/// Fields a person approves when creating an agent.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentSpec {
    /// Its declared harness.
    pub harness: AgentHarness,
    /// One sentence stating its job.
    pub purpose: String,
    /// The service it builds, when it is a builder.
    pub service: Option<String>,
}

/// Events retained forever in the exchange's store.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(
    tag = "event",
    content = "detail",
    rename_all = "snake_case",
    deny_unknown_fields
)]
pub enum AgentEvent {
    /// A person approved a new station.
    Create(AgentSpec),
    /// Lys answered the deliberate start.
    Start,
    /// Lys answered the deliberate stop.
    Stop,
    /// A verified ticket subscribed its inbox.
    Present {
        /// The connection's process id.
        connection: u64,
    },
    /// That connection closed with this stable transport or disconnect name.
    Gone {
        /// The closed connection.
        connection: u64,
        /// The stable reason, never a transport's payload.
        reason: String,
    },
    /// The host negotiated this session; old histories have no such event.
    Session {
        session: String,
        protocol_version: u8,
        load_session: bool,
    },
    /// An observed host ending never invents resume capability.
    SessionClosed {
        exit_status: i32,
        session_state: String,
    },
    /// The station is permanently retired; its history remains.
    Retire,
}

/// A transition always names its actor and time.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentTransition {
    /// The actor proved by the pass, or the server for observed departures.
    pub holder: String,
    /// Epoch milliseconds of this transition.
    pub at_ms: u64,
    /// What changed.
    pub change: AgentEvent,
}

/// One station, as returned after restoration.
#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentRecord {
    /// One segment, exactly the Lys agent holder id.
    pub name: String,
    /// The profile's harness label.
    pub harness: AgentHarness,
    /// One sentence stating its job.
    pub purpose: String,
    /// The person who created it.
    pub owner: String,
    /// The service it builds.
    pub service: Option<String>,
    /// Epoch milliseconds of creation.
    pub created_at_ms: u64,
    /// Every transition, oldest first; retirement never removes one.
    pub transitions: Vec<AgentTransition>,
}

impl AgentRecord {
    /// Whether this station still occupies a configured slot.
    #[must_use]
    pub fn active(&self) -> bool {
        self.transitions
            .last()
            .is_none_or(|transition| !matches!(transition.change, AgentEvent::Retire))
    }

    /// Its last observed connection; lifecycle requests do not invent presence.
    #[must_use]
    pub fn connection(&self) -> Option<u64> {
        self.seat().map(|(connection, _)| connection)
    }

    /// The present connection and the epoch milliseconds it was admitted.
    #[must_use]
    pub fn seat(&self) -> Option<(u64, u64)> {
        self.transitions
            .iter()
            .rev()
            .find_map(|transition| match transition.change {
                AgentEvent::Present { connection } => Some(Some((connection, transition.at_ms))),
                AgentEvent::Gone { .. } | AgentEvent::Retire => Some(None),
                _ => None,
            })
            .flatten()
    }
}

impl std::fmt::Debug for AgentRecord {
    fn fmt(&self, out: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        out.debug_struct("AgentRecord")
            .field("harness", &self.harness)
            .field("transitions", &self.transitions.len())
            .finish_non_exhaustive()
    }
}

/// Immutable prepared words, supplied only by the approved-draft executor.
#[derive(Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AgentAct {
    /// Name of the placed agent.
    pub name: String,
    /// The original verified holder, kept in the immutable draft.
    pub holder: String,
    /// The caller's responsible person, proved by Lys; never a request override.
    pub owner: String,
    /// Preparation time, in epoch milliseconds.
    pub at_ms: u64,
    /// The deliberate event.
    pub change: AgentEvent,
}

impl AgentAct {
    /// Prepares immutable words from verified claims. An agent's builders
    /// count against its responsible person, so changing creator agent ids
    /// cannot evade the person's configured bound.
    ///
    /// # Errors
    /// The verified holder is neither a person nor an agent with a responsible person.
    pub fn prepare(
        name: String,
        change: AgentEvent,
        pass: &lys_pass::VerifiedPass,
        at_ms: u64,
    ) -> Result<Self, super::error::AgentError> {
        let holder = &pass.claims().holder;
        let owner = match holder.kind.as_str() {
            "person" => holder.id.as_str(),
            "agent" => holder
                .responsible
                .as_deref()
                .ok_or(super::error::AgentError::Invalid(
                    "agent responsible person",
                ))?,
            _ => {
                return Err(super::error::AgentError::Invalid(
                    "agent creation holder kind",
                ));
            }
        };
        if owner.is_empty() {
            return Err(super::error::AgentError::Invalid(
                "agent responsible person",
            ));
        }
        Ok(Self {
            name,
            holder: holder.id.clone(),
            owner: owner.into(),
            at_ms,
            change,
        })
    }
}