cellstate_core/
agent.rs

1//! Agent types for multi-agent coordination.
2//!
3//! This module contains agent identity, memory access control, and message types
4//! that were consolidated from cellstate-agents into cellstate-core.
5
6use crate::event::EvidenceRef;
7use crate::{
8    identity::EntityIdType, AbstractionLevel, ActionId, AgentId, BeliefId, EnumParseError, GoalId,
9    LearningId, ObservationId, PlanId, StepId, Timestamp, TrajectoryId,
10};
11use serde::{Deserialize, Serialize};
12use std::fmt;
13use std::str::FromStr;
14use uuid::Uuid;
15
16// ============================================================================
17// MESSAGE TYPES (from cellstate-agents)
18// ============================================================================
19
20/// Type of agent message.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
24pub enum MessageType {
25    /// Task delegation request
26    TaskDelegation,
27    /// Result of a delegated task
28    TaskResult,
29    /// Request for context from another agent
30    ContextRequest,
31    /// Sharing context with another agent
32    ContextShare,
33    /// Coordination signal (e.g., ready, waiting)
34    CoordinationSignal,
35    /// Handoff request
36    Handoff,
37    /// Interrupt signal
38    Interrupt,
39    /// Heartbeat/keepalive
40    Heartbeat,
41}
42
43impl MessageType {
44    /// Convert to database string representation.
45    pub fn as_db_str(&self) -> &'static str {
46        match self {
47            MessageType::TaskDelegation => "task_delegation",
48            MessageType::TaskResult => "task_result",
49            MessageType::ContextRequest => "context_request",
50            MessageType::ContextShare => "context_share",
51            MessageType::CoordinationSignal => "coordination_signal",
52            MessageType::Handoff => "handoff",
53            MessageType::Interrupt => "interrupt",
54            MessageType::Heartbeat => "heartbeat",
55        }
56    }
57
58    /// Parse from database string representation.
59    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
60        match s.to_lowercase().replace('_', "").as_str() {
61            "taskdelegation" => Ok(MessageType::TaskDelegation),
62            "taskresult" => Ok(MessageType::TaskResult),
63            "contextrequest" => Ok(MessageType::ContextRequest),
64            "contextshare" => Ok(MessageType::ContextShare),
65            "coordinationsignal" => Ok(MessageType::CoordinationSignal),
66            "handoff" => Ok(MessageType::Handoff),
67            "interrupt" => Ok(MessageType::Interrupt),
68            "heartbeat" => Ok(MessageType::Heartbeat),
69            _ => Err(EnumParseError {
70                enum_name: "MessageType",
71                input: s.to_string(),
72            }),
73        }
74    }
75}
76
77impl fmt::Display for MessageType {
78    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
79        write!(f, "{}", self.as_db_str())
80    }
81}
82
83impl FromStr for MessageType {
84    type Err = EnumParseError;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        Self::from_db_str(s)
88    }
89}
90
91/// Priority level for messages.
92#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
93#[serde(rename_all = "snake_case")]
94#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
95pub enum MessagePriority {
96    /// Low priority - can be delayed
97    Low,
98    /// Normal priority
99    #[default]
100    Normal,
101    /// High priority - should be processed soon
102    High,
103    /// Critical - must be processed immediately
104    Critical,
105}
106
107impl MessagePriority {
108    /// Convert to database string representation.
109    pub fn as_db_str(&self) -> &'static str {
110        match self {
111            MessagePriority::Low => "low",
112            MessagePriority::Normal => "normal",
113            MessagePriority::High => "high",
114            MessagePriority::Critical => "critical",
115        }
116    }
117
118    /// Parse from database string representation.
119    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
120        match s.to_lowercase().as_str() {
121            "low" => Ok(MessagePriority::Low),
122            "normal" => Ok(MessagePriority::Normal),
123            "high" => Ok(MessagePriority::High),
124            "critical" => Ok(MessagePriority::Critical),
125            _ => Err(EnumParseError {
126                enum_name: "MessagePriority",
127                input: s.to_string(),
128            }),
129        }
130    }
131}
132
133impl fmt::Display for MessagePriority {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        write!(f, "{}", self.as_db_str())
136    }
137}
138
139impl FromStr for MessagePriority {
140    type Err = EnumParseError;
141
142    fn from_str(s: &str) -> Result<Self, Self::Err> {
143        Self::from_db_str(s)
144    }
145}
146
147/// Target for a message or delegation: either a specific agent by ID or any agent of a given type.
148#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
149#[serde(rename_all = "snake_case")]
150#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
151pub enum AgentTarget {
152    ById(AgentId),
153    ByType(crate::AgentType),
154}
155
156impl AgentTarget {
157    /// Agent ID when target is ById, otherwise None.
158    pub fn as_agent_id(&self) -> Option<AgentId> {
159        match self {
160            AgentTarget::ById(id) => Some(*id),
161            AgentTarget::ByType(_) => None,
162        }
163    }
164
165    /// Agent type when target is ByType, otherwise None.
166    pub fn as_agent_type(&self) -> Option<&crate::AgentType> {
167        match self {
168            AgentTarget::ById(_) => None,
169            AgentTarget::ByType(t) => Some(t),
170        }
171    }
172}
173
174// ============================================================================
175// MEMORY REGIONS AND ACCESS CONTROL
176// ============================================================================
177
178/// Permission scope for memory access.
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
180#[serde(rename_all = "snake_case")]
181#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
182pub enum PermissionScope {
183    /// Only own resources
184    Own,
185    /// Resources belonging to same team
186    Team,
187    /// All resources (global access)
188    Global,
189}
190
191/// Type of memory region.
192#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
193#[serde(rename_all = "snake_case")]
194#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
195pub enum MemoryRegion {
196    /// Only owning agent can access
197    Private,
198    /// Agents in same team can access
199    Team,
200    /// Any agent can read, owner can write
201    Public,
202    /// Any agent can read/write with coordination
203    Collaborative,
204}
205
206/// A single memory permission entry.
207#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
208#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
209pub struct MemoryPermission {
210    /// Type of memory (artifact, note, turn, working_set, or "*" for all)
211    pub memory_type: crate::MemoryType,
212    /// Scope of the permission
213    pub scope: PermissionScope,
214    /// Optional filter expression (serialized)
215    pub filter: Option<String>,
216}
217
218/// Memory access configuration for an agent.
219#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
220#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
221pub struct MemoryAccess {
222    /// Read permissions
223    pub read: Vec<MemoryPermission>,
224    /// Write permissions
225    pub write: Vec<MemoryPermission>,
226}
227
228impl Default for MemoryAccess {
229    fn default() -> Self {
230        Self {
231            read: vec![MemoryPermission {
232                memory_type: crate::MemoryType::All,
233                scope: PermissionScope::Own,
234                filter: None,
235            }],
236            write: vec![MemoryPermission {
237                memory_type: crate::MemoryType::All,
238                scope: PermissionScope::Own,
239                filter: None,
240            }],
241        }
242    }
243}
244
245// ============================================================================
246// HANDOFF REASON
247// ============================================================================
248
249/// Reason for a handoff.
250#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
251#[serde(rename_all = "snake_case")]
252#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
253pub enum HandoffReason {
254    /// Current agent lacks required capability
255    CapabilityMismatch,
256    /// Load balancing across agents
257    LoadBalancing,
258    /// Task requires specialized agent
259    Specialization,
260    /// Escalation to supervisor
261    Escalation,
262    /// Agent timed out
263    Timeout,
264    /// Agent failed
265    Failure,
266    /// Scheduled handoff
267    Scheduled,
268}
269
270impl HandoffReason {
271    /// Convert to database string representation.
272    pub fn as_db_str(&self) -> &'static str {
273        match self {
274            HandoffReason::CapabilityMismatch => "capability_mismatch",
275            HandoffReason::LoadBalancing => "load_balancing",
276            HandoffReason::Specialization => "specialization",
277            HandoffReason::Escalation => "escalation",
278            HandoffReason::Timeout => "timeout",
279            HandoffReason::Failure => "failure",
280            HandoffReason::Scheduled => "scheduled",
281        }
282    }
283
284    /// Parse from database string representation.
285    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
286        match s.to_lowercase().replace('_', "").as_str() {
287            "capabilitymismatch" => Ok(HandoffReason::CapabilityMismatch),
288            "loadbalancing" => Ok(HandoffReason::LoadBalancing),
289            "specialization" => Ok(HandoffReason::Specialization),
290            "escalation" => Ok(HandoffReason::Escalation),
291            "timeout" => Ok(HandoffReason::Timeout),
292            "failure" | "failed" => Ok(HandoffReason::Failure),
293            "scheduled" => Ok(HandoffReason::Scheduled),
294            _ => Err(EnumParseError {
295                enum_name: "HandoffReason",
296                input: s.to_string(),
297            }),
298        }
299    }
300}
301
302impl fmt::Display for HandoffReason {
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        write!(f, "{}", self.as_db_str())
305    }
306}
307
308impl FromStr for HandoffReason {
309    type Err = EnumParseError;
310
311    fn from_str(s: &str) -> Result<Self, Self::Err> {
312        Self::from_db_str(s)
313    }
314}
315
316// ============================================================================
317// CONFLICT TYPES
318// ============================================================================
319
320/// Type of conflict.
321#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
322#[serde(rename_all = "snake_case")]
323#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
324pub enum ConflictType {
325    /// Two agents wrote to the same resource concurrently
326    ConcurrentWrite,
327    /// Two facts contradict each other
328    ContradictingFact,
329    /// Two decisions are incompatible
330    IncompatibleDecision,
331    /// Two agents are contending for the same resource
332    ResourceContention,
333    /// Two agents have conflicting goals
334    GoalConflict,
335}
336
337impl ConflictType {
338    /// Convert to database string representation.
339    pub fn as_db_str(&self) -> &'static str {
340        match self {
341            ConflictType::ConcurrentWrite => "concurrent_write",
342            ConflictType::ContradictingFact => "contradicting_fact",
343            ConflictType::IncompatibleDecision => "incompatible_decision",
344            ConflictType::ResourceContention => "resource_contention",
345            ConflictType::GoalConflict => "goal_conflict",
346        }
347    }
348
349    /// Parse from database string representation.
350    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
351        match s.to_lowercase().replace('_', "").as_str() {
352            "concurrentwrite" => Ok(ConflictType::ConcurrentWrite),
353            "contradictingfact" => Ok(ConflictType::ContradictingFact),
354            "incompatibledecision" => Ok(ConflictType::IncompatibleDecision),
355            "resourcecontention" => Ok(ConflictType::ResourceContention),
356            "goalconflict" => Ok(ConflictType::GoalConflict),
357            _ => Err(EnumParseError {
358                enum_name: "ConflictType",
359                input: s.to_string(),
360            }),
361        }
362    }
363}
364
365impl fmt::Display for ConflictType {
366    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
367        write!(f, "{}", self.as_db_str())
368    }
369}
370
371impl FromStr for ConflictType {
372    type Err = EnumParseError;
373
374    fn from_str(s: &str) -> Result<Self, Self::Err> {
375        Self::from_db_str(s)
376    }
377}
378
379/// Status of a conflict.
380#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
381#[serde(rename_all = "snake_case")]
382#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
383pub enum ConflictStatus {
384    /// Conflict has been detected
385    Detected,
386    /// Conflict is being resolved
387    Resolving,
388    /// Conflict has been resolved
389    Resolved,
390    /// Conflict has been escalated
391    Escalated,
392}
393
394impl ConflictStatus {
395    /// Convert to database string representation.
396    pub fn as_db_str(&self) -> &'static str {
397        match self {
398            ConflictStatus::Detected => "detected",
399            ConflictStatus::Resolving => "resolving",
400            ConflictStatus::Resolved => "resolved",
401            ConflictStatus::Escalated => "escalated",
402        }
403    }
404
405    /// Parse from database string representation.
406    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
407        match s.to_lowercase().as_str() {
408            "detected" => Ok(ConflictStatus::Detected),
409            "resolving" => Ok(ConflictStatus::Resolving),
410            "resolved" => Ok(ConflictStatus::Resolved),
411            "escalated" => Ok(ConflictStatus::Escalated),
412            _ => Err(EnumParseError {
413                enum_name: "ConflictStatus",
414                input: s.to_string(),
415            }),
416        }
417    }
418
419    /// Check if this is a terminal state.
420    pub fn is_terminal(&self) -> bool {
421        matches!(self, ConflictStatus::Resolved | ConflictStatus::Escalated)
422    }
423}
424
425impl fmt::Display for ConflictStatus {
426    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
427        write!(f, "{}", self.as_db_str())
428    }
429}
430
431impl FromStr for ConflictStatus {
432    type Err = EnumParseError;
433
434    fn from_str(s: &str) -> Result<Self, Self::Err> {
435        Self::from_db_str(s)
436    }
437}
438
439/// Strategy for resolving a conflict.
440#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
441#[serde(rename_all = "snake_case")]
442#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
443pub enum ResolutionStrategy {
444    /// Last write wins
445    LastWriteWins,
446    /// First write wins
447    FirstWriteWins,
448    /// Highest confidence wins
449    HighestConfidence,
450    /// Merge the conflicting items
451    Merge,
452    /// Escalate to human or supervisor
453    Escalate,
454    /// Reject both items
455    RejectBoth,
456}
457
458impl ResolutionStrategy {
459    /// Convert to database string representation.
460    pub fn as_db_str(&self) -> &'static str {
461        match self {
462            ResolutionStrategy::LastWriteWins => "last_write_wins",
463            ResolutionStrategy::FirstWriteWins => "first_write_wins",
464            ResolutionStrategy::HighestConfidence => "highest_confidence",
465            ResolutionStrategy::Merge => "merge",
466            ResolutionStrategy::Escalate => "escalate",
467            ResolutionStrategy::RejectBoth => "reject_both",
468        }
469    }
470
471    /// Parse from database string representation.
472    pub fn from_db_str(s: &str) -> Result<Self, EnumParseError> {
473        match s.to_lowercase().replace('_', "").as_str() {
474            "lastwritewins" => Ok(ResolutionStrategy::LastWriteWins),
475            "firstwritewins" => Ok(ResolutionStrategy::FirstWriteWins),
476            "highestconfidence" => Ok(ResolutionStrategy::HighestConfidence),
477            "merge" => Ok(ResolutionStrategy::Merge),
478            "escalate" => Ok(ResolutionStrategy::Escalate),
479            "rejectboth" => Ok(ResolutionStrategy::RejectBoth),
480            _ => Err(EnumParseError {
481                enum_name: "ResolutionStrategy",
482                input: s.to_string(),
483            }),
484        }
485    }
486}
487
488impl fmt::Display for ResolutionStrategy {
489    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
490        write!(f, "{}", self.as_db_str())
491    }
492}
493
494impl FromStr for ResolutionStrategy {
495    type Err = EnumParseError;
496
497    fn from_str(s: &str) -> Result<Self, Self::Err> {
498        Self::from_db_str(s)
499    }
500}
501
502// ============================================================================
503// CONFLICT RESOLUTION
504// ============================================================================
505
506/// Strategy for resolving conflicts.
507#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
508#[serde(rename_all = "snake_case")]
509#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
510pub enum ConflictResolution {
511    /// Last write wins
512    #[default]
513    LastWriteWins,
514    /// Highest confidence wins
515    HighestConfidence,
516    /// Escalate to user/admin
517    Escalate,
518}
519
520// ============================================================================
521// MEMORY REGION CONFIG
522// ============================================================================
523
524/// Configuration for a memory region.
525#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
526#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
527pub struct MemoryRegionConfig {
528    /// Unique identifier for this region
529    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
530    pub region_id: Uuid,
531    /// Type of region
532    pub region_type: MemoryRegion,
533    /// Agent that owns this region
534    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
535    pub owner_agent_id: Uuid,
536    /// Team this region belongs to (if applicable)
537    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
538    pub team_id: Option<Uuid>,
539
540    /// Agents with read access
541    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
542    pub readers: Vec<Uuid>,
543    /// Agents with write access
544    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
545    pub writers: Vec<Uuid>,
546
547    /// Whether writes require a lock
548    pub require_lock: bool,
549    /// How to resolve conflicts
550    pub conflict_resolution: ConflictResolution,
551    /// Whether to track versions
552    pub version_tracking: bool,
553}
554
555impl MemoryRegionConfig {
556    /// Create a new private region.
557    pub fn private(owner_agent_id: Uuid) -> Self {
558        Self {
559            region_id: Uuid::now_v7(),
560            region_type: MemoryRegion::Private,
561            owner_agent_id,
562            team_id: None,
563            readers: vec![owner_agent_id],
564            writers: vec![owner_agent_id],
565            require_lock: false,
566            conflict_resolution: ConflictResolution::LastWriteWins,
567            version_tracking: false,
568        }
569    }
570
571    /// Create a new team region.
572    pub fn team(owner_agent_id: Uuid, team_id: Uuid) -> Self {
573        Self {
574            region_id: Uuid::now_v7(),
575            region_type: MemoryRegion::Team,
576            owner_agent_id,
577            team_id: Some(team_id),
578            readers: Vec::new(),
579            writers: Vec::new(),
580            require_lock: false,
581            conflict_resolution: ConflictResolution::LastWriteWins,
582            version_tracking: true,
583        }
584    }
585
586    /// Create a new public region.
587    pub fn public(owner_agent_id: Uuid) -> Self {
588        Self {
589            region_id: Uuid::now_v7(),
590            region_type: MemoryRegion::Public,
591            owner_agent_id,
592            team_id: None,
593            readers: Vec::new(),
594            writers: vec![owner_agent_id],
595            require_lock: false,
596            conflict_resolution: ConflictResolution::LastWriteWins,
597            version_tracking: false,
598        }
599    }
600
601    /// Create a new collaborative region.
602    pub fn collaborative(owner_agent_id: Uuid) -> Self {
603        Self {
604            region_id: Uuid::now_v7(),
605            region_type: MemoryRegion::Collaborative,
606            owner_agent_id,
607            team_id: None,
608            readers: Vec::new(),
609            writers: Vec::new(),
610            require_lock: true,
611            conflict_resolution: ConflictResolution::Escalate,
612            version_tracking: true,
613        }
614    }
615
616    /// Add a reader to the region.
617    pub fn add_reader(&mut self, agent_id: Uuid) {
618        if !self.readers.contains(&agent_id) {
619            self.readers.push(agent_id);
620        }
621    }
622
623    /// Add a writer to the region.
624    pub fn add_writer(&mut self, agent_id: Uuid) {
625        if !self.writers.contains(&agent_id) {
626            self.writers.push(agent_id);
627        }
628    }
629
630    /// Check if an agent can read from this region.
631    pub fn can_read(&self, agent_id: Uuid) -> bool {
632        match self.region_type {
633            MemoryRegion::Private => agent_id == self.owner_agent_id,
634            MemoryRegion::Team => {
635                agent_id == self.owner_agent_id || self.readers.contains(&agent_id)
636            }
637            MemoryRegion::Public | MemoryRegion::Collaborative => true,
638        }
639    }
640
641    /// Check if an agent can write to this region.
642    pub fn can_write(&self, agent_id: Uuid) -> bool {
643        match self.region_type {
644            MemoryRegion::Private => agent_id == self.owner_agent_id,
645            MemoryRegion::Team => {
646                agent_id == self.owner_agent_id || self.writers.contains(&agent_id)
647            }
648            MemoryRegion::Public => agent_id == self.owner_agent_id,
649            MemoryRegion::Collaborative => true,
650        }
651    }
652}
653
654// ============================================================================
655// TESTS
656// ============================================================================
657
658// ============================================================================
659// BDI PRIMITIVES: GOAL SYSTEM (Phase 2.2)
660// ============================================================================
661
662/// Status of an agent goal.
663#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
664#[serde(rename_all = "snake_case")]
665#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
666pub enum GoalStatus {
667    /// Awaiting activation
668    #[default]
669    Pending,
670    /// Currently being pursued
671    Active,
672    /// Successfully completed
673    Achieved,
674    /// Permanently failed
675    Failed,
676    /// Intentionally dropped
677    Abandoned,
678    /// Temporarily paused
679    Suspended,
680}
681
682/// Type of goal.
683#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
684#[serde(rename_all = "snake_case")]
685#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
686pub enum GoalType {
687    /// End goal - ultimate objective
688    #[default]
689    Terminal,
690    /// Decomposed from parent goal
691    Subgoal,
692    /// Progress checkpoint
693    Milestone,
694    /// Constraint that must always hold
695    Invariant,
696}
697
698/// A measurable criterion for goal success.
699#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
700#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
701pub struct SuccessCriterion {
702    /// Description of the criterion
703    pub description: String,
704    /// Whether this criterion is measurable
705    pub measurable: bool,
706    /// Target value (if measurable)
707    pub target_value: Option<String>,
708    /// Current value (if measured)
709    pub current_value: Option<String>,
710    /// Whether this criterion is satisfied
711    pub satisfied: bool,
712}
713
714impl SuccessCriterion {
715    /// Create a new success criterion.
716    pub fn new(description: impl Into<String>) -> Self {
717        Self {
718            description: description.into(),
719            measurable: false,
720            target_value: None,
721            current_value: None,
722            satisfied: false,
723        }
724    }
725
726    /// Make this criterion measurable.
727    pub fn measurable(mut self, target: impl Into<String>) -> Self {
728        self.measurable = true;
729        self.target_value = Some(target.into());
730        self
731    }
732
733    /// Update the current value.
734    pub fn update(&mut self, value: impl Into<String>) {
735        self.current_value = Some(value.into());
736    }
737
738    /// Mark as satisfied.
739    pub fn satisfy(&mut self) {
740        self.satisfied = true;
741    }
742}
743
744/// A goal that an agent is pursuing.
745#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
746#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
747pub struct AgentGoal {
748    /// Unique identifier for this goal
749    pub goal_id: GoalId,
750    /// Agent pursuing this goal
751    pub agent_id: AgentId,
752    /// Trajectory this goal belongs to (if any)
753    pub trajectory_id: Option<TrajectoryId>,
754    /// Description of the goal
755    pub description: String,
756    /// Type of goal
757    pub goal_type: GoalType,
758    /// Current status
759    pub status: GoalStatus,
760    /// Criteria for success
761    pub success_criteria: Vec<SuccessCriterion>,
762    /// Priority (higher = more important)
763    pub priority: i32,
764    /// Deadline (if any)
765    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
766    pub deadline: Option<Timestamp>,
767    /// Parent goal (for subgoals)
768    pub parent_goal_id: Option<GoalId>,
769    /// Child goals (subgoals)
770    pub child_goal_ids: Vec<GoalId>,
771    /// When this goal was created
772    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
773    pub created_at: Timestamp,
774    /// When work started
775    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
776    pub started_at: Option<Timestamp>,
777    /// When completed
778    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
779    pub completed_at: Option<Timestamp>,
780    /// Reason for failure (if failed)
781    pub failure_reason: Option<String>,
782}
783
784impl AgentGoal {
785    /// Create a new goal.
786    pub fn new(agent_id: AgentId, description: impl Into<String>, goal_type: GoalType) -> Self {
787        Self {
788            goal_id: GoalId::now_v7(),
789            agent_id,
790            trajectory_id: None,
791            description: description.into(),
792            goal_type,
793            status: GoalStatus::Pending,
794            success_criteria: Vec::new(),
795            priority: 0,
796            deadline: None,
797            parent_goal_id: None,
798            child_goal_ids: Vec::new(),
799            created_at: chrono::Utc::now(),
800            started_at: None,
801            completed_at: None,
802            failure_reason: None,
803        }
804    }
805
806    /// Set trajectory.
807    pub fn with_trajectory(mut self, trajectory_id: TrajectoryId) -> Self {
808        self.trajectory_id = Some(trajectory_id);
809        self
810    }
811
812    /// Set parent goal.
813    pub fn with_parent(mut self, parent_id: GoalId) -> Self {
814        self.parent_goal_id = Some(parent_id);
815        self
816    }
817
818    /// Add a success criterion.
819    pub fn with_criterion(mut self, criterion: SuccessCriterion) -> Self {
820        self.success_criteria.push(criterion);
821        self
822    }
823
824    /// Set priority.
825    pub fn with_priority(mut self, priority: i32) -> Self {
826        self.priority = priority;
827        self
828    }
829
830    /// Set deadline.
831    pub fn with_deadline(mut self, deadline: Timestamp) -> Self {
832        self.deadline = Some(deadline);
833        self
834    }
835
836    /// Start pursuing this goal.
837    pub fn start(&mut self) {
838        self.status = GoalStatus::Active;
839        self.started_at = Some(chrono::Utc::now());
840    }
841
842    /// Mark as achieved.
843    pub fn achieve(&mut self) {
844        self.status = GoalStatus::Achieved;
845        self.completed_at = Some(chrono::Utc::now());
846    }
847
848    /// Mark as failed.
849    pub fn fail(&mut self, reason: impl Into<String>) {
850        self.status = GoalStatus::Failed;
851        self.failure_reason = Some(reason.into());
852        self.completed_at = Some(chrono::Utc::now());
853    }
854
855    /// Check if all criteria are satisfied.
856    pub fn all_criteria_satisfied(&self) -> bool {
857        self.success_criteria.iter().all(|c| c.satisfied)
858    }
859}
860
861// ============================================================================
862// BDI PRIMITIVES: PLAN SYSTEM (Phase 2.3)
863// ============================================================================
864
865/// Status of a plan.
866#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
867#[serde(rename_all = "snake_case")]
868#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
869pub enum PlanStatus {
870    /// Being formulated
871    #[default]
872    Draft,
873    /// Approved, awaiting execution
874    Ready,
875    /// Actively executing
876    InProgress,
877    /// All steps done
878    Completed,
879    /// Execution failed
880    Failed,
881    /// Cancelled
882    Abandoned,
883}
884
885/// Status of a plan step.
886#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
887#[serde(rename_all = "snake_case")]
888#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
889pub enum StepStatus {
890    /// Not yet started
891    #[default]
892    Pending,
893    /// Preconditions met
894    Ready,
895    /// Currently executing
896    InProgress,
897    /// Successfully finished
898    Completed,
899    /// Execution failed
900    Failed,
901    /// Intentionally skipped
902    Skipped,
903    /// Waiting on dependency
904    Blocked,
905}
906
907/// A single step in a plan.
908#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
909#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
910pub struct PlanStep {
911    /// Unique identifier for this step
912    pub step_id: StepId,
913    /// Order in the plan
914    pub index: i32,
915    /// Description of what this step does
916    pub description: String,
917    /// Type of action required
918    pub action_type: ActionType,
919    /// What must be true before this step
920    pub preconditions: Vec<String>,
921    /// What will be true after this step
922    pub postconditions: Vec<String>,
923    /// Steps this depends on
924    pub depends_on: Vec<StepId>,
925    /// Estimated token cost
926    pub estimated_tokens: Option<i32>,
927    /// Current status
928    pub status: StepStatus,
929}
930
931impl PlanStep {
932    /// Create a new plan step.
933    pub fn new(index: i32, description: impl Into<String>, action_type: ActionType) -> Self {
934        Self {
935            step_id: StepId::now_v7(),
936            index,
937            description: description.into(),
938            action_type,
939            preconditions: Vec::new(),
940            postconditions: Vec::new(),
941            depends_on: Vec::new(),
942            estimated_tokens: None,
943            status: StepStatus::Pending,
944        }
945    }
946
947    /// Add a precondition.
948    pub fn with_precondition(mut self, condition: impl Into<String>) -> Self {
949        self.preconditions.push(condition.into());
950        self
951    }
952
953    /// Add a postcondition.
954    pub fn with_postcondition(mut self, condition: impl Into<String>) -> Self {
955        self.postconditions.push(condition.into());
956        self
957    }
958
959    /// Add a dependency.
960    pub fn depends_on(mut self, step_id: StepId) -> Self {
961        self.depends_on.push(step_id);
962        self
963    }
964}
965
966/// Cost estimate for a plan.
967#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
968#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
969pub struct PlanCost {
970    /// Estimated token usage
971    pub estimated_tokens: i32,
972    /// Estimated duration in milliseconds
973    pub estimated_duration_ms: i64,
974    /// Monetary cost in USD (if applicable)
975    pub monetary_cost_usd: Option<f64>,
976}
977
978impl PlanCost {
979    /// Create a new cost estimate.
980    pub fn new(tokens: i32, duration_ms: i64) -> Self {
981        Self {
982            estimated_tokens: tokens,
983            estimated_duration_ms: duration_ms,
984            monetary_cost_usd: None,
985        }
986    }
987
988    /// Add monetary cost.
989    pub fn with_monetary_cost(mut self, cost_usd: f64) -> Self {
990        self.monetary_cost_usd = Some(cost_usd);
991        self
992    }
993}
994
995/// A plan to achieve a goal.
996#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
997#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
998pub struct AgentPlan {
999    /// Unique identifier for this plan
1000    pub plan_id: PlanId,
1001    /// Agent that created this plan
1002    pub agent_id: AgentId,
1003    /// Goal this plan is for
1004    pub goal_id: GoalId,
1005    /// Description of the plan
1006    pub description: String,
1007    /// Current status
1008    pub status: PlanStatus,
1009    /// Steps in the plan
1010    pub steps: Vec<PlanStep>,
1011    /// Estimated cost
1012    pub estimated_cost: Option<PlanCost>,
1013    /// Actual cost (after execution)
1014    pub actual_cost: Option<PlanCost>,
1015    /// When this plan was created
1016    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1017    pub created_at: Timestamp,
1018    /// When execution started
1019    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
1020    pub started_at: Option<Timestamp>,
1021    /// When completed
1022    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
1023    pub completed_at: Option<Timestamp>,
1024}
1025
1026impl AgentPlan {
1027    /// Create a new plan.
1028    pub fn new(agent_id: AgentId, goal_id: GoalId, description: impl Into<String>) -> Self {
1029        Self {
1030            plan_id: PlanId::now_v7(),
1031            agent_id,
1032            goal_id,
1033            description: description.into(),
1034            status: PlanStatus::Draft,
1035            steps: Vec::new(),
1036            estimated_cost: None,
1037            actual_cost: None,
1038            created_at: chrono::Utc::now(),
1039            started_at: None,
1040            completed_at: None,
1041        }
1042    }
1043
1044    /// Add a step to the plan.
1045    pub fn add_step(&mut self, step: PlanStep) {
1046        self.steps.push(step);
1047    }
1048
1049    /// Set estimated cost.
1050    pub fn with_estimated_cost(mut self, cost: PlanCost) -> Self {
1051        self.estimated_cost = Some(cost);
1052        self
1053    }
1054
1055    /// Mark as ready for execution.
1056    pub fn ready(&mut self) {
1057        self.status = PlanStatus::Ready;
1058    }
1059
1060    /// Start execution.
1061    pub fn start(&mut self) {
1062        self.status = PlanStatus::InProgress;
1063        self.started_at = Some(chrono::Utc::now());
1064    }
1065
1066    /// Mark as completed.
1067    pub fn complete(&mut self, actual_cost: Option<PlanCost>) {
1068        self.status = PlanStatus::Completed;
1069        self.actual_cost = actual_cost;
1070        self.completed_at = Some(chrono::Utc::now());
1071    }
1072
1073    /// Get next pending step.
1074    pub fn next_step(&self) -> Option<&PlanStep> {
1075        self.steps
1076            .iter()
1077            .find(|s| s.status == StepStatus::Pending || s.status == StepStatus::Ready)
1078    }
1079}
1080
1081// ============================================================================
1082// BDI PRIMITIVES: ACTION SYSTEM (Phase 2.4)
1083// ============================================================================
1084
1085/// Type of action an agent can take.
1086#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1087#[serde(rename_all = "snake_case")]
1088#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1089pub enum ActionType {
1090    /// Direct system operation
1091    #[default]
1092    Operation,
1093    /// External tool invocation
1094    ToolCall,
1095    /// LLM inference
1096    ModelQuery,
1097    /// Deliberation/choice
1098    Decision,
1099    /// Message another agent
1100    Communication,
1101    /// Sense/perceive environment
1102    Observation,
1103    /// Read/write memory
1104    MemoryAccess,
1105}
1106
1107/// Status of an action.
1108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1109#[serde(rename_all = "snake_case")]
1110#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1111pub enum ActionStatus {
1112    /// Not yet started
1113    #[default]
1114    Pending,
1115    /// Currently executing
1116    InProgress,
1117    /// Successfully finished
1118    Completed,
1119    /// Execution failed
1120    Failed,
1121    /// Being retried
1122    Retrying,
1123    /// Cancelled
1124    Cancelled,
1125}
1126
1127/// Backoff strategy for retries.
1128#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
1129#[serde(rename_all = "snake_case")]
1130#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1131pub enum BackoffStrategy {
1132    /// No backoff
1133    #[default]
1134    None,
1135    /// Fixed delay
1136    Fixed { delay_ms: i64 },
1137    /// Linear backoff
1138    Linear { base_ms: i64, increment_ms: i64 },
1139    /// Exponential backoff
1140    Exponential {
1141        base_ms: i64,
1142        multiplier: f64,
1143        max_ms: i64,
1144    },
1145}
1146
1147impl BackoffStrategy {
1148    /// Calculate delay for a given attempt number.
1149    pub fn delay_for_attempt(&self, attempt: i32) -> i64 {
1150        match self {
1151            BackoffStrategy::None => 0,
1152            BackoffStrategy::Fixed { delay_ms } => *delay_ms,
1153            BackoffStrategy::Linear {
1154                base_ms,
1155                increment_ms,
1156            } => base_ms + (attempt as i64 * increment_ms),
1157            BackoffStrategy::Exponential {
1158                base_ms,
1159                multiplier,
1160                max_ms,
1161            } => {
1162                let delay = (*base_ms as f64) * multiplier.powi(attempt);
1163                (delay as i64).min(*max_ms)
1164            }
1165        }
1166    }
1167}
1168
1169/// Policy for retrying failed actions.
1170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1171#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1172pub struct RetryPolicy {
1173    /// Maximum number of attempts
1174    pub max_attempts: i32,
1175    /// Backoff strategy
1176    pub backoff: BackoffStrategy,
1177    /// Timeout per attempt in milliseconds
1178    pub timeout_per_attempt_ms: i64,
1179}
1180
1181impl Default for RetryPolicy {
1182    fn default() -> Self {
1183        Self {
1184            max_attempts: 3,
1185            backoff: BackoffStrategy::Exponential {
1186                base_ms: 100,
1187                multiplier: 2.0,
1188                max_ms: 10_000,
1189            },
1190            timeout_per_attempt_ms: 30_000,
1191        }
1192    }
1193}
1194
1195/// An action taken by an agent.
1196#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1197#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1198pub struct AgentAction {
1199    /// Unique identifier for this action
1200    pub action_id: ActionId,
1201    /// Agent performing this action
1202    pub agent_id: AgentId,
1203    /// Plan step this action is part of (if any)
1204    pub step_id: Option<StepId>,
1205    /// Type of action
1206    pub action_type: ActionType,
1207    /// Description of the action
1208    pub description: String,
1209    /// Action parameters (JSON)
1210    pub parameters: Option<serde_json::Value>,
1211    /// Retry policy
1212    pub retry_policy: Option<RetryPolicy>,
1213    /// Timeout in milliseconds
1214    pub timeout_ms: Option<i64>,
1215    /// Current status
1216    pub status: ActionStatus,
1217    /// Number of attempts made
1218    pub attempt_count: i32,
1219    /// When this action was created
1220    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1221    pub created_at: Timestamp,
1222    /// When execution started
1223    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
1224    pub started_at: Option<Timestamp>,
1225    /// When completed
1226    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
1227    pub completed_at: Option<Timestamp>,
1228}
1229
1230impl AgentAction {
1231    /// Create a new action.
1232    pub fn new(agent_id: AgentId, action_type: ActionType, description: impl Into<String>) -> Self {
1233        Self {
1234            action_id: ActionId::now_v7(),
1235            agent_id,
1236            step_id: None,
1237            action_type,
1238            description: description.into(),
1239            parameters: None,
1240            retry_policy: None,
1241            timeout_ms: None,
1242            status: ActionStatus::Pending,
1243            attempt_count: 0,
1244            created_at: chrono::Utc::now(),
1245            started_at: None,
1246            completed_at: None,
1247        }
1248    }
1249
1250    /// Set plan step.
1251    pub fn with_step(mut self, step_id: StepId) -> Self {
1252        self.step_id = Some(step_id);
1253        self
1254    }
1255
1256    /// Set parameters.
1257    pub fn with_parameters(mut self, params: serde_json::Value) -> Self {
1258        self.parameters = Some(params);
1259        self
1260    }
1261
1262    /// Set retry policy.
1263    pub fn with_retry_policy(mut self, policy: RetryPolicy) -> Self {
1264        self.retry_policy = Some(policy);
1265        self
1266    }
1267
1268    /// Set timeout.
1269    pub fn with_timeout(mut self, timeout_ms: i64) -> Self {
1270        self.timeout_ms = Some(timeout_ms);
1271        self
1272    }
1273
1274    /// Start execution.
1275    pub fn start(&mut self) {
1276        self.status = ActionStatus::InProgress;
1277        self.attempt_count += 1;
1278        if self.started_at.is_none() {
1279            self.started_at = Some(chrono::Utc::now());
1280        }
1281    }
1282
1283    /// Mark as completed.
1284    pub fn complete(&mut self) {
1285        self.status = ActionStatus::Completed;
1286        self.completed_at = Some(chrono::Utc::now());
1287    }
1288
1289    /// Mark as failed.
1290    pub fn fail(&mut self) {
1291        self.status = ActionStatus::Failed;
1292        self.completed_at = Some(chrono::Utc::now());
1293    }
1294
1295    /// Check if retry is allowed.
1296    pub fn can_retry(&self) -> bool {
1297        if let Some(policy) = &self.retry_policy {
1298            self.attempt_count < policy.max_attempts
1299        } else {
1300            false
1301        }
1302    }
1303}
1304
1305// ============================================================================
1306// BDI PRIMITIVES: BELIEF SYSTEM (Phase 2.5)
1307// ============================================================================
1308
1309/// Source of a belief.
1310#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1311#[serde(rename_all = "snake_case")]
1312#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1313pub enum BeliefSource {
1314    /// Direct sensing/perception
1315    #[default]
1316    Observation,
1317    /// Logical deduction
1318    Inference,
1319    /// From another agent
1320    Communication,
1321    /// From persisted knowledge
1322    MemoryRecall,
1323    /// Assumed without proof
1324    Assumption,
1325    /// Explicitly provided by user
1326    UserProvided,
1327}
1328
1329/// Type of belief.
1330#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1331#[serde(rename_all = "snake_case")]
1332#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1333pub enum BeliefType {
1334    /// Known to be true
1335    #[default]
1336    Fact,
1337    /// Suspected to be true
1338    Hypothesis,
1339    /// Unknown, needs resolution
1340    Uncertainty,
1341}
1342
1343/// A belief held by an agent.
1344#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1345#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1346pub struct Belief {
1347    /// Unique identifier for this belief
1348    pub belief_id: BeliefId,
1349    /// Agent holding this belief
1350    pub agent_id: AgentId,
1351    /// Type of belief
1352    pub belief_type: BeliefType,
1353    /// Content of the belief
1354    pub content: String,
1355    /// Confidence level (0.0 to 1.0)
1356    pub confidence: f32,
1357    /// Source of the belief
1358    pub source: BeliefSource,
1359    /// Evidence supporting this belief
1360    pub evidence_refs: Vec<EvidenceRef>,
1361    /// When this belief was formed
1362    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1363    pub created_at: Timestamp,
1364    /// When last updated
1365    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1366    pub updated_at: Timestamp,
1367    /// Belief that supersedes this one (if any)
1368    pub superseded_by: Option<BeliefId>,
1369}
1370
1371impl Belief {
1372    /// Create a new belief.
1373    pub fn new(
1374        agent_id: AgentId,
1375        content: impl Into<String>,
1376        belief_type: BeliefType,
1377        source: BeliefSource,
1378    ) -> Self {
1379        let now = chrono::Utc::now();
1380        Self {
1381            belief_id: BeliefId::now_v7(),
1382            agent_id,
1383            belief_type,
1384            content: content.into(),
1385            confidence: 1.0,
1386            source,
1387            evidence_refs: Vec::new(),
1388            created_at: now,
1389            updated_at: now,
1390            superseded_by: None,
1391        }
1392    }
1393
1394    /// Set confidence level.
1395    pub fn with_confidence(mut self, confidence: f32) -> Self {
1396        self.confidence = confidence.clamp(0.0, 1.0);
1397        self
1398    }
1399
1400    /// Add evidence reference.
1401    pub fn with_evidence(mut self, evidence: EvidenceRef) -> Self {
1402        self.evidence_refs.push(evidence);
1403        self
1404    }
1405
1406    /// Update confidence.
1407    pub fn update_confidence(&mut self, confidence: f32) {
1408        self.confidence = confidence.clamp(0.0, 1.0);
1409        self.updated_at = chrono::Utc::now();
1410    }
1411
1412    /// Mark as superseded.
1413    pub fn supersede(&mut self, new_belief_id: BeliefId) {
1414        self.superseded_by = Some(new_belief_id);
1415        self.updated_at = chrono::Utc::now();
1416    }
1417
1418    /// Check if this belief is still active.
1419    pub fn is_active(&self) -> bool {
1420        self.superseded_by.is_none()
1421    }
1422}
1423
1424/// Collection of an agent's beliefs.
1425#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1426#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1427pub struct AgentBeliefs {
1428    /// Agent these beliefs belong to
1429    pub agent_id: AgentId,
1430    /// Beliefs classified as facts
1431    pub facts: Vec<Belief>,
1432    /// Beliefs classified as hypotheses
1433    pub hypotheses: Vec<Belief>,
1434    /// Beliefs classified as uncertainties
1435    pub uncertainties: Vec<Belief>,
1436    /// When last updated
1437    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1438    pub last_updated: Timestamp,
1439}
1440
1441impl AgentBeliefs {
1442    /// Create a new empty belief set.
1443    pub fn new(agent_id: AgentId) -> Self {
1444        Self {
1445            agent_id,
1446            facts: Vec::new(),
1447            hypotheses: Vec::new(),
1448            uncertainties: Vec::new(),
1449            last_updated: chrono::Utc::now(),
1450        }
1451    }
1452
1453    /// Add a belief.
1454    pub fn add(&mut self, belief: Belief) {
1455        match belief.belief_type {
1456            BeliefType::Fact => self.facts.push(belief),
1457            BeliefType::Hypothesis => self.hypotheses.push(belief),
1458            BeliefType::Uncertainty => self.uncertainties.push(belief),
1459        }
1460        self.last_updated = chrono::Utc::now();
1461    }
1462
1463    /// Get all active beliefs.
1464    pub fn active_beliefs(&self) -> impl Iterator<Item = &Belief> {
1465        self.facts
1466            .iter()
1467            .chain(self.hypotheses.iter())
1468            .chain(self.uncertainties.iter())
1469            .filter(|b| b.is_active())
1470    }
1471}
1472
1473// ============================================================================
1474// BDI PRIMITIVES: OBSERVATION & LEARNING (Phase 2.6)
1475// ============================================================================
1476
1477/// An observation made by an agent after an action.
1478#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1479#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1480pub struct AgentObservation {
1481    /// Unique identifier for this observation
1482    pub observation_id: ObservationId,
1483    /// Agent that made the observation
1484    pub agent_id: AgentId,
1485    /// Action that led to this observation
1486    pub action_id: ActionId,
1487    /// Whether the action succeeded
1488    pub success: bool,
1489    /// Result data (JSON)
1490    pub result: Option<serde_json::Value>,
1491    /// Error message (if failed)
1492    pub error: Option<String>,
1493    /// Duration in milliseconds
1494    pub duration_ms: i64,
1495    /// Tokens used (if applicable)
1496    pub tokens_used: Option<i32>,
1497    /// Cost in USD (if applicable)
1498    pub cost_usd: Option<f64>,
1499    /// When this observation was made
1500    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1501    pub timestamp: Timestamp,
1502    /// Beliefs created/updated from this observation
1503    pub belief_updates: Vec<BeliefId>,
1504    /// Learnings extracted
1505    pub learnings: Vec<Learning>,
1506}
1507
1508impl AgentObservation {
1509    /// Create a new observation.
1510    pub fn new(agent_id: AgentId, action_id: ActionId, success: bool, duration_ms: i64) -> Self {
1511        Self {
1512            observation_id: ObservationId::now_v7(),
1513            agent_id,
1514            action_id,
1515            success,
1516            result: None,
1517            error: None,
1518            duration_ms,
1519            tokens_used: None,
1520            cost_usd: None,
1521            timestamp: chrono::Utc::now(),
1522            belief_updates: Vec::new(),
1523            learnings: Vec::new(),
1524        }
1525    }
1526
1527    /// Set result.
1528    pub fn with_result(mut self, result: serde_json::Value) -> Self {
1529        self.result = Some(result);
1530        self
1531    }
1532
1533    /// Set error.
1534    pub fn with_error(mut self, error: impl Into<String>) -> Self {
1535        self.error = Some(error.into());
1536        self
1537    }
1538
1539    /// Set token usage.
1540    pub fn with_tokens(mut self, tokens: i32) -> Self {
1541        self.tokens_used = Some(tokens);
1542        self
1543    }
1544
1545    /// Set cost.
1546    pub fn with_cost(mut self, cost_usd: f64) -> Self {
1547        self.cost_usd = Some(cost_usd);
1548        self
1549    }
1550
1551    /// Add belief update.
1552    pub fn add_belief_update(&mut self, belief_id: BeliefId) {
1553        self.belief_updates.push(belief_id);
1554    }
1555
1556    /// Add learning.
1557    pub fn add_learning(&mut self, learning: Learning) {
1558        self.learnings.push(learning);
1559    }
1560}
1561
1562/// Type of learning.
1563#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Default)]
1564#[serde(rename_all = "snake_case")]
1565#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1566pub enum LearningType {
1567    /// New fact discovered
1568    #[default]
1569    FactualUpdate,
1570    /// Pattern observed
1571    PatternRecognition,
1572    /// Improvement to approach
1573    StrategyRefinement,
1574    /// Correction of wrong belief
1575    ErrorCorrection,
1576    /// New capability identified
1577    CapabilityUpdate,
1578}
1579
1580/// A learning extracted from an observation.
1581#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1582#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
1583pub struct Learning {
1584    /// Unique identifier for this learning
1585    pub learning_id: LearningId,
1586    /// Observation this came from
1587    pub observation_id: ObservationId,
1588    /// Type of learning
1589    pub learning_type: LearningType,
1590    /// Content of the learning
1591    pub content: String,
1592    /// Level of abstraction
1593    pub abstraction_level: AbstractionLevel,
1594    /// Where this learning applies
1595    pub applicability: Option<String>,
1596    /// Confidence in this learning
1597    pub confidence: f32,
1598    /// When this was learned
1599    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
1600    pub created_at: Timestamp,
1601}
1602
1603impl Learning {
1604    /// Create a new learning.
1605    pub fn new(
1606        observation_id: ObservationId,
1607        learning_type: LearningType,
1608        content: impl Into<String>,
1609    ) -> Self {
1610        Self {
1611            learning_id: LearningId::now_v7(),
1612            observation_id,
1613            learning_type,
1614            content: content.into(),
1615            abstraction_level: AbstractionLevel::Raw,
1616            applicability: None,
1617            confidence: 1.0,
1618            created_at: chrono::Utc::now(),
1619        }
1620    }
1621
1622    /// Set abstraction level.
1623    pub fn with_abstraction(mut self, level: AbstractionLevel) -> Self {
1624        self.abstraction_level = level;
1625        self
1626    }
1627
1628    /// Set applicability.
1629    pub fn with_applicability(mut self, applicability: impl Into<String>) -> Self {
1630        self.applicability = Some(applicability.into());
1631        self
1632    }
1633
1634    /// Set confidence.
1635    pub fn with_confidence(mut self, confidence: f32) -> Self {
1636        self.confidence = confidence.clamp(0.0, 1.0);
1637        self
1638    }
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644    use serde_json::json;
1645
1646    #[test]
1647    fn test_message_type_roundtrip() {
1648        for mt in [
1649            MessageType::TaskDelegation,
1650            MessageType::TaskResult,
1651            MessageType::ContextRequest,
1652            MessageType::ContextShare,
1653            MessageType::CoordinationSignal,
1654            MessageType::Handoff,
1655            MessageType::Interrupt,
1656            MessageType::Heartbeat,
1657        ] {
1658            let s = mt.as_db_str();
1659            let parsed = MessageType::from_db_str(s).expect("MessageType roundtrip should succeed");
1660            assert_eq!(mt, parsed);
1661        }
1662    }
1663
1664    #[test]
1665    fn test_message_priority_roundtrip() {
1666        for mp in [
1667            MessagePriority::Low,
1668            MessagePriority::Normal,
1669            MessagePriority::High,
1670            MessagePriority::Critical,
1671        ] {
1672            let s = mp.as_db_str();
1673            let parsed =
1674                MessagePriority::from_db_str(s).expect("MessagePriority roundtrip should succeed");
1675            assert_eq!(mp, parsed);
1676        }
1677    }
1678
1679    #[test]
1680    fn test_conflict_status_terminal() {
1681        assert!(!ConflictStatus::Detected.is_terminal());
1682        assert!(!ConflictStatus::Resolving.is_terminal());
1683        assert!(ConflictStatus::Resolved.is_terminal());
1684        assert!(ConflictStatus::Escalated.is_terminal());
1685    }
1686
1687    #[test]
1688    fn test_agent_goal_builders_and_lifecycle() {
1689        let agent_id = AgentId::now_v7();
1690        let trajectory_id = TrajectoryId::now_v7();
1691        let parent_id = GoalId::now_v7();
1692        let deadline = chrono::Utc::now();
1693        let criterion = SuccessCriterion::new("done");
1694
1695        let mut goal = AgentGoal::new(agent_id, "ship", GoalType::Terminal)
1696            .with_trajectory(trajectory_id)
1697            .with_parent(parent_id)
1698            .with_criterion(criterion)
1699            .with_priority(5)
1700            .with_deadline(deadline);
1701
1702        assert_eq!(goal.agent_id, agent_id);
1703        assert_eq!(goal.trajectory_id, Some(trajectory_id));
1704        assert_eq!(goal.parent_goal_id, Some(parent_id));
1705        assert_eq!(goal.priority, 5);
1706        assert_eq!(goal.deadline, Some(deadline));
1707        assert_eq!(goal.status, GoalStatus::Pending);
1708        assert!(goal.started_at.is_none());
1709        assert!(goal.completed_at.is_none());
1710
1711        let before_start = chrono::Utc::now();
1712        goal.start();
1713        assert_eq!(goal.status, GoalStatus::Active);
1714        assert!(goal.started_at.is_some());
1715        assert!(
1716            goal.started_at
1717                .expect("started_at should be set after start")
1718                >= before_start
1719        );
1720
1721        goal.achieve();
1722        assert_eq!(goal.status, GoalStatus::Achieved);
1723        assert!(goal.completed_at.is_some());
1724
1725        let mut failed = AgentGoal::new(agent_id, "fail", GoalType::Milestone);
1726        failed.fail("nope");
1727        assert_eq!(failed.status, GoalStatus::Failed);
1728        assert_eq!(failed.failure_reason.as_deref(), Some("nope"));
1729        assert!(failed.completed_at.is_some());
1730    }
1731
1732    #[test]
1733    fn test_agent_goal_criteria_satisfaction() {
1734        let agent_id = AgentId::now_v7();
1735        let goal = AgentGoal::new(agent_id, "empty", GoalType::Terminal);
1736        assert!(goal.all_criteria_satisfied());
1737
1738        let mut unsatisfied = AgentGoal::new(agent_id, "needs work", GoalType::Subgoal)
1739            .with_criterion(SuccessCriterion::new("a"))
1740            .with_criterion(SuccessCriterion::new("b"));
1741        assert!(!unsatisfied.all_criteria_satisfied());
1742        unsatisfied.success_criteria[0].satisfied = true;
1743        unsatisfied.success_criteria[1].satisfied = true;
1744        assert!(unsatisfied.all_criteria_satisfied());
1745    }
1746
1747    #[test]
1748    fn test_plan_step_and_cost_builders() {
1749        let dep = StepId::now_v7();
1750        let step = PlanStep::new(1, "do", ActionType::Operation)
1751            .with_precondition("ready")
1752            .with_postcondition("done")
1753            .depends_on(dep);
1754
1755        assert_eq!(step.index, 1);
1756        assert_eq!(step.preconditions, vec!["ready".to_string()]);
1757        assert_eq!(step.postconditions, vec!["done".to_string()]);
1758        assert_eq!(step.depends_on, vec![dep]);
1759        assert_eq!(step.status, StepStatus::Pending);
1760
1761        let cost = PlanCost::new(100, 2500).with_monetary_cost(1.25);
1762        assert_eq!(cost.estimated_tokens, 100);
1763        assert_eq!(cost.estimated_duration_ms, 2500);
1764        assert_eq!(cost.monetary_cost_usd, Some(1.25));
1765    }
1766
1767    #[test]
1768    fn test_agent_plan_flow_and_next_step() {
1769        let agent_id = AgentId::now_v7();
1770        let goal_id = GoalId::now_v7();
1771        let mut plan = AgentPlan::new(agent_id, goal_id, "plan it");
1772        assert_eq!(plan.status, PlanStatus::Draft);
1773
1774        let mut step_ready = PlanStep::new(1, "prep", ActionType::Operation);
1775        step_ready.status = StepStatus::Ready;
1776        let step_pending = PlanStep::new(2, "execute", ActionType::ToolCall);
1777
1778        plan.add_step(step_ready.clone());
1779        plan.add_step(step_pending.clone());
1780
1781        let next = plan.next_step().expect("expected next step");
1782        assert_eq!(next.step_id, step_ready.step_id);
1783
1784        plan.ready();
1785        assert_eq!(plan.status, PlanStatus::Ready);
1786
1787        let before_start = chrono::Utc::now();
1788        plan.start();
1789        assert_eq!(plan.status, PlanStatus::InProgress);
1790        assert!(plan.started_at.is_some());
1791        assert!(
1792            plan.started_at
1793                .expect("started_at should be set after start")
1794                >= before_start
1795        );
1796
1797        let cost = PlanCost::new(10, 100);
1798        plan.complete(Some(cost.clone()));
1799        assert_eq!(plan.status, PlanStatus::Completed);
1800        assert_eq!(plan.actual_cost, Some(cost));
1801        assert!(plan.completed_at.is_some());
1802    }
1803
1804    #[test]
1805    fn test_backoff_strategy_delay() {
1806        let fixed = BackoffStrategy::Fixed { delay_ms: 100 };
1807        assert_eq!(fixed.delay_for_attempt(3), 100);
1808
1809        let linear = BackoffStrategy::Linear {
1810            base_ms: 50,
1811            increment_ms: 10,
1812        };
1813        assert_eq!(linear.delay_for_attempt(0), 50);
1814        assert_eq!(linear.delay_for_attempt(3), 80);
1815
1816        let exp = BackoffStrategy::Exponential {
1817            base_ms: 100,
1818            multiplier: 2.0,
1819            max_ms: 1000,
1820        };
1821        assert_eq!(exp.delay_for_attempt(0), 100);
1822        assert_eq!(exp.delay_for_attempt(3), 800);
1823        assert_eq!(exp.delay_for_attempt(4), 1000);
1824    }
1825
1826    #[test]
1827    fn test_retry_policy_default_values() {
1828        let policy = RetryPolicy::default();
1829        assert_eq!(policy.max_attempts, 3);
1830        assert_eq!(policy.timeout_per_attempt_ms, 30_000);
1831        match policy.backoff {
1832            BackoffStrategy::Exponential {
1833                base_ms,
1834                multiplier,
1835                max_ms,
1836            } => {
1837                assert_eq!(base_ms, 100);
1838                assert!((multiplier - 2.0).abs() < f64::EPSILON);
1839                assert_eq!(max_ms, 10_000);
1840            }
1841            _ => panic!("expected exponential backoff"),
1842        }
1843    }
1844
1845    #[test]
1846    fn test_agent_action_flow_and_retry() {
1847        let agent_id = AgentId::now_v7();
1848        let policy = RetryPolicy {
1849            max_attempts: 2,
1850            backoff: BackoffStrategy::None,
1851            timeout_per_attempt_ms: 1000,
1852        };
1853
1854        let mut action = AgentAction::new(agent_id, ActionType::Operation, "do")
1855            .with_retry_policy(policy)
1856            .with_parameters(json!({"k": "v"}))
1857            .with_timeout(500);
1858
1859        assert_eq!(action.status, ActionStatus::Pending);
1860        assert_eq!(action.attempt_count, 0);
1861        assert!(action.can_retry());
1862
1863        action.start();
1864        assert_eq!(action.status, ActionStatus::InProgress);
1865        assert_eq!(action.attempt_count, 1);
1866        let started_at = action.started_at;
1867
1868        action.start();
1869        assert_eq!(action.attempt_count, 2);
1870        assert_eq!(action.started_at, started_at);
1871
1872        action.complete();
1873        assert_eq!(action.status, ActionStatus::Completed);
1874        assert!(action.completed_at.is_some());
1875
1876        action.attempt_count = 2;
1877        assert!(!action.can_retry());
1878    }
1879
1880    #[test]
1881    fn test_agent_action_without_retry_policy() {
1882        let agent_id = AgentId::now_v7();
1883        let action = AgentAction::new(agent_id, ActionType::Operation, "do");
1884        assert!(!action.can_retry());
1885    }
1886
1887    #[test]
1888    fn test_success_criterion_defaults() {
1889        let criterion = SuccessCriterion::new("ok");
1890        assert_eq!(criterion.description, "ok");
1891        assert!(!criterion.measurable);
1892        assert!(criterion.target_value.is_none());
1893        assert!(criterion.current_value.is_none());
1894        assert!(!criterion.satisfied);
1895    }
1896
1897    #[test]
1898    fn test_belief_confidence_clamp_and_active() {
1899        let agent_id = AgentId::now_v7();
1900        let mut belief = Belief::new(
1901            agent_id,
1902            "fact",
1903            BeliefType::Fact,
1904            BeliefSource::Observation,
1905        )
1906        .with_confidence(2.5);
1907        assert_eq!(belief.confidence, 1.0);
1908
1909        let before_update = belief.updated_at;
1910        belief.update_confidence(-5.0);
1911        assert_eq!(belief.confidence, 0.0);
1912        assert!(belief.updated_at >= before_update);
1913
1914        let new_id = BeliefId::now_v7();
1915        belief.supersede(new_id);
1916        assert_eq!(belief.superseded_by, Some(new_id));
1917        assert!(!belief.is_active());
1918    }
1919
1920    #[test]
1921    fn test_agent_beliefs_add_and_active_filter() {
1922        let agent_id = AgentId::now_v7();
1923        let mut beliefs = AgentBeliefs::new(agent_id);
1924
1925        let fact = Belief::new(
1926            agent_id,
1927            "fact",
1928            BeliefType::Fact,
1929            BeliefSource::MemoryRecall,
1930        );
1931        let hypothesis = Belief::new(
1932            agent_id,
1933            "maybe",
1934            BeliefType::Hypothesis,
1935            BeliefSource::Inference,
1936        );
1937        let mut superseded =
1938            Belief::new(agent_id, "old", BeliefType::Fact, BeliefSource::Observation);
1939        superseded.supersede(BeliefId::now_v7());
1940
1941        beliefs.add(fact.clone());
1942        beliefs.add(hypothesis.clone());
1943        beliefs.add(superseded.clone());
1944
1945        assert_eq!(beliefs.facts.len(), 2);
1946        assert_eq!(beliefs.hypotheses.len(), 1);
1947        assert_eq!(beliefs.uncertainties.len(), 0);
1948
1949        let active: Vec<_> = beliefs.active_beliefs().collect();
1950        assert!(active.iter().all(|b| b.is_active()));
1951        assert_eq!(active.len(), 2);
1952    }
1953
1954    #[test]
1955    fn test_agent_observation_and_learning_builders() {
1956        let agent_id = AgentId::now_v7();
1957        let action_id = ActionId::now_v7();
1958        let belief_id = BeliefId::now_v7();
1959
1960        let learning = Learning::new(
1961            ObservationId::now_v7(),
1962            LearningType::PatternRecognition,
1963            "pattern",
1964        )
1965        .with_abstraction(AbstractionLevel::Summary)
1966        .with_applicability("global")
1967        .with_confidence(0.4);
1968
1969        let mut obs = AgentObservation::new(agent_id, action_id, true, 150)
1970            .with_result(json!({"ok": true}))
1971            .with_error("none")
1972            .with_tokens(42)
1973            .with_cost(0.25);
1974
1975        obs.add_belief_update(belief_id);
1976        obs.add_learning(learning);
1977
1978        assert_eq!(obs.agent_id, agent_id);
1979        assert_eq!(obs.action_id, action_id);
1980        assert_eq!(obs.duration_ms, 150);
1981        assert_eq!(obs.tokens_used, Some(42));
1982        assert_eq!(obs.cost_usd, Some(0.25));
1983        assert_eq!(obs.belief_updates, vec![belief_id]);
1984        assert_eq!(obs.learnings.len(), 1);
1985        assert!(obs.result.is_some());
1986        assert!(obs.error.is_some());
1987    }
1988
1989    #[test]
1990    fn test_message_type_parsing_variants_and_errors() {
1991        let underscored =
1992            MessageType::from_db_str("task_delegation").expect("underscore variant should parse");
1993        assert_eq!(underscored, MessageType::TaskDelegation);
1994
1995        let mixed = MessageType::from_db_str("Coordination_Signal")
1996            .expect("mixed case underscore variant should parse");
1997        assert_eq!(mixed, MessageType::CoordinationSignal);
1998
1999        let err = MessageType::from_db_str("unknown_type")
2000            .expect_err("unknown type should fail to parse");
2001        assert_eq!(err.input, "unknown_type");
2002    }
2003
2004    #[test]
2005    fn test_message_priority_parsing_variants_and_errors() {
2006        let high = MessagePriority::from_db_str("HIGH").expect("uppercase should parse");
2007        assert_eq!(high, MessagePriority::High);
2008
2009        let normal = MessagePriority::from_db_str("normal").expect("lowercase should parse");
2010        assert_eq!(normal, MessagePriority::Normal);
2011
2012        let err = MessagePriority::from_db_str("urgent")
2013            .expect_err("unknown priority should fail to parse");
2014        assert_eq!(err.input, "urgent");
2015    }
2016
2017    #[test]
2018    fn test_handoff_reason_parsing_aliases() {
2019        let failed =
2020            HandoffReason::from_db_str("failed").expect("failed alias should parse to Failure");
2021        assert_eq!(failed, HandoffReason::Failure);
2022
2023        let load =
2024            HandoffReason::from_db_str("load_balancing").expect("underscore variant should parse");
2025        assert_eq!(load, HandoffReason::LoadBalancing);
2026
2027        let err = HandoffReason::from_db_str("nonsense")
2028            .expect_err("unknown reason should fail to parse");
2029        assert_eq!(err.input, "nonsense");
2030    }
2031
2032    #[test]
2033    fn test_memory_access_default_permissions() {
2034        let access = MemoryAccess::default();
2035        assert_eq!(access.read.len(), 1);
2036        assert_eq!(access.write.len(), 1);
2037        assert_eq!(access.read[0].memory_type, crate::MemoryType::All);
2038        assert_eq!(access.write[0].memory_type, crate::MemoryType::All);
2039        assert_eq!(access.read[0].scope, PermissionScope::Own);
2040        assert_eq!(access.write[0].scope, PermissionScope::Own);
2041        assert!(access.read[0].filter.is_none());
2042        assert!(access.write[0].filter.is_none());
2043    }
2044}