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};
14use abk::cli::ResumeInfo;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SessionSummary {
19 pub session_id: String,
20 pub project_name: String,
21 pub project_path: String,
22 pub checkpoint_count: usize,
23 pub created_at: chrono::DateTime<chrono::Utc>,
24 pub last_accessed: chrono::DateTime<chrono::Utc>,
25 pub description: Option<String>,
26 pub is_current_project: bool,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct CheckpointSummary {
32 pub checkpoint_id: String,
33 pub session_id: String,
34 pub iteration: u32,
35 pub workflow_step: String,
36 pub created_at: chrono::DateTime<chrono::Utc>,
37}
38
39fn session_to_summary(
44 session: &AbkSessionMetadata,
45 project: &AbkProjectMetadata,
46 is_current: bool,
47) -> SessionSummary {
48 SessionSummary {
49 session_id: session.session_id.clone(),
50 project_name: project.name.clone(),
51 project_path: project.project_path.to_string_lossy().to_string(),
52 checkpoint_count: session.checkpoint_count as usize,
53 created_at: session.created_at,
54 last_accessed: session.last_accessed,
55 description: session.description.clone(),
56 is_current_project: is_current,
57 }
58}
59
60fn checkpoint_to_summary(cp: &CheckpointMetadata) -> CheckpointSummary {
61 CheckpointSummary {
62 checkpoint_id: cp.checkpoint_id.clone(),
63 session_id: cp.session_id.clone(),
64 iteration: cp.iteration,
65 workflow_step: format!("{:?}", cp.workflow_step),
66 created_at: cp.created_at,
67 }
68}
69
70fn config_working_dir(config_toml: &str) -> std::path::PathBuf {
75 if let Ok(value) = toml::from_str::<toml::Value>(config_toml) {
76 if let Some(agent) = value.get("agent") {
77 if let Some(wd) = agent.get("working_dir").and_then(|v| v.as_str()) {
78 return std::path::PathBuf::from(wd);
79 }
80 }
81 }
82 std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
83}
84
85fn paths_match(a: &std::path::Path, b: &std::path::Path) -> bool {
87 let ca = a.canonicalize().unwrap_or_else(|_| a.to_path_buf());
88 let cb = b.canonicalize().unwrap_or_else(|_| b.to_path_buf());
89 ca == cb
90}
91
92pub async fn list_all_sessions(config_toml: &str) -> anyhow::Result<Vec<SessionSummary>> {
101 let current_dir = config_working_dir(config_toml);
102 let manager = get_storage_manager()
103 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))?;
104
105 let projects = manager
106 .list_projects()
107 .await
108 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
109
110 let mut summaries = Vec::new();
111
112 for project in &projects {
113 let project_storage = match manager.get_project_storage(&project.project_path).await {
116 Ok(ps) => ps,
117 Err(e) => {
118 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
119 continue;
120 }
121 };
122
123 let sessions = match project_storage.list_sessions().await {
124 Ok(s) => s,
125 Err(e) => {
126 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
127 continue;
128 }
129 };
130
131 let is_current = paths_match(&project.project_path, ¤t_dir);
132
133 for session in sessions {
134 if session.checkpoint_count > 0 {
136 summaries.push(session_to_summary(&session, project, is_current));
137 }
138 }
139 }
140
141 summaries.sort_by(|a, b| match (a.is_current_project, b.is_current_project) {
143 (true, false) => std::cmp::Ordering::Less,
144 (false, true) => std::cmp::Ordering::Greater,
145 _ => b.last_accessed.cmp(&a.last_accessed),
146 });
147
148 Ok(summaries)
149}
150
151pub async fn get_session_detail(
156 _config_toml: &str,
157 session_id: &str,
158) -> anyhow::Result<Option<(SessionSummary, Vec<CheckpointSummary>)>> {
159 let manager = get_storage_manager()
160 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))?;
161
162 let projects = manager
163 .list_projects()
164 .await
165 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
166
167 for project in &projects {
168 let project_storage = match manager.get_project_storage(&project.project_path).await {
169 Ok(ps) => ps,
170 Err(e) => {
171 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
172 continue;
173 }
174 };
175
176 let sessions = match project_storage.list_sessions().await {
177 Ok(s) => s,
178 Err(e) => {
179 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
180 continue;
181 }
182 };
183
184 if let Some(session) = sessions.iter().find(|s| s.session_id == session_id) {
185 let session_storage = project_storage
186 .create_session(session_id)
187 .await
188 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
189
190 let checkpoints = session_storage
191 .list_checkpoints()
192 .await
193 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
194
195 let summary = session_to_summary(session, project, false);
196 let cp_summaries: Vec<CheckpointSummary> =
197 checkpoints.iter().map(checkpoint_to_summary).collect();
198
199 return Ok(Some((summary, cp_summaries)));
200 }
201 }
202
203 Ok(None)
204}
205
206pub async fn create_resume_info(
212 _config_toml: &str,
213 session_id: &str,
214) -> anyhow::Result<Option<ResumeInfo>> {
215 let manager = get_storage_manager()
216 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))?;
217
218 let projects = manager
219 .list_projects()
220 .await
221 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
222
223 for project in &projects {
224 let project_storage = match manager.get_project_storage(&project.project_path).await {
225 Ok(ps) => ps,
226 Err(e) => {
227 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
228 continue;
229 }
230 };
231
232 let sessions = match project_storage.list_sessions().await {
233 Ok(s) => s,
234 Err(e) => {
235 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
236 continue;
237 }
238 };
239
240 if sessions.iter().any(|s| s.session_id == session_id) {
241 let session_storage = project_storage
242 .create_session(session_id)
243 .await
244 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
245
246 let checkpoints = session_storage
247 .list_checkpoints()
248 .await
249 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
250
251 if let Some(latest) = checkpoints.iter().max_by_key(|cp| cp.created_at) {
252 return Ok(Some(ResumeInfo {
253 session_id: session_id.to_string(),
254 checkpoint_id: latest.checkpoint_id.clone(),
255 iteration: latest.iteration,
256 project_path: Some(project.project_path.clone()),
257 }));
258 }
259
260 return Ok(None);
262 }
263 }
264
265 Ok(None)
266}
267
268#[derive(Debug, Clone, Serialize, Deserialize)]
281pub struct HistoryMessage {
282 pub role: String,
284 pub content: String,
286 pub reasoning: Option<String>,
288 pub tool_calls: Option<Vec<HistoryToolCall>>,
290 pub name: Option<String>,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize)]
296pub struct HistoryToolCall {
297 pub name: String,
298 pub hint: String,
300}
301
302#[derive(Debug, Clone, Serialize, Deserialize)]
304pub struct SessionHistory {
305 pub session_id: String,
306 pub checkpoint_id: String,
307 pub task_description: String,
308 pub iteration: u32,
309 pub total_messages: usize,
310 pub messages: Vec<HistoryMessage>,
311}
312
313pub async fn load_session_history(
319 session_id: &str,
320) -> anyhow::Result<Option<SessionHistory>> {
321 let manager = get_storage_manager()
322 .map_err(|e| anyhow::anyhow!("Failed to get storage manager: {}", e))?;
323
324 let projects = manager
325 .list_projects()
326 .await
327 .map_err(|e| anyhow::anyhow!("Failed to list projects: {}", e))?;
328
329 for project in &projects {
330 let project_storage = match manager.get_project_storage(&project.project_path).await {
331 Ok(ps) => ps,
332 Err(e) => {
333 tracing::debug!("Skipping project {}: {}", project.project_path.display(), e);
334 continue;
335 }
336 };
337
338 let sessions = match project_storage.list_sessions().await {
339 Ok(s) => s,
340 Err(e) => {
341 tracing::debug!("Failed to list sessions for {}: {}", project.project_path.display(), e);
342 continue;
343 }
344 };
345
346 if !sessions.iter().any(|s| s.session_id == session_id) {
347 continue;
348 }
349
350 let session_storage = project_storage
351 .create_session(session_id)
352 .await
353 .map_err(|e| anyhow::anyhow!("Failed to get session storage: {}", e))?;
354
355 let checkpoints = session_storage
356 .list_checkpoints()
357 .await
358 .map_err(|e| anyhow::anyhow!("Failed to list checkpoints: {}", e))?;
359
360 let latest = match checkpoints.iter().max_by_key(|cp| cp.created_at) {
361 Some(cp) => cp,
362 None => return Ok(None),
363 };
364
365 let checkpoint_id = latest.checkpoint_id.clone();
366 let iteration = latest.iteration;
367
368 let checkpoint = session_storage
369 .load_checkpoint(&checkpoint_id)
370 .await
371 .map_err(|e| anyhow::anyhow!("Failed to load checkpoint: {}", e))?;
372
373 let task_description = checkpoint.agent_state.task_description.clone();
374 let total_messages = checkpoint.conversation_state.messages.len();
375
376 let messages = convert_messages(&checkpoint.conversation_state.messages);
377
378 return Ok(Some(SessionHistory {
379 session_id: session_id.to_string(),
380 checkpoint_id,
381 task_description,
382 iteration,
383 total_messages,
384 messages,
385 }));
386 }
387
388 Ok(None)
389}
390
391fn convert_messages(messages: &[ChatMessage]) -> Vec<HistoryMessage> {
397 const MAX_CONTENT_LEN: usize = 10_000;
398
399 let mut result = Vec::new();
400
401 for msg in messages {
402 if msg.role == "system" {
404 continue;
405 }
406
407 if msg.role == "tool" {
410 continue;
411 }
412
413 let content = if msg.content.len() > MAX_CONTENT_LEN {
414 format!("{}...\n[truncated]", &msg.content[..MAX_CONTENT_LEN])
415 } else {
416 msg.content.clone()
417 };
418
419 let tool_calls = msg.tool_calls.as_ref().map(|calls| {
421 calls
422 .iter()
423 .map(|tc| {
424 let hint = summarize_tool_args(&tc.function.name, &tc.function.arguments);
425 HistoryToolCall {
426 name: tc.function.name.clone(),
427 hint,
428 }
429 })
430 .collect::<Vec<_>>()
431 });
432
433 if msg.role == "assistant" && content.is_empty() && tool_calls.is_some() {
436 result.push(HistoryMessage {
437 role: "assistant".to_string(),
438 content: String::new(),
439 reasoning: msg.reasoning.clone(),
440 tool_calls,
441 name: None,
442 });
443 continue;
444 }
445
446 result.push(HistoryMessage {
447 role: msg.role.clone(),
448 content,
449 reasoning: msg.reasoning.clone(),
450 tool_calls,
451 name: msg.name.clone(),
452 });
453 }
454
455 result
456}
457
458fn summarize_tool_args(name: &str, args: &str) -> String {
463 let parsed: serde_json::Value = match serde_json::from_str(args) {
464 Ok(v) => v,
465 Err(_) => return args.chars().take(200).collect(),
466 };
467
468 let obj = match parsed.as_object() {
469 Some(o) => o,
470 None => return args.chars().take(200).collect(),
471 };
472
473 match name {
474 "bash" | "execute_command" => {
475 obj.get("command").and_then(|v| v.as_str()).map(|s| s.to_string())
476 }
477 "read" | "read_file" => {
478 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
479 }
480 "write" | "write_file" | "edit" => {
481 obj.get("file_path").and_then(|v| v.as_str()).map(|s| s.to_string())
482 }
483 "grep" | "search" => {
484 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
485 }
486 "glob" => {
487 obj.get("pattern").and_then(|v| v.as_str()).map(|s| s.to_string())
488 }
489 "todowrite" => {
490 let count = obj.get("todos").and_then(|v| v.as_array()).map(|a| a.len()).unwrap_or(0);
491 Some(format!("{} items", count))
492 }
493 "websearch" | "webfetch" => {
494 obj.get("query").or_else(|| obj.get("url")).and_then(|v| v.as_str()).map(|s| s.to_string())
495 }
496 _ => {
497 obj.values().next().and_then(|v| match v {
498 serde_json::Value::String(s) => Some(s.clone()),
499 _ => Some(v.to_string()),
500 })
501 }
502 }
503 .unwrap_or_default()
504 .chars()
505 .take(200)
506 .collect()
507}