Skip to main content

embassy_pack/
lib.rs

1// Copyright 2024-2026 Reflective Labs
2// SPDX-License-Identifier: MIT
3
4//! Shared contract surface for embassy ports.
5//!
6//! Every port in `embassy/*` emits provenanced observations and accepts a
7//! call context. These types are the cross-port contract — independent of
8//! any specific external service.
9
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Default)]
14pub struct CallContext {
15    pub correlation_id: Option<String>,
16    pub metadata: HashMap<String, String>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
20pub struct Observation<T> {
21    pub observation_id: String,
22    pub request_hash: String,
23    pub vendor: String,
24    pub model: String,
25    pub latency_ms: u64,
26    pub cost_estimate: Option<f64>,
27    pub tokens: Option<u64>,
28    pub content: T,
29    pub raw_response: Option<String>,
30}
31
32pub fn content_hash(input: &str) -> String {
33    use std::collections::hash_map::DefaultHasher;
34    use std::hash::{Hash, Hasher};
35    let mut hasher = DefaultHasher::new();
36    input.hash(&mut hasher);
37    format!("{:016x}", hasher.finish())
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn content_hash_is_deterministic() {
46        assert_eq!(content_hash("hello"), content_hash("hello"));
47    }
48
49    #[test]
50    fn content_hash_differs_for_distinct_inputs() {
51        assert_ne!(content_hash("hello"), content_hash("world"));
52    }
53
54    #[test]
55    fn content_hash_is_sixteen_hex_chars() {
56        let h = content_hash("anything");
57        assert_eq!(h.len(), 16);
58        assert!(h.chars().all(|c| c.is_ascii_hexdigit()));
59    }
60
61    #[test]
62    fn call_context_default_is_empty() {
63        let ctx = CallContext::default();
64        assert!(ctx.correlation_id.is_none());
65        assert!(ctx.metadata.is_empty());
66    }
67
68    #[test]
69    fn call_context_carries_metadata() {
70        let mut ctx = CallContext {
71            correlation_id: Some("trace-1".to_string()),
72            metadata: HashMap::new(),
73        };
74        ctx.metadata.insert("k".into(), "v".into());
75        assert_eq!(ctx.correlation_id.as_deref(), Some("trace-1"));
76        assert_eq!(ctx.metadata.get("k").map(String::as_str), Some("v"));
77    }
78
79    #[test]
80    fn observation_round_trips_through_json() {
81        let obs = Observation {
82            observation_id: "obs:1".to_string(),
83            request_hash: content_hash("req"),
84            vendor: "vendor".to_string(),
85            model: "model".to_string(),
86            latency_ms: 42,
87            cost_estimate: Some(0.5),
88            tokens: Some(7),
89            content: 99u32,
90            raw_response: Some("{}".to_string()),
91        };
92        let json = serde_json::to_string(&obs).expect("serialize");
93        let back: Observation<u32> = serde_json::from_str(&json).expect("deserialize");
94        assert_eq!(back.observation_id, obs.observation_id);
95        assert_eq!(back.request_hash, obs.request_hash);
96        assert_eq!(back.vendor, obs.vendor);
97        assert_eq!(back.model, obs.model);
98        assert_eq!(back.latency_ms, obs.latency_ms);
99        assert_eq!(back.cost_estimate, obs.cost_estimate);
100        assert_eq!(back.tokens, obs.tokens);
101        assert_eq!(back.content, obs.content);
102        assert_eq!(back.raw_response, obs.raw_response);
103    }
104}