pulsedb/activity/types.rs
1//! Data types for agent activity tracking.
2//!
3//! Activities represent the presence and current state of an agent
4//! within a collective. Unlike other PulseDB entities, activities are
5//! keyed by `(collective_id, agent_id)` composite rather than a UUID,
6//! since each agent can have at most one active session per collective.
7
8use serde::{Deserialize, Serialize};
9
10use crate::types::{CollectiveId, Timestamp};
11
12/// A stored agent activity — presence record within a collective.
13///
14/// Activities track which agents are currently operating in a collective,
15/// what they're working on, and when they last checked in. This enables
16/// agent coordination and discovery.
17///
18/// # Key Design
19///
20/// Activities are uniquely identified by `(collective_id, agent_id)`.
21/// Re-registering with the same pair replaces the existing activity
22/// (upsert semantics).
23///
24/// # Staleness
25///
26/// An activity is considered stale when `last_heartbeat` is older than
27/// `Config::activity::stale_threshold`. Stale activities are excluded
28/// from `get_active_agents()` results but remain in storage until
29/// explicitly ended or the collective is deleted.
30#[derive(Clone, Debug, Serialize, Deserialize)]
31pub struct Activity {
32 /// The agent's identifier (e.g., "claude-opus", "agent-47").
33 pub agent_id: String,
34
35 /// The collective this activity belongs to.
36 pub collective_id: CollectiveId,
37
38 /// What the agent is currently working on (max 1KB).
39 pub current_task: Option<String>,
40
41 /// Summary of the agent's current context (max 1KB).
42 pub context_summary: Option<String>,
43
44 /// When this activity was first registered.
45 pub started_at: Timestamp,
46
47 /// When the agent last sent a heartbeat.
48 pub last_heartbeat: Timestamp,
49}
50
51/// Input for registering a new agent activity.
52///
53/// The `started_at` and `last_heartbeat` timestamps are set automatically
54/// to `Timestamp::now()` when the activity is registered.
55///
56/// # Example
57///
58/// ```rust
59/// # fn main() -> pulsedb::Result<()> {
60/// # let dir = tempfile::tempdir().unwrap();
61/// # let db = pulsedb::PulseDB::open(dir.path().join("test.db"), pulsedb::Config::default())?;
62/// # let collective_id = db.create_collective("example")?;
63/// use pulsedb::NewActivity;
64///
65/// let activity = NewActivity {
66/// agent_id: "claude-opus".to_string(),
67/// collective_id,
68/// current_task: Some("Implementing error handling".to_string()),
69/// context_summary: Some("Working on src/error.rs".to_string()),
70/// };
71/// db.register_activity(activity)?;
72/// # Ok(())
73/// # }
74/// ```
75pub struct NewActivity {
76 /// The agent's identifier (non-empty, max 255 bytes).
77 pub agent_id: String,
78
79 /// The collective to register in (must exist).
80 pub collective_id: CollectiveId,
81
82 /// What the agent is currently working on (max 1KB).
83 pub current_task: Option<String>,
84
85 /// Summary of the agent's current context (max 1KB).
86 pub context_summary: Option<String>,
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_activity_bincode_roundtrip() {
95 let activity = Activity {
96 agent_id: "claude-opus".to_string(),
97 collective_id: CollectiveId::new(),
98 current_task: Some("Implementing feature X".to_string()),
99 context_summary: Some("Working on module Y".to_string()),
100 started_at: Timestamp::now(),
101 last_heartbeat: Timestamp::now(),
102 };
103
104 let bytes = bincode::serialize(&activity).unwrap();
105 let restored: Activity = bincode::deserialize(&bytes).unwrap();
106
107 assert_eq!(activity.agent_id, restored.agent_id);
108 assert_eq!(activity.collective_id, restored.collective_id);
109 assert_eq!(activity.current_task, restored.current_task);
110 assert_eq!(activity.context_summary, restored.context_summary);
111 assert_eq!(activity.started_at, restored.started_at);
112 assert_eq!(activity.last_heartbeat, restored.last_heartbeat);
113 }
114
115 #[test]
116 fn test_activity_with_optional_fields_roundtrip() {
117 let activity = Activity {
118 agent_id: "agent-minimal".to_string(),
119 collective_id: CollectiveId::new(),
120 current_task: None,
121 context_summary: None,
122 started_at: Timestamp::from_millis(1000),
123 last_heartbeat: Timestamp::from_millis(2000),
124 };
125
126 let bytes = bincode::serialize(&activity).unwrap();
127 let restored: Activity = bincode::deserialize(&bytes).unwrap();
128
129 assert_eq!(activity.agent_id, restored.agent_id);
130 assert!(restored.current_task.is_none());
131 assert!(restored.context_summary.is_none());
132 assert_eq!(activity.started_at, restored.started_at);
133 assert_eq!(activity.last_heartbeat, restored.last_heartbeat);
134 }
135}