Skip to main content

crabtalk_runtime/
conversation.rs

1//! Conversation history persistence — reading JSONL files from `~/.crabtalk/sessions/`.
2
3use anyhow::{Context, Result};
4use serde::{Deserialize, Serialize};
5use std::{
6    fs,
7    io::{BufRead, BufReader},
8    path::Path,
9};
10use wcore::model::Message;
11
12/// Conversation metadata (first line of a JSONL conversation file).
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct ConversationMeta {
15    pub agent: String,
16    pub created_by: String,
17    pub created_at: String,
18}
19
20/// List all persisted conversations. Returns `(filename_stem, meta)` pairs.
21pub fn list_conversations(conversations_dir: &Path) -> Result<Vec<(String, ConversationMeta)>> {
22    let mut results = Vec::new();
23
24    let entries = match fs::read_dir(conversations_dir) {
25        Ok(e) => e,
26        Err(_) => return Ok(results),
27    };
28
29    for entry in entries.flatten() {
30        let path = entry.path();
31        if path.extension().and_then(|e| e.to_str()) != Some("jsonl") {
32            continue;
33        }
34        let stem = path
35            .file_stem()
36            .and_then(|s| s.to_str())
37            .unwrap_or("")
38            .to_owned();
39
40        if let Ok(meta) = read_meta(&path) {
41            results.push((stem, meta));
42        }
43    }
44
45    results.sort_by(|a, b| b.1.created_at.cmp(&a.1.created_at));
46    Ok(results)
47}
48
49/// Load a full conversation from disk: metadata + all messages.
50pub fn load_conversation(path: &Path) -> Result<(ConversationMeta, Vec<Message>)> {
51    let file = fs::File::open(path)
52        .with_context(|| format!("open conversation file: {}", path.display()))?;
53    let reader = BufReader::new(file);
54    let mut lines = reader.lines();
55
56    let meta_line = lines
57        .next()
58        .ok_or_else(|| anyhow::anyhow!("empty conversation file"))?
59        .context("read meta line")?;
60    let meta: ConversationMeta =
61        serde_json::from_str(&meta_line).context("parse conversation meta")?;
62
63    let mut messages = Vec::new();
64    for line in lines {
65        let line = line.context("read message line")?;
66        if line.trim().is_empty() {
67            continue;
68        }
69        let msg: Message = serde_json::from_str(&line).context("parse message")?;
70        messages.push(msg);
71    }
72
73    Ok((meta, messages))
74}
75
76/// Read just the metadata (first line) from a conversation file.
77fn read_meta(path: &Path) -> Result<ConversationMeta> {
78    let file = fs::File::open(path)?;
79    let mut reader = BufReader::new(file);
80    let mut line = String::new();
81    reader.read_line(&mut line)?;
82    Ok(serde_json::from_str(line.trim())?)
83}