Skip to main content

prismer_sdk/
types.rs

1use serde::{Deserialize, Serialize};
2
3/// IM message content types (v1.8.2).
4///
5/// The wire protocol uses strings; this module exposes them as `&'static str`
6/// constants so Rust users get autocomplete and catch typos at compile time.
7pub mod message_type {
8    pub const TEXT: &str = "text";
9    pub const MARKDOWN: &str = "markdown";
10    pub const CODE: &str = "code";
11    pub const IMAGE: &str = "image";
12    pub const FILE: &str = "file";
13    pub const VOICE: &str = "voice"; // v1.8.2
14    pub const LOCATION: &str = "location"; // v1.8.2
15    pub const ARTIFACT: &str = "artifact"; // v1.8.2
16    pub const TOOL_CALL: &str = "tool_call";
17    pub const TOOL_RESULT: &str = "tool_result";
18    /// Deprecated — use `SYSTEM` with `metadata.action`.
19    pub const SYSTEM_EVENT: &str = "system_event";
20    pub const SYSTEM: &str = "system"; // v1.8.2
21    pub const THINKING: &str = "thinking";
22}
23
24/// Artifact sub-types for `message_type::ARTIFACT` (v1.8.2).
25/// Passed via `metadata.artifactType`.
26pub mod artifact_type {
27    pub const PDF: &str = "pdf";
28    pub const CODE: &str = "code";
29    pub const DOCUMENT: &str = "document";
30    pub const DATASET: &str = "dataset";
31    pub const CHART: &str = "chart";
32    pub const NOTEBOOK: &str = "notebook";
33    pub const LATEX: &str = "latex";
34    pub const OTHER: &str = "other";
35}
36
37/// Standard API response wrapper.
38#[derive(Debug, Deserialize)]
39pub struct ApiResponse<T> {
40    pub success: Option<bool>,
41    pub ok: Option<bool>,
42    pub data: Option<T>,
43    pub error: Option<ApiError>,
44}
45
46impl<T> ApiResponse<T> {
47    pub fn is_ok(&self) -> bool {
48        self.success.unwrap_or(false) || self.ok.unwrap_or(false)
49    }
50}
51
52#[derive(Debug, Deserialize)]
53pub struct ApiError {
54    pub code: Option<String>,
55    pub message: Option<String>,
56}
57
58/// SDK error types.
59#[derive(Debug)]
60pub enum PrismerError {
61    Network(String),
62    Api { status: u16, message: String },
63    Parse(String),
64}
65
66impl std::fmt::Display for PrismerError {
67    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
68        match self {
69            PrismerError::Network(e) => write!(f, "Network error: {}", e),
70            PrismerError::Api { status, message } => write!(f, "API error {}: {}", status, message),
71            PrismerError::Parse(e) => write!(f, "Parse error: {}", e),
72        }
73    }
74}
75
76impl std::error::Error for PrismerError {}
77
78/// Context Load result.
79#[derive(Debug, Serialize, Deserialize)]
80pub struct ContextLoadResult {
81    pub results: Option<Vec<ContextItem>>,
82    #[serde(rename = "processingTime")]
83    pub processing_time: Option<u64>,
84}
85
86#[derive(Debug, Serialize, Deserialize)]
87pub struct ContextItem {
88    pub title: Option<String>,
89    pub url: Option<String>,
90    pub content: Option<String>,
91    pub score: Option<f64>,
92}
93
94/// Parse result.
95#[derive(Debug, Serialize, Deserialize)]
96pub struct ParseResult {
97    #[serde(rename = "taskId")]
98    pub task_id: Option<String>,
99    pub status: Option<String>,
100    pub document: Option<serde_json::Value>,
101}
102
103/// IM types.
104#[derive(Debug, Serialize, Deserialize)]
105pub struct SignalTag {
106    #[serde(rename = "type")]
107    pub signal_type: String,
108    pub provider: Option<String>,
109    pub stage: Option<String>,
110    pub severity: Option<String>,
111}
112
113#[derive(Debug, Clone, Serialize, Deserialize)]
114pub struct Gene {
115    pub id: String,
116    pub category: Option<String>,
117    pub title: Option<String>,
118    pub signals_match: Option<Vec<serde_json::Value>>,
119    pub strategy: Option<Vec<String>>,
120    pub visibility: Option<String>,
121    pub success_count: Option<i64>,
122    pub failure_count: Option<i64>,
123}
124
125#[derive(Debug, Serialize, Deserialize)]
126pub struct EvolutionAdvice {
127    pub action: String,
128    pub gene: Option<Gene>,
129    pub confidence: Option<f64>,
130    pub signals: Option<Vec<serde_json::Value>>,
131}
132
133#[derive(Debug, Serialize, Deserialize)]
134pub struct EvolutionMetrics {
135    pub standard: Option<serde_json::Value>,
136    pub hypergraph: Option<serde_json::Value>,
137    pub verdict: Option<String>,
138}
139
140// ============================================================================
141// IM Task Types
142// ============================================================================
143
144/// A task returned from the API.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct IMTask {
147    pub id: String,
148    pub title: String,
149    pub description: Option<String>,
150    pub capability: Option<String>,
151    pub input: Option<serde_json::Value>,
152    #[serde(rename = "contextUri")]
153    pub context_uri: Option<String>,
154    #[serde(rename = "creatorId")]
155    pub creator_id: String,
156    #[serde(rename = "assigneeId")]
157    pub assignee_id: Option<String>,
158    pub status: String,
159    pub progress: Option<f64>,
160    #[serde(rename = "statusMessage")]
161    pub status_message: Option<String>,
162    #[serde(rename = "conversationId")]
163    pub conversation_id: Option<String>,
164    #[serde(rename = "completedAt")]
165    pub completed_at: Option<String>,
166    #[serde(rename = "ownerId")]
167    pub owner_id: String,
168    #[serde(rename = "ownerType")]
169    pub owner_type: Option<String>,
170    #[serde(rename = "ownerName")]
171    pub owner_name: Option<String>,
172    #[serde(rename = "assigneeType")]
173    pub assignee_type: Option<String>,
174    #[serde(rename = "assigneeName")]
175    pub assignee_name: Option<String>,
176    #[serde(rename = "scheduleType")]
177    pub schedule_type: Option<String>,
178    #[serde(rename = "scheduleCron")]
179    pub schedule_cron: Option<String>,
180    #[serde(rename = "intervalMs")]
181    pub interval_ms: Option<i64>,
182    #[serde(rename = "nextRunAt")]
183    pub next_run_at: Option<String>,
184    #[serde(rename = "lastRunAt")]
185    pub last_run_at: Option<String>,
186    #[serde(rename = "runCount")]
187    pub run_count: Option<i64>,
188    #[serde(rename = "maxRuns")]
189    pub max_runs: Option<i64>,
190    pub result: Option<serde_json::Value>,
191    #[serde(rename = "resultUri")]
192    pub result_uri: Option<String>,
193    pub error: Option<String>,
194    pub budget: Option<f64>,
195    pub cost: Option<f64>,
196    #[serde(rename = "timeoutMs")]
197    pub timeout_ms: Option<i64>,
198    pub deadline: Option<String>,
199    #[serde(rename = "maxRetries")]
200    pub max_retries: Option<i64>,
201    #[serde(rename = "retryDelayMs")]
202    pub retry_delay_ms: Option<i64>,
203    #[serde(rename = "retryCount")]
204    pub retry_count: Option<i64>,
205    pub metadata: Option<serde_json::Value>,
206    #[serde(rename = "createdAt")]
207    pub created_at: String,
208    #[serde(rename = "updatedAt")]
209    pub updated_at: String,
210}
211
212/// A single log entry for a task.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct IMTaskLog {
215    pub id: String,
216    #[serde(rename = "taskId")]
217    pub task_id: String,
218    #[serde(rename = "actorId")]
219    pub actor_id: Option<String>,
220    pub action: String,
221    pub message: Option<String>,
222    pub metadata: Option<serde_json::Value>,
223    #[serde(rename = "createdAt")]
224    pub created_at: String,
225}
226
227/// A task with its logs.
228#[derive(Debug, Clone, Serialize, Deserialize)]
229pub struct IMTaskDetail {
230    pub task: IMTask,
231    pub logs: Vec<IMTaskLog>,
232}
233
234// ============================================================================
235// Realtime Event Payloads
236// ============================================================================
237
238/// Payload for a new message event.
239#[derive(Debug, Serialize, Deserialize)]
240pub struct MessageNewPayload {
241    pub id: String,
242    #[serde(rename = "conversationId")]
243    pub conversation_id: String,
244    pub content: String,
245    #[serde(rename = "type")]
246    pub msg_type: String,
247    #[serde(rename = "senderId")]
248    pub sender_id: String,
249    pub routing: Option<serde_json::Value>,
250    pub metadata: Option<serde_json::Value>,
251    #[serde(rename = "createdAt")]
252    pub created_at: String,
253}
254
255/// Payload for a message edit event.
256#[derive(Debug, Serialize, Deserialize)]
257pub struct MessageEditPayload {
258    pub id: String,
259    #[serde(rename = "conversationId")]
260    pub conversation_id: String,
261    pub content: String,
262    #[serde(rename = "type")]
263    pub msg_type: String,
264    #[serde(rename = "editedAt")]
265    pub edited_at: String,
266    #[serde(rename = "editedBy")]
267    pub edited_by: String,
268    pub metadata: Option<serde_json::Value>,
269}
270
271/// Payload for a message deleted event.
272#[derive(Debug, Serialize, Deserialize)]
273pub struct MessageDeletedPayload {
274    pub id: String,
275    #[serde(rename = "conversationId")]
276    pub conversation_id: String,
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use serde_json::json;
283
284    // ── ApiResponse ──────────────────────────────────────
285
286    #[test]
287    fn api_response_is_ok_with_success_true() {
288        let resp: ApiResponse<()> = ApiResponse {
289            success: Some(true),
290            ok: None,
291            data: None,
292            error: None,
293        };
294        assert!(resp.is_ok());
295    }
296
297    #[test]
298    fn api_response_is_ok_with_ok_true() {
299        let resp: ApiResponse<()> = ApiResponse {
300            success: None,
301            ok: Some(true),
302            data: None,
303            error: None,
304        };
305        assert!(resp.is_ok());
306    }
307
308    #[test]
309    fn api_response_is_ok_both_false() {
310        let resp: ApiResponse<()> = ApiResponse {
311            success: Some(false),
312            ok: Some(false),
313            data: None,
314            error: None,
315        };
316        assert!(!resp.is_ok());
317    }
318
319    #[test]
320    fn api_response_is_ok_all_none() {
321        let resp: ApiResponse<()> = ApiResponse {
322            success: None,
323            ok: None,
324            data: None,
325            error: None,
326        };
327        assert!(!resp.is_ok());
328    }
329
330    #[test]
331    fn api_response_deserialize_success_field() {
332        let json_str = r#"{"success": true, "data": {"results": []}}"#;
333        let resp: ApiResponse<ContextLoadResult> = serde_json::from_str(json_str).unwrap();
334        assert!(resp.is_ok());
335        assert!(resp.data.is_some());
336    }
337
338    #[test]
339    fn api_response_deserialize_ok_field() {
340        let json_str = r#"{"ok": true, "data": null}"#;
341        let resp: ApiResponse<serde_json::Value> = serde_json::from_str(json_str).unwrap();
342        assert!(resp.is_ok());
343    }
344
345    #[test]
346    fn api_response_deserialize_with_error() {
347        let json_str = r#"{"success": false, "error": {"code": "UNAUTHORIZED", "message": "Bad key"}}"#;
348        let resp: ApiResponse<()> = serde_json::from_str(json_str).unwrap();
349        assert!(!resp.is_ok());
350        let err = resp.error.unwrap();
351        assert_eq!(err.code.as_deref(), Some("UNAUTHORIZED"));
352        assert_eq!(err.message.as_deref(), Some("Bad key"));
353    }
354
355    // ── PrismerError Display ─────────────────────────────
356
357    #[test]
358    fn error_display_network() {
359        let e = PrismerError::Network("connection refused".to_string());
360        assert_eq!(e.to_string(), "Network error: connection refused");
361    }
362
363    #[test]
364    fn error_display_api() {
365        let e = PrismerError::Api { status: 401, message: "Unauthorized".to_string() };
366        assert_eq!(e.to_string(), "API error 401: Unauthorized");
367    }
368
369    #[test]
370    fn error_display_parse() {
371        let e = PrismerError::Parse("invalid json".to_string());
372        assert_eq!(e.to_string(), "Parse error: invalid json");
373    }
374
375    #[test]
376    fn error_implements_std_error() {
377        let e: Box<dyn std::error::Error> = Box::new(PrismerError::Network("test".into()));
378        assert!(e.to_string().contains("Network error"));
379    }
380
381    // ── ContextLoadResult serde roundtrip ────────────────
382
383    #[test]
384    fn context_load_result_roundtrip() {
385        let result = ContextLoadResult {
386            results: Some(vec![ContextItem {
387                title: Some("Test".to_string()),
388                url: Some("https://example.com".to_string()),
389                content: Some("Hello".to_string()),
390                score: Some(0.95),
391            }]),
392            processing_time: Some(123),
393        };
394        let json = serde_json::to_string(&result).unwrap();
395        let decoded: ContextLoadResult = serde_json::from_str(&json).unwrap();
396        assert_eq!(decoded.processing_time, Some(123));
397        assert_eq!(decoded.results.as_ref().unwrap().len(), 1);
398        assert_eq!(decoded.results.as_ref().unwrap()[0].title.as_deref(), Some("Test"));
399    }
400
401    #[test]
402    fn context_load_result_processing_time_rename() {
403        let json_str = r#"{"processingTime": 456, "results": null}"#;
404        let decoded: ContextLoadResult = serde_json::from_str(json_str).unwrap();
405        assert_eq!(decoded.processing_time, Some(456));
406    }
407
408    // ── ParseResult serde ────────────────────────────────
409
410    #[test]
411    fn parse_result_roundtrip() {
412        let result = ParseResult {
413            task_id: Some("task-123".to_string()),
414            status: Some("completed".to_string()),
415            document: Some(json!({"pages": 5})),
416        };
417        let json = serde_json::to_string(&result).unwrap();
418        assert!(json.contains("taskId")); // verify rename
419        let decoded: ParseResult = serde_json::from_str(&json).unwrap();
420        assert_eq!(decoded.task_id.as_deref(), Some("task-123"));
421    }
422
423    // ── SignalTag serde ──────────────────────────────────
424
425    #[test]
426    fn signal_tag_roundtrip() {
427        let tag = SignalTag {
428            signal_type: "error:timeout".to_string(),
429            provider: Some("openai".to_string()),
430            stage: Some("fetch".to_string()),
431            severity: Some("high".to_string()),
432        };
433        let json = serde_json::to_string(&tag).unwrap();
434        assert!(json.contains(r#""type":"error:timeout"#));
435        let decoded: SignalTag = serde_json::from_str(&json).unwrap();
436        assert_eq!(decoded.signal_type, "error:timeout");
437        assert_eq!(decoded.provider.as_deref(), Some("openai"));
438    }
439
440    #[test]
441    fn signal_tag_minimal() {
442        let json_str = r#"{"type": "task.completed"}"#;
443        let tag: SignalTag = serde_json::from_str(json_str).unwrap();
444        assert_eq!(tag.signal_type, "task.completed");
445        assert!(tag.provider.is_none());
446    }
447
448    // ── Gene serde ───────────────────────────────────────
449
450    #[test]
451    fn gene_roundtrip() {
452        let gene = Gene {
453            id: "gene-1".to_string(),
454            category: Some("error-handling".to_string()),
455            title: Some("Timeout Fix".to_string()),
456            signals_match: Some(vec![json!("error:timeout")]),
457            strategy: Some(vec!["increase timeout".to_string()]),
458            visibility: Some("public".to_string()),
459            success_count: Some(10),
460            failure_count: Some(2),
461        };
462        let json = serde_json::to_string(&gene).unwrap();
463        let decoded: Gene = serde_json::from_str(&json).unwrap();
464        assert_eq!(decoded.id, "gene-1");
465        assert_eq!(decoded.strategy.as_ref().unwrap()[0], "increase timeout");
466    }
467
468    #[test]
469    fn gene_clone() {
470        let gene = Gene {
471            id: "g1".to_string(),
472            category: None,
473            title: None,
474            signals_match: None,
475            strategy: None,
476            visibility: None,
477            success_count: None,
478            failure_count: None,
479        };
480        let cloned = gene.clone();
481        assert_eq!(cloned.id, "g1");
482    }
483
484    // ── EvolutionAdvice serde ────────────────────────────
485
486    #[test]
487    fn evolution_advice_roundtrip() {
488        let advice = EvolutionAdvice {
489            action: "apply_gene".to_string(),
490            gene: Some(Gene {
491                id: "g1".into(), category: None, title: None,
492                signals_match: None, strategy: None, visibility: None,
493                success_count: None, failure_count: None,
494            }),
495            confidence: Some(0.85),
496            signals: Some(vec![json!({"type": "error:timeout"})]),
497        };
498        let json = serde_json::to_string(&advice).unwrap();
499        let decoded: EvolutionAdvice = serde_json::from_str(&json).unwrap();
500        assert_eq!(decoded.action, "apply_gene");
501        assert_eq!(decoded.confidence, Some(0.85));
502    }
503
504    // ── MessageNewPayload serde ──────────────────────────
505
506    #[test]
507    fn message_new_payload_roundtrip() {
508        let payload = MessageNewPayload {
509            id: "msg-1".to_string(),
510            conversation_id: "conv-1".to_string(),
511            content: "Hello".to_string(),
512            msg_type: "text".to_string(),
513            sender_id: "user-1".to_string(),
514            routing: None,
515            metadata: Some(json!({"key": "value"})),
516            created_at: "2026-01-01T00:00:00Z".to_string(),
517        };
518        let json = serde_json::to_string(&payload).unwrap();
519        assert!(json.contains("conversationId"));
520        assert!(json.contains("senderId"));
521        assert!(json.contains("createdAt"));
522        let decoded: MessageNewPayload = serde_json::from_str(&json).unwrap();
523        assert_eq!(decoded.id, "msg-1");
524        assert_eq!(decoded.conversation_id, "conv-1");
525    }
526
527    // ── MessageEditPayload serde ─────────────────────────
528
529    #[test]
530    fn message_edit_payload_roundtrip() {
531        let payload = MessageEditPayload {
532            id: "msg-1".to_string(),
533            conversation_id: "conv-1".to_string(),
534            content: "Edited".to_string(),
535            msg_type: "text".to_string(),
536            edited_at: "2026-01-01T01:00:00Z".to_string(),
537            edited_by: "user-2".to_string(),
538            metadata: None,
539        };
540        let json = serde_json::to_string(&payload).unwrap();
541        assert!(json.contains("editedAt"));
542        assert!(json.contains("editedBy"));
543        let decoded: MessageEditPayload = serde_json::from_str(&json).unwrap();
544        assert_eq!(decoded.edited_by, "user-2");
545    }
546
547    // ── MessageDeletedPayload serde ──────────────────────
548
549    #[test]
550    fn message_deleted_payload_roundtrip() {
551        let payload = MessageDeletedPayload {
552            id: "msg-99".to_string(),
553            conversation_id: "conv-5".to_string(),
554        };
555        let json = serde_json::to_string(&payload).unwrap();
556        let decoded: MessageDeletedPayload = serde_json::from_str(&json).unwrap();
557        assert_eq!(decoded.id, "msg-99");
558        assert_eq!(decoded.conversation_id, "conv-5");
559    }
560}