Skip to main content

cvkg_core/
agents.rs

1//! # CVKG Multi-Agent Conflict Resolution Subsystem
2//!
3//! This module implements Priority 3.1 of the CVKG roadmap.
4//! It provides identity and priority tracking for UI state mutations,
5//! enabling predictable resolution when multiple AI agents contend for the same state.
6
7use serde::{Deserialize, Serialize};
8
9/// Unique identifier for an AI agent or system component issuing state mutations.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub struct AgentId(pub u64);
12
13impl AgentId {
14    /// Reserved ID for the core framework.
15    pub const SYSTEM: Self = Self(0);
16    /// Reserved ID for direct user input.
17    pub const USER: Self = Self(1);
18    /// Starting ID for dynamic agents.
19    pub const AGENT_START: Self = Self(100);
20}
21
22/// Priority level for conflict resolution. Higher values take precedence.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
24pub struct AgentPriority(pub u32);
25
26impl AgentPriority {
27    pub const LOW: Self = Self(0);
28    pub const NORMAL: Self = Self(100);
29    pub const HIGH: Self = Self(1000);
30    pub const CRITICAL: Self = Self(u32::MAX);
31}
32
33/// Metadata describing the context of a state mutation.
34#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub struct MutationMetadata {
36    pub agent_id: AgentId,
37    pub priority: AgentPriority,
38    pub timestamp_ms: u64,
39}
40
41/// Strategies for resolving concurrent writes to the same piece of state.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum ConflictResolution {
44    /// The most recent write always wins, regardless of priority.
45    #[default]
46    LastWriterWins,
47    /// Writes with higher priority overwrite lower priority ones.
48    /// If priorities are equal, the last writer wins.
49    PriorityWins,
50    /// Indicates that a semantic merge should be attempted (requires custom implementation).
51    Merge,
52}
53
54thread_local! {
55    static CURRENT_AGENT: std::cell::RefCell<Option<MutationMetadata>> = const { std::cell::RefCell::new(None) };
56}
57
58/// Executes a closure within the context of a specific agent.
59/// Any State mutations performed within this block will be attributed to this agent.
60pub fn with_agent<F, R>(agent_id: AgentId, priority: AgentPriority, f: F) -> R
61where
62    F: FnOnce() -> R,
63{
64    let now = std::time::SystemTime::now()
65        .duration_since(std::time::UNIX_EPOCH)
66        .unwrap_or_default()
67        .as_millis() as u64;
68
69    let meta = MutationMetadata {
70        agent_id,
71        priority,
72        timestamp_ms: now,
73    };
74
75    let prev = CURRENT_AGENT.with(|a| a.replace(Some(meta)));
76    let result = f();
77    CURRENT_AGENT.with(|a| a.replace(prev));
78    result
79}
80
81/// A transactional unit of work issued by an AI agent.
82pub struct AgentTransaction<F> {
83    pub agent_id: AgentId,
84    pub priority: AgentPriority,
85    pub mutation: F,
86}
87
88impl<F, R> AgentTransaction<F>
89where
90    F: FnOnce() -> R,
91{
92    pub fn new(agent_id: AgentId, priority: AgentPriority, mutation: F) -> Self {
93        Self {
94            agent_id,
95            priority,
96            mutation,
97        }
98    }
99
100    pub fn execute(self) -> R {
101        with_agent(self.agent_id, self.priority, self.mutation)
102    }
103}
104
105/// Description of a conflict between two agents.
106#[derive(Debug, Clone)]
107pub struct ConflictEvent {
108    pub agent_id: AgentId,
109    pub priority: AgentPriority,
110    pub existing_agent_id: AgentId,
111    pub existing_priority: AgentPriority,
112    pub timestamp_ms: u64,
113}
114
115type ConflictHandlerList =
116    std::sync::Arc<std::sync::Mutex<Vec<Box<dyn Fn(ConflictEvent) + Send + Sync>>>>;
117static CONFLICT_HANDLERS: once_cell::sync::Lazy<ConflictHandlerList> =
118    once_cell::sync::Lazy::new(|| std::sync::Arc::new(std::sync::Mutex::new(Vec::new())));
119
120/// Register a global handler for agentic conflicts.
121pub fn on_conflict<F>(handler: F)
122where
123    F: Fn(ConflictEvent) + Send + Sync + 'static,
124{
125    CONFLICT_HANDLERS.lock().unwrap().push(Box::new(handler));
126}
127
128pub(crate) fn notify_conflict(event: ConflictEvent) {
129    let handlers = CONFLICT_HANDLERS.lock().unwrap();
130    for handler in handlers.iter() {
131        handler(event.clone());
132    }
133}
134
135/// A stable API surface for AI agents to interact with the CVKG framework.
136pub trait AgentSurface: Send + Sync {
137    /// Retrieve the unique identifier of the agent using this surface.
138    fn agent_id(&self) -> AgentId;
139
140    /// Query the current value of a piece of state.
141    fn query<T: Clone + Send + Sync + 'static>(&self, state: &crate::State<T>) -> T;
142
143    /// Update a piece of state with a specific priority.
144    fn update<T: Clone + Send + Sync + 'static>(
145        &self,
146        state: &crate::State<T>,
147        value: T,
148        priority: AgentPriority,
149    );
150
151    /// Execute a set of mutations atomically under this agent's identity.
152    fn transact<R, F>(&self, priority: AgentPriority, f: F) -> R
153    where
154        F: FnOnce() -> R;
155}
156
157/// Standard implementation of the AgentSurface.
158pub struct DefaultAgentSurface {
159    id: AgentId,
160}
161
162impl DefaultAgentSurface {
163    pub fn new(id: AgentId) -> Self {
164        Self { id }
165    }
166}
167
168impl AgentSurface for DefaultAgentSurface {
169    fn agent_id(&self) -> AgentId {
170        self.id
171    }
172
173    fn query<T: Clone + Send + Sync + 'static>(&self, state: &crate::State<T>) -> T {
174        state.get()
175    }
176
177    fn update<T: Clone + Send + Sync + 'static>(
178        &self,
179        state: &crate::State<T>,
180        value: T,
181        priority: AgentPriority,
182    ) {
183        with_agent(self.id, priority, || {
184            state.set(value);
185        });
186    }
187
188    fn transact<R, F>(&self, priority: AgentPriority, f: F) -> R
189    where
190        F: FnOnce() -> R,
191    {
192        with_agent(self.id, priority, f)
193    }
194}
195
196/// Internal helper to retrieve the current agent context.
197pub(crate) fn get_current_mutation_metadata() -> Option<MutationMetadata> {
198    CURRENT_AGENT.with(|a| *a.borrow())
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::State;
205
206    #[test]
207    fn test_conflict_resolution() {
208        let state = State::new(0).with_resolution(ConflictResolution::PriorityWins);
209        let surface_a = DefaultAgentSurface::new(AgentId(101));
210        let surface_b = DefaultAgentSurface::new(AgentId(102));
211
212        // Agent A sets with NORMAL priority
213        surface_a.update(&state, 10, AgentPriority::NORMAL);
214        assert_eq!(state.get(), 10);
215
216        // Agent B tries to set with LOW priority - should be ignored
217        surface_b.update(&state, 20, AgentPriority::LOW);
218        assert_eq!(state.get(), 10); // Still 10
219
220        // Agent B sets with HIGH priority - should win
221        surface_b.update(&state, 30, AgentPriority::HIGH);
222        assert_eq!(state.get(), 30);
223    }
224}