Skip to main content

ldp_protocol/types/
contract.rs

1//! LDP delegation contract types.
2
3use serde::{Deserialize, Serialize};
4
5/// A delegation contract — bounded expectations for a task.
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct DelegationContract {
8    pub contract_id: String,
9    pub objective: String,
10    pub success_criteria: Vec<String>,
11    #[serde(default)]
12    pub policy: PolicyEnvelope,
13    #[serde(skip_serializing_if = "Option::is_none")]
14    pub deadline: Option<String>,
15    pub created_at: String,
16}
17
18impl DelegationContract {
19    pub fn new(objective: impl Into<String>, success_criteria: Vec<String>) -> Self {
20        Self {
21            contract_id: uuid::Uuid::new_v4().to_string(),
22            objective: objective.into(),
23            success_criteria,
24            policy: PolicyEnvelope::default(),
25            deadline: None,
26            created_at: chrono::Utc::now().to_rfc3339(),
27        }
28    }
29
30    pub fn with_deadline(mut self, deadline: impl Into<String>) -> Self {
31        self.deadline = Some(deadline.into());
32        self
33    }
34
35    pub fn with_budget(mut self, budget: BudgetPolicy) -> Self {
36        self.policy.budget = Some(budget);
37        self
38    }
39
40    pub fn with_failure_policy(mut self, policy: FailurePolicy) -> Self {
41        self.policy.failure_policy = policy;
42        self
43    }
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
47pub struct PolicyEnvelope {
48    #[serde(default)]
49    pub failure_policy: FailurePolicy,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub budget: Option<BudgetPolicy>,
52    #[serde(default)]
53    pub safety_constraints: Vec<String>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub max_delegation_depth: Option<u32>,
56}
57
58impl Default for PolicyEnvelope {
59    fn default() -> Self {
60        Self {
61            failure_policy: FailurePolicy::FailOpen,
62            budget: None,
63            safety_constraints: Vec::new(),
64            max_delegation_depth: None,
65        }
66    }
67}
68
69#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
70#[serde(rename_all = "snake_case")]
71pub enum FailurePolicy {
72    FailClosed,
73    #[default]
74    FailOpen,
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize)]
78pub struct BudgetPolicy {
79    #[serde(skip_serializing_if = "Option::is_none")]
80    pub max_tokens: Option<u64>,
81    #[serde(skip_serializing_if = "Option::is_none")]
82    pub max_cost_usd: Option<f64>,
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn contract_creation() {
91        let contract =
92            DelegationContract::new("Summarize the document", vec!["<=300 words".into()]);
93        assert!(!contract.contract_id.is_empty());
94        assert_eq!(contract.objective, "Summarize the document");
95        assert!(contract.deadline.is_none());
96    }
97
98    #[test]
99    fn contract_with_deadline() {
100        let contract =
101            DelegationContract::new("task", vec![]).with_deadline("2026-12-31T23:59:59Z");
102        assert_eq!(contract.deadline.as_deref(), Some("2026-12-31T23:59:59Z"));
103    }
104
105    #[test]
106    fn contract_with_budget() {
107        let contract = DelegationContract::new("task", vec![]).with_budget(BudgetPolicy {
108            max_tokens: Some(5000),
109            max_cost_usd: Some(0.05),
110        });
111        assert_eq!(
112            contract.policy.budget.as_ref().unwrap().max_tokens,
113            Some(5000)
114        );
115    }
116
117    #[test]
118    fn default_failure_policy_is_fail_open() {
119        let contract = DelegationContract::new("task", vec![]);
120        assert_eq!(contract.policy.failure_policy, FailurePolicy::FailOpen);
121    }
122
123    #[test]
124    fn serialization_roundtrip() {
125        let contract = DelegationContract::new("Analyze data", vec!["accuracy > 0.9".into()])
126            .with_deadline("2026-06-01T00:00:00Z")
127            .with_budget(BudgetPolicy {
128                max_tokens: Some(10000),
129                max_cost_usd: None,
130            })
131            .with_failure_policy(FailurePolicy::FailClosed);
132        let json = serde_json::to_value(&contract).unwrap();
133        let restored: DelegationContract = serde_json::from_value(json).unwrap();
134        assert_eq!(restored.objective, "Analyze data");
135        assert_eq!(restored.policy.failure_policy, FailurePolicy::FailClosed);
136    }
137
138    #[test]
139    fn policy_envelope_defaults() {
140        let policy = PolicyEnvelope::default();
141        assert_eq!(policy.failure_policy, FailurePolicy::FailOpen);
142        assert!(policy.budget.is_none());
143        assert!(policy.safety_constraints.is_empty());
144    }
145}