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
96async fn make_storage_manager(
105 config_toml: &str,
106 home_dir: Option<&std::path::Path>,
107 agent_name: &str,
108) -> anyhow::Result<CheckpointStorageManager> {
109 let backend_config = parse_storage_backend_config(config_toml);
111
112 match (home_dir, backend_config) {
113 (Some(dir), Some(backend)) => {
115 CheckpointStorageManager::with_home_dir_and_backend(
116 dir.to_path_buf(),
117 agent_name,
118 backend,
119 )
120 .await
121 .map_err(|e| anyhow::anyhow!("Failed to create storage manager with backend: {}", e))
122 }
123 (Some(dir), None) => {
125 CheckpointStorageManager::with_home_dir(dir.to_path_buf(), agent_name)
126 .map_err(|e| anyhow::anyhow!("Failed to create storage manager: {}", e))
127 }
128 (None, _) => {
130 get_storage_manager()
131 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))
132 }
133 }
134}
135
136fn parse_storage_backend_config(config_toml: &str) -> Option<abk::checkpoint::StorageBackendConfig> {
140 let value = toml::from_str::<toml::Value>(config_toml).ok()?;
141 let checkpointing = value.get("checkpointing")?;
142 let storage_backend = checkpointing.get("storage_backend")?;
143 let backend: abk::checkpoint::StorageBackendConfig =
144 storage_backend.clone().try_into().ok()?;
145
146 match backend.backend_type {
148 abk::checkpoint::StorageBackendType::DocumentDB
149 | abk::checkpoint::StorageBackendType::MongoDB => Some(backend),
150 abk::checkpoint::StorageBackendType::File => None,
151 }
152}
153
154pub async fn list_all_sessions(
166 config_toml: &str,
167 home_dir: Option<&std::path::Path>,
168) -> anyhow::Result<Vec<SessionSummary>> {
169 let current_dir = config_working_dir(config_toml);
170 let manager = make_storage_manager(config_toml, home_dir, "trustee").await?;
171
172 let projects = manager
173 .list_projects()
174 .await
175 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
176
177 let mut summaries = Vec::new();
178
179 for project in &projects {
180 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
183 Ok(ps) => ps,
184 Err(e) => {
185 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
186 continue;
187 }
188 };
189
190 let sessions = match project_storage.list_sessions().await {
191 Ok(s) => s,
192 Err(e) => {
193 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
194 continue;
195 }
196 };
197
198 let is_current = paths_match(&project.project_path, ¤t_dir);
199
200 for session in sessions {
201 if session.checkpoint_count > 0 {
203 summaries.push(session_to_summary(&session, project, is_current));
204 }
205 }
206 }
207
208 summaries.sort_by(|a, b| match (a.is_current_project, b.is_current_project) {
210 (true, false) => std::cmp::Ordering::Less,
211 (false, true) => std::cmp::Ordering::Greater,
212 _ => b.last_accessed.cmp(&a.last_accessed),
213 });
214
215 Ok(summaries)
216}
217
218pub async fn get_session_detail(
225 _config_toml: &str,
226 session_id: &str,
227 home_dir: Option<&std::path::Path>,
228) -> anyhow::Result<Option<(SessionSummary, Vec<CheckpointSummary>)>> {
229 let manager = make_storage_manager(_config_toml, home_dir, "trustee").await?;
230
231 let projects = manager
232 .list_projects()
233 .await
234 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
235
236 for project in &projects {
237 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
238 Ok(ps) => ps,
239 Err(e) => {
240 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
241 continue;
242 }
243 };
244
245 let sessions = match project_storage.list_sessions().await {
246 Ok(s) => s,
247 Err(e) => {
248 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
249 continue;
250 }
251 };
252
253 if let Some(session) = sessions.iter().find(|s| s.session_id == session_id) {
254 let session_storage = project_storage
255 .create_session(session_id)
256 .await
257 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
258
259 let checkpoints = session_storage
260 .list_checkpoints()
261 .await
262 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
263
264 let summary = session_to_summary(session, project, false);
265 let cp_summaries: Vec<CheckpointSummary> =
266 checkpoints.iter().map(checkpoint_to_summary).collect();
267
268 return Ok(Some((summary, cp_summaries)));
269 }
270 }
271
272 Ok(None)
273}
274
275pub async fn create_resume_info(
283 _config_toml: &str,
284 session_id: &str,
285 home_dir: Option<&std::path::Path>,
286) -> anyhow::Result<Option<ResumeInfo>> {
287 let manager = make_storage_manager(_config_toml, home_dir, "trustee").await?;
288
289 let projects = manager
290 .list_projects()
291 .await
292 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
293
294 for project in &projects {
295 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
296 Ok(ps) => ps,
297 Err(e) => {
298 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
299 continue;
300 }
301 };
302
303 let sessions = match project_storage.list_sessions().await {
304 Ok(s) => s,
305 Err(e) => {
306 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
307 continue;
308 }
309 };
310
311 if sessions.iter().any(|s| s.session_id == session_id) {
312 let session_storage = project_storage
313 .create_session(session_id)
314 .await
315 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
316
317 let checkpoints = session_storage
318 .list_checkpoints()
319 .await
320 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
321
322 if let Some(latest) = checkpoints.iter().max_by_key(|cp| cp.created_at) {
323 return Ok(Some(ResumeInfo {
324 session_id: session_id.to_string(),
325 checkpoint_id: latest.checkpoint_id.clone(),
326 iteration: latest.iteration,
327 project_path: Some(project.project_path.clone()),
328 }));
329 }
330
331 return Ok(None);
333 }
334 }
335
336 Ok(None)
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
352pub struct HistoryMessage {
353 pub role: String,
355 pub content: String,
357 pub reasoning: Option<String>,
359 pub tool_calls: Option<Vec<HistoryToolCall>>,
361 pub name: Option<String>,
363}
364
365#[derive(Debug, Clone, Serialize, Deserialize)]
367pub struct HistoryToolCall {
368 pub name: String,
369 pub hint: String,
371}
372
373#[derive(Debug, Clone, Serialize, Deserialize)]
375pub struct SessionHistory {
376 pub session_id: String,
377 pub checkpoint_id: String,
378 pub task_description: String,
379 pub iteration: u32,
380 pub total_messages: usize,
381 pub messages: Vec<HistoryMessage>,
382}
383
384pub async fn load_session_history(
392 session_id: &str,
393 home_dir: Option<&std::path::Path>,
394) -> anyhow::Result<Option<SessionHistory>> {
395 let manager = make_storage_manager("", home_dir, "trustee").await?;
396
397 let projects = manager
398 .list_projects()
399 .await
400 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
401
402 for project in &projects {
403 let project_storage = match manager.get_project_storage_with_id(&project.project_path, &project.project_hash).await {
404 Ok(ps) => ps,
405 Err(e) => {
406 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
407 continue;
408 }
409 };
410
411 let sessions = match project_storage.list_sessions().await {
412 Ok(s) => s,
413 Err(e) => {
414 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
415 continue;
416 }
417 };
418
419 if !sessions.iter().any(|s| s.session_id == session_id) {
420 continue;
421 }
422
423 let session_storage = project_storage
424 .create_session(session_id)
425 .await
426 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
427
428 let checkpoints = session_storage
429 .list_checkpoints()
430 .await
431 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
432
433 let latest = match checkpoints.iter().max_by_key(|cp| cp.created_at) {
434 Some(cp) => cp,
435 None => return Ok(None),
436 };
437
438 let checkpoint_id = latest.checkpoint_id.clone();
439 let iteration = latest.iteration;
440
441 let checkpoint = session_storage
442 .load_checkpoint(&checkpoint_id)
443 .await
444 .map_err(|e| anyhow::anyhow!("Failed to load checkpoint: {}", e))?;
445
446 let task_description = checkpoint.agent_state.task_description.clone();
447 let total_messages = checkpoint.conversation_state.messages.len();
448
449 let messages = convert_messages(&checkpoint.conversation_state.messages);
450
451 return Ok(Some(SessionHistory {
452 session_id: session_id.to_string(),
453 checkpoint_id,
454 task_description,
455 iteration,
456 total_messages,
457 messages,
458 }));
459 }
460
461 Ok(None)
462}
463
464fn convert_messages(messages: &[ChatMessage]) -> Vec<HistoryMessage> {
470 const MAX_CONTENT_LEN: usize = 10_000;
471
472 let mut result = Vec::new();
473
474 for msg in messages {
475 if msg.role == "system" {
477 continue;
478 }
479
480 if msg.role == "tool" {
483 continue;
484 }
485
486 let content = if msg.content.len() > MAX_CONTENT_LEN {
487 format!("{}...\n[truncated]", &msg.content[..MAX_CONTENT_LEN])
488 } else {
489 msg.content.clone()
490 };
491
492 let tool_calls = msg.tool_calls.as_ref().map(|calls| {
494 calls
495 .iter()
496 .map(|tc| {
497 let hint = summarize_tool_args(&tc.function.name, &tc.function.arguments);
498 HistoryToolCall {
499 name: tc.function.name.clone(),
500 hint,
501 }
502 })
503 .collect::<Vec<_>>()
504 });
505
506 if msg.role == "assistant" && content.is_empty() && tool_calls.is_some() {
509 result.push(HistoryMessage {
510 role: "assistant".to_string(),
511 content: String::new(),
512 reasoning: msg.reasoning.clone(),
513 tool_calls,
514 name: None,
515 });
516 continue;
517 }
518
519 result.push(HistoryMessage {
520 role: msg.role.clone(),
521 content,
522 reasoning: msg.reasoning.clone(),
523 tool_calls,
524 name: msg.name.clone(),
525 });
526 }
527
528 result
529}
530
531fn summarize_tool_args(name: &str, args: &str) -> String {
536 let parsed: serde_json::Value = match serde_json::from_str(args) {
537 Ok(v) => v,
538 Err(_) => return args.chars().take(200).collect(),
539 };
540
541 let obj = match parsed.as_object() {
542 Some(o) => o,
543 None => return args.chars().take(200).collect(),
544 };
545
546 match name {
547 "bash" | "execute_command" => {
548 obj.get("command").and_then(|v| v.as_str()).map(|s| s.to_string())
549 }
550 "read" | "read_file" => {
551 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
552 }
553 "write" | "write_file" | "edit" => {
554 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
555 }
556 "grep" | "search" => {
557 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
558 }
559 "glob" => {
560 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
561 }
562 "todowrite" => {
563 let count = obj.get("todos").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
564 Some(format!("{} items", count))
565 }
566 "websearch" | "webfetch" => {
567 obj.get("query").or_else(|| obj.get("url")).and_then(|v| v.as_str()).map(|s| s.to_string())
568 }
569 _ => {
570 obj.values().next().and_then(|v| match v {
571 serde_json::Value::String(s) => Some(s.clone()),
572 _ => Some(v.to_string()),
573 })
574 }
575 }
576 .unwrap_or_default()
577 .chars()
578 .take(200)
579 .collect()
580}