1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4#[derive(Clone, Debug, Serialize, Deserialize, Default)]
7pub struct DecisionPolicyRules {
8 #[serde(default)]
9 pub voting: VotingRules,
10 #[serde(default)]
11 pub objection_handling: ObjectionHandlingRules,
12 #[serde(default)]
13 pub evaluation: EvaluationRules,
14 #[serde(default)]
15 pub commitment: CommitmentRules,
16}
17
18#[derive(Clone, Debug, Serialize, Deserialize)]
19pub struct VotingRules {
20 #[serde(default = "default_algorithm")]
21 pub algorithm: String,
22 #[serde(default = "default_threshold")]
23 pub threshold: f64,
24 #[serde(default)]
25 pub quorum: QuorumRules,
26 #[serde(default)]
27 pub weights: HashMap<String, f64>,
28}
29
30impl Default for VotingRules {
31 fn default() -> Self {
32 Self {
33 algorithm: default_algorithm(),
34 threshold: default_threshold(),
35 quorum: QuorumRules::default(),
36 weights: HashMap::new(),
37 }
38 }
39}
40
41fn default_algorithm() -> String {
42 "none".into()
43}
44
45fn default_threshold() -> f64 {
46 0.5
47}
48
49#[derive(Clone, Debug, Serialize, Deserialize)]
51pub struct QuorumRules {
52 #[serde(default = "default_quorum_type", rename = "type")]
53 pub quorum_type: String,
54 #[serde(default)]
55 pub value: f64,
56}
57
58impl Default for QuorumRules {
59 fn default() -> Self {
60 Self {
61 quorum_type: default_quorum_type(),
62 value: 0.0,
63 }
64 }
65}
66
67fn default_quorum_type() -> String {
68 "count".into()
69}
70
71#[derive(Clone, Debug, Serialize, Deserialize)]
72pub struct ObjectionHandlingRules {
73 #[serde(default, alias = "critical_severity_vetoes")]
75 pub critical_severity_vetoes: bool,
76 #[serde(default = "default_veto_threshold")]
77 pub veto_threshold: u32,
78 #[serde(default)]
85 pub critical_objection_action: CriticalObjectionAction,
86}
87
88impl Default for ObjectionHandlingRules {
89 fn default() -> Self {
90 Self {
91 critical_severity_vetoes: false,
92 veto_threshold: default_veto_threshold(),
93 critical_objection_action: CriticalObjectionAction::default(),
94 }
95 }
96}
97
98fn default_veto_threshold() -> u32 {
99 1
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
104#[serde(rename_all = "snake_case")]
105pub enum CriticalObjectionAction {
106 #[default]
109 Deny,
110 FinalizeDecline,
113 Hold,
118}
119
120#[derive(Clone, Debug, Serialize, Deserialize)]
121pub struct EvaluationRules {
122 #[serde(default)]
123 pub required_before_voting: bool,
124 #[serde(default)]
125 pub minimum_confidence: f64,
126}
127
128impl Default for EvaluationRules {
129 fn default() -> Self {
130 Self {
131 required_before_voting: false,
132 minimum_confidence: 0.0,
133 }
134 }
135}
136
137pub use super::CommitmentRules;
142
143#[derive(Clone, Debug, Serialize, Deserialize, Default)]
146pub struct ProposalPolicyRules {
147 #[serde(default)]
148 pub acceptance: ProposalAcceptanceRules,
149 #[serde(default)]
150 pub counter_proposal: CounterProposalRules,
151 #[serde(default)]
152 pub rejection: RejectionRules,
153 #[serde(default)]
154 pub commitment: CommitmentRules,
155}
156
157#[derive(Clone, Debug, Serialize, Deserialize)]
158pub struct ProposalAcceptanceRules {
159 #[serde(default = "default_acceptance_criterion")]
160 pub criterion: String,
161}
162
163impl Default for ProposalAcceptanceRules {
164 fn default() -> Self {
165 Self {
166 criterion: default_acceptance_criterion(),
167 }
168 }
169}
170
171fn default_acceptance_criterion() -> String {
172 "all_parties".into()
173}
174
175#[derive(Clone, Debug, Default, Serialize, Deserialize)]
176pub struct CounterProposalRules {
177 #[serde(default)]
178 pub max_rounds: usize,
179}
180
181#[derive(Clone, Debug, Default, Serialize, Deserialize)]
182pub struct RejectionRules {
183 #[serde(default)]
184 pub terminal_on_any_reject: bool,
185}
186
187#[derive(Clone, Debug, Serialize, Deserialize, Default)]
190pub struct TaskPolicyRules {
191 #[serde(default)]
192 pub assignment: TaskAssignmentRules,
193 #[serde(default)]
194 pub completion: TaskCompletionRules,
195 #[serde(default)]
196 pub commitment: CommitmentRules,
197}
198
199#[derive(Clone, Debug, Default, Serialize, Deserialize)]
200pub struct TaskAssignmentRules {
201 #[serde(default)]
202 pub allow_reassignment_on_reject: bool,
203}
204
205#[derive(Clone, Debug, Default, Serialize, Deserialize)]
206pub struct TaskCompletionRules {
207 #[serde(default)]
208 pub require_output: bool,
209}
210
211#[derive(Clone, Debug, Serialize, Deserialize, Default)]
214pub struct HandoffPolicyRules {
215 #[serde(default)]
216 pub acceptance: HandoffAcceptanceRules,
217 #[serde(default)]
218 pub commitment: CommitmentRules,
219}
220
221#[derive(Clone, Debug, Default, Serialize, Deserialize)]
222pub struct HandoffAcceptanceRules {
223 #[serde(default)]
224 pub implicit_accept_timeout_ms: u64,
225}
226
227#[derive(Clone, Debug, Serialize, Deserialize, Default)]
230pub struct QuorumPolicyRules {
231 #[serde(default)]
232 pub threshold: QuorumThreshold,
233 #[serde(default)]
234 pub abstention: AbstentionRules,
235 #[serde(default)]
236 pub commitment: CommitmentRules,
237}
238
239#[derive(Clone, Debug, Serialize, Deserialize)]
241pub struct QuorumThreshold {
242 #[serde(default = "default_threshold_type", rename = "type")]
243 pub threshold_type: String,
244 #[serde(default)]
245 pub value: f64,
246}
247
248impl Default for QuorumThreshold {
249 fn default() -> Self {
250 Self {
251 threshold_type: default_threshold_type(),
252 value: 0.0,
253 }
254 }
255}
256
257fn default_threshold_type() -> String {
258 "n_of_m".into()
259}
260
261#[derive(Clone, Copy, Debug, PartialEq, Eq)]
289pub enum EffectiveThreshold {
290 Inert,
298 Approvals(u32),
301 Unsatisfiable,
316}
317
318impl QuorumThreshold {
319 pub fn effective(&self, total_participants: usize) -> EffectiveThreshold {
329 if self.value.partial_cmp(&0.0) != Some(std::cmp::Ordering::Greater) {
332 return EffectiveThreshold::Inert;
333 }
334 let required: f64 = match self.threshold_type.as_str() {
335 "percentage" => {
336 if total_participants == 0 {
337 return EffectiveThreshold::Unsatisfiable;
341 }
342 (self.value / 100.0) * total_participants as f64
343 }
344 "n_of_m" | "count" => self.value,
348 _ => return EffectiveThreshold::Unsatisfiable,
349 };
350 EffectiveThreshold::Approvals((required.ceil() as u32).max(1))
353 }
354}
355
356#[derive(Clone, Debug, Serialize, Deserialize)]
357pub struct AbstentionRules {
358 #[serde(default)]
359 pub counts_toward_quorum: bool,
360 #[serde(default = "default_interpretation")]
361 pub interpretation: String,
362}
363
364impl Default for AbstentionRules {
365 fn default() -> Self {
366 Self {
367 counts_toward_quorum: false,
368 interpretation: default_interpretation(),
369 }
370 }
371}
372
373fn default_interpretation() -> String {
374 "neutral".into()
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380
381 #[test]
382 fn decision_policy_rules_defaults() {
383 let rules = DecisionPolicyRules::default();
384 assert_eq!(rules.voting.algorithm, "none");
385 assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
386 assert_eq!(rules.voting.quorum.quorum_type, "count");
387 assert!(!rules.objection_handling.critical_severity_vetoes);
388 assert_eq!(rules.objection_handling.veto_threshold, 1);
389 assert_eq!(
390 rules.objection_handling.critical_objection_action,
391 CriticalObjectionAction::Deny
392 );
393 assert!(!rules.commitment.allow_decline_over_approval);
394 assert!(!rules.evaluation.required_before_voting);
395 assert!((rules.evaluation.minimum_confidence).abs() < f64::EPSILON);
396 assert_eq!(rules.commitment.authority, "initiator_only");
397 assert!(rules.commitment.designated_roles.is_empty());
398 assert!(!rules.commitment.require_vote_quorum);
399 }
400
401 #[test]
402 fn decision_policy_rules_deserialization() {
403 let json = serde_json::json!({
404 "voting": {
405 "algorithm": "majority",
406 "threshold": 0.6,
407 "quorum": { "type": "percentage", "value": 75.0 },
408 "weights": { "agent://fraud": 2.0, "agent://growth": 1.0 }
409 },
410 "objection_handling": {
411 "critical_severity_vetoes": true,
412 "veto_threshold": 2
413 },
414 "evaluation": {
415 "required_before_voting": true,
416 "minimum_confidence": 0.8
417 },
418 "commitment": {
419 "authority": "designated_role",
420 "designated_roles": ["agent://lead"],
421 "require_vote_quorum": true
422 }
423 });
424
425 let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
426 assert_eq!(rules.voting.algorithm, "majority");
427 assert!((rules.voting.threshold - 0.6).abs() < f64::EPSILON);
428 assert_eq!(rules.voting.quorum.quorum_type, "percentage");
429 assert!((rules.voting.quorum.value - 75.0).abs() < f64::EPSILON);
430 assert_eq!(*rules.voting.weights.get("agent://fraud").unwrap(), 2.0);
431 assert!(rules.objection_handling.critical_severity_vetoes);
432 assert_eq!(rules.objection_handling.veto_threshold, 2);
433 assert!(rules.evaluation.required_before_voting);
434 assert!((rules.evaluation.minimum_confidence - 0.8).abs() < f64::EPSILON);
435 assert_eq!(rules.commitment.authority, "designated_role");
436 assert_eq!(rules.commitment.designated_roles, vec!["agent://lead"]);
437 assert!(rules.commitment.require_vote_quorum);
438 }
439
440 #[test]
441 fn partial_deserialization_fills_defaults() {
442 let json = serde_json::json!({
443 "voting": { "algorithm": "unanimous" }
444 });
445 let rules: DecisionPolicyRules = serde_json::from_value(json).unwrap();
446 assert_eq!(rules.voting.algorithm, "unanimous");
447 assert!((rules.voting.threshold - 0.5).abs() < f64::EPSILON);
448 assert!(!rules.objection_handling.critical_severity_vetoes);
449 assert_eq!(rules.objection_handling.veto_threshold, 1);
450 }
451
452 #[test]
453 fn proposal_policy_rules_defaults() {
454 let rules = ProposalPolicyRules::default();
455 assert_eq!(rules.acceptance.criterion, "all_parties");
456 assert_eq!(rules.counter_proposal.max_rounds, 0);
457 assert!(!rules.rejection.terminal_on_any_reject);
458 assert_eq!(rules.commitment.authority, "initiator_only");
459 }
460
461 #[test]
462 fn proposal_policy_rules_deserialization() {
463 let json = serde_json::json!({
464 "acceptance": { "criterion": "counterparty" },
465 "counter_proposal": { "max_rounds": 3 },
466 "rejection": { "terminal_on_any_reject": true },
467 "commitment": { "authority": "any_participant" }
468 });
469 let rules: ProposalPolicyRules = serde_json::from_value(json).unwrap();
470 assert_eq!(rules.acceptance.criterion, "counterparty");
471 assert_eq!(rules.counter_proposal.max_rounds, 3);
472 assert!(rules.rejection.terminal_on_any_reject);
473 assert_eq!(rules.commitment.authority, "any_participant");
474 }
475
476 #[test]
477 fn task_policy_rules_defaults() {
478 let rules = TaskPolicyRules::default();
479 assert!(!rules.assignment.allow_reassignment_on_reject);
480 assert!(!rules.completion.require_output);
481 assert_eq!(rules.commitment.authority, "initiator_only");
482 }
483
484 #[test]
485 fn task_policy_rules_deserialization() {
486 let json = serde_json::json!({
487 "assignment": { "allow_reassignment_on_reject": true },
488 "completion": { "require_output": true },
489 "commitment": { "authority": "initiator_only" }
490 });
491 let rules: TaskPolicyRules = serde_json::from_value(json).unwrap();
492 assert!(rules.assignment.allow_reassignment_on_reject);
493 assert!(rules.completion.require_output);
494 }
495
496 #[test]
497 fn handoff_policy_rules_defaults() {
498 let rules = HandoffPolicyRules::default();
499 assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 0);
500 assert_eq!(rules.commitment.authority, "initiator_only");
501 }
502
503 #[test]
504 fn handoff_policy_rules_deserialization() {
505 let json = serde_json::json!({
506 "acceptance": { "implicit_accept_timeout_ms": 5000 },
507 "commitment": { "authority": "any_participant" }
508 });
509 let rules: HandoffPolicyRules = serde_json::from_value(json).unwrap();
510 assert_eq!(rules.acceptance.implicit_accept_timeout_ms, 5000);
511 assert_eq!(rules.commitment.authority, "any_participant");
512 }
513
514 #[test]
515 fn quorum_policy_rules_defaults() {
516 let rules = QuorumPolicyRules::default();
517 assert_eq!(rules.threshold.threshold_type, "n_of_m");
518 assert!((rules.threshold.value).abs() < f64::EPSILON);
519 assert!(!rules.abstention.counts_toward_quorum);
520 assert_eq!(rules.abstention.interpretation, "neutral");
521 assert_eq!(rules.commitment.authority, "initiator_only");
522 }
523
524 #[test]
525 fn effective_threshold_ceils_and_floors_at_one() {
526 let t = |kind: &str, value: f64| QuorumThreshold {
527 threshold_type: kind.into(),
528 value,
529 };
530 assert_eq!(
532 t("n_of_m", 0.5).effective(3),
533 EffectiveThreshold::Approvals(1)
534 );
535 assert_eq!(
536 t("count", 2.4).effective(3),
537 EffectiveThreshold::Approvals(3)
538 );
539 assert_eq!(
541 t("percentage", 50.0).effective(3),
542 EffectiveThreshold::Approvals(2)
543 );
544 assert_eq!(
546 t("percentage", 0.5).effective(3),
547 EffectiveThreshold::Approvals(1)
548 );
549 assert_eq!(t("n_of_m", 0.0).effective(3), EffectiveThreshold::Inert);
551 assert_eq!(t("n_of_m", -1.0).effective(3), EffectiveThreshold::Inert);
552 assert_eq!(
553 t("n_of_m", f64::NAN).effective(3),
554 EffectiveThreshold::Inert
555 );
556 assert_eq!(
559 t("weighted", 2.0).effective(3),
560 EffectiveThreshold::Unsatisfiable
561 );
562 assert_eq!(
563 t("two_thirds", 2.0).effective(3),
564 EffectiveThreshold::Unsatisfiable
565 );
566 assert_eq!(
567 t("percentage", 50.0).effective(0),
568 EffectiveThreshold::Unsatisfiable
569 );
570 assert_eq!(
572 t("n_of_m", 1e30).effective(3),
573 EffectiveThreshold::Approvals(u32::MAX)
574 );
575 }
576
577 #[test]
578 fn quorum_policy_rules_deserialization() {
579 let json = serde_json::json!({
580 "threshold": { "type": "percentage", "value": 75.0 },
581 "abstention": { "counts_toward_quorum": true, "interpretation": "implicit_reject" },
582 "commitment": { "authority": "initiator_only" }
583 });
584 let rules: QuorumPolicyRules = serde_json::from_value(json).unwrap();
585 assert_eq!(rules.threshold.threshold_type, "percentage");
586 assert!((rules.threshold.value - 75.0).abs() < f64::EPSILON);
587 assert!(rules.abstention.counts_toward_quorum);
588 assert_eq!(rules.abstention.interpretation, "implicit_reject");
589 }
590}