- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! `SessionLog::derive_with_plan` (docs/design/68-context-engine.md §4/§10): - 4
//! a `WorkingSetPlan`'s fidelity choice — not a fixed policy — decides - 5
//! whether a closed turn projects as a full record, a `<turns>` card line, - 6
//! or nothing (left to an existing/incremental `Compaction` entry). - 7
- 8
use std::path::PathBuf; - 9
- 10
use tempfile::tempdir; - 11
- 12
use vak_llm::{ContentBlock, Message}; - 13
use vak_session::types::{FrozenContract, MessageRecord, SessionHeader, TurnCardRecord}; - 14
use vak_session::{Fidelity, SessionLog, TurnIndex, WorkingSetPlan}; - 15
- 16
fn header() -> SessionHeader { - 17
SessionHeader { - 18
agent: None, - 19
session_id: "s-test".into(), - 20
created_at: chrono::Utc::now(), - 21
cwd: PathBuf::from("/tmp/proj"), - 22
parent_session_id: None, - 23
contract_id: None, - 24
work_item_id: None, - 25
conversation: None, - 26
contract: FrozenContract { - 27
app_version: "0.1.0".into(), - 28
provider: "anthropic".into(), - 29
model: "claude-sonnet-4-5".into(), - 30
route_ladder: Vec::new(), - 31
route_objective: String::new(), - 32
route_annotations: Vec::new(), - 33
system_prompt: "system prompt v1".into(), - 34
permission_mode: "workspace-write".into(), - 35
capabilities: Vec::new(), - 36
prompt_layers: Vec::new(), - 37
}, - 38
} - 39
} - 40
- 41
fn user_msg(text: &str) -> MessageRecord { - 42
MessageRecord { - 43
message: Message::user_text(text), - 44
meta: None, - 45
} - 46
} - 47
- 48
fn assistant_msg(text: &str) -> MessageRecord { - 49
MessageRecord { - 50
message: Message::assistant(vec![ContentBlock::text(text)]), - 51
meta: None, - 52
} - 53
} - 54
- 55
/// Three closed turns, each carrying a `TurnCard` built the same way the - 56
/// real turn-close hook does. - 57
fn three_closed_turns(log: &mut SessionLog) -> Vec<String> { - 58
let mut ids = Vec::new(); - 59
for (q, a) in [ - 60
("first question", "first answer"), - 61
("second question", "second answer"), - 62
("third question", "third answer"), - 63
] { - 64
let id = log.append_message(user_msg(q)).unwrap().id; - 65
log.append_message(assistant_msg(a)).unwrap(); - 66
let card = TurnIndex::from_log(log) - 67
.turn_by_id(&id) - 68
.unwrap() - 69
.build_card("completed", a.to_string(), &|s| s.len() as u64 / 4); - 70
log.append_turn_card(TurnCardRecord { - 71
turn_id: id.clone(), - 72
card, - 73
}) - 74
.unwrap(); - 75
ids.push(id); - 76
} - 77
ids - 78
} - 79
- 80
#[test] - 81
fn full_fidelity_projects_the_two_message_record() { - 82
let dir = tempdir().unwrap(); - 83
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 84
let ids = three_closed_turns(&mut log); - 85
- 86
let plan = WorkingSetPlan { - 87
per_turn: ids.iter().map(|id| (id.clone(), Fidelity::Full)).collect(), - 88
packet_range: None, - 89
retrieved: Vec::new(), - 90
budget: 10_000, - 91
spent: 0, - 92
}; - 93
let messages = log.derive_with_plan(&plan); - 94
// 3 turns x 2 messages each, no <turns> block, no compaction summary. - 95
assert_eq!(messages.len(), 6); - 96
let joined: String = messages - 97
.iter() - 98
.map(Message::text_content) - 99
.collect::<Vec<_>>() - 100
.join("\n"); - 101
assert!(joined.contains("first question") && joined.contains("first answer")); - 102
assert!(joined.contains("third question") && joined.contains("third answer")); - 103
assert!(!joined.contains("<turns>")); - 104
} - 105
- 106
#[test] - 107
fn card_fidelity_collapses_into_one_turns_block() { - 108
let dir = tempdir().unwrap(); - 109
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 110
let ids = three_closed_turns(&mut log); - 111
- 112
// Newest turn stays Full; the first two are cards. - 113
let plan = WorkingSetPlan { - 114
per_turn: vec![ - 115
(ids[0].clone(), Fidelity::Card), - 116
(ids[1].clone(), Fidelity::Card), - 117
(ids[2].clone(), Fidelity::Full), - 118
], - 119
packet_range: None, - 120
retrieved: Vec::new(), - 121
budget: 10_000, - 122
spent: 0, - 123
}; - 124
let messages = log.derive_with_plan(&plan); - 125
// One <turns> block message plus the Full turn's own two messages - 126
// (directive + trace/answer) — the card turns never get a directive - 127
// message of their own. - 128
assert_eq!(messages.len(), 3); - 129
let joined: String = messages - 130
.iter() - 131
.map(Message::text_content) - 132
.collect::<Vec<_>>() - 133
.join("\n"); - 134
assert_eq!(joined.matches("<turns>").count(), 1); - 135
assert!(joined.contains("#1 asked:") && joined.contains("#2 asked:")); - 136
assert!(joined.contains("third question") && joined.contains("third answer")); - 137
} - 138
- 139
#[test] - 140
fn a_packet_range_with_no_stored_packet_renders_as_cards_never_as_nothing() { - 141
let dir = tempdir().unwrap(); - 142
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 143
let ids = three_closed_turns(&mut log); - 144
- 145
// Only the newest turn is planned; the older two are a packet range - 146
// that no packet covers yet (the agent normally writes it before - 147
// projecting). The no-cut invariant still holds: those turns ride as - 148
// card lines, never as their raw records and never as nothing. - 149
let plan = WorkingSetPlan { - 150
per_turn: vec![(ids[2].clone(), Fidelity::Full)], - 151
packet_range: Some((ids[0].clone(), ids[1].clone())), - 152
retrieved: Vec::new(), - 153
budget: 100, - 154
spent: 50, - 155
}; - 156
let messages = log.derive_with_plan(&plan); - 157
let joined: String = messages - 158
.iter() - 159
.map(Message::text_content) - 160
.collect::<Vec<_>>() - 161
.join("\n"); - 162
assert!(!joined.contains("<context_summary>")); - 163
// One <turns> block carries both, and neither turn's own two-message - 164
// record is present: "first question" appears only inside that block. - 165
assert_eq!(joined.matches("<turns>").count(), 1); - 166
assert!( - 167
joined.contains("#1 asked:") && joined.contains("#2 asked:"), - 168
"{joined}" - 169
); - 170
let standalone: Vec<&Message> = messages - 171
.iter() - 172
.filter(|m| !m.text_content().starts_with("<turns>")) - 173
.collect(); - 174
assert!( - 175
standalone - 176
.iter() - 177
.all(|m| !m.text_content().contains("first question")), - 178
"{joined}" - 179
); - 180
assert!(joined.contains("third question") && joined.contains("third answer")); - 181
} - 182
- 183
/// A packet never moves a boundary: the plan-free projection (all-Full) - 184
/// is untouched by it, and the plan that asked for it renders it — before - 185
/// and after a reopen from disk. - 186
#[test] - 187
fn incremental_compaction_writes_a_packet_that_survives_reopen_and_hides_nothing() { - 188
let dir = tempdir().unwrap(); - 189
let path = dir.path().join("s.jsonl"); - 190
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 191
let ids = three_closed_turns(&mut log); - 192
- 193
log.append_incremental_compaction( - 194
&ids[0], - 195
&ids[1], - 196
"small-model", - 197
"summary of turns 1-2".to_string(), - 198
1_234, - 199
) - 200
.unwrap(); - 201
- 202
let joined: String = log - 203
.derive_messages() - 204
.iter() - 205
.map(Message::text_content) - 206
.collect::<Vec<_>>() - 207
.join("\n"); - 208
assert!(!joined.contains("<context_summary>"), "{joined}"); - 209
assert!(joined.contains("first question") && joined.contains("second question")); - 210
assert!(joined.contains("third question") && joined.contains("third answer")); - 211
- 212
let plan = WorkingSetPlan { - 213
per_turn: vec![(ids[2].clone(), Fidelity::Full)], - 214
packet_range: Some((ids[0].clone(), ids[1].clone())), - 215
retrieved: Vec::new(), - 216
budget: 100, - 217
spent: 50, - 218
}; - 219
drop(log); - 220
let reopened = SessionLog::open(path).unwrap(); - 221
let joined_after_reopen: String = reopened - 222
.derive_with_plan(&plan) - 223
.iter() - 224
.map(Message::text_content) - 225
.collect::<Vec<_>>() - 226
.join("\n"); - 227
assert!(joined_after_reopen.contains("summary of turns 1-2")); - 228
assert!(!joined_after_reopen.contains("first question")); - 229
assert!(joined_after_reopen.contains("third question")); - 230
let packet = reopened - 231
.packet_for(&ids[0], &ids[1]) - 232
.expect("packet stored"); - 233
assert_eq!(packet.model, "small-model"); - 234
} - 235
- 236
#[test] - 237
fn packet_needs_compaction_until_an_incremental_entry_covers_it() { - 238
let dir = tempdir().unwrap(); - 239
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 240
let ids = three_closed_turns(&mut log); - 241
- 242
assert!( - 243
log.packet_needs_compaction(&ids[0], &ids[1]), - 244
"nothing compacted yet, so the range still needs it" - 245
); - 246
let (transcript, _) = log.packet_transcript(&ids[0], &ids[1]); - 247
assert!(transcript.contains("#1 asked:") && transcript.contains("#2 asked:")); - 248
assert!( - 249
!transcript.contains("third question"), - 250
"the transcript must stop at last_turn_id, not run to the end" - 251
); - 252
- 253
log.append_incremental_compaction( - 254
&ids[0], - 255
&ids[1], - 256
"m", - 257
"summary of turns 1-2".to_string(), - 258
999, - 259
) - 260
.unwrap(); - 261
assert!( - 262
!log.packet_needs_compaction(&ids[0], &ids[1]), - 263
"a packet now covers exactly this range" - 264
); - 265
assert!( - 266
log.packet_needs_compaction(&ids[0], &ids[2]), - 267
"a different range is a different packet" - 268
); - 269
assert!( - 270
log.packet_needs_compaction(&ids[1], &ids[1]), - 271
"a packet is matched on its whole range, never on a sub-range" - 272
); - 273
} - 274
- 275
#[test] - 276
fn packet_transcript_carries_the_prior_summary_forward() { - 277
let dir = tempdir().unwrap(); - 278
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 279
let ids = three_closed_turns(&mut log); - 280
log.append_incremental_compaction(&ids[0], &ids[0], "m", "FIRST-SUMMARY".to_string(), 100) - 281
.unwrap(); - 282
- 283
assert!(log.packet_needs_compaction(&ids[0], &ids[1])); - 284
let (transcript, _) = log.packet_transcript(&ids[0], &ids[1]); - 285
assert!( - 286
transcript.contains("FIRST-SUMMARY"), - 287
"the prior summary must carry forward: {transcript}" - 288
); - 289
assert!( - 290
!transcript.contains("#1 asked:"), - 291
"seeded turn re-read: {transcript}" - 292
); - 293
assert!(transcript.contains("#2 asked:")); - 294
} - 295
- 296
#[test] - 297
fn a_packet_over_an_unknown_turn_is_rejected() { - 298
let dir = tempdir().unwrap(); - 299
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 300
let ids = three_closed_turns(&mut log); - 301
assert!( - 302
log.append_incremental_compaction(&ids[0], "no-such-turn", "m", "bad".to_string(), 0) - 303
.is_err() - 304
); - 305
} - 306
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.