Skip to main content

pulse_system_types/
lib.rs

1//! Shared contract types for the pulse-null plugin ecosystem.
2//!
3//! This crate defines the types that plugin crates and pulse-null both depend on.
4//! It contains no logic — just structs, enums, and trait-adjacent types that form
5//! the contract between the orchestrator and its plugins.
6
7pub mod llm;
8pub mod monitoring;
9pub mod plugin;
10pub mod tool;
11
12use serde::{Deserialize, Serialize};
13use std::fmt;
14
15/// Plugin health status, returned by plugin `health()` methods.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17#[serde(tag = "status", content = "message")]
18pub enum HealthStatus {
19    Healthy,
20    Degraded(String),
21    Down(String),
22}
23
24impl fmt::Display for HealthStatus {
25    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26        match self {
27            Self::Healthy => write!(f, "healthy"),
28            Self::Degraded(msg) => write!(f, "degraded: {msg}"),
29            Self::Down(msg) => write!(f, "down: {msg}"),
30        }
31    }
32}
33
34/// Plugin metadata — identity and version.
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct PluginMeta {
37    pub name: String,
38    pub version: String,
39    pub description: String,
40}
41
42/// A setup prompt for the init wizard.
43///
44/// Plugins return a list of these to tell pulse-null what configuration
45/// values they need during first-time setup.
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct SetupPrompt {
48    /// Config key (e.g. "twilio_account_sid")
49    pub key: String,
50    /// Human-readable question (e.g. "Twilio Account SID:")
51    pub question: String,
52    /// Default value if the user presses enter
53    pub default: Option<String>,
54    /// Whether a value is required
55    pub required: bool,
56    /// Whether the value is sensitive (mask input)
57    pub secret: bool,
58}
59
60/// A scheduled task definition that plugins can contribute.
61#[derive(Debug, Clone, Serialize, Deserialize)]
62pub struct ScheduledTask {
63    pub id: String,
64    pub name: String,
65    /// Cron expression (6-field: sec min hour dom month dow)
66    pub cron: String,
67    /// Trust channel for the session
68    pub channel: String,
69    /// Prompt sent to the LLM
70    pub prompt: String,
71    /// How to route the output
72    #[serde(default)]
73    pub output_routing: OutputRouting,
74    /// Whether this task is active
75    #[serde(default = "default_true")]
76    pub enabled: bool,
77    /// Who created this task
78    #[serde(default)]
79    pub created_by: TaskCreator,
80}
81
82fn default_true() -> bool {
83    true
84}
85
86/// How to route a scheduled task's output.
87#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
88#[serde(rename_all = "lowercase")]
89pub enum OutputRouting {
90    #[default]
91    Silent,
92    Share,
93    Call,
94}
95
96/// Who created the task.
97#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
98#[serde(rename_all = "lowercase")]
99pub enum TaskCreator {
100    #[default]
101    System,
102    Entity,
103    User,
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn health_status_display() {
112        assert_eq!(HealthStatus::Healthy.to_string(), "healthy");
113        assert_eq!(
114            HealthStatus::Degraded("high latency".into()).to_string(),
115            "degraded: high latency"
116        );
117        assert_eq!(
118            HealthStatus::Down("connection refused".into()).to_string(),
119            "down: connection refused"
120        );
121    }
122
123    #[test]
124    fn health_status_serializes() {
125        let json = serde_json::to_string(&HealthStatus::Healthy).unwrap();
126        assert!(json.contains("Healthy"));
127
128        let json = serde_json::to_string(&HealthStatus::Degraded("slow".into())).unwrap();
129        assert!(json.contains("Degraded"));
130        assert!(json.contains("slow"));
131    }
132
133    #[test]
134    fn plugin_meta_roundtrip() {
135        let meta = PluginMeta {
136            name: "voice-echo".into(),
137            version: "0.7.0".into(),
138            description: "Phone calls via Twilio".into(),
139        };
140        let json = serde_json::to_string(&meta).unwrap();
141        let back: PluginMeta = serde_json::from_str(&json).unwrap();
142        assert_eq!(back.name, "voice-echo");
143        assert_eq!(back.version, "0.7.0");
144    }
145
146    #[test]
147    fn setup_prompt_with_defaults() {
148        let prompt = SetupPrompt {
149            key: "host".into(),
150            question: "Bind address:".into(),
151            default: Some("0.0.0.0".into()),
152            required: false,
153            secret: false,
154        };
155        assert_eq!(prompt.default.as_deref(), Some("0.0.0.0"));
156        assert!(!prompt.secret);
157    }
158
159    #[test]
160    fn setup_prompt_secret() {
161        let prompt = SetupPrompt {
162            key: "api_key".into(),
163            question: "API Key:".into(),
164            default: None,
165            required: true,
166            secret: true,
167        };
168        assert!(prompt.secret);
169        assert!(prompt.required);
170        assert!(prompt.default.is_none());
171    }
172
173    #[test]
174    fn scheduled_task_defaults() {
175        let json = r#"{
176            "id": "morning",
177            "name": "Morning check",
178            "cron": "0 0 8 * * *",
179            "channel": "reflection",
180            "prompt": "Good morning."
181        }"#;
182        let task: ScheduledTask = serde_json::from_str(json).unwrap();
183        assert_eq!(task.output_routing, OutputRouting::Silent);
184        assert!(task.enabled);
185        assert_eq!(task.created_by, TaskCreator::System);
186    }
187
188    #[test]
189    fn output_routing_variants() {
190        let json = r#""share""#;
191        let routing: OutputRouting = serde_json::from_str(json).unwrap();
192        assert_eq!(routing, OutputRouting::Share);
193    }
194}