Skip to main content

rustyclaw_core/threads/
model.rs

1//! Thread model — core types for the unified thread system.
2
3use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5use std::time::SystemTime;
6
7/// Unique identifier for a thread.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ThreadId(pub u64);
10
11impl ThreadId {
12    /// Generate a new unique thread ID.
13    pub fn new() -> Self {
14        use std::sync::atomic::{AtomicU64, Ordering};
15        static COUNTER: AtomicU64 = AtomicU64::new(1);
16        Self(COUNTER.fetch_add(1, Ordering::SeqCst))
17    }
18}
19
20impl Default for ThreadId {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl std::fmt::Display for ThreadId {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        write!(f, "#{}", self.0)
29    }
30}
31
32/// What kind of thread this is.
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub enum ThreadKind {
35    /// User-interactive chat thread (persistent, has messages)
36    Chat,
37    
38    /// Spawned sub-agent (ephemeral, autonomous, may return result)
39    SubAgent {
40        /// Agent ID that's running this
41        agent_id: String,
42        /// The task/prompt given to the sub-agent
43        task: String,
44    },
45    
46    /// Long-running background work (persistent, autonomous)
47    Background {
48        /// What this background thread is monitoring/doing
49        purpose: String,
50    },
51    
52    /// One-shot task (ephemeral, returns result and exits)
53    Task {
54        /// What this task is doing
55        action: String,
56    },
57}
58
59impl ThreadKind {
60    /// Display name for the kind.
61    pub fn display_name(&self) -> &str {
62        match self {
63            Self::Chat => "Chat",
64            Self::SubAgent { .. } => "Sub-agent",
65            Self::Background { .. } => "Background",
66            Self::Task { .. } => "Task",
67        }
68    }
69    
70    /// Icon for sidebar display.
71    pub fn icon(&self) -> &str {
72        match self {
73            Self::Chat => "💬",
74            Self::SubAgent { .. } => "🤖",
75            Self::Background { .. } => "⚙️",
76            Self::Task { .. } => "📋",
77        }
78    }
79    
80    /// Is this an interactive thread?
81    pub fn is_interactive(&self) -> bool {
82        matches!(self, Self::Chat)
83    }
84    
85    /// Is this ephemeral (auto-cleanup when done)?
86    pub fn is_ephemeral(&self) -> bool {
87        matches!(self, Self::SubAgent { .. } | Self::Task { .. })
88    }
89}
90
91/// Thread status.
92#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub enum ThreadStatus {
94    /// Thread is active/running
95    Active,
96    
97    /// Thread is running but backgrounded (not user-focused)
98    Running {
99        /// Progress indicator (0.0 - 1.0) if known
100        progress: Option<f32>,
101        /// Current status message
102        message: Option<String>,
103    },
104    
105    /// Thread is waiting for user input
106    WaitingForInput {
107        prompt: String,
108    },
109    
110    /// Thread is paused
111    Paused,
112    
113    /// Thread completed successfully
114    Completed {
115        /// Summary of what was accomplished
116        summary: Option<String>,
117    },
118    
119    /// Thread failed
120    Failed {
121        error: String,
122    },
123    
124    /// Thread was cancelled
125    Cancelled,
126}
127
128impl ThreadStatus {
129    /// Is this a terminal state?
130    pub fn is_terminal(&self) -> bool {
131        matches!(self, Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled)
132    }
133    
134    /// Is the thread actively running?
135    pub fn is_running(&self) -> bool {
136        matches!(self, Self::Active | Self::Running { .. })
137    }
138    
139    /// Status icon for display.
140    pub fn icon(&self) -> &str {
141        match self {
142            Self::Active => "▶",
143            Self::Running { .. } => "▶",
144            Self::WaitingForInput { .. } => "⏸",
145            Self::Paused => "⏸",
146            Self::Completed { .. } => "✓",
147            Self::Failed { .. } => "✗",
148            Self::Cancelled => "⊘",
149        }
150    }
151    
152    /// Short display string.
153    pub fn display(&self) -> String {
154        match self {
155            Self::Active => "Active".to_string(),
156            Self::Running { message, .. } => {
157                message.clone().unwrap_or_else(|| "Running".to_string())
158            }
159            Self::WaitingForInput { prompt } => format!("Waiting: {}", prompt),
160            Self::Paused => "Paused".to_string(),
161            Self::Completed { summary } => {
162                summary.clone().unwrap_or_else(|| "Completed".to_string())
163            }
164            Self::Failed { error } => format!("Failed: {}", error),
165            Self::Cancelled => "Cancelled".to_string(),
166        }
167    }
168}
169
170/// A message in a thread's conversation history.
171#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ThreadMessage {
173    pub role: MessageRole,
174    pub content: String,
175    pub timestamp: SystemTime,
176}
177
178/// Message role.
179#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub enum MessageRole {
181    User,
182    Assistant,
183    System,
184    Tool,
185}
186
187/// An agent thread — the unified representation of all concurrent work.
188#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct AgentThread {
190    /// Unique identifier
191    pub id: ThreadId,
192    
193    /// What kind of thread this is
194    pub kind: ThreadKind,
195    
196    /// User-visible label
197    pub label: String,
198    
199    /// Agent-settable description of current activity
200    pub description: Option<String>,
201    
202    /// Current status
203    pub status: ThreadStatus,
204    
205    /// Parent thread that spawned this (if any)
206    pub parent_id: Option<ThreadId>,
207    
208    /// When the thread was created
209    pub created_at: SystemTime,
210    
211    /// When the thread last had activity
212    pub last_activity: SystemTime,
213    
214    /// Is this the foreground (user-focused) thread?
215    pub is_foreground: bool,
216    
217    /// Conversation history (for interactive threads)
218    pub messages: VecDeque<ThreadMessage>,
219    
220    /// Compacted summary of older messages
221    pub compact_summary: Option<String>,
222    
223    /// Result value (for task/sub-agent threads)
224    pub result: Option<String>,
225    
226    /// Should this thread's context be shared with parent?
227    pub share_context: bool,
228}
229
230impl AgentThread {
231    /// Backwards-compatible alias for id (used by gateway code).
232    pub fn task_id(&self) -> ThreadId {
233        self.id
234    }
235    
236    /// Create a new chat thread.
237    pub fn new_chat(label: impl Into<String>) -> Self {
238        let now = SystemTime::now();
239        Self {
240            id: ThreadId::new(),
241            kind: ThreadKind::Chat,
242            label: label.into(),
243            description: None,
244            status: ThreadStatus::Active,
245            parent_id: None,
246            created_at: now,
247            last_activity: now,
248            is_foreground: false,
249            messages: VecDeque::new(),
250            compact_summary: None,
251            result: None,
252            share_context: true,
253        }
254    }
255    
256    /// Create a new sub-agent thread.
257    pub fn new_subagent(
258        label: impl Into<String>,
259        agent_id: impl Into<String>,
260        task: impl Into<String>,
261        parent_id: Option<ThreadId>,
262    ) -> Self {
263        let now = SystemTime::now();
264        Self {
265            id: ThreadId::new(),
266            kind: ThreadKind::SubAgent {
267                agent_id: agent_id.into(),
268                task: task.into(),
269            },
270            label: label.into(),
271            description: None,
272            status: ThreadStatus::Running { progress: None, message: None },
273            parent_id,
274            created_at: now,
275            last_activity: now,
276            is_foreground: false,
277            messages: VecDeque::new(),
278            compact_summary: None,
279            result: None,
280            share_context: true,
281        }
282    }
283    
284    /// Create a new background thread.
285    pub fn new_background(
286        label: impl Into<String>,
287        purpose: impl Into<String>,
288        parent_id: Option<ThreadId>,
289    ) -> Self {
290        let now = SystemTime::now();
291        Self {
292            id: ThreadId::new(),
293            kind: ThreadKind::Background {
294                purpose: purpose.into(),
295            },
296            label: label.into(),
297            description: None,
298            status: ThreadStatus::Running { progress: None, message: None },
299            parent_id,
300            created_at: now,
301            last_activity: now,
302            is_foreground: false,
303            messages: VecDeque::new(),
304            compact_summary: None,
305            result: None,
306            share_context: false,
307        }
308    }
309    
310    /// Create a new task thread.
311    pub fn new_task(
312        label: impl Into<String>,
313        action: impl Into<String>,
314        parent_id: Option<ThreadId>,
315    ) -> Self {
316        let now = SystemTime::now();
317        Self {
318            id: ThreadId::new(),
319            kind: ThreadKind::Task {
320                action: action.into(),
321            },
322            label: label.into(),
323            description: None,
324            status: ThreadStatus::Running { progress: None, message: None },
325            parent_id,
326            created_at: now,
327            last_activity: now,
328            is_foreground: false,
329            messages: VecDeque::new(),
330            compact_summary: None,
331            result: None,
332            share_context: true,
333        }
334    }
335    
336    /// Update the description.
337    pub fn set_description(&mut self, description: impl Into<String>) {
338        self.description = Some(description.into());
339        self.last_activity = SystemTime::now();
340    }
341    
342    /// Update the status.
343    pub fn set_status(&mut self, status: ThreadStatus) {
344        self.status = status;
345        self.last_activity = SystemTime::now();
346    }
347    
348    /// Mark as completed with optional result.
349    pub fn complete(&mut self, summary: Option<String>, result: Option<String>) {
350        self.status = ThreadStatus::Completed { summary };
351        self.result = result;
352        self.last_activity = SystemTime::now();
353    }
354    
355    /// Mark as failed.
356    pub fn fail(&mut self, error: impl Into<String>) {
357        self.status = ThreadStatus::Failed { error: error.into() };
358        self.last_activity = SystemTime::now();
359    }
360    
361    /// Add a message to the conversation history.
362    pub fn add_message(&mut self, role: MessageRole, content: impl Into<String>) {
363        self.messages.push_back(ThreadMessage {
364            role,
365            content: content.into(),
366            timestamp: SystemTime::now(),
367        });
368        self.last_activity = SystemTime::now();
369    }
370    
371    /// Get message count.
372    pub fn message_count(&self) -> usize {
373        self.messages.len()
374    }
375    
376    /// Generate a prompt for compacting this thread's conversation.
377    pub fn compaction_prompt(&self) -> String {
378        let mut prompt = String::from(
379            "Summarize the following conversation in 2-3 sentences, \
380             capturing the key topics, decisions, and any pending items:\n\n",
381        );
382        
383        for msg in &self.messages {
384            let role = match msg.role {
385                MessageRole::User => "User",
386                MessageRole::Assistant => "Assistant",
387                MessageRole::System => "System",
388                MessageRole::Tool => "Tool",
389            };
390            prompt.push_str(&format!("{}: {}\n", role, msg.content));
391        }
392        
393        prompt
394    }
395    
396    /// Apply a compaction summary, keeping only recent messages.
397    pub fn apply_compaction(&mut self, summary: String) {
398        // Keep the last 3 messages
399        const KEEP_RECENT: usize = 3;
400        
401        while self.messages.len() > KEEP_RECENT {
402            self.messages.pop_front();
403        }
404        
405        self.compact_summary = Some(summary);
406        self.last_activity = SystemTime::now();
407    }
408    
409    /// Build context string for this thread (for system prompt injection).
410    pub fn build_context(&self) -> String {
411        let mut ctx = String::new();
412        
413        // Include compact summary if present
414        if let Some(summary) = &self.compact_summary {
415            ctx.push_str("## Previous Context\n");
416            ctx.push_str(summary);
417            ctx.push_str("\n\n");
418        }
419        
420        // Include recent messages
421        if !self.messages.is_empty() {
422            ctx.push_str("## Recent Messages\n");
423            for msg in &self.messages {
424                let role = match msg.role {
425                    MessageRole::User => "User",
426                    MessageRole::Assistant => "Assistant",
427                    MessageRole::System => "System",
428                    MessageRole::Tool => "Tool",
429                };
430                ctx.push_str(&format!("{}: {}\n", role, msg.content));
431            }
432        }
433        
434        ctx
435    }
436    
437    /// Get info for sidebar display.
438    pub fn to_info(&self) -> ThreadInfo {
439        ThreadInfo {
440            id: self.id,
441            kind: self.kind.display_name().to_string(),
442            icon: self.kind.icon().to_string(),
443            label: self.label.clone(),
444            description: self.description.clone(),
445            status: self.status.display(),
446            status_icon: self.status.icon().to_string(),
447            is_foreground: self.is_foreground,
448            is_interactive: self.kind.is_interactive(),
449            message_count: self.messages.len(),
450            has_summary: self.compact_summary.is_some(),
451            has_result: self.result.is_some(),
452        }
453    }
454}
455
456/// Summary info for sidebar display.
457#[derive(Debug, Clone, Serialize, Deserialize)]
458pub struct ThreadInfo {
459    pub id: ThreadId,
460    pub kind: String,
461    pub icon: String,
462    pub label: String,
463    pub description: Option<String>,
464    pub status: String,
465    pub status_icon: String,
466    pub is_foreground: bool,
467    pub is_interactive: bool,
468    pub message_count: usize,
469    pub has_summary: bool,
470    pub has_result: bool,
471}