1use serde::{Deserialize, Serialize};
8
9use abk::checkpoint::{
10 get_storage_manager,
11 models::{CheckpointMetadata, ChatMessage, SessionMetadata as AbkSessionMetadata},
12 storage::ProjectMetadata as AbkProjectMetadata,
13 CheckpointStorageManager,
14};
15use abk::cli::ResumeInfo;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct SessionSummary {
20 pub session_id: String,
21 pub project_name: String,
22 pub project_path: String,
23 pub project_id: String,
25 pub checkpoint_count: usize,
26 pub created_at: chrono::DateTime<chrono::Utc>,
27 pub last_accessed: chrono::DateTime<chrono::Utc>,
28 pub description: Option<String>,
29 pub is_current_project: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct CheckpointSummary {
35 pub checkpoint_id: String,
36 pub session_id: String,
37 pub iteration: u32,
38 pub workflow_step: String,
39 pub created_at: chrono::DateTime<chrono::Utc>,
40}
41
42fn session_to_summary(
47 session: &AbkSessionMetadata,
48 project: &AbkProjectMetadata,
49 is_current: bool,
50) -> SessionSummary {
51 SessionSummary {
52 session_id: session.session_id.clone(),
53 project_name: project.name.clone(),
54 project_path: project.project_path.to_string_lossy().to_string(),
55 project_id: project.project_hash.clone(),
56 checkpoint_count: session.checkpoint_count as usize,
57 created_at: session.created_at,
58 last_accessed: session.last_accessed,
59 description: session.description.clone(),
60 is_current_project: is_current,
61 }
62}
63
64fn checkpoint_to_summary(cp: &CheckpointMetadata) -> CheckpointSummary {
65 CheckpointSummary {
66 checkpoint_id: cp.checkpoint_id.clone(),
67 session_id: cp.session_id.clone(),
68 iteration: cp.iteration,
69 workflow_step: format!("{:?}", cp.workflow_step),
70 created_at: cp.created_at,
71 }
72}
73
74fn config_working_dir(config_toml: &str) -> std::path::PathBuf {
79 if let Ok(value) = toml::from_str::<toml::Value>(config_toml) {
80 if let Some(agent) = value.get("agent") {
81 if let Some(wd) = agent.get("working_dir").and_then(|v| v.as_str()) {
82 return std::path::PathBuf::from(wd);
83 }
84 }
85 }
86 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
87}
88
89fn paths_match(a: &std::path::Path, b: &std::path::Path) -> bool {
91 let ca = a.canonicalize().unwrap_or_else(|_| a.to_path_buf());
92 let cb = b.canonicalize().unwrap_or_else(|_| b.to_path_buf());
93 ca == cb
94}
95
96fn make_storage_manager(
101 home_dir: Option<&std::path::Path>,
102 agent_name: &str,
103) -> anyhow::Result<CheckpointStorageManager> {
104 if let Some(dir) = home_dir {
105 CheckpointStorageManager::with_home_dir(dir.to_path_buf(), agent_name)
106 .map_err(|e| anyhow::anyhow!("Failed to create storage manager: {}", e))
107 } else {
108 get_storage_manager()
109 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))
110 }
111}
112
113pub async fn list_all_sessions(
125 config_toml: &str,
126 home_dir: Option<&std::path::Path>,
127) -> anyhow::Result<Vec<SessionSummary>> {
128 let current_dir = config_working_dir(config_toml);
129 let manager = make_storage_manager(home_dir, "trustee")?;
130
131 let projects = manager
132 .list_projects()
133 .await
134 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
135
136 let mut summaries = Vec::new();
137
138 for project in &projects {
139 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
142 Ok(ps) => ps,
143 Err(e) => {
144 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
145 continue;
146 }
147 };
148
149 let sessions = match project_storage.list_sessions().await {
150 Ok(s) => s,
151 Err(e) => {
152 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
153 continue;
154 }
155 };
156
157 let is_current = paths_match(&project.project_path, ¤t_dir);
158
159 for session in sessions {
160 if session.checkpoint_count > 0 {
162 summaries.push(session_to_summary(&session, project, is_current));
163 }
164 }
165 }
166
167 summaries.sort_by(|a, b| match (a.is_current_project, b.is_current_project) {
169 (true, false) => std::cmp::Ordering::Less,
170 (false, true) => std::cmp::Ordering::Greater,
171 _ => b.last_accessed.cmp(&a.last_accessed),
172 });
173
174 Ok(summaries)
175}
176
177pub async fn get_session_detail(
184 _config_toml: &str,
185 session_id: &str,
186 home_dir: Option<&std::path::Path>,
187) -> anyhow::Result<Option<(SessionSummary, Vec<CheckpointSummary>)>> {
188 let manager = make_storage_manager(home_dir, "trustee")?;
189
190 let projects = manager
191 .list_projects()
192 .await
193 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
194
195 for project in &projects {
196 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
197 Ok(ps) => ps,
198 Err(e) => {
199 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
200 continue;
201 }
202 };
203
204 let sessions = match project_storage.list_sessions().await {
205 Ok(s) => s,
206 Err(e) => {
207 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
208 continue;
209 }
210 };
211
212 if let Some(session) = sessions.iter().find(|s| s.session_id == session_id) {
213 let session_storage = project_storage
214 .create_session(session_id)
215 .await
216 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
217
218 let checkpoints = session_storage
219 .list_checkpoints()
220 .await
221 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
222
223 let summary = session_to_summary(session, project, false);
224 let cp_summaries: Vec<CheckpointSummary> =
225 checkpoints.iter().map(checkpoint_to_summary).collect();
226
227 return Ok(Some((summary, cp_summaries)));
228 }
229 }
230
231 Ok(None)
232}
233
234pub async fn create_resume_info(
242 _config_toml: &str,
243 session_id: &str,
244 home_dir: Option<&std::path::Path>,
245) -> anyhow::Result<Option<ResumeInfo>> {
246 let manager = make_storage_manager(home_dir, "trustee")?;
247
248 let projects = manager
249 .list_projects()
250 .await
251 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
252
253 for project in &projects {
254 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
255 Ok(ps) => ps,
256 Err(e) => {
257 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
258 continue;
259 }
260 };
261
262 let sessions = match project_storage.list_sessions().await {
263 Ok(s) => s,
264 Err(e) => {
265 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
266 continue;
267 }
268 };
269
270 if sessions.iter().any(|s| s.session_id == session_id) {
271 let session_storage = project_storage
272 .create_session(session_id)
273 .await
274 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
275
276 let checkpoints = session_storage
277 .list_checkpoints()
278 .await
279 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
280
281 if let Some(latest) = checkpoints.iter().max_by_key(|cp| cp.created_at) {
282 return Ok(Some(ResumeInfo {
283 session_id: session_id.to_string(),
284 checkpoint_id: latest.checkpoint_id.clone(),
285 iteration: latest.iteration,
286 project_path: Some(project.project_path.clone()),
287 }));
288 }
289
290 return Ok(None);
292 }
293 }
294
295 Ok(None)
296}
297
298#[derive(Debug, Clone, Serialize, Deserialize)]
311pub struct HistoryMessage {
312 pub role: String,
314 pub content: String,
316 pub reasoning: Option<String>,
318 pub tool_calls: Option<Vec<HistoryToolCall>>,
320 pub name: Option<String>,
322}
323
324#[derive(Debug, Clone, Serialize, Deserialize)]
326pub struct HistoryToolCall {
327 pub name: String,
328 pub hint: String,
330}
331
332#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct SessionHistory {
335 pub session_id: String,
336 pub checkpoint_id: String,
337 pub task_description: String,
338 pub iteration: u32,
339 pub total_messages: usize,
340 pub messages: Vec<HistoryMessage>,
341}
342
343pub async fn load_session_history(
351 session_id: &str,
352 home_dir: Option<&std::path::Path>,
353) -> anyhow::Result<Option<SessionHistory>> {
354 let manager = make_storage_manager(home_dir, "trustee")?;;
355
356 let projects = manager
357 .list_projects()
358 .await
359 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
360
361 for project in &projects {
362 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
363 Ok(ps) => ps,
364 Err(e) => {
365 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
366 continue;
367 }
368 };
369
370 let sessions = match project_storage.list_sessions().await {
371 Ok(s) => s,
372 Err(e) => {
373 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
374 continue;
375 }
376 };
377
378 if !sessions.iter().any(|s| s.session_id == session_id) {
379 continue;
380 }
381
382 let session_storage = project_storage
383 .create_session(session_id)
384 .await
385 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
386
387 let checkpoints = session_storage
388 .list_checkpoints()
389 .await
390 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
391
392 let latest = match checkpoints.iter().max_by_key(|cp| cp.created_at) {
393 Some(cp) => cp,
394 None => return Ok(None),
395 };
396
397 let checkpoint_id = latest.checkpoint_id.clone();
398 let iteration = latest.iteration;
399
400 let checkpoint = session_storage
401 .load_checkpoint(&checkpoint_id)
402 .await
403 .map_err(|e| anyhow::anyhow!("Failed to load checkpoint: {}", e))?;
404
405 let task_description = checkpoint.agent_state.task_description.clone();
406 let total_messages = checkpoint.conversation_state.messages.len();
407
408 let messages = convert_messages(&checkpoint.conversation_state.messages);
409
410 return Ok(Some(SessionHistory {
411 session_id: session_id.to_string(),
412 checkpoint_id,
413 task_description,
414 iteration,
415 total_messages,
416 messages,
417 }));
418 }
419
420 Ok(None)
421}
422
423fn convert_messages(messages: &[ChatMessage]) -> Vec<HistoryMessage> {
429 const MAX_CONTENT_LEN: usize = 10_000;
430
431 let mut result = Vec::new();
432
433 for msg in messages {
434 if msg.role == "system" {
436 continue;
437 }
438
439 if msg.role == "tool" {
442 continue;
443 }
444
445 let content = if msg.content.len() > MAX_CONTENT_LEN {
446 format!("{}...\n[truncated]", &msg.content[..MAX_CONTENT_LEN])
447 } else {
448 msg.content.clone()
449 };
450
451 let tool_calls = msg.tool_calls.as_ref().map(|calls| {
453 calls
454 .iter()
455 .map(|tc| {
456 let hint = summarize_tool_args(&tc.function.name, &tc.function.arguments);
457 HistoryToolCall {
458 name: tc.function.name.clone(),
459 hint,
460 }
461 })
462 .collect::<Vec<_>>()
463 });
464
465 if msg.role == "assistant" && content.is_empty() && tool_calls.is_some() {
468 result.push(HistoryMessage {
469 role: "assistant".to_string(),
470 content: String::new(),
471 reasoning: msg.reasoning.clone(),
472 tool_calls,
473 name: None,
474 });
475 continue;
476 }
477
478 result.push(HistoryMessage {
479 role: msg.role.clone(),
480 content,
481 reasoning: msg.reasoning.clone(),
482 tool_calls,
483 name: msg.name.clone(),
484 });
485 }
486
487 result
488}
489
490fn summarize_tool_args(name: &str, args: &str) -> String {
495 let parsed: serde_json::Value = match serde_json::from_str(args) {
496 Ok(v) => v,
497 Err(_) => return args.chars().take(200).collect(),
498 };
499
500 let obj = match parsed.as_object() {
501 Some(o) => o,
502 None => return args.chars().take(200).collect(),
503 };
504
505 match name {
506 "bash" | "execute_command" => {
507 obj.get("command").and_then(|v| v.as_str()).map(|s| s.to_string())
508 }
509 "read" | "read_file" => {
510 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
511 }
512 "write" | "write_file" | "edit" => {
513 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
514 }
515 "grep" | "search" => {
516 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
517 }
518 "glob" => {
519 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
520 }
521 "todowrite" => {
522 let count = obj.get("todos").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
523 Some(format!("{} items", count))
524 }
525 "websearch" | "webfetch" => {
526 obj.get("query").or_else(|| obj.get("url")).and_then(|v| v.as_str()).map(|s| s.to_string())
527 }
528 _ => {
529 obj.values().next().and_then(|v| match v {
530 serde_json::Value::String(s) => Some(s.clone()),
531 _ => Some(v.to_string()),
532 })
533 }
534 }
535 .unwrap_or_default()
536 .chars()
537 .take(200)
538 .collect()
539}