Skip to main content

moloch_core/agent/
coordination.rs

1//! Multi-agent coordination for accountable collaboration.
2//!
3//! Multi-agent coordination answers: "How do we track responsibility when
4//! multiple agents work together?"
5
6use 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
18/// Duration in milliseconds.
19pub type DurationMs = i64;
20
21/// Unique coordination identifier.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
23pub struct CoordinationId(pub [u8; 16]);
24
25impl CoordinationId {
26    /// Generate a new random coordination ID.
27    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    /// Create from bytes.
35    pub fn from_bytes(bytes: [u8; 16]) -> Self {
36        Self(bytes)
37    }
38
39    /// Get the bytes.
40    pub fn as_bytes(&self) -> &[u8; 16] {
41        &self.0
42    }
43
44    /// Convert to hex string.
45    pub fn to_hex(&self) -> String {
46        hex::encode(self.0)
47    }
48
49    /// Parse from hex string.
50    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/// Unique task identifier.
68#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
69pub struct TaskId(pub [u8; 16]);
70
71impl TaskId {
72    /// Generate a new random task ID.
73    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    /// Create from bytes.
81    pub fn from_bytes(bytes: [u8; 16]) -> Self {
82        Self(bytes)
83    }
84
85    /// Get the bytes.
86    pub fn as_bytes(&self) -> &[u8; 16] {
87        &self.0
88    }
89
90    /// Convert to hex string.
91    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/// Type of coordination between agents.
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104#[serde(tag = "type", rename_all = "snake_case")]
105pub enum CoordinationType {
106    /// Agents working on same goal in parallel.
107    Parallel,
108    /// Agents in a pipeline (output → input).
109    Pipeline,
110    /// Agents voting/consensus.
111    Consensus,
112    /// One agent supervising others.
113    Supervised,
114    /// Agents competing (first to succeed wins).
115    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/// Role of a participant in coordination.
131#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
132#[serde(tag = "role", rename_all = "snake_case")]
133pub enum ParticipantRole {
134    /// Leading the coordination.
135    Coordinator,
136    /// Participating as a peer.
137    Peer,
138    /// Supervising the coordination.
139    Supervisor,
140    /// Providing a specific service.
141    ServiceProvider {
142        /// Service being provided.
143        service: String,
144    },
145    /// Observing only.
146    Observer,
147}
148
149impl ParticipantRole {
150    /// Create a service provider role.
151    pub fn service_provider(service: impl Into<String>) -> Self {
152        Self::ServiceProvider {
153            service: service.into(),
154        }
155    }
156
157    /// Check if this role can execute tasks.
158    pub fn can_execute(&self) -> bool {
159        !matches!(self, ParticipantRole::Observer)
160    }
161
162    /// Check if this role can supervise.
163    pub fn can_supervise(&self) -> bool {
164        matches!(
165            self,
166            ParticipantRole::Coordinator | ParticipantRole::Supervisor
167        )
168    }
169}
170
171/// How responsibility is assigned to a participant.
172#[derive(Debug, Clone, Serialize, Deserialize)]
173#[serde(tag = "type", rename_all = "snake_case")]
174pub enum Responsibility {
175    /// Full responsibility for own actions.
176    Individual,
177    /// Shared responsibility with other participants.
178    Shared {
179        /// Share of responsibility (0.0 - 1.0).
180        share: f64,
181    },
182    /// Delegated from another agent.
183    Delegated {
184        /// Agent who delegated responsibility.
185        delegator: PublicKey,
186    },
187    /// Supervised (supervisor bears responsibility).
188    Supervised {
189        /// Supervisor who bears responsibility.
190        supervisor: PublicKey,
191    },
192}
193
194impl Responsibility {
195    /// Create individual responsibility.
196    pub fn individual() -> Self {
197        Self::Individual
198    }
199
200    /// Create shared responsibility.
201    pub fn shared(share: f64) -> Self {
202        Self::Shared {
203            share: share.clamp(0.0, 1.0),
204        }
205    }
206
207    /// Create delegated responsibility.
208    pub fn delegated(delegator: PublicKey) -> Self {
209        Self::Delegated { delegator }
210    }
211
212    /// Create supervised responsibility.
213    pub fn supervised(supervisor: PublicKey) -> Self {
214        Self::Supervised { supervisor }
215    }
216
217    /// Get the responsibility share (1.0 for individual).
218    pub fn share(&self) -> f64 {
219        match self {
220            Responsibility::Individual => 1.0,
221            Responsibility::Shared { share } => *share,
222            Responsibility::Delegated { .. } => 0.0, // Delegator bears responsibility
223            Responsibility::Supervised { .. } => 0.0, // Supervisor bears responsibility
224        }
225    }
226}
227
228/// A participant in a coordinated action.
229#[derive(Debug, Clone, Serialize, Deserialize)]
230pub struct Participant {
231    /// Agent identity.
232    agent: PublicKey,
233    /// Role in the coordination.
234    role: ParticipantRole,
235    /// Capabilities this agent is contributing.
236    capabilities: Vec<CapabilityId>,
237    /// Responsibility assignment.
238    responsibility: Responsibility,
239    /// Agent's commitment (signature on coordination spec).
240    commitment: Sig,
241}
242
243impl Participant {
244    /// Create a new participant.
245    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    /// Create a new participant with a commitment signature.
261    ///
262    /// This is the preferred constructor. The `commitment` should be a signature
263    /// over the [`CoordinatedActionSpec::canonical_bytes()`] using the agent's secret key,
264    /// per INV-COORD-2.
265    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    /// Add capabilities.
281    pub fn with_capabilities(mut self, capabilities: Vec<CapabilityId>) -> Self {
282        self.capabilities = capabilities;
283        self
284    }
285
286    /// Get the agent.
287    pub fn agent(&self) -> &PublicKey {
288        &self.agent
289    }
290
291    /// Get the role.
292    pub fn role(&self) -> &ParticipantRole {
293        &self.role
294    }
295
296    /// Get the capabilities.
297    pub fn capabilities(&self) -> &[CapabilityId] {
298        &self.capabilities
299    }
300
301    /// Get the responsibility.
302    pub fn responsibility(&self) -> &Responsibility {
303        &self.responsibility
304    }
305
306    /// Get the commitment signature.
307    pub fn commitment(&self) -> &Sig {
308        &self.commitment
309    }
310
311    /// Check if this participant is the coordinator.
312    pub fn is_coordinator(&self) -> bool {
313        matches!(self.role, ParticipantRole::Coordinator)
314    }
315
316    /// Verify that this participant's commitment is a valid signature
317    /// over the given action specification, using the participant's own agent key.
318    ///
319    /// Enforces INV-COORD-2: every participant's commitment must cryptographically
320    /// verify against the coordination's action specification.
321    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/// A task within a coordinated action.
330#[derive(Debug, Clone, Serialize, Deserialize)]
331pub struct Task {
332    /// Task identifier.
333    id: TaskId,
334    /// Description of the task.
335    description: String,
336    /// Required capabilities.
337    required_capabilities: Vec<CapabilityKind>,
338    /// Deadline for the task (Unix timestamp ms).
339    deadline: Option<i64>,
340}
341
342impl Task {
343    /// Create a new task.
344    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    /// Create with a specific ID.
354    pub fn with_id(mut self, id: TaskId) -> Self {
355        self.id = id;
356        self
357    }
358
359    /// Set required capabilities.
360    pub fn with_capabilities(mut self, capabilities: Vec<CapabilityKind>) -> Self {
361        self.required_capabilities = capabilities;
362        self
363    }
364
365    /// Set deadline.
366    pub fn with_deadline(mut self, deadline: i64) -> Self {
367        self.deadline = Some(deadline);
368        self
369    }
370
371    /// Get the ID.
372    pub fn id(&self) -> TaskId {
373        self.id
374    }
375
376    /// Get the description.
377    pub fn description(&self) -> &str {
378        &self.description
379    }
380
381    /// Get required capabilities.
382    pub fn required_capabilities(&self) -> &[CapabilityKind] {
383        &self.required_capabilities
384    }
385
386    /// Get the deadline.
387    pub fn deadline(&self) -> Option<i64> {
388        self.deadline
389    }
390
391    /// Check if the task is overdue.
392    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/// Dependency between tasks.
402#[derive(Debug, Clone, Serialize, Deserialize)]
403pub struct TaskDependency {
404    /// Task that has dependencies.
405    task: TaskId,
406    /// Tasks this task depends on.
407    depends_on: Vec<TaskId>,
408}
409
410impl TaskDependency {
411    /// Create a new task dependency.
412    pub fn new(task: TaskId, depends_on: Vec<TaskId>) -> Self {
413        Self { task, depends_on }
414    }
415
416    /// Get the task.
417    pub fn task(&self) -> TaskId {
418        self.task
419    }
420
421    /// Get dependencies.
422    pub fn depends_on(&self) -> &[TaskId] {
423        &self.depends_on
424    }
425}
426
427/// What happens if coordination fails.
428#[derive(Debug, Clone, Serialize, Deserialize)]
429#[serde(tag = "type", rename_all = "snake_case")]
430pub enum FailureHandling {
431    /// Abort entire coordination.
432    AbortAll,
433    /// Continue with partial results.
434    ContinuePartial,
435    /// Retry failed tasks.
436    Retry {
437        /// Maximum retry attempts.
438        max_attempts: u32,
439    },
440    /// Escalate to humans.
441    Escalate,
442}
443
444impl Default for FailureHandling {
445    fn default() -> Self {
446        Self::AbortAll
447    }
448}
449
450/// Specification of a coordinated action.
451#[derive(Debug, Clone, Serialize, Deserialize)]
452pub struct CoordinatedActionSpec {
453    /// What the group is trying to accomplish.
454    goal: String,
455    /// Sub-tasks assigned to each participant.
456    tasks: HashMap<PublicKey, Vec<Task>>,
457    /// Dependencies between tasks.
458    dependencies: Vec<TaskDependency>,
459    /// Success criteria for the coordination.
460    success_criteria: Vec<String>,
461    /// What happens if coordination fails.
462    failure_handling: FailureHandling,
463}
464
465impl CoordinatedActionSpec {
466    /// Create a new coordinated action spec.
467    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    /// Add tasks for a participant.
478    pub fn with_tasks(mut self, agent: &PublicKey, tasks: Vec<Task>) -> Self {
479        self.tasks.insert(agent.clone(), tasks);
480        self
481    }
482
483    /// Add a dependency.
484    pub fn with_dependency(mut self, dependency: TaskDependency) -> Self {
485        self.dependencies.push(dependency);
486        self
487    }
488
489    /// Add a success criterion.
490    pub fn with_criterion(mut self, criterion: impl Into<String>) -> Self {
491        self.success_criteria.push(criterion.into());
492        self
493    }
494
495    /// Set failure handling.
496    pub fn with_failure_handling(mut self, handling: FailureHandling) -> Self {
497        self.failure_handling = handling;
498        self
499    }
500
501    /// Get the goal.
502    pub fn goal(&self) -> &str {
503        &self.goal
504    }
505
506    /// Get tasks for a participant.
507    pub fn tasks_for(&self, agent: &PublicKey) -> Option<&[Task]> {
508        self.tasks.get(agent).map(|v| v.as_slice())
509    }
510
511    /// Get all tasks.
512    pub fn all_tasks(&self) -> impl Iterator<Item = &Task> {
513        self.tasks.values().flat_map(|v| v.iter())
514    }
515
516    /// Get dependencies.
517    pub fn dependencies(&self) -> &[TaskDependency] {
518        &self.dependencies
519    }
520
521    /// Get success criteria.
522    pub fn success_criteria(&self) -> &[String] {
523        &self.success_criteria
524    }
525
526    /// Get failure handling.
527    pub fn failure_handling(&self) -> &FailureHandling {
528        &self.failure_handling
529    }
530
531    /// Compute canonical bytes for commitment signing and verification.
532    pub fn canonical_bytes(&self) -> Vec<u8> {
533        serde_json::to_vec(self).unwrap_or_default()
534    }
535
536    /// Compute a hash of this spec for signing.
537    pub fn hash(&self) -> Hash {
538        hash(&self.canonical_bytes())
539    }
540}
541
542/// Coordination protocol used.
543#[derive(Debug, Clone, Serialize, Deserialize)]
544#[serde(tag = "type", rename_all = "snake_case")]
545pub enum CoordinationProtocol {
546    /// Simple two-phase commit.
547    TwoPhaseCommit,
548    /// Multi-agent consensus.
549    Consensus {
550        /// Threshold for consensus (0.0 - 1.0).
551        threshold: f64,
552    },
553    /// Leader-based coordination.
554    LeaderFollower,
555    /// Asynchronous coordination.
556    Async {
557        /// Timeout in milliseconds.
558        timeout: DurationMs,
559    },
560    /// Custom protocol.
561    Custom {
562        /// Protocol identifier.
563        protocol_id: String,
564    },
565}
566
567impl CoordinationProtocol {
568    /// Create a consensus protocol.
569    pub fn consensus(threshold: f64) -> Self {
570        Self::Consensus {
571            threshold: threshold.clamp(0.0, 1.0),
572        }
573    }
574
575    /// Create an async protocol.
576    pub fn async_with_timeout(timeout: DurationMs) -> Self {
577        Self::Async { timeout }
578    }
579
580    /// Create a custom protocol.
581    pub fn custom(protocol_id: impl Into<String>) -> Self {
582        Self::Custom {
583            protocol_id: protocol_id.into(),
584        }
585    }
586}
587
588/// Metrics about coordination execution.
589#[derive(Debug, Clone, Serialize, Deserialize)]
590pub struct CoordinationMetrics {
591    /// Total duration in milliseconds.
592    total_duration: DurationMs,
593    /// Per-agent duration.
594    per_agent_duration: HashMap<PublicKey, DurationMs>,
595    /// Communication overhead in milliseconds.
596    communication_overhead: DurationMs,
597    /// Number of retries.
598    retry_count: u32,
599}
600
601impl CoordinationMetrics {
602    /// Create new metrics.
603    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    /// Set agent duration.
613    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    /// Set communication overhead.
619    pub fn with_overhead(mut self, overhead: DurationMs) -> Self {
620        self.communication_overhead = overhead;
621        self
622    }
623
624    /// Set retry count.
625    pub fn with_retries(mut self, count: u32) -> Self {
626        self.retry_count = count;
627        self
628    }
629
630    /// Get total duration.
631    pub fn total_duration(&self) -> DurationMs {
632        self.total_duration
633    }
634
635    /// Get agent duration.
636    pub fn agent_duration(&self, agent: &PublicKey) -> Option<DurationMs> {
637        self.per_agent_duration.get(agent).copied()
638    }
639
640    /// Get communication overhead.
641    pub fn communication_overhead(&self) -> DurationMs {
642        self.communication_overhead
643    }
644
645    /// Get retry count.
646    pub fn retry_count(&self) -> u32 {
647        self.retry_count
648    }
649}
650
651/// Result of a coordination.
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct CoordinationResult {
654    /// Overall outcome.
655    outcome: ActionOutcome,
656    /// Per-agent outcomes.
657    agent_outcomes: HashMap<PublicKey, ActionOutcome>,
658    /// Combined output.
659    output: serde_json::Value,
660    /// Coordination metrics.
661    metrics: CoordinationMetrics,
662}
663
664impl CoordinationResult {
665    /// Create a new coordination result.
666    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    /// Add an agent outcome.
680    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    /// Get the overall outcome.
686    pub fn outcome(&self) -> &ActionOutcome {
687        &self.outcome
688    }
689
690    /// Get agent outcome.
691    pub fn agent_outcome(&self, agent: &PublicKey) -> Option<&ActionOutcome> {
692        self.agent_outcomes.get(agent)
693    }
694
695    /// Get the output.
696    pub fn output(&self) -> &serde_json::Value {
697        &self.output
698    }
699
700    /// Get the metrics.
701    pub fn metrics(&self) -> &CoordinationMetrics {
702        &self.metrics
703    }
704
705    /// Check if coordination succeeded.
706    pub fn is_success(&self) -> bool {
707        self.outcome.is_success()
708    }
709}
710
711/// Status of a coordination.
712#[derive(Debug, Clone, Serialize, Deserialize)]
713#[serde(tag = "status", rename_all = "snake_case")]
714pub enum CoordinationStatus {
715    /// Coordination is being set up.
716    Initializing,
717    /// Waiting for all participants to commit.
718    WaitingCommitment,
719    /// Coordination is active.
720    Active {
721        /// Progress (0.0 - 1.0).
722        progress: f64,
723    },
724    /// All tasks completed successfully.
725    Completed {
726        /// Coordination result.
727        result: CoordinationResult,
728    },
729    /// Coordination failed.
730    Failed {
731        /// Reason for failure.
732        reason: String,
733        /// Partial result if any.
734        partial_result: Option<CoordinationResult>,
735    },
736    /// Coordination was aborted.
737    Aborted {
738        /// Reason for abort.
739        reason: String,
740        /// Who aborted (agent public key).
741        aborted_by: PublicKey,
742    },
743}
744
745impl CoordinationStatus {
746    /// Create an active status.
747    pub fn active(progress: f64) -> Self {
748        Self::Active {
749            progress: progress.clamp(0.0, 1.0),
750        }
751    }
752
753    /// Create a completed status.
754    pub fn completed(result: CoordinationResult) -> Self {
755        Self::Completed { result }
756    }
757
758    /// Create a failed status.
759    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    /// Create an aborted status.
767    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    /// Check if coordination is in progress.
775    pub fn is_active(&self) -> bool {
776        matches!(
777            self,
778            CoordinationStatus::Initializing
779                | CoordinationStatus::WaitingCommitment
780                | CoordinationStatus::Active { .. }
781        )
782    }
783
784    /// Check if coordination is complete (succeeded or failed).
785    pub fn is_terminal(&self) -> bool {
786        !self.is_active()
787    }
788
789    /// Get progress if active.
790    pub fn progress(&self) -> Option<f64> {
791        match self {
792            CoordinationStatus::Active { progress } => Some(*progress),
793            _ => None,
794        }
795    }
796}
797
798/// A coordinated action involving multiple agents.
799#[derive(Debug, Clone, Serialize, Deserialize)]
800pub struct CoordinatedAction {
801    /// Unique coordination identifier.
802    id: CoordinationId,
803    /// Type of coordination.
804    coordination_type: CoordinationType,
805    /// Participating agents.
806    participants: Vec<Participant>,
807    /// The coordinated action specification.
808    action: CoordinatedActionSpec,
809    /// Coordination protocol used.
810    protocol: CoordinationProtocol,
811    /// When coordination started (Unix timestamp ms).
812    started_at: i64,
813    /// Current status.
814    status: CoordinationStatus,
815    /// Combined causal context.
816    causal_context: CausalContext,
817}
818
819impl CoordinatedAction {
820    /// Create a new coordinated action builder.
821    pub fn builder() -> CoordinatedActionBuilder {
822        CoordinatedActionBuilder::new()
823    }
824
825    /// Get the ID.
826    pub fn id(&self) -> CoordinationId {
827        self.id
828    }
829
830    /// Get the coordination type.
831    pub fn coordination_type(&self) -> &CoordinationType {
832        &self.coordination_type
833    }
834
835    /// Get the participants.
836    pub fn participants(&self) -> &[Participant] {
837        &self.participants
838    }
839
840    /// Get the action specification.
841    pub fn action(&self) -> &CoordinatedActionSpec {
842        &self.action
843    }
844
845    /// Get the protocol.
846    pub fn protocol(&self) -> &CoordinationProtocol {
847        &self.protocol
848    }
849
850    /// Get the start time.
851    pub fn started_at(&self) -> i64 {
852        self.started_at
853    }
854
855    /// Get the status.
856    pub fn status(&self) -> &CoordinationStatus {
857        &self.status
858    }
859
860    /// Get the causal context.
861    pub fn causal_context(&self) -> &CausalContext {
862        &self.causal_context
863    }
864
865    /// Update the status.
866    pub fn set_status(&mut self, status: CoordinationStatus) {
867        self.status = status;
868    }
869
870    /// Get the coordinator participant.
871    pub fn coordinator(&self) -> Option<&Participant> {
872        self.participants.iter().find(|p| p.is_coordinator())
873    }
874
875    /// Validate that the coordination has exactly one coordinator per rule 10.3.1.
876    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    /// Validate that all participant commitments verify against the action spec per INV-COORD-2.
894    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    /// Validate that shared responsibility sums to 1.0 per rule 10.3.3.
907    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 there are shared responsibilities, they must sum to 1.0
918        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/// Builder for CoordinatedAction.
930#[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    /// Create a new builder.
944    pub fn new() -> Self {
945        Self::default()
946    }
947
948    /// Set the ID.
949    pub fn id(mut self, id: CoordinationId) -> Self {
950        self.id = Some(id);
951        self
952    }
953
954    /// Set the coordination type.
955    pub fn coordination_type(mut self, coordination_type: CoordinationType) -> Self {
956        self.coordination_type = Some(coordination_type);
957        self
958    }
959
960    /// Add a participant.
961    pub fn participant(mut self, participant: Participant) -> Self {
962        self.participants.push(participant);
963        self
964    }
965
966    /// Set the action specification.
967    pub fn action(mut self, action: CoordinatedActionSpec) -> Self {
968        self.action = Some(action);
969        self
970    }
971
972    /// Set the protocol.
973    pub fn protocol(mut self, protocol: CoordinationProtocol) -> Self {
974        self.protocol = Some(protocol);
975        self
976    }
977
978    /// Set the start time.
979    pub fn started_at(mut self, timestamp: i64) -> Self {
980        self.started_at = Some(timestamp);
981        self
982    }
983
984    /// Set started to now.
985    pub fn started_now(mut self) -> Self {
986        self.started_at = Some(chrono::Utc::now().timestamp_millis());
987        self
988    }
989
990    /// Set the status.
991    pub fn status(mut self, status: CoordinationStatus) -> Self {
992        self.status = Some(status);
993        self
994    }
995
996    /// Set the causal context.
997    pub fn causal_context(mut self, context: CausalContext) -> Self {
998        self.causal_context = Some(context);
999        self
1000    }
1001
1002    /// Build the coordinated action.
1003    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        // Validate
1044        coordination.validate_coordinator()?;
1045        coordination.validate_responsibility()?;
1046
1047        Ok(coordination)
1048    }
1049
1050    /// Build with full commitment verification per INV-COORD-2.
1051    ///
1052    /// Like [`build`](Self::build), but additionally validates that every participant's
1053    /// commitment signature verifies against the action specification.
1054    pub fn build_verified(self) -> Result<CoordinatedAction> {
1055        let coordination = self.build()?;
1056        coordination.validate_commitments()?;
1057        Ok(coordination)
1058    }
1059}
1060
1061/// Event recording coordination lifecycle.
1062#[derive(Debug, Clone, Serialize, Deserialize)]
1063#[serde(tag = "type", rename_all = "snake_case")]
1064pub enum CoordinationEvent {
1065    /// Coordination initiated.
1066    Started {
1067        /// The coordinated action.
1068        action: CoordinatedAction,
1069    },
1070    /// Participant joined.
1071    ParticipantJoined {
1072        /// Coordination ID.
1073        coordination_id: CoordinationId,
1074        /// Participant who joined.
1075        participant: Participant,
1076    },
1077    /// Task assigned.
1078    TaskAssigned {
1079        /// Coordination ID.
1080        coordination_id: CoordinationId,
1081        /// Agent assigned the task.
1082        agent: PublicKey,
1083        /// The task.
1084        task: Task,
1085    },
1086    /// Task completed.
1087    TaskCompleted {
1088        /// Coordination ID.
1089        coordination_id: CoordinationId,
1090        /// Task ID.
1091        task_id: TaskId,
1092        /// Agent who completed.
1093        agent: PublicKey,
1094        /// Outcome.
1095        outcome: ActionOutcome,
1096    },
1097    /// Agent-to-agent message.
1098    Message {
1099        /// Coordination ID.
1100        coordination_id: CoordinationId,
1101        /// Sender.
1102        from: PublicKey,
1103        /// Recipient.
1104        to: PublicKey,
1105        /// Hash of message content.
1106        message_hash: Hash,
1107    },
1108    /// Disagreement between agents.
1109    Disagreement {
1110        /// Coordination ID.
1111        coordination_id: CoordinationId,
1112        /// Agents involved.
1113        agents: Vec<PublicKey>,
1114        /// Subject of disagreement.
1115        subject: String,
1116        /// Positions held by each agent.
1117        positions: HashMap<PublicKey, String>,
1118    },
1119    /// Coordination completed.
1120    Completed {
1121        /// Coordination ID.
1122        coordination_id: CoordinationId,
1123        /// Result.
1124        result: CoordinationResult,
1125    },
1126    /// Coordination failed.
1127    Failed {
1128        /// Coordination ID.
1129        coordination_id: CoordinationId,
1130        /// Reason.
1131        reason: String,
1132        /// When failed (Unix timestamp ms).
1133        failed_at: i64,
1134    },
1135}
1136
1137impl CoordinationEvent {
1138    /// Create a started event.
1139    pub fn started(action: CoordinatedAction) -> Self {
1140        Self::Started { action }
1141    }
1142
1143    /// Create a participant joined event.
1144    pub fn participant_joined(coordination_id: CoordinationId, participant: Participant) -> Self {
1145        Self::ParticipantJoined {
1146            coordination_id,
1147            participant,
1148        }
1149    }
1150
1151    /// Create a task assigned event.
1152    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    /// Create a task completed event.
1161    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    /// Create a message event.
1176    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    /// Create a disagreement event.
1191    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    /// Create a completed event.
1205    pub fn completed(coordination_id: CoordinationId, result: CoordinationResult) -> Self {
1206        Self::Completed {
1207            coordination_id,
1208            result,
1209        }
1210    }
1211
1212    /// Create a failed event.
1213    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    /// Get the coordination ID from any event variant.
1222    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/// Quorum policy for consensus-based coordination (G-7.1).
1251///
1252/// Defines the threshold for agreement in multi-agent coordination.
1253#[derive(Debug, Clone, Serialize, Deserialize)]
1254pub struct QuorumPolicy {
1255    /// Minimum number of agents that must agree.
1256    pub required_votes: u32,
1257    /// Total number of agents participating.
1258    pub total_participants: u32,
1259    /// Maximum time to wait for votes (milliseconds).
1260    pub timeout_ms: u64,
1261    /// Whether abstentions count toward quorum.
1262    pub abstentions_count: bool,
1263}
1264
1265impl QuorumPolicy {
1266    /// Create a majority quorum (> 50%).
1267    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    /// Create a unanimous quorum (100%).
1277    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    /// Create a custom threshold quorum.
1287    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    /// Set the timeout.
1297    pub fn with_timeout(mut self, timeout_ms: u64) -> Self {
1298        self.timeout_ms = timeout_ms;
1299        self
1300    }
1301
1302    /// Check if the quorum is met given the number of votes.
1303    pub fn is_met(&self, votes_for: u32) -> bool {
1304        votes_for >= self.required_votes
1305    }
1306}
1307
1308/// A conflict between agents operating on shared resources (G-7.2).
1309#[derive(Debug, Clone, Serialize, Deserialize)]
1310pub struct Conflict {
1311    /// Unique conflict identifier.
1312    pub id: ConflictId,
1313    /// Agents involved in the conflict.
1314    pub agents: Vec<PublicKey>,
1315    /// Resources under contention.
1316    pub resources: Vec<ResourceId>,
1317    /// Description of the conflict.
1318    pub description: String,
1319    /// When the conflict was detected (Unix ms).
1320    pub detected_at: i64,
1321    /// Current resolution status.
1322    pub status: ConflictStatus,
1323}
1324
1325define_id!(ConflictId, "conflict identifier");
1326
1327/// Status of a conflict.
1328#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1329#[serde(tag = "status", rename_all = "snake_case")]
1330pub enum ConflictStatus {
1331    /// Conflict detected, awaiting resolution.
1332    Detected,
1333    /// Resolution in progress.
1334    Resolving,
1335    /// Conflict resolved.
1336    Resolved {
1337        /// How it was resolved.
1338        resolution: Box<ConflictResolutionMethod>,
1339        /// When it was resolved (Unix ms).
1340        resolved_at: i64,
1341    },
1342}
1343
1344/// Methods for resolving agent conflicts.
1345#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1346#[serde(tag = "method", rename_all = "snake_case")]
1347pub enum ConflictResolutionMethod {
1348    /// First agent to claim wins.
1349    FirstWriter,
1350    /// Priority-based (higher priority agent wins).
1351    Priority { winner: PublicKey },
1352    /// Human arbitration.
1353    HumanArbitration { arbiter: PrincipalId },
1354    /// Agents merge their changes.
1355    Merge,
1356    /// All conflicting changes rolled back.
1357    Rollback,
1358}
1359
1360/// Cost tracking for multi-agent coordination (G-10.1).
1361#[derive(Debug, Clone, Default, Serialize, Deserialize)]
1362pub struct CoordinationCostTracker {
1363    /// Total compute cost (in abstract units).
1364    pub compute_cost: u64,
1365    /// Total API call count.
1366    pub api_calls: u64,
1367    /// Total tokens consumed (for LLM agents).
1368    pub tokens_consumed: u64,
1369    /// Budget limit (None = unlimited).
1370    pub budget_limit: Option<u64>,
1371    /// Cost entries per agent.
1372    pub per_agent_costs: std::collections::HashMap<PublicKey, u64>,
1373}
1374
1375impl CoordinationCostTracker {
1376    /// Create a new tracker with optional budget.
1377    pub fn new() -> Self {
1378        Self::default()
1379    }
1380
1381    /// Set a budget limit.
1382    pub fn with_budget(mut self, limit: u64) -> Self {
1383        self.budget_limit = Some(limit);
1384        self
1385    }
1386
1387    /// Record a cost for an agent.
1388    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    /// Record an API call.
1394    pub fn record_api_call(&mut self) {
1395        self.api_calls += 1;
1396    }
1397
1398    /// Record token consumption.
1399    pub fn record_tokens(&mut self, tokens: u64) {
1400        self.tokens_consumed = self.tokens_consumed.saturating_add(tokens);
1401    }
1402
1403    /// Check if the budget is exceeded.
1404    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    /// Get remaining budget (None if unlimited).
1412    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    // === CoordinationId Tests ===
1444
1445    #[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    // === TaskId Tests ===
1461
1462    #[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    // === CoordinationType Tests ===
1470
1471    #[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    // === ParticipantRole Tests ===
1479
1480    #[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    // === Responsibility Tests ===
1496
1497    #[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    // === Task Tests ===
1514
1515    #[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    // === CoordinatedActionSpec Tests ===
1538
1539    #[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    // === CoordinationStatus Tests ===
1547
1548    #[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    // === CoordinatedAction Tests ===
1565
1566    #[test]
1567    fn coordinated_action_requires_coordinator() {
1568        let key = test_key();
1569        let participant = Participant::new(
1570            key.public_key(),
1571            ParticipantRole::Peer, // Not a coordinator
1572            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        // Shared responsibilities that don't sum to 1.0
1628        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), // Total = 0.6, not 1.0
1639            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), // Total = 1.0
1670            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    // === Commitment Verification Tests (Phase 1, Finding 1.2) ===
1686
1687    #[test]
1688    fn participant_commitment_verifies_against_spec() {
1689        let key = test_key();
1690        let spec = CoordinatedActionSpec::new("deploy-service");
1691
1692        // Create participant with proper commitment (signature over spec)
1693        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        // Commitment was for spec_a, verifying against spec_b must fail
1720        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(), // Claims wrong_key identity
1732            ParticipantRole::Peer,
1733            Responsibility::individual(),
1734            commitment, // Signed by real_key
1735        );
1736
1737        // Agent key doesn't match the commitment signer
1738        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        // Using Sig::empty() as commitment should fail verification
1747        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        // Peer has empty (invalid) commitment
1772        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    // === CoordinationEvent Tests ===
1827
1828    #[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    // === CoordinationResult Tests ===
1886
1887    #[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    // === CoordinationMetrics Tests ===
1912
1913    #[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    // === Phase 9: Type-safe key lookups ===
1928
1929    #[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}