1use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::crypto::{hash, Hash, PublicKey, Sig};
10use crate::error::{Error, Result};
11use crate::event::ResourceId;
12
13use super::capability::{CapabilityId, CapabilityKind};
14use super::causality::CausalContext;
15use super::outcome::ActionOutcome;
16use super::principal::PrincipalId;
17
18pub type DurationMs = i64;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct CoordinationId(pub [u8; 16]);
24
25impl CoordinationId {
26 pub fn generate() -> Self {
28 use rand::RngCore;
29 let mut bytes = [0u8; 16];
30 rand::thread_rng().fill_bytes(&mut bytes);
31 Self(bytes)
32 }
33
34 pub fn from_bytes(bytes: [u8; 16]) -> Self {
36 Self(bytes)
37 }
38
39 pub fn as_bytes(&self) -> &[u8; 16] {
41 &self.0
42 }
43
44 pub fn to_hex(&self) -> String {
46 hex::encode(self.0)
47 }
48
49 pub fn from_hex(s: &str) -> Result<Self> {
51 let bytes = hex::decode(s).map_err(|_| Error::invalid_input("invalid hex"))?;
52 if bytes.len() != 16 {
53 return Err(Error::invalid_input("coordination ID must be 16 bytes"));
54 }
55 let mut arr = [0u8; 16];
56 arr.copy_from_slice(&bytes);
57 Ok(Self(arr))
58 }
59}
60
61impl std::fmt::Display for CoordinationId {
62 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63 write!(f, "{}", self.to_hex())
64 }
65}
66
67#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub struct TaskId(pub [u8; 16]);
70
71impl TaskId {
72 pub fn generate() -> Self {
74 use rand::RngCore;
75 let mut bytes = [0u8; 16];
76 rand::thread_rng().fill_bytes(&mut bytes);
77 Self(bytes)
78 }
79
80 pub fn from_bytes(bytes: [u8; 16]) -> Self {
82 Self(bytes)
83 }
84
85 pub fn as_bytes(&self) -> &[u8; 16] {
87 &self.0
88 }
89
90 pub fn to_hex(&self) -> String {
92 hex::encode(self.0)
93 }
94}
95
96impl std::fmt::Display for TaskId {
97 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98 write!(f, "{}", self.to_hex())
99 }
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(tag = "type", rename_all = "snake_case")]
105pub enum CoordinationType {
106 Parallel,
108 Pipeline,
110 Consensus,
112 Supervised,
114 Competitive,
116}
117
118impl std::fmt::Display for CoordinationType {
119 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
120 match self {
121 CoordinationType::Parallel => write!(f, "parallel"),
122 CoordinationType::Pipeline => write!(f, "pipeline"),
123 CoordinationType::Consensus => write!(f, "consensus"),
124 CoordinationType::Supervised => write!(f, "supervised"),
125 CoordinationType::Competitive => write!(f, "competitive"),
126 }
127 }
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(tag = "role", rename_all = "snake_case")]
133pub enum ParticipantRole {
134 Coordinator,
136 Peer,
138 Supervisor,
140 ServiceProvider {
142 service: String,
144 },
145 Observer,
147}
148
149impl ParticipantRole {
150 pub fn service_provider(service: impl Into<String>) -> Self {
152 Self::ServiceProvider {
153 service: service.into(),
154 }
155 }
156
157 pub fn can_execute(&self) -> bool {
159 !matches!(self, ParticipantRole::Observer)
160 }
161
162 pub fn can_supervise(&self) -> bool {
164 matches!(
165 self,
166 ParticipantRole::Coordinator | ParticipantRole::Supervisor
167 )
168 }
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "type", rename_all = "snake_case")]
174pub enum Responsibility {
175 Individual,
177 Shared {
179 share: f64,
181 },
182 Delegated {
184 delegator: PublicKey,
186 },
187 Supervised {
189 supervisor: PublicKey,
191 },
192}
193
194impl Responsibility {
195 pub fn individual() -> Self {
197 Self::Individual
198 }
199
200 pub fn shared(share: f64) -> Self {
202 Self::Shared {
203 share: share.clamp(0.0, 1.0),
204 }
205 }
206
207 pub fn delegated(delegator: PublicKey) -> Self {
209 Self::Delegated { delegator }
210 }
211
212 pub fn supervised(supervisor: PublicKey) -> Self {
214 Self::Supervised { supervisor }
215 }
216
217 pub fn share(&self) -> f64 {
219 match self {
220 Responsibility::Individual => 1.0,
221 Responsibility::Shared { share } => *share,
222 Responsibility::Delegated { .. } => 0.0, Responsibility::Supervised { .. } => 0.0, }
225 }
226}
227
228#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Participant {
231 agent: PublicKey,
233 role: ParticipantRole,
235 capabilities: Vec<CapabilityId>,
237 responsibility: Responsibility,
239 commitment: Sig,
241}
242
243impl Participant {
244 pub fn new(
246 agent: PublicKey,
247 role: ParticipantRole,
248 responsibility: Responsibility,
249 commitment: Sig,
250 ) -> Self {
251 Self {
252 agent,
253 role,
254 capabilities: Vec::new(),
255 responsibility,
256 commitment,
257 }
258 }
259
260 pub fn with_commitment(
266 agent: PublicKey,
267 role: ParticipantRole,
268 responsibility: Responsibility,
269 commitment: Sig,
270 ) -> Self {
271 Self {
272 agent,
273 role,
274 capabilities: Vec::new(),
275 responsibility,
276 commitment,
277 }
278 }
279
280 pub fn with_capabilities(mut self, capabilities: Vec<CapabilityId>) -> Self {
282 self.capabilities = capabilities;
283 self
284 }
285
286 pub fn agent(&self) -> &PublicKey {
288 &self.agent
289 }
290
291 pub fn role(&self) -> &ParticipantRole {
293 &self.role
294 }
295
296 pub fn capabilities(&self) -> &[CapabilityId] {
298 &self.capabilities
299 }
300
301 pub fn responsibility(&self) -> &Responsibility {
303 &self.responsibility
304 }
305
306 pub fn commitment(&self) -> &Sig {
308 &self.commitment
309 }
310
311 pub fn is_coordinator(&self) -> bool {
313 matches!(self.role, ParticipantRole::Coordinator)
314 }
315
316 pub fn verify_commitment(&self, spec: &CoordinatedActionSpec) -> Result<()> {
322 let message = spec.canonical_bytes();
323 self.agent.verify(&message, &self.commitment).map_err(|_| {
324 Error::invalid_input("participant commitment does not verify against spec")
325 })
326 }
327}
328
329#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Task {
332 id: TaskId,
334 description: String,
336 required_capabilities: Vec<CapabilityKind>,
338 deadline: Option<i64>,
340}
341
342impl Task {
343 pub fn new(description: impl Into<String>) -> Self {
345 Self {
346 id: TaskId::generate(),
347 description: description.into(),
348 required_capabilities: Vec::new(),
349 deadline: None,
350 }
351 }
352
353 pub fn with_id(mut self, id: TaskId) -> Self {
355 self.id = id;
356 self
357 }
358
359 pub fn with_capabilities(mut self, capabilities: Vec<CapabilityKind>) -> Self {
361 self.required_capabilities = capabilities;
362 self
363 }
364
365 pub fn with_deadline(mut self, deadline: i64) -> Self {
367 self.deadline = Some(deadline);
368 self
369 }
370
371 pub fn id(&self) -> TaskId {
373 self.id
374 }
375
376 pub fn description(&self) -> &str {
378 &self.description
379 }
380
381 pub fn required_capabilities(&self) -> &[CapabilityKind] {
383 &self.required_capabilities
384 }
385
386 pub fn deadline(&self) -> Option<i64> {
388 self.deadline
389 }
390
391 pub fn is_overdue(&self) -> bool {
393 if let Some(deadline) = self.deadline {
394 chrono::Utc::now().timestamp_millis() > deadline
395 } else {
396 false
397 }
398 }
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct TaskDependency {
404 task: TaskId,
406 depends_on: Vec<TaskId>,
408}
409
410impl TaskDependency {
411 pub fn new(task: TaskId, depends_on: Vec<TaskId>) -> Self {
413 Self { task, depends_on }
414 }
415
416 pub fn task(&self) -> TaskId {
418 self.task
419 }
420
421 pub fn depends_on(&self) -> &[TaskId] {
423 &self.depends_on
424 }
425}
426
427#[derive(Debug, Clone, Serialize, Deserialize)]
429#[serde(tag = "type", rename_all = "snake_case")]
430pub enum FailureHandling {
431 AbortAll,
433 ContinuePartial,
435 Retry {
437 max_attempts: u32,
439 },
440 Escalate,
442}
443
444impl Default for FailureHandling {
445 fn default() -> Self {
446 Self::AbortAll
447 }
448}
449
450#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct CoordinatedActionSpec {
453 goal: String,
455 tasks: HashMap<PublicKey, Vec<Task>>,
457 dependencies: Vec<TaskDependency>,
459 success_criteria: Vec<String>,
461 failure_handling: FailureHandling,
463}
464
465impl CoordinatedActionSpec {
466 pub fn new(goal: impl Into<String>) -> Self {
468 Self {
469 goal: goal.into(),
470 tasks: HashMap::new(),
471 dependencies: Vec::new(),
472 success_criteria: Vec::new(),
473 failure_handling: FailureHandling::default(),
474 }
475 }
476
477 pub fn with_tasks(mut self, agent: &PublicKey, tasks: Vec<Task>) -> Self {
479 self.tasks.insert(agent.clone(), tasks);
480 self
481 }
482
483 pub fn with_dependency(mut self, dependency: TaskDependency) -> Self {
485 self.dependencies.push(dependency);
486 self
487 }
488
489 pub fn with_criterion(mut self, criterion: impl Into<String>) -> Self {
491 self.success_criteria.push(criterion.into());
492 self
493 }
494
495 pub fn with_failure_handling(mut self, handling: FailureHandling) -> Self {
497 self.failure_handling = handling;
498 self
499 }
500
501 pub fn goal(&self) -> &str {
503 &self.goal
504 }
505
506 pub fn tasks_for(&self, agent: &PublicKey) -> Option<&[Task]> {
508 self.tasks.get(agent).map(|v| v.as_slice())
509 }
510
511 pub fn all_tasks(&self) -> impl Iterator<Item = &Task> {
513 self.tasks.values().flat_map(|v| v.iter())
514 }
515
516 pub fn dependencies(&self) -> &[TaskDependency] {
518 &self.dependencies
519 }
520
521 pub fn success_criteria(&self) -> &[String] {
523 &self.success_criteria
524 }
525
526 pub fn failure_handling(&self) -> &FailureHandling {
528 &self.failure_handling
529 }
530
531 pub fn canonical_bytes(&self) -> Vec<u8> {
533 serde_json::to_vec(self).unwrap_or_default()
534 }
535
536 pub fn hash(&self) -> Hash {
538 hash(&self.canonical_bytes())
539 }
540}
541
542#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(tag = "type", rename_all = "snake_case")]
545pub enum CoordinationProtocol {
546 TwoPhaseCommit,
548 Consensus {
550 threshold: f64,
552 },
553 LeaderFollower,
555 Async {
557 timeout: DurationMs,
559 },
560 Custom {
562 protocol_id: String,
564 },
565}
566
567impl CoordinationProtocol {
568 pub fn consensus(threshold: f64) -> Self {
570 Self::Consensus {
571 threshold: threshold.clamp(0.0, 1.0),
572 }
573 }
574
575 pub fn async_with_timeout(timeout: DurationMs) -> Self {
577 Self::Async { timeout }
578 }
579
580 pub fn custom(protocol_id: impl Into<String>) -> Self {
582 Self::Custom {
583 protocol_id: protocol_id.into(),
584 }
585 }
586}
587
588#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct CoordinationMetrics {
591 total_duration: DurationMs,
593 per_agent_duration: HashMap<PublicKey, DurationMs>,
595 communication_overhead: DurationMs,
597 retry_count: u32,
599}
600
601impl CoordinationMetrics {
602 pub fn new(total_duration: DurationMs) -> Self {
604 Self {
605 total_duration,
606 per_agent_duration: HashMap::new(),
607 communication_overhead: 0,
608 retry_count: 0,
609 }
610 }
611
612 pub fn with_agent_duration(mut self, agent: &PublicKey, duration: DurationMs) -> Self {
614 self.per_agent_duration.insert(agent.clone(), duration);
615 self
616 }
617
618 pub fn with_overhead(mut self, overhead: DurationMs) -> Self {
620 self.communication_overhead = overhead;
621 self
622 }
623
624 pub fn with_retries(mut self, count: u32) -> Self {
626 self.retry_count = count;
627 self
628 }
629
630 pub fn total_duration(&self) -> DurationMs {
632 self.total_duration
633 }
634
635 pub fn agent_duration(&self, agent: &PublicKey) -> Option<DurationMs> {
637 self.per_agent_duration.get(agent).copied()
638 }
639
640 pub fn communication_overhead(&self) -> DurationMs {
642 self.communication_overhead
643 }
644
645 pub fn retry_count(&self) -> u32 {
647 self.retry_count
648 }
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct CoordinationResult {
654 outcome: ActionOutcome,
656 agent_outcomes: HashMap<PublicKey, ActionOutcome>,
658 output: serde_json::Value,
660 metrics: CoordinationMetrics,
662}
663
664impl CoordinationResult {
665 pub fn new(
667 outcome: ActionOutcome,
668 output: serde_json::Value,
669 metrics: CoordinationMetrics,
670 ) -> Self {
671 Self {
672 outcome,
673 agent_outcomes: HashMap::new(),
674 output,
675 metrics,
676 }
677 }
678
679 pub fn with_agent_outcome(mut self, agent: &PublicKey, outcome: ActionOutcome) -> Self {
681 self.agent_outcomes.insert(agent.clone(), outcome);
682 self
683 }
684
685 pub fn outcome(&self) -> &ActionOutcome {
687 &self.outcome
688 }
689
690 pub fn agent_outcome(&self, agent: &PublicKey) -> Option<&ActionOutcome> {
692 self.agent_outcomes.get(agent)
693 }
694
695 pub fn output(&self) -> &serde_json::Value {
697 &self.output
698 }
699
700 pub fn metrics(&self) -> &CoordinationMetrics {
702 &self.metrics
703 }
704
705 pub fn is_success(&self) -> bool {
707 self.outcome.is_success()
708 }
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize)]
713#[serde(tag = "status", rename_all = "snake_case")]
714pub enum CoordinationStatus {
715 Initializing,
717 WaitingCommitment,
719 Active {
721 progress: f64,
723 },
724 Completed {
726 result: CoordinationResult,
728 },
729 Failed {
731 reason: String,
733 partial_result: Option<CoordinationResult>,
735 },
736 Aborted {
738 reason: String,
740 aborted_by: PublicKey,
742 },
743}
744
745impl CoordinationStatus {
746 pub fn active(progress: f64) -> Self {
748 Self::Active {
749 progress: progress.clamp(0.0, 1.0),
750 }
751 }
752
753 pub fn completed(result: CoordinationResult) -> Self {
755 Self::Completed { result }
756 }
757
758 pub fn failed(reason: impl Into<String>, partial_result: Option<CoordinationResult>) -> Self {
760 Self::Failed {
761 reason: reason.into(),
762 partial_result,
763 }
764 }
765
766 pub fn aborted(reason: impl Into<String>, aborted_by: PublicKey) -> Self {
768 Self::Aborted {
769 reason: reason.into(),
770 aborted_by,
771 }
772 }
773
774 pub fn is_active(&self) -> bool {
776 matches!(
777 self,
778 CoordinationStatus::Initializing
779 | CoordinationStatus::WaitingCommitment
780 | CoordinationStatus::Active { .. }
781 )
782 }
783
784 pub fn is_terminal(&self) -> bool {
786 !self.is_active()
787 }
788
789 pub fn progress(&self) -> Option<f64> {
791 match self {
792 CoordinationStatus::Active { progress } => Some(*progress),
793 _ => None,
794 }
795 }
796}
797
798#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct CoordinatedAction {
801 id: CoordinationId,
803 coordination_type: CoordinationType,
805 participants: Vec<Participant>,
807 action: CoordinatedActionSpec,
809 protocol: CoordinationProtocol,
811 started_at: i64,
813 status: CoordinationStatus,
815 causal_context: CausalContext,
817}
818
819impl CoordinatedAction {
820 pub fn builder() -> CoordinatedActionBuilder {
822 CoordinatedActionBuilder::new()
823 }
824
825 pub fn id(&self) -> CoordinationId {
827 self.id
828 }
829
830 pub fn coordination_type(&self) -> &CoordinationType {
832 &self.coordination_type
833 }
834
835 pub fn participants(&self) -> &[Participant] {
837 &self.participants
838 }
839
840 pub fn action(&self) -> &CoordinatedActionSpec {
842 &self.action
843 }
844
845 pub fn protocol(&self) -> &CoordinationProtocol {
847 &self.protocol
848 }
849
850 pub fn started_at(&self) -> i64 {
852 self.started_at
853 }
854
855 pub fn status(&self) -> &CoordinationStatus {
857 &self.status
858 }
859
860 pub fn causal_context(&self) -> &CausalContext {
862 &self.causal_context
863 }
864
865 pub fn set_status(&mut self, status: CoordinationStatus) {
867 self.status = status;
868 }
869
870 pub fn coordinator(&self) -> Option<&Participant> {
872 self.participants.iter().find(|p| p.is_coordinator())
873 }
874
875 pub fn validate_coordinator(&self) -> Result<()> {
877 let coordinator_count = self
878 .participants
879 .iter()
880 .filter(|p| p.is_coordinator())
881 .count();
882
883 if coordinator_count != 1 {
884 return Err(Error::invalid_input(format!(
885 "coordination must have exactly one coordinator, found {}",
886 coordinator_count
887 )));
888 }
889
890 Ok(())
891 }
892
893 pub fn validate_commitments(&self) -> Result<()> {
895 for (i, participant) in self.participants.iter().enumerate() {
896 participant.verify_commitment(&self.action).map_err(|_| {
897 Error::invalid_input(format!(
898 "participant {} commitment does not verify against action spec",
899 i
900 ))
901 })?;
902 }
903 Ok(())
904 }
905
906 pub fn validate_responsibility(&self) -> Result<()> {
908 let shared_sum: f64 = self
909 .participants
910 .iter()
911 .filter_map(|p| match &p.responsibility {
912 Responsibility::Shared { share } => Some(*share),
913 _ => None,
914 })
915 .sum();
916
917 if shared_sum > 0.0 && (shared_sum - 1.0).abs() > 0.001 {
919 return Err(Error::invalid_input(format!(
920 "shared responsibility must sum to 1.0, got {}",
921 shared_sum
922 )));
923 }
924
925 Ok(())
926 }
927}
928
929#[derive(Debug, Default)]
931pub struct CoordinatedActionBuilder {
932 id: Option<CoordinationId>,
933 coordination_type: Option<CoordinationType>,
934 participants: Vec<Participant>,
935 action: Option<CoordinatedActionSpec>,
936 protocol: Option<CoordinationProtocol>,
937 started_at: Option<i64>,
938 status: Option<CoordinationStatus>,
939 causal_context: Option<CausalContext>,
940}
941
942impl CoordinatedActionBuilder {
943 pub fn new() -> Self {
945 Self::default()
946 }
947
948 pub fn id(mut self, id: CoordinationId) -> Self {
950 self.id = Some(id);
951 self
952 }
953
954 pub fn coordination_type(mut self, coordination_type: CoordinationType) -> Self {
956 self.coordination_type = Some(coordination_type);
957 self
958 }
959
960 pub fn participant(mut self, participant: Participant) -> Self {
962 self.participants.push(participant);
963 self
964 }
965
966 pub fn action(mut self, action: CoordinatedActionSpec) -> Self {
968 self.action = Some(action);
969 self
970 }
971
972 pub fn protocol(mut self, protocol: CoordinationProtocol) -> Self {
974 self.protocol = Some(protocol);
975 self
976 }
977
978 pub fn started_at(mut self, timestamp: i64) -> Self {
980 self.started_at = Some(timestamp);
981 self
982 }
983
984 pub fn started_now(mut self) -> Self {
986 self.started_at = Some(chrono::Utc::now().timestamp_millis());
987 self
988 }
989
990 pub fn status(mut self, status: CoordinationStatus) -> Self {
992 self.status = Some(status);
993 self
994 }
995
996 pub fn causal_context(mut self, context: CausalContext) -> Self {
998 self.causal_context = Some(context);
999 self
1000 }
1001
1002 pub fn build(self) -> Result<CoordinatedAction> {
1004 let id = self.id.unwrap_or_else(CoordinationId::generate);
1005
1006 let coordination_type = self
1007 .coordination_type
1008 .ok_or_else(|| Error::invalid_input("coordination_type is required"))?;
1009
1010 if self.participants.is_empty() {
1011 return Err(Error::invalid_input("at least one participant is required"));
1012 }
1013
1014 let action = self
1015 .action
1016 .ok_or_else(|| Error::invalid_input("action is required"))?;
1017
1018 let protocol = self
1019 .protocol
1020 .ok_or_else(|| Error::invalid_input("protocol is required"))?;
1021
1022 let started_at = self
1023 .started_at
1024 .unwrap_or_else(|| chrono::Utc::now().timestamp_millis());
1025
1026 let status = self.status.unwrap_or(CoordinationStatus::Initializing);
1027
1028 let causal_context = self
1029 .causal_context
1030 .ok_or_else(|| Error::invalid_input("causal_context is required"))?;
1031
1032 let coordination = CoordinatedAction {
1033 id,
1034 coordination_type,
1035 participants: self.participants,
1036 action,
1037 protocol,
1038 started_at,
1039 status,
1040 causal_context,
1041 };
1042
1043 coordination.validate_coordinator()?;
1045 coordination.validate_responsibility()?;
1046
1047 Ok(coordination)
1048 }
1049
1050 pub fn build_verified(self) -> Result<CoordinatedAction> {
1055 let coordination = self.build()?;
1056 coordination.validate_commitments()?;
1057 Ok(coordination)
1058 }
1059}
1060
1061#[derive(Debug, Clone, Serialize, Deserialize)]
1063#[serde(tag = "type", rename_all = "snake_case")]
1064pub enum CoordinationEvent {
1065 Started {
1067 action: CoordinatedAction,
1069 },
1070 ParticipantJoined {
1072 coordination_id: CoordinationId,
1074 participant: Participant,
1076 },
1077 TaskAssigned {
1079 coordination_id: CoordinationId,
1081 agent: PublicKey,
1083 task: Task,
1085 },
1086 TaskCompleted {
1088 coordination_id: CoordinationId,
1090 task_id: TaskId,
1092 agent: PublicKey,
1094 outcome: ActionOutcome,
1096 },
1097 Message {
1099 coordination_id: CoordinationId,
1101 from: PublicKey,
1103 to: PublicKey,
1105 message_hash: Hash,
1107 },
1108 Disagreement {
1110 coordination_id: CoordinationId,
1112 agents: Vec<PublicKey>,
1114 subject: String,
1116 positions: HashMap<PublicKey, String>,
1118 },
1119 Completed {
1121 coordination_id: CoordinationId,
1123 result: CoordinationResult,
1125 },
1126 Failed {
1128 coordination_id: CoordinationId,
1130 reason: String,
1132 failed_at: i64,
1134 },
1135}
1136
1137impl CoordinationEvent {
1138 pub fn started(action: CoordinatedAction) -> Self {
1140 Self::Started { action }
1141 }
1142
1143 pub fn participant_joined(coordination_id: CoordinationId, participant: Participant) -> Self {
1145 Self::ParticipantJoined {
1146 coordination_id,
1147 participant,
1148 }
1149 }
1150
1151 pub fn task_assigned(coordination_id: CoordinationId, agent: PublicKey, task: Task) -> Self {
1153 Self::TaskAssigned {
1154 coordination_id,
1155 agent,
1156 task,
1157 }
1158 }
1159
1160 pub fn task_completed(
1162 coordination_id: CoordinationId,
1163 task_id: TaskId,
1164 agent: PublicKey,
1165 outcome: ActionOutcome,
1166 ) -> Self {
1167 Self::TaskCompleted {
1168 coordination_id,
1169 task_id,
1170 agent,
1171 outcome,
1172 }
1173 }
1174
1175 pub fn message(
1177 coordination_id: CoordinationId,
1178 from: PublicKey,
1179 to: PublicKey,
1180 message_hash: Hash,
1181 ) -> Self {
1182 Self::Message {
1183 coordination_id,
1184 from,
1185 to,
1186 message_hash,
1187 }
1188 }
1189
1190 pub fn disagreement(
1192 coordination_id: CoordinationId,
1193 agents: Vec<PublicKey>,
1194 subject: impl Into<String>,
1195 ) -> Self {
1196 Self::Disagreement {
1197 coordination_id,
1198 agents,
1199 subject: subject.into(),
1200 positions: HashMap::new(),
1201 }
1202 }
1203
1204 pub fn completed(coordination_id: CoordinationId, result: CoordinationResult) -> Self {
1206 Self::Completed {
1207 coordination_id,
1208 result,
1209 }
1210 }
1211
1212 pub fn failed(coordination_id: CoordinationId, reason: impl Into<String>) -> Self {
1214 Self::Failed {
1215 coordination_id,
1216 reason: reason.into(),
1217 failed_at: chrono::Utc::now().timestamp_millis(),
1218 }
1219 }
1220
1221 pub fn coordination_id(&self) -> CoordinationId {
1223 match self {
1224 CoordinationEvent::Started { action } => action.id(),
1225 CoordinationEvent::ParticipantJoined {
1226 coordination_id, ..
1227 } => *coordination_id,
1228 CoordinationEvent::TaskAssigned {
1229 coordination_id, ..
1230 } => *coordination_id,
1231 CoordinationEvent::TaskCompleted {
1232 coordination_id, ..
1233 } => *coordination_id,
1234 CoordinationEvent::Message {
1235 coordination_id, ..
1236 } => *coordination_id,
1237 CoordinationEvent::Disagreement {
1238 coordination_id, ..
1239 } => *coordination_id,
1240 CoordinationEvent::Completed {
1241 coordination_id, ..
1242 } => *coordination_id,
1243 CoordinationEvent::Failed {
1244 coordination_id, ..
1245 } => *coordination_id,
1246 }
1247 }
1248}
1249
1250#[derive(Debug, Clone, Serialize, Deserialize)]
1254pub struct QuorumPolicy {
1255 pub required_votes: u32,
1257 pub total_participants: u32,
1259 pub timeout_ms: u64,
1261 pub abstentions_count: bool,
1263}
1264
1265impl QuorumPolicy {
1266 pub fn majority(total: u32) -> Self {
1268 Self {
1269 required_votes: total / 2 + 1,
1270 total_participants: total,
1271 timeout_ms: 30_000,
1272 abstentions_count: false,
1273 }
1274 }
1275
1276 pub fn unanimous(total: u32) -> Self {
1278 Self {
1279 required_votes: total,
1280 total_participants: total,
1281 timeout_ms: 60_000,
1282 abstentions_count: false,
1283 }
1284 }
1285
1286 pub fn threshold(required: u32, total: u32) -> Self {
1288 Self {
1289 required_votes: required,
1290 total_participants: total,
1291 timeout_ms: 30_000,
1292 abstentions_count: false,
1293 }
1294 }
1295
1296 pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
1298 self.timeout_ms = timeout_ms;
1299 self
1300 }
1301
1302 pub fn is_met(&self, votes_for: u32) -> bool {
1304 votes_for >= self.required_votes
1305 }
1306}
1307
1308#[derive(Debug, Clone, Serialize, Deserialize)]
1310pub struct Conflict {
1311 pub id: ConflictId,
1313 pub agents: Vec<PublicKey>,
1315 pub resources: Vec<ResourceId>,
1317 pub description: String,
1319 pub detected_at: i64,
1321 pub status: ConflictStatus,
1323}
1324
1325define_id!(ConflictId, "conflict identifier");
1326
1327#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1329#[serde(tag = "status", rename_all = "snake_case")]
1330pub enum ConflictStatus {
1331 Detected,
1333 Resolving,
1335 Resolved {
1337 resolution: Box<ConflictResolutionMethod>,
1339 resolved_at: i64,
1341 },
1342}
1343
1344#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1346#[serde(tag = "method", rename_all = "snake_case")]
1347pub enum ConflictResolutionMethod {
1348 FirstWriter,
1350 Priority { winner: PublicKey },
1352 HumanArbitration { arbiter: PrincipalId },
1354 Merge,
1356 Rollback,
1358}
1359
1360#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1362pub struct CoordinationCostTracker {
1363 pub compute_cost: u64,
1365 pub api_calls: u64,
1367 pub tokens_consumed: u64,
1369 pub budget_limit: Option<u64>,
1371 pub per_agent_costs: std::collections::HashMap<PublicKey, u64>,
1373}
1374
1375impl CoordinationCostTracker {
1376 pub fn new() -> Self {
1378 Self::default()
1379 }
1380
1381 pub fn with_budget(mut self, limit: u64) -> Self {
1383 self.budget_limit = Some(limit);
1384 self
1385 }
1386
1387 pub fn record_cost(&mut self, agent: &PublicKey, cost: u64) {
1389 self.compute_cost = self.compute_cost.saturating_add(cost);
1390 *self.per_agent_costs.entry(agent.clone()).or_default() += cost;
1391 }
1392
1393 pub fn record_api_call(&mut self) {
1395 self.api_calls += 1;
1396 }
1397
1398 pub fn record_tokens(&mut self, tokens: u64) {
1400 self.tokens_consumed = self.tokens_consumed.saturating_add(tokens);
1401 }
1402
1403 pub fn is_over_budget(&self) -> bool {
1405 match self.budget_limit {
1406 Some(limit) => self.compute_cost >= limit,
1407 None => false,
1408 }
1409 }
1410
1411 pub fn remaining_budget(&self) -> Option<u64> {
1413 self.budget_limit
1414 .map(|limit| limit.saturating_sub(self.compute_cost))
1415 }
1416}
1417
1418#[cfg(test)]
1419mod tests {
1420 use super::*;
1421 use crate::agent::principal::PrincipalId;
1422 use crate::agent::session::SessionId;
1423 use crate::crypto::SecretKey;
1424 use crate::event::EventId;
1425
1426 fn test_key() -> SecretKey {
1427 SecretKey::generate()
1428 }
1429
1430 fn test_causal_context() -> CausalContext {
1431 let principal = PrincipalId::user("test@example.com").unwrap();
1432 CausalContext::builder()
1433 .parent_event_id(EventId(hash(b"parent")))
1434 .root_event_id(EventId(hash(b"root")))
1435 .session_id(SessionId::random())
1436 .principal(principal)
1437 .sequence(1)
1438 .depth(1)
1439 .build()
1440 .unwrap()
1441 }
1442
1443 #[test]
1446 fn coordination_id_generates_unique() {
1447 let id1 = CoordinationId::generate();
1448 let id2 = CoordinationId::generate();
1449 assert_ne!(id1, id2);
1450 }
1451
1452 #[test]
1453 fn coordination_id_hex_roundtrip() {
1454 let id = CoordinationId::generate();
1455 let hex = id.to_hex();
1456 let restored = CoordinationId::from_hex(&hex).unwrap();
1457 assert_eq!(id, restored);
1458 }
1459
1460 #[test]
1463 fn task_id_generates_unique() {
1464 let id1 = TaskId::generate();
1465 let id2 = TaskId::generate();
1466 assert_ne!(id1, id2);
1467 }
1468
1469 #[test]
1472 fn coordination_type_display() {
1473 assert_eq!(CoordinationType::Parallel.to_string(), "parallel");
1474 assert_eq!(CoordinationType::Pipeline.to_string(), "pipeline");
1475 assert_eq!(CoordinationType::Consensus.to_string(), "consensus");
1476 }
1477
1478 #[test]
1481 fn participant_role_can_execute() {
1482 assert!(ParticipantRole::Coordinator.can_execute());
1483 assert!(ParticipantRole::Peer.can_execute());
1484 assert!(!ParticipantRole::Observer.can_execute());
1485 }
1486
1487 #[test]
1488 fn participant_role_can_supervise() {
1489 assert!(ParticipantRole::Coordinator.can_supervise());
1490 assert!(ParticipantRole::Supervisor.can_supervise());
1491 assert!(!ParticipantRole::Peer.can_supervise());
1492 assert!(!ParticipantRole::Observer.can_supervise());
1493 }
1494
1495 #[test]
1498 fn responsibility_share() {
1499 assert_eq!(Responsibility::individual().share(), 1.0);
1500 assert_eq!(Responsibility::shared(0.5).share(), 0.5);
1501
1502 let key = test_key();
1503 assert_eq!(Responsibility::delegated(key.public_key()).share(), 0.0);
1504 assert_eq!(Responsibility::supervised(key.public_key()).share(), 0.0);
1505 }
1506
1507 #[test]
1508 fn responsibility_share_clamped() {
1509 assert_eq!(Responsibility::shared(1.5).share(), 1.0);
1510 assert_eq!(Responsibility::shared(-0.5).share(), 0.0);
1511 }
1512
1513 #[test]
1516 fn task_creation() {
1517 let task = Task::new("Process data")
1518 .with_capabilities(vec![CapabilityKind::Read])
1519 .with_deadline(chrono::Utc::now().timestamp_millis() + 60000);
1520
1521 assert_eq!(task.description(), "Process data");
1522 assert_eq!(task.required_capabilities().len(), 1);
1523 assert!(task.deadline().is_some());
1524 }
1525
1526 #[test]
1527 fn task_overdue() {
1528 let past = chrono::Utc::now().timestamp_millis() - 1000;
1529 let task = Task::new("Late task").with_deadline(past);
1530 assert!(task.is_overdue());
1531
1532 let future = chrono::Utc::now().timestamp_millis() + 60000;
1533 let task = Task::new("Future task").with_deadline(future);
1534 assert!(!task.is_overdue());
1535 }
1536
1537 #[test]
1540 fn action_spec_hash_deterministic() {
1541 let spec1 = CoordinatedActionSpec::new("Complete task");
1542 let spec2 = CoordinatedActionSpec::new("Complete task");
1543 assert_eq!(spec1.hash(), spec2.hash());
1544 }
1545
1546 #[test]
1549 fn coordination_status_is_active() {
1550 assert!(CoordinationStatus::Initializing.is_active());
1551 assert!(CoordinationStatus::WaitingCommitment.is_active());
1552 assert!(CoordinationStatus::active(0.5).is_active());
1553
1554 let key = test_key();
1555 assert!(!CoordinationStatus::aborted("test", key.public_key()).is_active());
1556 }
1557
1558 #[test]
1559 fn coordination_status_progress() {
1560 assert_eq!(CoordinationStatus::active(0.75).progress(), Some(0.75));
1561 assert_eq!(CoordinationStatus::Initializing.progress(), None);
1562 }
1563
1564 #[test]
1567 fn coordinated_action_requires_coordinator() {
1568 let key = test_key();
1569 let participant = Participant::new(
1570 key.public_key(),
1571 ParticipantRole::Peer, Responsibility::individual(),
1573 Sig::empty(),
1574 );
1575
1576 let result = CoordinatedAction::builder()
1577 .coordination_type(CoordinationType::Parallel)
1578 .participant(participant)
1579 .action(CoordinatedActionSpec::new("Test"))
1580 .protocol(CoordinationProtocol::TwoPhaseCommit)
1581 .causal_context(test_causal_context())
1582 .started_now()
1583 .build();
1584
1585 assert!(result.is_err());
1586 }
1587
1588 #[test]
1589 fn coordinated_action_valid() {
1590 let coordinator_key = test_key();
1591 let peer_key = test_key();
1592
1593 let coordinator = Participant::new(
1594 coordinator_key.public_key(),
1595 ParticipantRole::Coordinator,
1596 Responsibility::individual(),
1597 Sig::empty(),
1598 );
1599
1600 let peer = Participant::new(
1601 peer_key.public_key(),
1602 ParticipantRole::Peer,
1603 Responsibility::individual(),
1604 Sig::empty(),
1605 );
1606
1607 let action = CoordinatedAction::builder()
1608 .coordination_type(CoordinationType::Parallel)
1609 .participant(coordinator)
1610 .participant(peer)
1611 .action(CoordinatedActionSpec::new("Complete task together"))
1612 .protocol(CoordinationProtocol::TwoPhaseCommit)
1613 .causal_context(test_causal_context())
1614 .started_now()
1615 .build()
1616 .unwrap();
1617
1618 assert!(action.coordinator().is_some());
1619 assert_eq!(action.participants().len(), 2);
1620 }
1621
1622 #[test]
1623 fn coordinated_action_shared_responsibility_must_sum() {
1624 let key1 = test_key();
1625 let key2 = test_key();
1626
1627 let p1 = Participant::new(
1629 key1.public_key(),
1630 ParticipantRole::Coordinator,
1631 Responsibility::shared(0.3),
1632 Sig::empty(),
1633 );
1634
1635 let p2 = Participant::new(
1636 key2.public_key(),
1637 ParticipantRole::Peer,
1638 Responsibility::shared(0.3), Sig::empty(),
1640 );
1641
1642 let result = CoordinatedAction::builder()
1643 .coordination_type(CoordinationType::Parallel)
1644 .participant(p1)
1645 .participant(p2)
1646 .action(CoordinatedActionSpec::new("Test"))
1647 .protocol(CoordinationProtocol::TwoPhaseCommit)
1648 .causal_context(test_causal_context())
1649 .build();
1650
1651 assert!(result.is_err());
1652 }
1653
1654 #[test]
1655 fn coordinated_action_valid_shared_responsibility() {
1656 let key1 = test_key();
1657 let key2 = test_key();
1658
1659 let p1 = Participant::new(
1660 key1.public_key(),
1661 ParticipantRole::Coordinator,
1662 Responsibility::shared(0.6),
1663 Sig::empty(),
1664 );
1665
1666 let p2 = Participant::new(
1667 key2.public_key(),
1668 ParticipantRole::Peer,
1669 Responsibility::shared(0.4), Sig::empty(),
1671 );
1672
1673 let result = CoordinatedAction::builder()
1674 .coordination_type(CoordinationType::Parallel)
1675 .participant(p1)
1676 .participant(p2)
1677 .action(CoordinatedActionSpec::new("Test"))
1678 .protocol(CoordinationProtocol::TwoPhaseCommit)
1679 .causal_context(test_causal_context())
1680 .build();
1681
1682 assert!(result.is_ok());
1683 }
1684
1685 #[test]
1688 fn participant_commitment_verifies_against_spec() {
1689 let key = test_key();
1690 let spec = CoordinatedActionSpec::new("deploy-service");
1691
1692 let spec_bytes = spec.canonical_bytes();
1694 let commitment = key.sign(&spec_bytes);
1695 let participant = Participant::with_commitment(
1696 key.public_key(),
1697 ParticipantRole::Coordinator,
1698 Responsibility::individual(),
1699 commitment,
1700 );
1701
1702 assert!(participant.verify_commitment(&spec).is_ok());
1703 }
1704
1705 #[test]
1706 fn participant_commitment_wrong_spec_rejected() {
1707 let key = test_key();
1708 let spec_a = CoordinatedActionSpec::new("deploy-service");
1709 let spec_b = CoordinatedActionSpec::new("rollback-service");
1710
1711 let commitment = key.sign(&spec_a.canonical_bytes());
1712 let participant = Participant::with_commitment(
1713 key.public_key(),
1714 ParticipantRole::Peer,
1715 Responsibility::individual(),
1716 commitment,
1717 );
1718
1719 assert!(participant.verify_commitment(&spec_b).is_err());
1721 }
1722
1723 #[test]
1724 fn participant_commitment_wrong_key_rejected() {
1725 let real_key = test_key();
1726 let wrong_key = test_key();
1727 let spec = CoordinatedActionSpec::new("deploy-service");
1728
1729 let commitment = real_key.sign(&spec.canonical_bytes());
1730 let participant = Participant::with_commitment(
1731 wrong_key.public_key(), ParticipantRole::Peer,
1733 Responsibility::individual(),
1734 commitment, );
1736
1737 assert!(participant.verify_commitment(&spec).is_err());
1739 }
1740
1741 #[test]
1742 fn participant_empty_commitment_rejected() {
1743 let key = test_key();
1744 let spec = CoordinatedActionSpec::new("deploy-service");
1745
1746 let participant = Participant::new(
1748 key.public_key(),
1749 ParticipantRole::Coordinator,
1750 Responsibility::individual(),
1751 Sig::empty(),
1752 );
1753
1754 assert!(participant.verify_commitment(&spec).is_err());
1755 }
1756
1757 #[test]
1758 fn coordinated_action_build_verified_validates_commitments() {
1759 let coord_key = test_key();
1760 let peer_key = test_key();
1761 let spec = CoordinatedActionSpec::new("deploy");
1762
1763 let coord_commitment = coord_key.sign(&spec.canonical_bytes());
1764 let p1 = Participant::with_commitment(
1765 coord_key.public_key(),
1766 ParticipantRole::Coordinator,
1767 Responsibility::individual(),
1768 coord_commitment,
1769 );
1770
1771 let p2 = Participant::new(
1773 peer_key.public_key(),
1774 ParticipantRole::Peer,
1775 Responsibility::individual(),
1776 Sig::empty(),
1777 );
1778
1779 let result = CoordinatedAction::builder()
1780 .coordination_type(CoordinationType::Supervised)
1781 .participant(p1)
1782 .participant(p2)
1783 .action(spec)
1784 .protocol(CoordinationProtocol::TwoPhaseCommit)
1785 .causal_context(test_causal_context())
1786 .build_verified();
1787
1788 assert!(result.is_err());
1789 }
1790
1791 #[test]
1792 fn coordinated_action_build_verified_succeeds_with_valid_commitments() {
1793 let coord_key = test_key();
1794 let peer_key = test_key();
1795 let spec = CoordinatedActionSpec::new("deploy");
1796
1797 let coord_commitment = coord_key.sign(&spec.canonical_bytes());
1798 let peer_commitment = peer_key.sign(&spec.canonical_bytes());
1799
1800 let p1 = Participant::with_commitment(
1801 coord_key.public_key(),
1802 ParticipantRole::Coordinator,
1803 Responsibility::individual(),
1804 coord_commitment,
1805 );
1806
1807 let p2 = Participant::with_commitment(
1808 peer_key.public_key(),
1809 ParticipantRole::Peer,
1810 Responsibility::individual(),
1811 peer_commitment,
1812 );
1813
1814 let result = CoordinatedAction::builder()
1815 .coordination_type(CoordinationType::Parallel)
1816 .participant(p1)
1817 .participant(p2)
1818 .action(spec)
1819 .protocol(CoordinationProtocol::TwoPhaseCommit)
1820 .causal_context(test_causal_context())
1821 .build_verified();
1822
1823 assert!(result.is_ok());
1824 }
1825
1826 #[test]
1829 fn coordination_event_started() {
1830 let key = test_key();
1831 let coordinator = Participant::new(
1832 key.public_key(),
1833 ParticipantRole::Coordinator,
1834 Responsibility::individual(),
1835 Sig::empty(),
1836 );
1837
1838 let action = CoordinatedAction::builder()
1839 .coordination_type(CoordinationType::Pipeline)
1840 .participant(coordinator)
1841 .action(CoordinatedActionSpec::new("Pipeline task"))
1842 .protocol(CoordinationProtocol::LeaderFollower)
1843 .causal_context(test_causal_context())
1844 .started_now()
1845 .build()
1846 .unwrap();
1847
1848 let coord_id = action.id();
1849 let event = CoordinationEvent::started(action);
1850 assert_eq!(event.coordination_id(), coord_id);
1851 }
1852
1853 #[test]
1854 fn coordination_event_task_completed() {
1855 let key = test_key();
1856 let coord_id = CoordinationId::generate();
1857 let task_id = TaskId::generate();
1858
1859 let event = CoordinationEvent::task_completed(
1860 coord_id,
1861 task_id,
1862 key.public_key(),
1863 ActionOutcome::success(serde_json::json!({"status": "done"})),
1864 );
1865
1866 assert_eq!(event.coordination_id(), coord_id);
1867 }
1868
1869 #[test]
1870 fn coordination_event_message() {
1871 let key1 = test_key();
1872 let key2 = test_key();
1873 let coord_id = CoordinationId::generate();
1874
1875 let event = CoordinationEvent::message(
1876 coord_id,
1877 key1.public_key(),
1878 key2.public_key(),
1879 hash(b"message content"),
1880 );
1881
1882 assert_eq!(event.coordination_id(), coord_id);
1883 }
1884
1885 #[test]
1888 fn coordination_result_with_agent_outcomes() {
1889 let key1 = test_key();
1890 let key2 = test_key();
1891
1892 let result = CoordinationResult::new(
1893 ActionOutcome::success(serde_json::json!({})),
1894 serde_json::json!({"combined": true}),
1895 CoordinationMetrics::new(5000),
1896 )
1897 .with_agent_outcome(
1898 &key1.public_key(),
1899 ActionOutcome::success(serde_json::json!({})),
1900 )
1901 .with_agent_outcome(
1902 &key2.public_key(),
1903 ActionOutcome::success(serde_json::json!({})),
1904 );
1905
1906 assert!(result.is_success());
1907 assert!(result.agent_outcome(&key1.public_key()).is_some());
1908 assert!(result.agent_outcome(&key2.public_key()).is_some());
1909 }
1910
1911 #[test]
1914 fn coordination_metrics() {
1915 let key = test_key();
1916 let metrics = CoordinationMetrics::new(10000)
1917 .with_agent_duration(&key.public_key(), 5000)
1918 .with_overhead(500)
1919 .with_retries(2);
1920
1921 assert_eq!(metrics.total_duration(), 10000);
1922 assert_eq!(metrics.agent_duration(&key.public_key()), Some(5000));
1923 assert_eq!(metrics.communication_overhead(), 500);
1924 assert_eq!(metrics.retry_count(), 2);
1925 }
1926
1927 #[test]
1930 fn coordination_task_lookup_by_public_key() {
1931 let key = test_key();
1932 let task = Task::new("deploy-service");
1933 let spec = CoordinatedActionSpec::new("deploy").with_tasks(&key.public_key(), vec![task]);
1934
1935 let tasks = spec.tasks_for(&key.public_key());
1936 assert!(tasks.is_some());
1937 assert_eq!(tasks.unwrap().len(), 1);
1938 }
1939
1940 #[test]
1941 fn coordination_task_lookup_different_key_returns_none() {
1942 let key1 = test_key();
1943 let key2 = test_key();
1944 let task = Task::new("deploy-service");
1945 let spec = CoordinatedActionSpec::new("deploy").with_tasks(&key1.public_key(), vec![task]);
1946
1947 assert!(spec.tasks_for(&key2.public_key()).is_none());
1948 }
1949
1950 #[test]
1951 fn coordination_result_agent_outcome_by_public_key() {
1952 let key1 = test_key();
1953 let key2 = test_key();
1954 let result = CoordinationResult::new(
1955 ActionOutcome::success(serde_json::json!({})),
1956 serde_json::json!({}),
1957 CoordinationMetrics::new(1000),
1958 )
1959 .with_agent_outcome(
1960 &key1.public_key(),
1961 ActionOutcome::success(serde_json::json!({})),
1962 );
1963
1964 assert!(result.agent_outcome(&key1.public_key()).is_some());
1965 assert!(result.agent_outcome(&key2.public_key()).is_none());
1966 }
1967
1968 #[test]
1969 fn coordination_metrics_agent_duration_by_public_key() {
1970 let key1 = test_key();
1971 let key2 = test_key();
1972 let metrics = CoordinationMetrics::new(5000).with_agent_duration(&key1.public_key(), 3000);
1973
1974 assert_eq!(metrics.agent_duration(&key1.public_key()), Some(3000));
1975 assert_eq!(metrics.agent_duration(&key2.public_key()), None);
1976 }
1977}