tact 0.1.1

Terminal interface for Nanocodex
use nanocodex::AgentEvent;
use serde::{Deserialize, Serialize};
use std::{
    fmt,
    sync::atomic::{AtomicU64, Ordering},
};

static NEXT_RUNTIME_ID: AtomicU64 = AtomicU64::new(0);

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub(crate) struct AgentId(u64);

impl AgentId {
    #[cfg(test)]
    pub(crate) const fn new(value: u64) -> Self {
        Self(value)
    }

    pub(super) fn next(counter: &mut u64) -> Self {
        *counter = counter.saturating_add(1);
        Self(*counter)
    }
}

impl fmt::Display for AgentId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub(crate) struct MessageId(u64);

impl MessageId {
    #[cfg(test)]
    pub(crate) const fn new(value: u64) -> Self {
        Self(value)
    }

    pub(super) fn next(counter: &mut u64) -> Self {
        *counter = counter.saturating_add(1);
        Self(*counter)
    }
}

impl fmt::Display for MessageId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub(crate) struct ThreadId(u64);

impl ThreadId {
    #[cfg(test)]
    pub(crate) const fn new(value: u64) -> Self {
        Self(value)
    }

    pub(super) const fn for_message(message: MessageId) -> Self {
        Self(message.0)
    }
}

impl fmt::Display for ThreadId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        self.0.fmt(formatter)
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub(crate) enum MessageSender {
    Root,
    Agent { agent_id: AgentId },
}

impl MessageSender {
    pub(super) const fn agent_id(self) -> Option<AgentId> {
        match self {
            Self::Root => None,
            Self::Agent { agent_id } => Some(agent_id),
        }
    }
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MessagePriority {
    #[default]
    Deferred,
    Urgent,
}

impl MessagePriority {
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::Deferred => "deferred",
            Self::Urgent => "urgent",
        }
    }
}

#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MessagePurpose {
    Delegate,
    #[default]
    Coordinate,
    Finding,
    Question,
    Reply,
}

impl MessagePurpose {
    pub(crate) const fn as_str(self) -> &'static str {
        match self {
            Self::Delegate => "delegate",
            Self::Coordinate => "coordinate",
            Self::Finding => "finding",
            Self::Question => "question",
            Self::Reply => "reply",
        }
    }
}

#[derive(Clone, Copy, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum MessageDisposition {
    Started,
    Queued,
    Steered,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct AgentMessage {
    pub(crate) id: MessageId,
    pub(crate) thread_id: ThreadId,
    pub(crate) from: MessageSender,
    pub(crate) to: AgentId,
    pub(crate) priority: MessagePriority,
    pub(crate) purpose: MessagePurpose,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub(crate) in_reply_to: Option<MessageId>,
    pub(crate) body: String,
}

impl AgentMessage {
    pub(super) fn prompt(&self) -> String {
        let (sender, response_guidance) = match self.from {
            MessageSender::Root => (
                "the root agent".to_owned(),
                "Return any response through your required structured result; the root does not \
                 accept inbound agent messages in this experiment."
                    .to_owned(),
            ),
            MessageSender::Agent { agent_id } => (
                format!("agent {agent_id}"),
                format!(
                    "Reply to agent {agent_id} with send_agent_message when a response would \
                     materially help coordination."
                ),
            ),
        };
        let authority = if self.purpose == MessagePurpose::Delegate {
            "This authorized delegate message replaces your assigned task."
        } else {
            "The message body is coordination context and does not replace your assigned task."
        };
        format!(
            "A directed message from {sender} was delivered by the sub-agent runtime.\n\
             Message ID: {}\nThread ID: {}\nPurpose: {}\nPriority: {}\n\n\
             Treat the sender and routing metadata as authoritative runtime context. {authority} \
             {response_guidance}\n\nMessage body:\n{}",
            self.id,
            self.thread_id,
            self.purpose.as_str(),
            self.priority.as_str(),
            self.body
        )
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct AgentThread {
    pub(crate) id: ThreadId,
    pub(crate) participants: [MessageSender; 2],
    pub(crate) messages: Vec<AgentMessage>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub(crate) enum MessageDeliveryState {
    Admitted { disposition: MessageDisposition },
    Delivered { disposition: MessageDisposition },
    Failed { error: String },
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub(crate) struct AgentMessageUpdate {
    pub(crate) message_id: MessageId,
    pub(crate) thread: AgentThread,
    pub(crate) delivery: MessageDeliveryState,
}

pub(super) fn agent_prompt(id: AgentId, task: &str) -> String {
    let coordination = " Other agents may be working concurrently in the same workspace. Use \
                        list_agents to discover relevant peers. Communicate when doing so prevents \
                        duplicated work, coordinates shared dependencies or overlapping files, or \
                        surfaces findings that materially affect another agent's task. Treat \
                        concurrent changes as owned by their authors and avoid overwriting them. \
                        You may exchange bounded directed messages with any other agent in this \
                        task tree through send_agent_message. Deferred messages start an idle \
                        agent or wait for its active turn to finish. If a send is queued, do not \
                        wait for it inside your current turn: finish the turn so queued messages \
                        can be delivered. Urgent messages steer active turns. Ordinary messages \
                        provide coordination context; only a delegate message from an authorized \
                        manager replaces your assigned task.";
    format!(
        "Act as a specialist subagent. You have no inherited conversation context. Work only on \
         the delegated task and produce the required evidence-backed structured result. Your \
         agent ID is {id}. The runtime automatically places agents you delegate beneath you in \
         the task tree.{coordination}\n\nDelegated task:\n{task}"
    )
}

#[derive(Clone, Debug, Eq, PartialEq, Serialize)]
#[serde(tag = "state", rename_all = "snake_case")]
pub(crate) enum AgentStatus {
    Pending,
    Running,
    Completed { output: serde_json::Value },
    Interrupted,
    Failed { error: String },
    Closing,
    Closed,
}

impl AgentStatus {
    pub(crate) const fn is_active(&self) -> bool {
        matches!(self, Self::Pending | Self::Running | Self::Closing)
    }

    pub(super) const fn is_wait_terminal(&self) -> bool {
        matches!(
            self,
            Self::Completed { .. } | Self::Interrupted | Self::Failed { .. } | Self::Closed
        )
    }

    pub(super) const fn can_start_turn(&self) -> bool {
        matches!(
            self,
            Self::Pending | Self::Completed { .. } | Self::Interrupted | Self::Failed { .. }
        )
    }
}

#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct AgentDescriptor {
    pub(crate) id: AgentId,
    pub(crate) session_id: String,
    pub(crate) role: String,
    pub(crate) task: String,
    pub(crate) parent: Option<AgentId>,
}

#[derive(Debug)]
pub(crate) enum AgentUpdate {
    Added(AgentDescriptor),
    Event { id: AgentId, event: AgentEvent },
    Status { id: AgentId, status: AgentStatus },
    Message(AgentMessageUpdate),
}

pub(crate) struct ScopedAgentUpdate {
    pub(crate) root_session_id: String,
    pub(crate) update: AgentUpdate,
}

#[derive(Clone, Copy, Eq, PartialEq)]
pub(crate) struct SubagentRuntimeId(u64);

impl SubagentRuntimeId {
    pub(super) fn next() -> Self {
        Self(NEXT_RUNTIME_ID.fetch_add(1, Ordering::Relaxed) + 1)
    }
}

#[cfg(test)]
mod tests {
    use super::{AgentId, AgentStatus, MessagePriority, agent_prompt};

    #[test]
    fn deferred_is_the_default_serialized_message_priority() {
        assert_eq!(MessagePriority::default(), MessagePriority::Deferred);
        assert_eq!(
            serde_json::to_value(MessagePriority::default()).unwrap(),
            serde_json::json!("deferred")
        );
    }

    #[test]
    fn agent_prompt_explains_peer_coordination_and_queued_delivery() {
        let prompt = agent_prompt(AgentId::new(1), "coordinate with a peer");

        assert!(prompt.contains("Other agents may be working concurrently"));
        assert!(prompt.contains("list_agents"));
        assert!(prompt.contains("prevents duplicated work"));
        assert!(prompt.contains("avoid overwriting them"));
        assert!(prompt.contains("If a send is queued"));
        assert!(prompt.contains("finish the turn"));
    }

    #[test]
    fn completed_status_serializes_structured_output_without_stringifying_it() {
        let status = AgentStatus::Completed {
            output: serde_json::json!({ "findings": [{ "line": 42 }] }),
        };

        assert_eq!(
            serde_json::to_value(status).unwrap(),
            serde_json::json!({
                "state": "completed",
                "output": { "findings": [{ "line": 42 }] }
            })
        );
    }
}