1use serde::{Deserialize, Serialize};
4use std::collections::VecDeque;
5use std::time::SystemTime;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
9pub struct ThreadId(pub u64);
10
11impl ThreadId {
12 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub enum ThreadKind {
35 Chat,
37
38 SubAgent {
40 agent_id: String,
42 task: String,
44 },
45
46 Background {
48 purpose: String,
50 },
51
52 Task {
54 action: String,
56 },
57}
58
59impl ThreadKind {
60 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 pub fn icon(&self) -> &str {
72 match self {
73 Self::Chat => "💬",
74 Self::SubAgent { .. } => "🤖",
75 Self::Background { .. } => "⚙️",
76 Self::Task { .. } => "📋",
77 }
78 }
79
80 pub fn is_interactive(&self) -> bool {
82 matches!(self, Self::Chat)
83 }
84
85 pub fn is_ephemeral(&self) -> bool {
87 matches!(self, Self::SubAgent { .. } | Self::Task { .. })
88 }
89}
90
91#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
93pub enum ThreadStatus {
94 Active,
96
97 Running {
99 progress: Option<f32>,
101 message: Option<String>,
103 },
104
105 WaitingForInput {
107 prompt: String,
108 },
109
110 Paused,
112
113 Completed {
115 summary: Option<String>,
117 },
118
119 Failed {
121 error: String,
122 },
123
124 Cancelled,
126}
127
128impl ThreadStatus {
129 pub fn is_terminal(&self) -> bool {
131 matches!(self, Self::Completed { .. } | Self::Failed { .. } | Self::Cancelled)
132 }
133
134 pub fn is_running(&self) -> bool {
136 matches!(self, Self::Active | Self::Running { .. })
137 }
138
139 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct ThreadMessage {
173 pub role: MessageRole,
174 pub content: String,
175 pub timestamp: SystemTime,
176}
177
178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
180pub enum MessageRole {
181 User,
182 Assistant,
183 System,
184 Tool,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct AgentThread {
190 pub id: ThreadId,
192
193 pub kind: ThreadKind,
195
196 pub label: String,
198
199 pub description: Option<String>,
201
202 pub status: ThreadStatus,
204
205 pub parent_id: Option<ThreadId>,
207
208 pub created_at: SystemTime,
210
211 pub last_activity: SystemTime,
213
214 pub is_foreground: bool,
216
217 pub messages: VecDeque<ThreadMessage>,
219
220 pub compact_summary: Option<String>,
222
223 pub result: Option<String>,
225
226 pub share_context: bool,
228}
229
230impl AgentThread {
231 pub fn task_id(&self) -> ThreadId {
233 self.id
234 }
235
236 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 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 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 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 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 pub fn set_status(&mut self, status: ThreadStatus) {
344 self.status = status;
345 self.last_activity = SystemTime::now();
346 }
347
348 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 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 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 pub fn message_count(&self) -> usize {
373 self.messages.len()
374 }
375
376 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 pub fn apply_compaction(&mut self, summary: String) {
398 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 pub fn build_context(&self) -> String {
411 let mut ctx = String::new();
412
413 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 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 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#[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}