use std::sync::Arc;
use tempfile::tempdir;
use theway_core::{
AgentHarness, AgentHarnessOptions, MemorySessionStorage, Session, SessionStorage, StreamFn,
ThinkingLevel,
};
use theway_llm_provider::{
AssistantMessage, AssistantMessageEvent, AssistantMessageEventStream, AssistantRole,
ContentBlock, DoneReason, ModelCost, StopReason, Usage,
};
use theway_storage::sqlite_repo::SqliteSessionRepo;
fn faux_model() -> theway_llm_provider::Model {
theway_llm_provider::Model {
id: "faux".into(),
name: "Faux".into(),
api: theway_llm_provider::Api::from("faux"),
provider: theway_llm_provider::Provider::from("faux"),
base_url: String::new(),
reasoning: false,
thinking_level_map: None,
input: vec![],
cost: ModelCost::default(),
context_window: 0,
max_tokens: 0,
headers: None,
compat: None,
}
}
fn faux_stream_fn(text: &'static str) -> StreamFn {
Arc::new(move |_, _, _| {
let (stream, mut sender) = AssistantMessageEventStream::new();
tokio::spawn(async move {
let msg = AssistantMessage {
role: AssistantRole::Assistant,
content: vec![ContentBlock::text(text)],
api: theway_llm_provider::Api::from("faux"),
provider: theway_llm_provider::Provider::from("faux"),
model: "faux".into(),
response_model: None,
response_id: None,
diagnostics: None,
usage: Usage::default(),
stop_reason: StopReason::Stop,
error_message: None,
timestamp: 0,
};
sender.push(AssistantMessageEvent::Start {
partial: msg.clone(),
});
sender.push(AssistantMessageEvent::Done {
reason: DoneReason::Stop,
message: msg,
});
});
stream
})
}
#[tokio::test]
async fn create_persist_reopen_resume_round_trips() {
let dir = tempdir().unwrap();
let session_id;
{
let repo = SqliteSessionRepo::new(dir.path());
let store = repo
.create("/some/cwd")
.await
.expect("create jsonl session");
session_id = theway_contract::session::SessionReader::get_metadata_json(&store)
.await
.unwrap()
.get("id")
.and_then(|v| v.as_str())
.unwrap()
.to_string();
let session = Session::from_store(Arc::new(store));
let mut opts = AgentHarnessOptions::new(faux_model(), session.clone());
opts.thinking_level = ThinkingLevel::Off;
opts.stream_fn = Some(faux_stream_fn("ack"));
let harness = AgentHarness::new(opts);
harness.prompt("first").await.unwrap();
harness.prompt("second").await.unwrap();
}
let repo = SqliteSessionRepo::new(dir.path());
let files = repo.list().await.unwrap();
assert_eq!(files.len(), 1, "expected exactly one session file");
let store = repo.open(&files[0]).await.unwrap();
let reopened_id = theway_contract::session::SessionReader::get_metadata_json(&store)
.await
.unwrap()
.get("id")
.and_then(|v| v.as_str())
.unwrap()
.to_string();
assert_eq!(
reopened_id, session_id,
"metadata id must survive close/reopen"
);
let reopened = Session::from_store(Arc::new(store));
let ctx = reopened.build_context().await.unwrap();
assert_eq!(
ctx.messages.len(),
4,
"expected 4 messages; got: {:#?}",
ctx.messages
);
let texts: Vec<String> = ctx
.messages
.iter()
.filter_map(|m| match m {
theway_core::AgentMessage::Llm(theway_llm_provider::Message::User(u)) => {
match &u.content {
theway_llm_provider::UserContent::Text(s) => Some(s.clone()),
_ => None,
}
}
theway_core::AgentMessage::Llm(theway_llm_provider::Message::Assistant(a)) => {
a.content.iter().find_map(|b| match b {
theway_llm_provider::ContentBlock::Text(t) => Some(t.text.clone()),
_ => None,
})
}
_ => None,
})
.collect();
assert_eq!(texts, vec!["first", "ack", "second", "ack"]);
}
#[tokio::test]
async fn rehydrate_after_reopen_mirrors_state_into_agent() {
let storage = Arc::new(MemorySessionStorage::new());
let session = Session::new(storage as Arc<dyn SessionStorage>);
session.append_thinking_level_change("high").await.unwrap();
session.append_model_change("faux", "faux").await.unwrap();
session
.append_message(theway_core::AgentMessage::Llm(
theway_llm_provider::Message::User(theway_llm_provider::UserMessage {
role: theway_llm_provider::UserRole::User,
content: theway_llm_provider::UserContent::Text("prior-prompt".into()),
timestamp: 0,
}),
))
.await
.unwrap();
let mut opts = AgentHarnessOptions::new(faux_model(), session.clone());
opts.thinking_level = ThinkingLevel::Off;
opts.stream_fn = Some(faux_stream_fn("unused"));
let harness = AgentHarness::new(opts);
let ctx = harness.rehydrate_from_session().await.unwrap();
assert_eq!(ctx.thinking_level, "high");
assert!(ctx.model.is_some());
let state = harness.agent().state();
assert_eq!(state.messages.len(), 1);
assert_eq!(state.thinking_level, Some(ThinkingLevel::High));
assert!(state.model.is_some());
}