ldp_protocol/types/
contract.rs1use serde::{Deserialize, Serialize};
4
5#[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}