use std::collections::HashMap;
use clawft_types::session::Session;
use super::sink::{ConversationSink, Turn};
pub fn filter_store1_subset(turns: &[Turn]) -> Vec<&Turn> {
turns
.iter()
.filter(|t| t.tool_calls.is_none() && t.tool_call_id.is_none())
.collect()
}
pub async fn hydrate_session(sink: &dyn ConversationSink, conv_id: &str, window: usize) -> Session {
let turns = sink.history(conv_id, window).await;
let mut session = Session::new(conv_id);
for turn in filter_store1_subset(&turns) {
session.add_message(&turn.role, &turn.content, None);
}
if let serde_json::Value::Object(map) = sink.meta(conv_id).await {
session.metadata = map.into_iter().collect::<HashMap<_, _>>();
}
session
}
#[cfg(test)]
mod tests {
use super::*;
use crate::agent::sink::InMemorySink;
use serde_json::json;
fn user(content: &str) -> Turn {
Turn {
turn_id: format!("u-{content}"),
role: "user".into(),
content: content.into(),
tool_calls: None,
tool_call_id: None,
ts_ms: 1_700_000_000_000,
voice_analysis: None,
audio: None,
}
}
fn assistant(content: &str) -> Turn {
Turn {
turn_id: format!("a-{content}"),
role: "assistant".into(),
content: content.into(),
tool_calls: None,
tool_call_id: None,
voice_analysis: None,
audio: None,
ts_ms: 1_700_000_000_000,
}
}
fn assistant_tool_call(content: &str) -> Turn {
Turn {
tool_calls: Some(vec![json!({"id": "c1", "name": "search"})]),
..assistant(content)
}
}
fn tool_result(content: &str) -> Turn {
Turn {
role: "tool".into(),
tool_call_id: Some("c1".into()),
..user(content)
}
}
#[test]
fn filter_drops_tool_intermediates_keeps_user_and_final_assistant() {
let superset = vec![
user("What is the capital of France?"),
assistant("The capital of France is Paris."),
user("What's the weather in Paris right now?"),
assistant_tool_call("Let me check the current conditions."),
tool_result("Paris: 18°C, clear skies, wind 6 km/h."),
assistant("It's currently 18°C and clear in Paris."),
user("Thanks, that's all."),
assistant("You're welcome! Happy to help."),
];
let subset = filter_store1_subset(&superset);
let shape: Vec<(&str, &str)> = subset
.iter()
.map(|t| (t.role.as_str(), t.content.as_str()))
.collect();
assert_eq!(
shape,
vec![
("user", "What is the capital of France?"),
("assistant", "The capital of France is Paris."),
("user", "What's the weather in Paris right now?"),
("assistant", "It's currently 18°C and clear in Paris."),
("user", "Thanks, that's all."),
("assistant", "You're welcome! Happy to help."),
],
);
assert!(
subset
.iter()
.all(|t| t.tool_calls.is_none() && t.tool_call_id.is_none()),
"no intermediate should survive the filter"
);
}
#[tokio::test]
async fn hydrate_reconstructs_store1_subset() {
let sink = InMemorySink::new();
for t in [
user("hi"),
assistant_tool_call("checking"),
tool_result("42"),
assistant("the answer is 42"),
] {
sink.append_turn("c:1", t).await.unwrap();
}
let session = hydrate_session(&sink, "c:1", 0).await;
let roles_contents: Vec<(String, String)> = session
.messages
.iter()
.map(|m| {
(
m["role"].as_str().unwrap_or_default().to_string(),
m["content"].as_str().unwrap_or_default().to_string(),
)
})
.collect();
assert_eq!(
roles_contents,
vec![
("user".into(), "hi".into()),
("assistant".into(), "the answer is 42".into()),
]
);
}
#[tokio::test]
async fn hydrate_reads_metadata_sidecar() {
let sink = InMemorySink::new();
sink.append_turn("c:1", user("hi")).await.unwrap();
sink.set_meta("c:1", json!({"hallucination_score": 0.42}))
.await;
let session = hydrate_session(&sink, "c:1", 0).await;
assert_eq!(
session.metadata.get("hallucination_score").and_then(|v| v.as_f64()),
Some(0.42)
);
}
#[tokio::test]
async fn hydrate_absent_metadata_is_empty() {
let sink = InMemorySink::new();
sink.append_turn("c:1", user("hi")).await.unwrap();
let session = hydrate_session(&sink, "c:1", 0).await;
assert!(session.metadata.is_empty());
assert_eq!(
session
.metadata
.get("hallucination_score")
.and_then(|v| v.as_f64())
.unwrap_or(0.0),
0.0
);
}
}