systemprompt-models 0.1.22

Shared data models and types for systemprompt.io OS
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use systemprompt_identifiers::{SkillId, TaskId};

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct StepId(pub String);

impl StepId {
    pub fn new() -> Self {
        Self(uuid::Uuid::new_v4().to_string())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

impl Default for StepId {
    fn default() -> Self {
        Self::new()
    }
}

impl From<String> for StepId {
    fn from(s: String) -> Self {
        Self(s)
    }
}

impl std::fmt::Display for StepId {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.0)
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum StepStatus {
    #[default]
    Pending,
    InProgress,
    Completed,
    Failed,
}

impl std::fmt::Display for StepStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Pending => write!(f, "pending"),
            Self::InProgress => write!(f, "in_progress"),
            Self::Completed => write!(f, "completed"),
            Self::Failed => write!(f, "failed"),
        }
    }
}

impl std::str::FromStr for StepStatus {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "pending" => Ok(Self::Pending),
            "in_progress" | "running" | "active" => Ok(Self::InProgress),
            "completed" | "done" | "success" => Ok(Self::Completed),
            "failed" | "error" => Ok(Self::Failed),
            _ => Err(format!("Invalid step status: {s}")),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "snake_case")]
pub enum StepType {
    #[default]
    Understanding,
    Planning,
    SkillUsage,
    ToolExecution,
    Completion,
}

impl std::fmt::Display for StepType {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Understanding => write!(f, "understanding"),
            Self::Planning => write!(f, "planning"),
            Self::SkillUsage => write!(f, "skill_usage"),
            Self::ToolExecution => write!(f, "tool_execution"),
            Self::Completion => write!(f, "completion"),
        }
    }
}

impl std::str::FromStr for StepType {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "understanding" => Ok(Self::Understanding),
            "planning" => Ok(Self::Planning),
            "skill_usage" => Ok(Self::SkillUsage),
            "tool_execution" | "toolexecution" => Ok(Self::ToolExecution),
            "completion" => Ok(Self::Completion),
            _ => Err(format!("Invalid step type: {s}")),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct PlannedTool {
    pub tool_name: String,
    pub arguments: serde_json::Value,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum StepContent {
    Understanding,
    Planning {
        #[serde(skip_serializing_if = "Option::is_none")]
        reasoning: Option<String>,
        #[serde(skip_serializing_if = "Option::is_none")]
        planned_tools: Option<Vec<PlannedTool>>,
    },
    SkillUsage {
        skill_id: SkillId,
        skill_name: String,
    },
    ToolExecution {
        tool_name: String,
        tool_arguments: serde_json::Value,
        #[serde(skip_serializing_if = "Option::is_none")]
        tool_result: Option<serde_json::Value>,
    },
    Completion,
}

impl StepContent {
    pub const fn understanding() -> Self {
        Self::Understanding
    }

    pub const fn planning(
        reasoning: Option<String>,
        planned_tools: Option<Vec<PlannedTool>>,
    ) -> Self {
        Self::Planning {
            reasoning,
            planned_tools,
        }
    }

    pub fn skill_usage(skill_id: SkillId, skill_name: impl Into<String>) -> Self {
        Self::SkillUsage {
            skill_id,
            skill_name: skill_name.into(),
        }
    }

    pub fn tool_execution(tool_name: impl Into<String>, tool_arguments: serde_json::Value) -> Self {
        Self::ToolExecution {
            tool_name: tool_name.into(),
            tool_arguments,
            tool_result: None,
        }
    }

    pub const fn completion() -> Self {
        Self::Completion
    }

    pub const fn step_type(&self) -> StepType {
        match self {
            Self::Understanding => StepType::Understanding,
            Self::Planning { .. } => StepType::Planning,
            Self::SkillUsage { .. } => StepType::SkillUsage,
            Self::ToolExecution { .. } => StepType::ToolExecution,
            Self::Completion => StepType::Completion,
        }
    }

    pub fn title(&self) -> String {
        match self {
            Self::Understanding => "Analyzing request...".to_string(),
            Self::Planning { .. } => "Planning response...".to_string(),
            Self::SkillUsage { skill_name, .. } => format!("Using {} skill...", skill_name),
            Self::ToolExecution { tool_name, .. } => format!("Running {}...", tool_name),
            Self::Completion => "Complete".to_string(),
        }
    }

    pub const fn is_instant(&self) -> bool {
        !matches!(self, Self::ToolExecution { .. })
    }

    pub fn tool_name(&self) -> Option<&str> {
        match self {
            Self::ToolExecution { tool_name, .. } => Some(tool_name),
            Self::SkillUsage { skill_name, .. } => Some(skill_name),
            Self::Understanding | Self::Planning { .. } | Self::Completion => None,
        }
    }

    pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
        match self {
            Self::ToolExecution { tool_arguments, .. } => Some(tool_arguments),
            Self::Understanding
            | Self::Planning { .. }
            | Self::SkillUsage { .. }
            | Self::Completion => None,
        }
    }

    pub const fn tool_result(&self) -> Option<&serde_json::Value> {
        match self {
            Self::ToolExecution { tool_result, .. } => tool_result.as_ref(),
            Self::Understanding
            | Self::Planning { .. }
            | Self::SkillUsage { .. }
            | Self::Completion => None,
        }
    }

    pub fn reasoning(&self) -> Option<&str> {
        match self {
            Self::Planning { reasoning, .. } => reasoning.as_deref(),
            Self::Understanding
            | Self::SkillUsage { .. }
            | Self::ToolExecution { .. }
            | Self::Completion => None,
        }
    }

    pub fn planned_tools(&self) -> Option<&[PlannedTool]> {
        match self {
            Self::Planning { planned_tools, .. } => planned_tools.as_deref(),
            Self::Understanding
            | Self::SkillUsage { .. }
            | Self::ToolExecution { .. }
            | Self::Completion => None,
        }
    }

    pub fn with_tool_result(self, result: serde_json::Value) -> Self {
        match self {
            Self::ToolExecution {
                tool_name,
                tool_arguments,
                ..
            } => Self::ToolExecution {
                tool_name,
                tool_arguments,
                tool_result: Some(result),
            },
            other @ (Self::Understanding
            | Self::Planning { .. }
            | Self::SkillUsage { .. }
            | Self::Completion) => other,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct ExecutionStep {
    pub step_id: StepId,
    pub task_id: TaskId,
    pub status: StepStatus,
    pub started_at: DateTime<Utc>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub completed_at: Option<DateTime<Utc>>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub duration_ms: Option<i32>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub error_message: Option<String>,
    pub content: StepContent,
}

impl ExecutionStep {
    pub fn new(task_id: TaskId, content: StepContent) -> Self {
        let status = if content.is_instant() {
            StepStatus::Completed
        } else {
            StepStatus::InProgress
        };
        let now = Utc::now();
        let (completed_at, duration_ms) = if content.is_instant() {
            (Some(now), Some(0))
        } else {
            (None, None)
        };

        Self {
            step_id: StepId::new(),
            task_id,
            status,
            started_at: now,
            completed_at,
            duration_ms,
            error_message: None,
            content,
        }
    }

    pub fn understanding(task_id: TaskId) -> Self {
        Self::new(task_id, StepContent::understanding())
    }

    pub fn planning(
        task_id: TaskId,
        reasoning: Option<String>,
        planned_tools: Option<Vec<PlannedTool>>,
    ) -> Self {
        Self::new(task_id, StepContent::planning(reasoning, planned_tools))
    }

    pub fn skill_usage(task_id: TaskId, skill_id: SkillId, skill_name: impl Into<String>) -> Self {
        Self::new(task_id, StepContent::skill_usage(skill_id, skill_name))
    }

    pub fn tool_execution(
        task_id: TaskId,
        tool_name: impl Into<String>,
        tool_arguments: serde_json::Value,
    ) -> Self {
        Self::new(
            task_id,
            StepContent::tool_execution(tool_name, tool_arguments),
        )
    }

    pub fn completion(task_id: TaskId) -> Self {
        Self::new(task_id, StepContent::completion())
    }

    pub const fn step_type(&self) -> StepType {
        self.content.step_type()
    }

    pub fn title(&self) -> String {
        self.content.title()
    }

    pub fn tool_name(&self) -> Option<&str> {
        self.content.tool_name()
    }

    pub const fn tool_arguments(&self) -> Option<&serde_json::Value> {
        self.content.tool_arguments()
    }

    pub const fn tool_result(&self) -> Option<&serde_json::Value> {
        self.content.tool_result()
    }

    pub fn reasoning(&self) -> Option<&str> {
        self.content.reasoning()
    }

    pub fn complete(&mut self, result: Option<serde_json::Value>) {
        let now = Utc::now();
        self.status = StepStatus::Completed;
        self.completed_at = Some(now);
        let duration = (now - self.started_at).num_milliseconds();
        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
        if let Some(r) = result {
            self.content = self.content.clone().with_tool_result(r);
        }
    }

    pub fn fail(&mut self, error: String) {
        let now = Utc::now();
        self.status = StepStatus::Failed;
        self.completed_at = Some(now);
        let duration = (now - self.started_at).num_milliseconds();
        self.duration_ms = Some(i32::try_from(duration).unwrap_or(i32::MAX));
        self.error_message = Some(error);
    }
}

#[derive(Debug, Clone)]
pub struct TrackedStep {
    pub step_id: StepId,
    pub started_at: DateTime<Utc>,
}

pub type StepDetail = StepContent;