- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! Directive drift (docs/design/68-context-engine.md §7): when the thread - 4
//! lists an earlier directive whose reading shares no domain with the - 5
//! current one, it is marked "(earlier, now paused)" — a re-weighting, not - 6
//! a cut. The directive itself still appears; nothing is removed. - 7
- 8
use std::path::PathBuf; - 9
- 10
use tempfile::tempdir; - 11
- 12
use vak_llm::{ContentBlock, Message}; - 13
use vak_session::types::{ - 14
FrozenContract, IntentRecord, MessageRecord, SessionHeader, TurnCardRecord, - 15
}; - 16
use vak_session::{SessionLog, TurnIndex, WorkingSetPlan}; - 17
- 18
/// A plan that packets exactly `turn_id` (and nothing else), with the - 19
/// packet already written — the state in which the thread has an earlier - 20
/// directive to list. - 21
fn packet_only(log: &mut SessionLog, turn_id: &str, summary: &str) -> WorkingSetPlan { - 22
log.append_packet(turn_id, turn_id, "fixture-model", summary.into(), 999) - 23
.unwrap(); - 24
WorkingSetPlan { - 25
packet_range: Some((turn_id.to_string(), turn_id.to_string())), - 26
..WorkingSetPlan::default() - 27
} - 28
} - 29
- 30
fn header() -> SessionHeader { - 31
SessionHeader { - 32
agent: None, - 33
session_id: "s-drift".into(), - 34
created_at: chrono::Utc::now(), - 35
cwd: PathBuf::from("/tmp/proj"), - 36
parent_session_id: None, - 37
contract_id: None, - 38
work_item_id: None, - 39
conversation: None, - 40
contract: FrozenContract { - 41
app_version: "0.1.0".into(), - 42
provider: "anthropic".into(), - 43
model: "claude-sonnet-4-5".into(), - 44
route_ladder: Vec::new(), - 45
route_objective: String::new(), - 46
route_annotations: Vec::new(), - 47
system_prompt: "system prompt v1".into(), - 48
permission_mode: "workspace-write".into(), - 49
capabilities: Vec::new(), - 50
prompt_layers: Vec::new(), - 51
}, - 52
} - 53
} - 54
- 55
fn reading_with_domains(domains: &[&str]) -> vak_intent::Reading { - 56
vak_intent::Reading { - 57
domains: domains.iter().map(|d| d.to_string()).collect(), - 58
..vak_intent::Reading::general() - 59
} - 60
} - 61
- 62
fn user_msg(text: &str) -> MessageRecord { - 63
MessageRecord { - 64
message: Message::user_text(text), - 65
meta: None, - 66
} - 67
} - 68
- 69
fn assistant_msg(text: &str) -> MessageRecord { - 70
MessageRecord { - 71
message: Message::assistant(vec![ContentBlock::text(text)]), - 72
meta: None, - 73
} - 74
} - 75
- 76
fn intent_record(reading: vak_intent::Reading) -> IntentRecord { - 77
IntentRecord { - 78
reading, - 79
engagement: vak_intent::Engagement::general(), - 80
provenance: vak_intent::Provenance::new(vak_intent::Tier::General, 1, Vec::new()), - 81
outcome: None, - 82
model_visible: None, - 83
commitment_id: None, - 84
strands: Vec::new(), - 85
strand_commitments: Default::default(), - 86
} - 87
} - 88
- 89
#[test] - 90
fn a_domain_disjoint_directive_is_marked_paused_not_removed() { - 91
let dir = tempdir().unwrap(); - 92
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 93
- 94
// Turn 1: finance topic, closed with a card carrying that reading. - 95
log.append_goal_update(vak_intent::GoalUpdate { - 96
revision: 1, - 97
relation: vak_intent::GoalRelation::New, - 98
request: "look up the sensex".into(), - 99
supersedes_revision: None, - 100
explicit: false, - 101
}) - 102
.unwrap(); - 103
let t1 = log - 104
.append_message(user_msg("look up the sensex")) - 105
.unwrap() - 106
.id; - 107
log.append_intent(intent_record(reading_with_domains(&["finance"]))) - 108
.unwrap(); - 109
log.append_message(assistant_msg("sensex is up")).unwrap(); - 110
let card1 = TurnIndex::from_log(&log) - 111
.turn_by_id(&t1) - 112
.unwrap() - 113
.build_card("completed", "sensex is up".to_string(), &|s| { - 114
s.len() as u64 / 4 - 115
}); - 116
log.append_turn_card(TurnCardRecord { - 117
turn_id: t1.clone(), - 118
card: card1, - 119
}) - 120
.unwrap(); - 121
- 122
// Turn 2: unrelated cooking topic — the current directive. - 123
log.append_goal_update(vak_intent::GoalUpdate { - 124
revision: 2, - 125
relation: vak_intent::GoalRelation::AddsTo, - 126
request: "how do I poach an egg".into(), - 127
supersedes_revision: None, - 128
explicit: false, - 129
}) - 130
.unwrap(); - 131
log.append_message(user_msg("how do I poach an egg")) - 132
.unwrap(); - 133
log.append_intent(intent_record(reading_with_domains(&["cooking"]))) - 134
.unwrap(); - 135
- 136
// Packet turn 1 away under the plan so the thread has something to - 137
// list. - 138
let plan = packet_only(&mut log, &t1, "summary of turn 1"); - 139
- 140
let thread = log - 141
.tail_sections(Some(&plan)) - 142
.thread - 143
.expect("thread must list the dropped finance directive"); - 144
assert!(thread.contains("look up the sensex"), "{thread}"); - 145
assert!( - 146
thread.contains("look up the sensex (earlier, now paused)"), - 147
"a domain-disjoint earlier directive must be marked paused: {thread}" - 148
); - 149
} - 150
- 151
#[test] - 152
fn a_domain_overlapping_directive_is_never_marked_paused() { - 153
let dir = tempdir().unwrap(); - 154
let mut log = SessionLog::create(dir.path().join("s.jsonl"), header()).unwrap(); - 155
- 156
log.append_goal_update(vak_intent::GoalUpdate { - 157
revision: 1, - 158
relation: vak_intent::GoalRelation::New, - 159
request: "look up the sensex".into(), - 160
supersedes_revision: None, - 161
explicit: false, - 162
}) - 163
.unwrap(); - 164
let t1 = log - 165
.append_message(user_msg("look up the sensex")) - 166
.unwrap() - 167
.id; - 168
log.append_intent(intent_record(reading_with_domains(&["finance"]))) - 169
.unwrap(); - 170
log.append_message(assistant_msg("sensex is up")).unwrap(); - 171
let card1 = TurnIndex::from_log(&log) - 172
.turn_by_id(&t1) - 173
.unwrap() - 174
.build_card("completed", "sensex is up".to_string(), &|s| { - 175
s.len() as u64 / 4 - 176
}); - 177
log.append_turn_card(TurnCardRecord { - 178
turn_id: t1.clone(), - 179
card: card1, - 180
}) - 181
.unwrap(); - 182
- 183
// Turn 2: same domain (finance) — still active, never paused. - 184
log.append_goal_update(vak_intent::GoalUpdate { - 185
revision: 2, - 186
relation: vak_intent::GoalRelation::AddsTo, - 187
request: "and the nifty too".into(), - 188
supersedes_revision: None, - 189
explicit: false, - 190
}) - 191
.unwrap(); - 192
log.append_message(user_msg("and the nifty too")).unwrap(); - 193
log.append_intent(intent_record(reading_with_domains(&["finance"]))) - 194
.unwrap(); - 195
- 196
let plan = packet_only(&mut log, &t1, "summary of turn 1"); - 197
- 198
let thread = log - 199
.tail_sections(Some(&plan)) - 200
.thread - 201
.expect("thread present"); - 202
assert!( - 203
!thread.contains("(earlier, now paused)"), - 204
"same-domain directives must not be marked paused: {thread}" - 205
); - 206
} - 207
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.