feat: Session(Segment 群の grouping)を導入
- SessionId 型を新設、各 SegmentStart に session_id を持たせる - compaction / 内部 fork は同 SessionId を継承、fork() は新 Session を発行 - Store API を (SessionId, SegmentId) ベースに、FsStore layout は <root>/<session_id>/<segment_id>.jsonl に - Store::list_sessions / list_segments(session_id) / lookup_session_of を追加 - restore_by_segment shim を session-store に提供(pod-cli --session で使用) - SegmentState に SegmentLocation (session_id, segment_id) を保持し ArcSwap で更新 - RestoredState に session_id: Option<SessionId> を追加 - Picker は Session 単位に列挙、leaf segment を解決して resume
This commit is contained in:
@@ -1,16 +1,32 @@
|
||||
use llm_worker::WorkerResult;
|
||||
use llm_worker::llm_client::types::{Item, RequestConfig};
|
||||
use session_store::{FsStore, LogEntry, Store, TraceEntry, collect_state, new_segment_id};
|
||||
use session_store::{
|
||||
FsStore, LogEntry, Store, TraceEntry, collect_state, new_segment_id, new_session_id,
|
||||
};
|
||||
|
||||
fn nil_session_start(ts: u64, session_id: uuid::Uuid) -> LogEntry {
|
||||
LogEntry::SegmentStart {
|
||||
ts,
|
||||
session_id,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round_trip_write_and_read() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
let entries = vec![
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
session_id: sid,
|
||||
system_prompt: Some("You are helpful.".into()),
|
||||
config: RequestConfig::default().with_max_tokens(1024),
|
||||
history: vec![],
|
||||
@@ -37,13 +53,14 @@ fn round_trip_write_and_read() {
|
||||
];
|
||||
|
||||
for entry in &entries {
|
||||
store.append(id, entry).unwrap();
|
||||
store.append(sid, segid, entry).unwrap();
|
||||
}
|
||||
|
||||
let read_back = store.read_all(id).unwrap();
|
||||
let read_back = store.read_all(sid, segid).unwrap();
|
||||
assert_eq!(read_back.len(), entries.len());
|
||||
|
||||
let state = collect_state(&read_back);
|
||||
assert_eq!(state.session_id, Some(sid));
|
||||
assert_eq!(state.system_prompt.as_deref(), Some("You are helpful."));
|
||||
assert_eq!(state.config.max_tokens, Some(1024));
|
||||
assert_eq!(state.history.len(), 2);
|
||||
@@ -53,13 +70,15 @@ fn round_trip_write_and_read() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_session_writes_all_entries() {
|
||||
fn create_segment_writes_all_entries() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
let entries = [LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
session_id: sid,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![
|
||||
@@ -70,74 +89,65 @@ fn create_session_writes_all_entries() {
|
||||
compacted_from: None,
|
||||
}];
|
||||
|
||||
store.create_segment(id, &entries).unwrap();
|
||||
let read_back = store.read_all(id).unwrap();
|
||||
store.create_segment(sid, segid, &entries).unwrap();
|
||||
let read_back = store.read_all(sid, segid).unwrap();
|
||||
assert_eq!(read_back.len(), 1);
|
||||
|
||||
let state = collect_state(&read_back);
|
||||
assert_eq!(state.history.len(), 2);
|
||||
assert_eq!(state.session_id, Some(sid));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_sessions_returns_newest_first() {
|
||||
fn list_sessions_and_segments() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
|
||||
let id1 = new_segment_id();
|
||||
// Small delay to ensure different UUID v7 timestamps
|
||||
let sid_a = new_session_id();
|
||||
std::thread::sleep(std::time::Duration::from_millis(2));
|
||||
let id2 = new_segment_id();
|
||||
let sid_b = new_session_id();
|
||||
|
||||
let entry = LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
};
|
||||
let seg_a1 = new_segment_id();
|
||||
let seg_a2 = new_segment_id();
|
||||
let seg_b1 = new_segment_id();
|
||||
|
||||
store.append(id1, &entry).unwrap();
|
||||
store.append(id2, &entry).unwrap();
|
||||
store.append(sid_a, seg_a1, &nil_session_start(1, sid_a)).unwrap();
|
||||
store.append(sid_a, seg_a2, &nil_session_start(2, sid_a)).unwrap();
|
||||
store.append(sid_b, seg_b1, &nil_session_start(3, sid_b)).unwrap();
|
||||
|
||||
let sessions = store.list_segments().unwrap();
|
||||
assert_eq!(sessions.len(), 2);
|
||||
assert_eq!(sessions[0], id2); // newest first
|
||||
assert_eq!(sessions[1], id1);
|
||||
let sessions = store.list_sessions().unwrap();
|
||||
assert_eq!(sessions, vec![sid_b, sid_a]); // newest first
|
||||
|
||||
let segs_a = store.list_segments(sid_a).unwrap();
|
||||
assert!(segs_a.contains(&seg_a1) && segs_a.contains(&seg_a2));
|
||||
assert_eq!(segs_a.len(), 2);
|
||||
|
||||
let segs_b = store.list_segments(sid_b).unwrap();
|
||||
assert_eq!(segs_b, vec![seg_b1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exists_returns_correct_state() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
assert!(!store.exists(id).unwrap());
|
||||
assert!(!store.exists(sid, segid).unwrap());
|
||||
|
||||
store
|
||||
.append(
|
||||
id,
|
||||
&LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
store.append(sid, segid, &nil_session_start(1000, sid)).unwrap();
|
||||
|
||||
assert!(store.exists(id).unwrap());
|
||||
assert!(store.exists(sid, segid).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_found_error_for_missing_session() {
|
||||
fn not_found_error_for_missing_segment() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
let result = store.read_all(id);
|
||||
let result = store.read_all(sid, segid);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
@@ -145,21 +155,10 @@ fn not_found_error_for_missing_session() {
|
||||
fn trace_entries_in_separate_file() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
store
|
||||
.append(
|
||||
id,
|
||||
&LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![],
|
||||
forked_from: None,
|
||||
compacted_from: None,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
store.append(sid, segid, &nil_session_start(1000, sid)).unwrap();
|
||||
|
||||
let trace = TraceEntry {
|
||||
ts: 1500,
|
||||
@@ -168,14 +167,17 @@ fn trace_entries_in_separate_file() {
|
||||
llm_worker::llm_client::event::PingEvent { timestamp: None },
|
||||
),
|
||||
};
|
||||
store.append_trace(id, &trace).unwrap();
|
||||
store.append_trace(sid, segid, &trace).unwrap();
|
||||
|
||||
// Log should have 1 entry, unaffected by trace
|
||||
let log = store.read_all(id).unwrap();
|
||||
let log = store.read_all(sid, segid).unwrap();
|
||||
assert_eq!(log.len(), 1);
|
||||
|
||||
// Trace file should exist separately
|
||||
let trace_path = dir.path().join(format!("{id}.trace.jsonl"));
|
||||
let trace_path = dir
|
||||
.path()
|
||||
.join(sid.to_string())
|
||||
.join(format!("{segid}.trace.jsonl"));
|
||||
assert!(trace_path.exists());
|
||||
}
|
||||
|
||||
@@ -183,11 +185,13 @@ fn trace_entries_in_separate_file() {
|
||||
fn read_entry_count_matches_append_tally() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let id = new_segment_id();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
let entries = [
|
||||
LogEntry::SegmentStart {
|
||||
ts: 1000,
|
||||
session_id: sid,
|
||||
system_prompt: None,
|
||||
config: RequestConfig::default(),
|
||||
history: vec![],
|
||||
@@ -201,8 +205,22 @@ fn read_entry_count_matches_append_tally() {
|
||||
];
|
||||
|
||||
for entry in &entries {
|
||||
store.append(id, entry).unwrap();
|
||||
store.append(sid, segid, entry).unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(store.read_entry_count(id).unwrap(), entries.len());
|
||||
assert_eq!(store.read_entry_count(sid, segid).unwrap(), entries.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lookup_session_of_finds_owning_session() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let store = FsStore::new(dir.path()).unwrap();
|
||||
let sid = new_session_id();
|
||||
let segid = new_segment_id();
|
||||
|
||||
assert_eq!(store.lookup_session_of(segid).unwrap(), None);
|
||||
|
||||
store.append(sid, segid, &nil_session_start(1, sid)).unwrap();
|
||||
|
||||
assert_eq!(store.lookup_session_of(segid).unwrap(), Some(sid));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user