Skip to main content

lit/commands/
delegate.rs

1//! Agent Task Delegation Protocol
2//!
3//! Enables formal agent-to-agent work assignment with tracking, status
4//! updates, and integration with the trust scoring system.
5
6use crate::errors::LitError;
7use serde::{Deserialize, Serialize};
8use std::fs;
9use std::path::Path;
10
11#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
12pub enum TaskStatus {
13    Pending,
14    Accepted,
15    InProgress,
16    Completed,
17    Failed,
18    Rejected,
19}
20
21impl std::fmt::Display for TaskStatus {
22    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23        match self {
24            TaskStatus::Pending => write!(f, "pending"),
25            TaskStatus::Accepted => write!(f, "accepted"),
26            TaskStatus::InProgress => write!(f, "in-progress"),
27            TaskStatus::Completed => write!(f, "completed"),
28            TaskStatus::Failed => write!(f, "failed"),
29            TaskStatus::Rejected => write!(f, "rejected"),
30        }
31    }
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
35pub enum TaskPriority {
36    Low,
37    Medium,
38    High,
39    Critical,
40}
41
42impl std::fmt::Display for TaskPriority {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        match self {
45            TaskPriority::Low => write!(f, "low"),
46            TaskPriority::Medium => write!(f, "medium"),
47            TaskPriority::High => write!(f, "high"),
48            TaskPriority::Critical => write!(f, "critical"),
49        }
50    }
51}
52
53/// A task delegation from one agent to another
54#[derive(Debug, Clone, Serialize, Deserialize)]
55pub struct DelegatedTask {
56    /// Unique task ID
57    pub id: String,
58    /// Delegator DID
59    pub delegator: String,
60    /// Delegatee DID
61    pub delegatee: String,
62    /// Task title
63    pub title: String,
64    /// Detailed description / specification
65    pub description: String,
66    /// Task priority
67    pub priority: TaskPriority,
68    /// Current status
69    pub status: TaskStatus,
70    /// UCAN token CID that authorizes this delegation (optional)
71    #[serde(skip_serializing_if = "Option::is_none")]
72    pub ucan_proof: Option<String>,
73    /// Specific files or paths this task applies to
74    #[serde(default)]
75    pub scope: Vec<String>,
76    /// Optional deadline
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub deadline: Option<String>,
79    /// Result or output when completed
80    #[serde(skip_serializing_if = "Option::is_none")]
81    pub result: Option<String>,
82    /// Status history
83    pub history: Vec<TaskHistoryEntry>,
84    pub created: String,
85    pub updated: String,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct TaskHistoryEntry {
90    pub status: TaskStatus,
91    pub timestamp: String,
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub message: Option<String>,
94}
95
96fn tasks_dir(repo_root: &Path) -> std::path::PathBuf {
97    repo_root.join(".lit").join("delegations")
98}
99
100/// Create a new delegated task
101#[allow(clippy::too_many_arguments)]
102pub fn create_task(
103    repo_root: &Path,
104    delegator: &str,
105    delegatee: &str,
106    title: &str,
107    description: &str,
108    priority: TaskPriority,
109    scope: Vec<String>,
110    deadline: Option<String>,
111    ucan_proof: Option<String>,
112) -> Result<DelegatedTask, LitError> {
113    let dir = tasks_dir(repo_root);
114    fs::create_dir_all(&dir)
115        .map_err(|e| LitError::io(format!("Failed to create delegations dir: {}", e)))?;
116
117    let now = chrono::Utc::now().to_rfc3339();
118    let id = format!("task-{}", chrono::Utc::now().timestamp_millis());
119    let task = DelegatedTask {
120        id: id.clone(),
121        delegator: delegator.to_string(),
122        delegatee: delegatee.to_string(),
123        title: title.to_string(),
124        description: description.to_string(),
125        priority,
126        status: TaskStatus::Pending,
127        ucan_proof,
128        scope,
129        deadline,
130        result: None,
131        history: vec![TaskHistoryEntry {
132            status: TaskStatus::Pending,
133            timestamp: now.clone(),
134            message: Some("Task created".to_string()),
135        }],
136        created: now.clone(),
137        updated: now,
138    };
139
140    let path = dir.join(format!("{}.json", id));
141    let json = serde_json::to_string_pretty(&task)
142        .map_err(|e| LitError::general(format!("Serialize: {}", e)))?;
143    fs::write(&path, json).map_err(|e| LitError::io(format!("Write: {}", e)))?;
144    Ok(task)
145}
146
147/// Update a task's status
148pub fn update_task_status(
149    repo_root: &Path,
150    task_id: &str,
151    new_status: TaskStatus,
152    message: Option<String>,
153) -> Result<DelegatedTask, LitError> {
154    let mut task = get_task(repo_root, task_id)?;
155    let now = chrono::Utc::now().to_rfc3339();
156
157    task.history.push(TaskHistoryEntry {
158        status: new_status.clone(),
159        timestamp: now.clone(),
160        message,
161    });
162    task.status = new_status;
163    task.updated = now;
164
165    save_task(repo_root, &task)?;
166    Ok(task)
167}
168
169/// Complete a task with a result message
170pub fn complete_task(
171    repo_root: &Path,
172    task_id: &str,
173    result: &str,
174) -> Result<DelegatedTask, LitError> {
175    let mut task = get_task(repo_root, task_id)?;
176    let now = chrono::Utc::now().to_rfc3339();
177
178    task.result = Some(result.to_string());
179    task.history.push(TaskHistoryEntry {
180        status: TaskStatus::Completed,
181        timestamp: now.clone(),
182        message: Some(result.to_string()),
183    });
184    task.status = TaskStatus::Completed;
185    task.updated = now;
186
187    save_task(repo_root, &task)?;
188    Ok(task)
189}
190
191/// Get a task by ID
192pub fn get_task(repo_root: &Path, task_id: &str) -> Result<DelegatedTask, LitError> {
193    let path = tasks_dir(repo_root).join(format!("{}.json", task_id));
194    if !path.exists() {
195        return Err(LitError::general(format!("Task not found: {}", task_id)));
196    }
197    let json = fs::read_to_string(&path).map_err(|e| LitError::io(format!("IO: {}", e)))?;
198    serde_json::from_str(&json).map_err(|e| LitError::general(format!("Parse: {}", e)))
199}
200
201/// List tasks, optionally filtered by delegator or delegatee
202pub fn list_tasks(
203    repo_root: &Path,
204    agent_did: Option<&str>,
205    status: Option<TaskStatus>,
206) -> Result<Vec<DelegatedTask>, LitError> {
207    let dir = tasks_dir(repo_root);
208    if !dir.exists() {
209        return Ok(Vec::new());
210    }
211
212    let mut tasks = Vec::new();
213    for entry in fs::read_dir(&dir).map_err(|e| LitError::io(format!("IO: {}", e)))? {
214        let entry = entry.map_err(|e| LitError::io(format!("IO: {}", e)))?;
215        if entry.path().extension().is_some_and(|e| e == "json") {
216            if let Ok(json) = fs::read_to_string(entry.path()) {
217                if let Ok(task) = serde_json::from_str::<DelegatedTask>(&json) {
218                    let agent_match =
219                        agent_did.is_none_or(|did| task.delegator == did || task.delegatee == did);
220                    let status_match = status.as_ref().is_none_or(|s| task.status == *s);
221                    if agent_match && status_match {
222                        tasks.push(task);
223                    }
224                }
225            }
226        }
227    }
228
229    tasks.sort_by(|a, b| b.created.cmp(&a.created));
230    Ok(tasks)
231}
232
233fn save_task(repo_root: &Path, task: &DelegatedTask) -> Result<(), LitError> {
234    let path = tasks_dir(repo_root).join(format!("{}.json", task.id));
235    let json = serde_json::to_string_pretty(task)
236        .map_err(|e| LitError::general(format!("Serialize: {}", e)))?;
237    fs::write(&path, json).map_err(|e| LitError::io(format!("Write: {}", e)))?;
238    Ok(())
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use std::path::PathBuf;
245    use std::sync::atomic::{AtomicU32, Ordering};
246
247    static COUNTER: AtomicU32 = AtomicU32::new(0);
248
249    fn tmp_dir() -> PathBuf {
250        let n = COUNTER.fetch_add(1, Ordering::SeqCst);
251        let dir =
252            std::env::temp_dir().join(format!("lit_delegate_test_{}_{}", std::process::id(), n));
253        fs::create_dir_all(&dir).unwrap();
254        dir
255    }
256
257    #[test]
258    fn test_create_and_list_tasks() {
259        let dir = tmp_dir();
260        let task = create_task(
261            &dir,
262            "did:lit:manager",
263            "did:lit:agent1",
264            "Fix merge bug",
265            "The merge function panics on empty branches",
266            TaskPriority::High,
267            vec!["src/commands/merge.rs".into()],
268            None,
269            None,
270        )
271        .unwrap();
272
273        assert_eq!(task.status, TaskStatus::Pending);
274        assert_eq!(task.delegatee, "did:lit:agent1");
275
276        let tasks = list_tasks(&dir, Some("did:lit:agent1"), None).unwrap();
277        assert_eq!(tasks.len(), 1);
278
279        let _ = fs::remove_dir_all(&dir);
280    }
281
282    #[test]
283    fn test_task_lifecycle() {
284        let dir = tmp_dir();
285        let task = create_task(
286            &dir,
287            "did:lit:a",
288            "did:lit:b",
289            "Task",
290            "Description",
291            TaskPriority::Medium,
292            vec![],
293            None,
294            None,
295        )
296        .unwrap();
297
298        let task =
299            update_task_status(&dir, &task.id, TaskStatus::Accepted, Some("On it".into())).unwrap();
300        assert_eq!(task.status, TaskStatus::Accepted);
301
302        let task = complete_task(&dir, &task.id, "Fixed in commit abc123").unwrap();
303        assert_eq!(task.status, TaskStatus::Completed);
304        assert_eq!(task.history.len(), 3);
305
306        let _ = fs::remove_dir_all(&dir);
307    }
308}