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