cellstate_core/
entities.rs

1//! Core entity structures
2
3use crate::agent::AgentTarget;
4use crate::{
5    identity::EntityIdType,
6    AbstractionLevel,
7    AgentId,
8    // Agent-related types
9    AgentStatus,
10    AgentType,
11    ArtifactId,
12    ArtifactType,
13    ConflictId,
14    ConflictStatus,
15    ConflictType,
16    ContentHash,
17    DelegationId,
18    DelegationResult,
19    DelegationResultStatus,
20    DelegationStatus,
21    EmbeddingVector,
22    // Other types
23    EntityType,
24    ExtractionMethod,
25    HandoffId,
26    HandoffReason,
27    HandoffStatus,
28    MemoryAccess,
29    MessageId,
30    MessagePriority,
31    MessageType,
32    NoteId,
33    NoteType,
34    OutcomeStatus,
35    PrincipalId,
36    PrincipalType,
37    RawContent,
38    ResolutionStrategy,
39    ScopeId,
40    TeamId,
41    TenantId,
42    Timestamp,
43    // ID types
44    TrajectoryId,
45    TrajectoryStatus,
46    TurnId,
47    TurnRole,
48    WorkingSetId,
49    TTL,
50};
51use chrono::Utc;
52use serde::{Deserialize, Serialize};
53use uuid::Uuid;
54
55/// Reference to an entity by type and ID.
56#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
57#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
58pub struct EntityRef {
59    pub entity_type: EntityType,
60    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
61    pub id: Uuid, // Keep as Uuid - this is intentional, represents ANY entity
62}
63
64/// Trajectory - top-level task container.
65/// A trajectory represents a complete task or goal being pursued.
66#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
67#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
68pub struct Trajectory {
69    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
70    pub trajectory_id: TrajectoryId,
71    pub name: String,
72    pub description: Option<String>,
73    pub status: TrajectoryStatus,
74    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
75    pub parent_trajectory_id: Option<TrajectoryId>,
76    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
77    pub root_trajectory_id: Option<TrajectoryId>,
78    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
79    pub agent_id: Option<AgentId>,
80    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
81    pub created_at: Timestamp,
82    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
83    pub updated_at: Timestamp,
84    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
85    pub completed_at: Option<Timestamp>,
86    pub outcome: Option<TrajectoryOutcome>,
87    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
88    pub metadata: Option<serde_json::Value>,
89}
90
91/// Outcome of a completed trajectory.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
94pub struct TrajectoryOutcome {
95    pub status: OutcomeStatus,
96    pub summary: String,
97    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
98    pub produced_artifacts: Vec<ArtifactId>,
99    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
100    pub produced_notes: Vec<NoteId>,
101    pub error: Option<String>,
102}
103
104/// Scope - partitioned context window within a trajectory.
105/// Scopes provide isolation and checkpointing boundaries.
106#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
107#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
108pub struct Scope {
109    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
110    pub scope_id: ScopeId,
111    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
112    pub trajectory_id: TrajectoryId,
113    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
114    pub parent_scope_id: Option<ScopeId>,
115    pub name: String,
116    pub purpose: Option<String>,
117    pub is_active: bool,
118    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
119    pub created_at: Timestamp,
120    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
121    pub closed_at: Option<Timestamp>,
122    pub checkpoint: Option<Checkpoint>,
123    pub token_budget: i32,
124    pub tokens_used: i32,
125    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
126    pub metadata: Option<serde_json::Value>,
127}
128
129/// Checkpoint for scope recovery.
130#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
131#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
132pub struct Checkpoint {
133    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "byte"))]
134    pub context_state: RawContent,
135    pub recoverable: bool,
136}
137
138/// Artifact - typed output preserved across scopes.
139/// Artifacts survive scope closure and can be referenced later.
140#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
141#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
142pub struct Artifact {
143    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
144    pub artifact_id: ArtifactId,
145    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
146    pub trajectory_id: TrajectoryId,
147    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
148    pub scope_id: ScopeId,
149    pub artifact_type: ArtifactType,
150    pub name: String,
151    pub content: String,
152    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "byte"))]
153    pub content_hash: ContentHash,
154    pub embedding: Option<EmbeddingVector>,
155    pub provenance: Provenance,
156    pub ttl: TTL,
157    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
158    pub created_at: Timestamp,
159    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
160    pub updated_at: Timestamp,
161    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
162    pub superseded_by: Option<ArtifactId>,
163    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
164    pub metadata: Option<serde_json::Value>,
165}
166
167/// Typed metadata for multimodal artifacts.
168/// This enum provides strongly-typed metadata schemas for different artifact types.
169/// It can be serialized to/from the generic `metadata` JSONB field.
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
171#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
172#[serde(tag = "type", rename_all = "snake_case")]
173pub enum ArtifactMetadata {
174    /// Metadata for audio artifacts
175    Audio {
176        /// Duration in seconds
177        duration_secs: f32,
178        /// Audio format (e.g., "mp3", "wav", "ogg")
179        format: String,
180        /// Optional linked transcript artifact
181        #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
182        transcript_id: Option<ArtifactId>,
183    },
184    /// Metadata for image artifacts
185    Image {
186        /// Image width in pixels
187        width: u32,
188        /// Image height in pixels
189        height: u32,
190        /// Image format (e.g., "png", "jpeg", "webp")
191        format: String,
192        /// Alt text description
193        alt_text: Option<String>,
194    },
195    /// Metadata for video artifacts
196    Video {
197        /// Duration in seconds
198        duration_secs: f32,
199        /// Video format (e.g., "mp4", "webm")
200        format: String,
201        /// Optional linked thumbnail artifact
202        #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
203        thumbnail_id: Option<ArtifactId>,
204    },
205    /// Metadata for transcript artifacts (audio→text)
206    Transcript {
207        /// Source audio artifact that was transcribed
208        #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
209        source_artifact_id: ArtifactId,
210        /// Transcription confidence score (0.0-1.0)
211        confidence: f32,
212        /// Language of the transcript (ISO 639-1 code)
213        language: Option<String>,
214    },
215    /// Metadata for screenshot artifacts
216    Screenshot {
217        /// When the screenshot was captured
218        #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
219        captured_at: Timestamp,
220        /// Context description (what was being viewed)
221        context: String,
222        /// Image width in pixels
223        width: Option<u32>,
224        /// Image height in pixels
225        height: Option<u32>,
226    },
227}
228
229impl ArtifactMetadata {
230    /// Convert to a generic JSON value for storage in the metadata field.
231    pub fn to_json(&self) -> serde_json::Value {
232        serde_json::to_value(self).unwrap_or_default()
233    }
234
235    /// Try to parse from a generic JSON value.
236    pub fn from_json(value: &serde_json::Value) -> Option<Self> {
237        serde_json::from_value(value.clone()).ok()
238    }
239}
240
241/// Provenance information for an artifact.
242#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
243#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
244pub struct Provenance {
245    pub source_turn: i32,
246    pub extraction_method: ExtractionMethod,
247    pub confidence: Option<f32>,
248}
249
250/// Note - long-term cross-trajectory knowledge.
251/// Notes persist beyond individual trajectories.
252#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
253#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
254pub struct Note {
255    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
256    pub note_id: NoteId,
257    pub note_type: NoteType,
258    pub title: String,
259    pub content: String,
260    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "byte"))]
261    pub content_hash: ContentHash,
262    pub embedding: Option<EmbeddingVector>,
263    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
264    pub source_trajectory_ids: Vec<TrajectoryId>,
265    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
266    pub source_artifact_ids: Vec<ArtifactId>,
267    pub ttl: TTL,
268    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
269    pub created_at: Timestamp,
270    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
271    pub updated_at: Timestamp,
272    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
273    pub accessed_at: Timestamp,
274    pub access_count: i32,
275    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
276    pub superseded_by: Option<NoteId>,
277    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
278    pub metadata: Option<serde_json::Value>,
279    // ══════════════════════════════════════════════════════════════════════════
280    // Battle Intel Feature 2: Abstraction levels (EVOLVE-MEM L0/L1/L2 hierarchy)
281    // ══════════════════════════════════════════════════════════════════════════
282    /// Semantic abstraction tier (Raw=L0, Summary=L1, Principle=L2)
283    pub abstraction_level: AbstractionLevel,
284    /// Notes this was derived from (for L1/L2 derivation chains)
285    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
286    pub source_note_ids: Vec<NoteId>,
287}
288
289/// Turn - ephemeral conversation buffer entry.
290/// Turns die with their scope.
291#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
292#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
293pub struct Turn {
294    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
295    pub turn_id: TurnId,
296    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
297    pub scope_id: ScopeId,
298    pub sequence: i32,
299    pub role: TurnRole,
300    pub content: String,
301    pub token_count: i32,
302    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
303    pub created_at: Timestamp,
304    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
305    pub tool_calls: Option<serde_json::Value>,
306    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
307    pub tool_results: Option<serde_json::Value>,
308    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
309    pub metadata: Option<serde_json::Value>,
310}
311
312// ============================================================================
313// AGENT ENTITIES (from cellstate-agents)
314// ============================================================================
315
316/// Principal identity for ownership and security scoping.
317#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
318#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
319pub struct Principal {
320    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
321    pub principal_id: PrincipalId,
322    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
323    pub tenant_id: TenantId,
324    pub principal_type: PrincipalType,
325    pub user_id: Option<String>,
326    pub display_name: Option<String>,
327    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
328    pub metadata: Option<serde_json::Value>,
329    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
330    pub created_at: Timestamp,
331    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
332    pub updated_at: Timestamp,
333}
334
335/// An agent in the multi-agent system.
336#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
337#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
338pub struct Agent {
339    /// Unique identifier for this agent
340    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
341    pub agent_id: AgentId,
342    /// Type of agent (e.g., tester, reviewer, planner)
343    pub agent_type: AgentType,
344    /// Capabilities this agent has
345    pub capabilities: Vec<String>,
346    /// Memory access permissions
347    pub memory_access: MemoryAccess,
348
349    /// Current status
350    pub status: AgentStatus,
351    /// Current trajectory being worked on
352    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
353    pub current_trajectory_id: Option<TrajectoryId>,
354    /// Current scope being worked on
355    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
356    pub current_scope_id: Option<ScopeId>,
357
358    /// Agent types this agent can delegate to
359    pub can_delegate_to: Vec<String>,
360    /// Supervisor agent (if any)
361    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
362    pub reports_to: Option<AgentId>,
363    /// Principal that owns this agent
364    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
365    pub owner_principal_id: PrincipalId,
366
367    /// When this agent was created
368    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
369    pub created_at: Timestamp,
370    /// Last heartbeat timestamp
371    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
372    pub last_heartbeat_at: Timestamp,
373}
374
375impl Agent {
376    /// Create a new agent.
377    pub fn new(agent_type: impl Into<AgentType>, capabilities: Vec<String>) -> Self {
378        let now = Utc::now();
379        Self {
380            agent_id: AgentId::new(Uuid::now_v7()),
381            agent_type: agent_type.into(),
382            capabilities,
383            memory_access: MemoryAccess::default(),
384            status: AgentStatus::Idle,
385            current_trajectory_id: None,
386            current_scope_id: None,
387            can_delegate_to: Vec::new(),
388            reports_to: None,
389            owner_principal_id: PrincipalId::nil(),
390            created_at: now,
391            last_heartbeat_at: now,
392        }
393    }
394
395    /// Set memory access permissions.
396    pub fn with_memory_access(mut self, access: MemoryAccess) -> Self {
397        self.memory_access = access;
398        self
399    }
400
401    /// Set delegation targets.
402    pub fn with_delegation_targets(mut self, targets: Vec<String>) -> Self {
403        self.can_delegate_to = targets;
404        self
405    }
406
407    /// Set supervisor.
408    pub fn with_supervisor(mut self, supervisor_id: AgentId) -> Self {
409        self.reports_to = Some(supervisor_id);
410        self
411    }
412
413    /// Set owner principal.
414    pub fn with_owner_principal(mut self, owner_principal_id: PrincipalId) -> Self {
415        self.owner_principal_id = owner_principal_id;
416        self
417    }
418
419    /// Update heartbeat timestamp.
420    pub fn heartbeat(&mut self) {
421        self.last_heartbeat_at = Utc::now();
422    }
423
424    /// Check if agent has a specific capability.
425    pub fn has_capability(&self, capability: &str) -> bool {
426        self.capabilities.iter().any(|c| c == capability)
427    }
428
429    /// Check if agent can delegate to a specific agent type.
430    pub fn can_delegate_to_type(&self, agent_type: &AgentType) -> bool {
431        self.can_delegate_to
432            .iter()
433            .any(|t| t == agent_type.as_str())
434    }
435}
436
437/// Ephemeral document/KV working memory entry for an agent.
438#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
439#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
440pub struct AgentWorkingSetEntry {
441    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
442    pub working_set_id: WorkingSetId,
443    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
444    pub tenant_id: TenantId,
445    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
446    pub agent_id: AgentId,
447    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
448    pub owner_principal_id: PrincipalId,
449    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
450    pub scope_id: Option<ScopeId>,
451    pub key: String,
452    #[cfg_attr(feature = "openapi", schema(value_type = Object))]
453    pub value: serde_json::Value,
454    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
455    pub expires_at: Option<Timestamp>,
456    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
457    pub updated_at: Timestamp,
458    pub version: i64,
459    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
460    pub metadata: Option<serde_json::Value>,
461}
462
463/// A message between agents.
464#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
465#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
466pub struct AgentMessage {
467    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
468    pub message_id: MessageId,
469    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
470    pub from_agent_id: AgentId,
471    /// Recipient: specific agent (by_id) or any agent of a type (by_type).
472    pub to: Option<AgentTarget>,
473    pub message_type: MessageType,
474    pub payload: String,
475    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
476    pub trajectory_id: Option<TrajectoryId>,
477    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
478    pub scope_id: Option<ScopeId>,
479    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
480    pub artifact_ids: Vec<ArtifactId>,
481    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
482    pub created_at: Timestamp,
483    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
484    pub delivered_at: Option<Timestamp>,
485    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
486    pub acknowledged_at: Option<Timestamp>,
487    pub priority: MessagePriority,
488    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
489    pub expires_at: Option<Timestamp>,
490}
491
492impl AgentMessage {
493    /// Create a message to a specific agent.
494    pub fn to_agent(from: AgentId, to: AgentId, msg_type: MessageType, payload: &str) -> Self {
495        Self {
496            message_id: MessageId::new(Uuid::now_v7()),
497            from_agent_id: from,
498            to: Some(AgentTarget::ById(to)),
499            message_type: msg_type,
500            payload: payload.to_string(),
501            trajectory_id: None,
502            scope_id: None,
503            artifact_ids: Vec::new(),
504            created_at: Utc::now(),
505            delivered_at: None,
506            acknowledged_at: None,
507            priority: MessagePriority::Normal,
508            expires_at: None,
509        }
510    }
511
512    /// Create a message to an agent type.
513    pub fn to_type(
514        from: AgentId,
515        agent_type: impl Into<AgentType>,
516        msg_type: MessageType,
517        payload: &str,
518    ) -> Self {
519        Self {
520            message_id: MessageId::new(Uuid::now_v7()),
521            from_agent_id: from,
522            to: Some(AgentTarget::ByType(agent_type.into())),
523            message_type: msg_type,
524            payload: payload.to_string(),
525            trajectory_id: None,
526            scope_id: None,
527            artifact_ids: Vec::new(),
528            created_at: Utc::now(),
529            delivered_at: None,
530            acknowledged_at: None,
531            priority: MessagePriority::Normal,
532            expires_at: None,
533        }
534    }
535
536    /// Associate with trajectory.
537    pub fn with_trajectory(mut self, trajectory_id: TrajectoryId) -> Self {
538        self.trajectory_id = Some(trajectory_id);
539        self
540    }
541
542    /// Associate with scope.
543    pub fn with_scope(mut self, scope_id: ScopeId) -> Self {
544        self.scope_id = Some(scope_id);
545        self
546    }
547
548    /// Add artifacts.
549    pub fn with_artifacts(mut self, artifacts: Vec<ArtifactId>) -> Self {
550        self.artifact_ids = artifacts;
551        self
552    }
553
554    /// Set priority.
555    pub fn with_priority(mut self, priority: MessagePriority) -> Self {
556        self.priority = priority;
557        self
558    }
559
560    /// Mark as delivered.
561    pub fn mark_delivered(&mut self) {
562        self.delivered_at = Some(Utc::now());
563    }
564
565    /// Mark as acknowledged.
566    pub fn mark_acknowledged(&mut self) {
567        self.acknowledged_at = Some(Utc::now());
568    }
569
570    /// Check if message is for a specific agent.
571    pub fn is_for_agent(&self, agent_id: AgentId) -> bool {
572        self.to.as_ref().and_then(AgentTarget::as_agent_id) == Some(agent_id)
573    }
574}
575
576/// A delegated task from one agent to another.
577#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
578#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
579pub struct DelegatedTask {
580    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
581    pub delegation_id: DelegationId,
582    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
583    pub delegator_agent_id: AgentId,
584    /// Delegatee: specific agent (by_id) or any agent of a type (by_type).
585    pub delegatee: Option<AgentTarget>,
586    pub task_description: String,
587    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
588    pub parent_trajectory_id: TrajectoryId,
589    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
590    pub child_trajectory_id: Option<TrajectoryId>,
591    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
592    pub shared_artifacts: Vec<ArtifactId>,
593    #[cfg_attr(feature = "openapi", schema(value_type = Vec<String>))]
594    pub shared_notes: Vec<NoteId>,
595    pub additional_context: Option<String>,
596    #[cfg_attr(feature = "openapi", schema(value_type = Option<Object>))]
597    pub constraints: Option<serde_json::Value>,
598    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
599    pub deadline: Option<Timestamp>,
600    pub status: DelegationStatus,
601    pub result: Option<DelegationResult>,
602    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
603    pub created_at: Timestamp,
604    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
605    pub accepted_at: Option<Timestamp>,
606    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
607    pub completed_at: Option<Timestamp>,
608}
609
610impl DelegatedTask {
611    /// Create a delegation to a specific agent.
612    pub fn to_agent(
613        from: AgentId,
614        to: AgentId,
615        trajectory: TrajectoryId,
616        description: &str,
617    ) -> Self {
618        Self {
619            delegation_id: DelegationId::new(Uuid::now_v7()),
620            delegator_agent_id: from,
621            delegatee: Some(AgentTarget::ById(to)),
622            task_description: description.to_string(),
623            parent_trajectory_id: trajectory,
624            child_trajectory_id: None,
625            shared_artifacts: Vec::new(),
626            shared_notes: Vec::new(),
627            additional_context: None,
628            constraints: None,
629            deadline: None,
630            status: DelegationStatus::Pending,
631            result: None,
632            created_at: Utc::now(),
633            accepted_at: None,
634            completed_at: None,
635        }
636    }
637
638    /// Create a delegation to an agent type.
639    pub fn to_type(
640        from: AgentId,
641        agent_type: impl Into<AgentType>,
642        trajectory: TrajectoryId,
643        description: &str,
644    ) -> Self {
645        Self {
646            delegation_id: DelegationId::new(Uuid::now_v7()),
647            delegator_agent_id: from,
648            delegatee: Some(AgentTarget::ByType(agent_type.into())),
649            task_description: description.to_string(),
650            parent_trajectory_id: trajectory,
651            child_trajectory_id: None,
652            shared_artifacts: Vec::new(),
653            shared_notes: Vec::new(),
654            additional_context: None,
655            constraints: None,
656            deadline: None,
657            status: DelegationStatus::Pending,
658            result: None,
659            created_at: Utc::now(),
660            accepted_at: None,
661            completed_at: None,
662        }
663    }
664
665    /// Add shared artifacts.
666    pub fn with_shared_artifacts(mut self, artifacts: Vec<ArtifactId>) -> Self {
667        self.shared_artifacts = artifacts;
668        self
669    }
670
671    /// Add shared notes.
672    pub fn with_shared_notes(mut self, notes: Vec<NoteId>) -> Self {
673        self.shared_notes = notes;
674        self
675    }
676
677    /// Set deadline.
678    pub fn with_deadline(mut self, deadline: Timestamp) -> Self {
679        self.deadline = Some(deadline);
680        self
681    }
682
683    /// Accept the delegation.
684    pub fn accept(&mut self) {
685        self.status = DelegationStatus::Accepted;
686        self.accepted_at = Some(Utc::now());
687    }
688
689    /// Complete the delegation.
690    pub fn complete(&mut self, result: DelegationResult) {
691        self.status = if result.status == DelegationResultStatus::Failure {
692            DelegationStatus::Failed
693        } else {
694            DelegationStatus::Completed
695        };
696        self.result = Some(result);
697        self.completed_at = Some(Utc::now());
698    }
699}
700
701/// A handoff from one agent to another.
702#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
703#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
704pub struct AgentHandoff {
705    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
706    pub handoff_id: HandoffId,
707    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
708    pub from_agent_id: AgentId,
709    /// Recipient: specific agent (by_id) or any agent of a type (by_type).
710    pub to: Option<AgentTarget>,
711    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
712    pub trajectory_id: TrajectoryId,
713    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
714    pub scope_id: ScopeId,
715    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
716    pub context_snapshot_id: Option<ArtifactId>,
717    pub handoff_notes: Option<String>,
718    pub next_steps: Vec<String>,
719    pub blockers: Vec<String>,
720    pub open_questions: Vec<String>,
721    pub status: HandoffStatus,
722    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
723    pub initiated_at: Timestamp,
724    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
725    pub accepted_at: Option<Timestamp>,
726    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
727    pub completed_at: Option<Timestamp>,
728    pub reason: HandoffReason,
729}
730
731impl AgentHandoff {
732    /// Create a handoff to a specific agent.
733    pub fn to_agent(
734        from: AgentId,
735        to: AgentId,
736        trajectory: TrajectoryId,
737        scope: ScopeId,
738        reason: HandoffReason,
739    ) -> Self {
740        Self {
741            handoff_id: HandoffId::new(Uuid::now_v7()),
742            from_agent_id: from,
743            to: Some(AgentTarget::ById(to)),
744            trajectory_id: trajectory,
745            scope_id: scope,
746            context_snapshot_id: None,
747            handoff_notes: None,
748            next_steps: Vec::new(),
749            blockers: Vec::new(),
750            open_questions: Vec::new(),
751            status: HandoffStatus::Initiated,
752            initiated_at: Utc::now(),
753            accepted_at: None,
754            completed_at: None,
755            reason,
756        }
757    }
758
759    /// Create a handoff to an agent type.
760    pub fn to_type(
761        from: AgentId,
762        agent_type: impl Into<AgentType>,
763        trajectory: TrajectoryId,
764        scope: ScopeId,
765        reason: HandoffReason,
766    ) -> Self {
767        Self {
768            handoff_id: HandoffId::new(Uuid::now_v7()),
769            from_agent_id: from,
770            to: Some(AgentTarget::ByType(agent_type.into())),
771            trajectory_id: trajectory,
772            scope_id: scope,
773            context_snapshot_id: None,
774            handoff_notes: None,
775            next_steps: Vec::new(),
776            blockers: Vec::new(),
777            open_questions: Vec::new(),
778            status: HandoffStatus::Initiated,
779            initiated_at: Utc::now(),
780            accepted_at: None,
781            completed_at: None,
782            reason,
783        }
784    }
785
786    /// Add handoff notes.
787    pub fn with_notes(mut self, notes: &str) -> Self {
788        self.handoff_notes = Some(notes.to_string());
789        self
790    }
791
792    /// Add next steps.
793    pub fn with_next_steps(mut self, steps: Vec<String>) -> Self {
794        self.next_steps = steps;
795        self
796    }
797
798    /// Accept the handoff.
799    pub fn accept(&mut self) {
800        self.status = HandoffStatus::Accepted;
801        self.accepted_at = Some(Utc::now());
802    }
803
804    /// Complete the handoff.
805    pub fn complete(&mut self) {
806        self.status = HandoffStatus::Completed;
807        self.completed_at = Some(Utc::now());
808    }
809}
810
811/// A conflict between memory items.
812#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
813#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
814pub struct Conflict {
815    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
816    pub conflict_id: ConflictId,
817    pub conflict_type: ConflictType,
818    pub item_a_type: EntityType,
819    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
820    pub item_a_id: Uuid,
821    pub item_b_type: EntityType,
822    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
823    pub item_b_id: Uuid,
824    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
825    pub agent_a_id: Option<AgentId>,
826    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
827    pub agent_b_id: Option<AgentId>,
828    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
829    pub trajectory_id: Option<TrajectoryId>,
830    pub status: ConflictStatus,
831    pub resolution: Option<ConflictResolutionRecord>,
832    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
833    pub detected_at: Timestamp,
834    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "date-time"))]
835    pub resolved_at: Option<Timestamp>,
836}
837
838impl Conflict {
839    /// Create a new conflict.
840    pub fn new(
841        conflict_type: ConflictType,
842        item_a_type: EntityType,
843        item_a_id: Uuid,
844        item_b_type: EntityType,
845        item_b_id: Uuid,
846    ) -> Self {
847        Self {
848            conflict_id: ConflictId::new(Uuid::now_v7()),
849            conflict_type,
850            item_a_type,
851            item_a_id,
852            item_b_type,
853            item_b_id,
854            agent_a_id: None,
855            agent_b_id: None,
856            trajectory_id: None,
857            status: ConflictStatus::Detected,
858            resolution: None,
859            detected_at: Utc::now(),
860            resolved_at: None,
861        }
862    }
863
864    /// Associate with agents.
865    pub fn with_agents(mut self, agent_a: AgentId, agent_b: AgentId) -> Self {
866        self.agent_a_id = Some(agent_a);
867        self.agent_b_id = Some(agent_b);
868        self
869    }
870
871    /// Associate with trajectory.
872    pub fn with_trajectory(mut self, trajectory_id: TrajectoryId) -> Self {
873        self.trajectory_id = Some(trajectory_id);
874        self
875    }
876
877    /// Resolve the conflict.
878    pub fn resolve(&mut self, resolution: ConflictResolutionRecord) {
879        self.status = ConflictStatus::Resolved;
880        self.resolution = Some(resolution);
881        self.resolved_at = Some(Utc::now());
882    }
883}
884
885/// Record of how a conflict was resolved.
886#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
887#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
888pub struct ConflictResolutionRecord {
889    pub strategy: ResolutionStrategy,
890    pub winner: Option<String>,
891    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
892    pub merged_result_id: Option<Uuid>,
893    pub reason: String,
894    #[cfg_attr(feature = "openapi", schema(value_type = Option<String>, format = "uuid"))]
895    pub resolved_by: Option<AgentId>,
896}
897
898impl ConflictResolutionRecord {
899    /// Create an automatic resolution.
900    pub fn automatic(strategy: ResolutionStrategy, reason: &str) -> Self {
901        Self {
902            strategy,
903            winner: None,
904            merged_result_id: None,
905            reason: reason.to_string(),
906            resolved_by: None,
907        }
908    }
909
910    /// Create a manual resolution.
911    pub fn manual(strategy: ResolutionStrategy, reason: &str, resolved_by: AgentId) -> Self {
912        Self {
913            strategy,
914            winner: None,
915            merged_result_id: None,
916            reason: reason.to_string(),
917            resolved_by: Some(resolved_by),
918        }
919    }
920}
921
922// =============================================================================
923// TEAM-BASED ACCESS CONTROL
924// =============================================================================
925
926/// Role within a team.
927#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
928#[serde(rename_all = "snake_case")]
929#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
930pub enum TeamRole {
931    /// Full control over team settings and membership
932    Owner,
933    /// Can manage members and team resources
934    Admin,
935    /// Standard team member with access to team resources
936    Member,
937}
938
939impl std::fmt::Display for TeamRole {
940    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
941        let s = match self {
942            TeamRole::Owner => "owner",
943            TeamRole::Admin => "admin",
944            TeamRole::Member => "member",
945        };
946        write!(f, "{}", s)
947    }
948}
949
950impl std::str::FromStr for TeamRole {
951    type Err = String;
952
953    fn from_str(s: &str) -> Result<Self, Self::Err> {
954        match s.trim().to_ascii_lowercase().as_str() {
955            "owner" => Ok(TeamRole::Owner),
956            "admin" => Ok(TeamRole::Admin),
957            "member" => Ok(TeamRole::Member),
958            _ => Err(format!("Invalid TeamRole: {}", s)),
959        }
960    }
961}
962
963/// A team within a tenant for group-based access control.
964#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
965#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
966pub struct Team {
967    /// Team ID
968    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
969    pub team_id: TeamId,
970    /// Tenant this team belongs to
971    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
972    pub tenant_id: TenantId,
973    /// Team name (unique within tenant)
974    pub name: String,
975    /// Optional description
976    #[serde(skip_serializing_if = "Option::is_none")]
977    pub description: Option<String>,
978    /// When the team was created
979    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
980    pub created_at: Timestamp,
981    /// When the team was last updated
982    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
983    pub updated_at: Timestamp,
984}
985
986/// A membership record linking a user to a team.
987#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
988#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
989pub struct TeamMember {
990    /// Team ID
991    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "uuid"))]
992    pub team_id: TeamId,
993    /// User ID (WorkOS user ID or similar)
994    pub user_id: String,
995    /// Role within the team
996    pub role: TeamRole,
997    /// When the user joined the team
998    #[cfg_attr(feature = "openapi", schema(value_type = String, format = "date-time"))]
999    pub joined_at: Timestamp,
1000}
1001
1002// =============================================================================
1003// TESTS
1004// =============================================================================
1005
1006#[cfg(test)]
1007mod tests {
1008    use super::*;
1009    use serde_json::json;
1010
1011    fn sample_hash() -> ContentHash {
1012        ContentHash::new([7u8; 32])
1013    }
1014
1015    fn sample_embedding() -> EmbeddingVector {
1016        EmbeddingVector::new(vec![0.0, 1.0, 0.5], "test-model".to_string())
1017    }
1018
1019    #[test]
1020    fn test_entity_ref_serde_roundtrip() {
1021        let entity = EntityRef {
1022            entity_type: EntityType::Trajectory,
1023            id: Uuid::now_v7(),
1024        };
1025        let json = serde_json::to_string(&entity).expect("EntityRef serialization should succeed");
1026        let restored: EntityRef =
1027            serde_json::from_str(&json).expect("EntityRef deserialization should succeed");
1028        assert_eq!(entity, restored);
1029    }
1030
1031    #[test]
1032    fn test_trajectory_serde_roundtrip() {
1033        let outcome = TrajectoryOutcome {
1034            status: OutcomeStatus::Success,
1035            summary: "done".to_string(),
1036            produced_artifacts: vec![ArtifactId::now_v7()],
1037            produced_notes: vec![NoteId::now_v7()],
1038            error: None,
1039        };
1040        let trajectory = Trajectory {
1041            trajectory_id: TrajectoryId::now_v7(),
1042            name: "test".to_string(),
1043            description: Some("desc".to_string()),
1044            status: TrajectoryStatus::Active,
1045            parent_trajectory_id: None,
1046            root_trajectory_id: None,
1047            agent_id: Some(AgentId::now_v7()),
1048            created_at: Utc::now(),
1049            updated_at: Utc::now(),
1050            completed_at: None,
1051            outcome: Some(outcome),
1052            metadata: Some(json!({"k": "v"})),
1053        };
1054        let json =
1055            serde_json::to_string(&trajectory).expect("Trajectory serialization should succeed");
1056        let restored: Trajectory =
1057            serde_json::from_str(&json).expect("Trajectory deserialization should succeed");
1058        assert_eq!(trajectory, restored);
1059    }
1060
1061    #[test]
1062    fn test_scope_serde_roundtrip() {
1063        let scope = Scope {
1064            scope_id: ScopeId::now_v7(),
1065            trajectory_id: TrajectoryId::now_v7(),
1066            parent_scope_id: None,
1067            name: "scope".to_string(),
1068            purpose: Some("purpose".to_string()),
1069            is_active: true,
1070            created_at: Utc::now(),
1071            closed_at: None,
1072            checkpoint: Some(Checkpoint {
1073                context_state: vec![1, 2, 3],
1074                recoverable: true,
1075            }),
1076            token_budget: 8000,
1077            tokens_used: 42,
1078            metadata: Some(json!({"a": 1})),
1079        };
1080        let json = serde_json::to_string(&scope).expect("Scope serialization should succeed");
1081        let restored: Scope =
1082            serde_json::from_str(&json).expect("Scope deserialization should succeed");
1083        assert_eq!(scope, restored);
1084    }
1085
1086    #[test]
1087    fn test_artifact_serde_roundtrip() {
1088        let artifact = Artifact {
1089            artifact_id: ArtifactId::now_v7(),
1090            trajectory_id: TrajectoryId::now_v7(),
1091            scope_id: ScopeId::now_v7(),
1092            artifact_type: ArtifactType::Fact,
1093            name: "artifact".to_string(),
1094            content: "content".to_string(),
1095            content_hash: sample_hash(),
1096            embedding: Some(sample_embedding()),
1097            provenance: Provenance {
1098                source_turn: 1,
1099                extraction_method: ExtractionMethod::Explicit,
1100                confidence: Some(1.0),
1101            },
1102            ttl: TTL::Persistent,
1103            created_at: Utc::now(),
1104            updated_at: Utc::now(),
1105            superseded_by: None,
1106            metadata: Some(json!({"meta": true})),
1107        };
1108        let json = serde_json::to_string(&artifact).expect("Artifact serialization should succeed");
1109        let restored: Artifact =
1110            serde_json::from_str(&json).expect("Artifact deserialization should succeed");
1111        assert_eq!(artifact, restored);
1112    }
1113
1114    #[test]
1115    fn test_note_serde_roundtrip() {
1116        let note = Note {
1117            note_id: NoteId::now_v7(),
1118            note_type: NoteType::Insight,
1119            title: "title".to_string(),
1120            content: "content".to_string(),
1121            content_hash: sample_hash(),
1122            embedding: Some(sample_embedding()),
1123            source_trajectory_ids: vec![TrajectoryId::now_v7()],
1124            source_artifact_ids: vec![ArtifactId::now_v7()],
1125            ttl: TTL::Persistent,
1126            created_at: Utc::now(),
1127            updated_at: Utc::now(),
1128            accessed_at: Utc::now(),
1129            access_count: 2,
1130            superseded_by: None,
1131            metadata: Some(json!({"n": 1})),
1132            abstraction_level: AbstractionLevel::Raw,
1133            source_note_ids: vec![NoteId::now_v7()],
1134        };
1135        let json = serde_json::to_string(&note).expect("Note serialization should succeed");
1136        let restored: Note =
1137            serde_json::from_str(&json).expect("Note deserialization should succeed");
1138        assert_eq!(note, restored);
1139    }
1140
1141    #[test]
1142    fn test_turn_serde_roundtrip() {
1143        let turn = Turn {
1144            turn_id: TurnId::now_v7(),
1145            scope_id: ScopeId::now_v7(),
1146            sequence: 1,
1147            role: TurnRole::User,
1148            content: "hi".to_string(),
1149            token_count: 10,
1150            created_at: Utc::now(),
1151            tool_calls: Some(json!({"tool": "search"})),
1152            tool_results: Some(json!({"result": "ok"})),
1153            metadata: Some(json!({"m": true})),
1154        };
1155        let json = serde_json::to_string(&turn).expect("Turn serialization should succeed");
1156        let restored: Turn =
1157            serde_json::from_str(&json).expect("Turn deserialization should succeed");
1158        assert_eq!(turn, restored);
1159    }
1160
1161    #[test]
1162    fn test_agent_builder_and_helpers() {
1163        let mut agent = Agent::new("coder", vec!["code".to_string(), "review".to_string()]);
1164        assert_eq!(agent.status, AgentStatus::Idle);
1165        assert!(agent.has_capability("code"));
1166        assert!(!agent.has_capability("design"));
1167
1168        let access = MemoryAccess::default();
1169        agent = agent.with_memory_access(access.clone());
1170        assert_eq!(agent.memory_access, access);
1171
1172        agent = agent.with_delegation_targets(vec!["planner".to_string()]);
1173        assert!(agent.can_delegate_to_type(&crate::AgentType::Planner));
1174
1175        let supervisor = AgentId::now_v7();
1176        agent = agent.with_supervisor(supervisor);
1177        assert_eq!(agent.reports_to, Some(supervisor));
1178
1179        let before = agent.last_heartbeat_at;
1180        agent.heartbeat();
1181        assert!(agent.last_heartbeat_at >= before);
1182    }
1183
1184    #[test]
1185    fn test_agent_message_builders_and_state() {
1186        let from = AgentId::now_v7();
1187        let to = AgentId::now_v7();
1188        let mut msg = AgentMessage::to_agent(from, to, MessageType::TaskDelegation, "do it")
1189            .with_trajectory(TrajectoryId::now_v7())
1190            .with_scope(ScopeId::now_v7())
1191            .with_artifacts(vec![ArtifactId::now_v7()])
1192            .with_priority(MessagePriority::High);
1193
1194        assert_eq!(msg.to.as_ref().and_then(AgentTarget::as_agent_id), Some(to));
1195        assert!(msg
1196            .to
1197            .as_ref()
1198            .and_then(AgentTarget::as_agent_type)
1199            .is_none());
1200        assert!(msg.is_for_agent(to));
1201        assert_eq!(msg.priority, MessagePriority::High);
1202
1203        msg.mark_delivered();
1204        assert!(msg.delivered_at.is_some());
1205        msg.mark_acknowledged();
1206        assert!(msg.acknowledged_at.is_some());
1207
1208        let by_type = AgentMessage::to_type(from, "planner", MessageType::Heartbeat, "ping");
1209        assert!(by_type
1210            .to
1211            .as_ref()
1212            .and_then(AgentTarget::as_agent_id)
1213            .is_none());
1214        assert_eq!(
1215            by_type.to.as_ref().and_then(AgentTarget::as_agent_type),
1216            Some(&AgentType::Planner)
1217        );
1218    }
1219
1220    #[test]
1221    fn test_delegated_task_lifecycle() {
1222        let from = AgentId::now_v7();
1223        let to = AgentId::now_v7();
1224        let trajectory = TrajectoryId::now_v7();
1225        let mut task = DelegatedTask::to_agent(from, to, trajectory, "do it")
1226            .with_shared_artifacts(vec![ArtifactId::now_v7()])
1227            .with_shared_notes(vec![NoteId::now_v7()]);
1228
1229        assert_eq!(task.status, DelegationStatus::Pending);
1230        assert!(task.accepted_at.is_none());
1231        task.accept();
1232        assert_eq!(task.status, DelegationStatus::Accepted);
1233        assert!(task.accepted_at.is_some());
1234
1235        let result = DelegationResult::success("ok", vec![]);
1236        task.complete(result.clone());
1237        assert_eq!(task.status, DelegationStatus::Completed);
1238        assert_eq!(task.result, Some(result));
1239        assert!(task.completed_at.is_some());
1240
1241        let mut failed = DelegatedTask::to_type(from, "planner", trajectory, "fail");
1242        let fail_result = DelegationResult::failure("bad");
1243        failed.complete(fail_result.clone());
1244        assert_eq!(failed.status, DelegationStatus::Failed);
1245        assert_eq!(failed.result, Some(fail_result));
1246    }
1247
1248    #[test]
1249    fn test_handoff_lifecycle() {
1250        let from = AgentId::now_v7();
1251        let to = AgentId::now_v7();
1252        let trajectory = TrajectoryId::now_v7();
1253        let scope = ScopeId::now_v7();
1254        let mut handoff =
1255            AgentHandoff::to_agent(from, to, trajectory, scope, HandoffReason::Timeout)
1256                .with_notes("note")
1257                .with_next_steps(vec!["step1".to_string()]);
1258
1259        assert_eq!(handoff.status, HandoffStatus::Initiated);
1260        assert_eq!(handoff.handoff_notes.as_deref(), Some("note"));
1261        assert_eq!(handoff.next_steps.len(), 1);
1262
1263        handoff.accept();
1264        assert_eq!(handoff.status, HandoffStatus::Accepted);
1265        assert!(handoff.accepted_at.is_some());
1266
1267        handoff.complete();
1268        assert_eq!(handoff.status, HandoffStatus::Completed);
1269        assert!(handoff.completed_at.is_some());
1270
1271        let by_type =
1272            AgentHandoff::to_type(from, "planner", trajectory, scope, HandoffReason::Failure);
1273        assert!(by_type
1274            .to
1275            .as_ref()
1276            .and_then(AgentTarget::as_agent_id)
1277            .is_none());
1278        assert_eq!(
1279            by_type.to.as_ref().and_then(AgentTarget::as_agent_type),
1280            Some(&AgentType::Planner)
1281        );
1282    }
1283
1284    #[test]
1285    fn test_conflict_resolution_flow() {
1286        let item_a = Uuid::now_v7();
1287        let item_b = Uuid::now_v7();
1288        let mut conflict = Conflict::new(
1289            ConflictType::ContradictingFact,
1290            EntityType::Artifact,
1291            item_a,
1292            EntityType::Note,
1293            item_b,
1294        );
1295        assert_eq!(conflict.status, ConflictStatus::Detected);
1296
1297        let agent_a = AgentId::now_v7();
1298        let agent_b = AgentId::now_v7();
1299        conflict = conflict.with_agents(agent_a, agent_b);
1300        assert_eq!(conflict.agent_a_id, Some(agent_a));
1301        assert_eq!(conflict.agent_b_id, Some(agent_b));
1302
1303        let trajectory_id = TrajectoryId::now_v7();
1304        conflict = conflict.with_trajectory(trajectory_id);
1305        assert_eq!(conflict.trajectory_id, Some(trajectory_id));
1306
1307        let resolution = ConflictResolutionRecord::automatic(ResolutionStrategy::Merge, "auto");
1308        conflict.resolve(resolution.clone());
1309        assert_eq!(conflict.status, ConflictStatus::Resolved);
1310        assert_eq!(conflict.resolution, Some(resolution));
1311        assert!(conflict.resolved_at.is_some());
1312    }
1313
1314    #[test]
1315    fn test_conflict_resolution_record_manual() {
1316        let resolver = AgentId::now_v7();
1317        let record = ConflictResolutionRecord::manual(ResolutionStrategy::Escalate, "ok", resolver);
1318        assert_eq!(record.resolved_by, Some(resolver));
1319        assert_eq!(record.reason, "ok");
1320    }
1321}