- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! Presentations are ledger entries (docs/design/68-context-engine.md §10): - 4
//! a validated card is written once, hash-linked like every other entry, - 5
//! never model-visible raw, and is the single source both the display - 6
//! channel and a later turn's history read. - 7
- 8
use std::path::PathBuf; - 9
- 10
use tempfile::tempdir; - 11
- 12
use vak_llm::{ContentBlock, Message, Role}; - 13
use vak_session::SessionLog; - 14
use vak_session::types::{ - 15
FrozenContract, MessageRecord, PresentationRecord, PresentationSource, SessionHeader, - 16
canonicalize_json, payload_digest, - 17
}; - 18
- 19
fn header() -> SessionHeader { - 20
SessionHeader { - 21
agent: None, - 22
session_id: "s-test".into(), - 23
created_at: chrono::Utc::now(), - 24
cwd: PathBuf::from("/tmp/proj"), - 25
parent_session_id: None, - 26
contract_id: None, - 27
work_item_id: None, - 28
conversation: None, - 29
contract: FrozenContract { - 30
app_version: "0.1.0".into(), - 31
provider: "anthropic".into(), - 32
model: "claude-sonnet-4-5".into(), - 33
route_ladder: Vec::new(), - 34
route_objective: String::new(), - 35
route_annotations: Vec::new(), - 36
system_prompt: "system prompt v1".into(), - 37
permission_mode: "workspace-write".into(), - 38
capabilities: Vec::new(), - 39
prompt_layers: Vec::new(), - 40
}, - 41
} - 42
} - 43
- 44
fn chart_payload(summary: &str) -> serde_json::Value { - 45
serde_json::json!({ - 46
"chart_type": "line", - 47
"series": [{"name": "s1", "points": [{"x": 1, "y": 2.0}]}], - 48
"accessible_summary": summary, - 49
}) - 50
} - 51
- 52
fn record_for(turn_id: &str, tool_use_id: &str, summary: &str) -> PresentationRecord { - 53
let payload = canonicalize_json(&chart_payload(summary)); - 54
PresentationRecord { - 55
turn_id: turn_id.into(), - 56
source: PresentationSource::ToolCall { - 57
tool_use_id: tool_use_id.into(), - 58
}, - 59
semantic_type: "chart".into(), - 60
skill_id: "core".into(), - 61
skill_version: "1.0.0".into(), - 62
schema_version: 1, - 63
payload_digest: payload_digest(&payload), - 64
payload, - 65
derived_from: Vec::new(), - 66
title: "Chart".into(), - 67
identity_digest: format!("summary: {summary}"), - 68
} - 69
} - 70
- 71
#[test] - 72
fn a_presentation_entry_round_trips_through_jsonl() { - 73
let dir = tempdir().unwrap(); - 74
let path = dir.path().join("presentation.jsonl"); - 75
let mut log = SessionLog::create(path.clone(), header()).unwrap(); - 76
let user = log - 77
.append_message(MessageRecord { - 78
message: Message::user_text("show me a chart"), - 79
meta: None, - 80
}) - 81
.unwrap(); - 82
let record = record_for(&user.id, "call-1", "rising"); - 83
let appended = log.append_presentation(record.clone()).unwrap(); - 84
drop(log); - 85
- 86
// Reopen from disk: the entry must parse back byte-identical in the - 87
// fields that matter, proving the JSONL round-trip. - 88
let reopened = SessionLog::open_read_only(path).unwrap(); - 89
let (id, reloaded) = reopened - 90
.presentations() - 91
.into_iter() - 92
.next() - 93
.expect("presentation entry must survive a reopen"); - 94
assert_eq!(id, appended.id); - 95
assert_eq!(reloaded.turn_id, record.turn_id); - 96
assert_eq!(reloaded.semantic_type, "chart"); - 97
assert_eq!(reloaded.payload_digest, record.payload_digest); - 98
assert_eq!(reloaded.identity_digest, "summary: rising"); - 99
match &reloaded.source { - 100
PresentationSource::ToolCall { tool_use_id } => assert_eq!(tool_use_id, "call-1"), - 101
other => panic!("expected ToolCall source, got {other:?}"), - 102
} - 103
} - 104
- 105
#[test] - 106
fn presentations_are_returned_in_chain_order() { - 107
let dir = tempdir().unwrap(); - 108
let path = dir.path().join("order.jsonl"); - 109
let mut log = SessionLog::create(path, header()).unwrap(); - 110
let user = log - 111
.append_message(MessageRecord { - 112
message: Message::user_text("go"), - 113
meta: None, - 114
}) - 115
.unwrap(); - 116
log.append_presentation(record_for(&user.id, "call-1", "first")) - 117
.unwrap(); - 118
log.append_presentation(record_for(&user.id, "call-2", "second")) - 119
.unwrap(); - 120
log.append_presentation(record_for(&user.id, "call-3", "third")) - 121
.unwrap(); - 122
- 123
let ids: Vec<String> = log - 124
.presentations() - 125
.into_iter() - 126
.map(|(_, record)| match &record.source { - 127
PresentationSource::ToolCall { tool_use_id } => tool_use_id.clone(), - 128
other => panic!("expected ToolCall source, got {other:?}"), - 129
}) - 130
.collect(); - 131
assert_eq!(ids, vec!["call-1", "call-2", "call-3"]); - 132
} - 133
- 134
#[test] - 135
fn has_presentation_dedupes_by_turn_and_payload_digest() { - 136
let dir = tempdir().unwrap(); - 137
let path = dir.path().join("dedupe.jsonl"); - 138
let mut log = SessionLog::create(path, header()).unwrap(); - 139
let user = log - 140
.append_message(MessageRecord { - 141
message: Message::user_text("go"), - 142
meta: None, - 143
}) - 144
.unwrap(); - 145
let record = record_for(&user.id, "call-1", "same"); - 146
let digest = record.payload_digest.clone(); - 147
assert!(!log.has_presentation(&user.id, &digest)); - 148
log.append_presentation(record).unwrap(); - 149
assert!(log.has_presentation(&user.id, &digest)); - 150
// A different turn with the same digest is not a duplicate. - 151
assert!(!log.has_presentation("some-other-turn", &digest)); - 152
// A different digest in the same turn is not a duplicate either. - 153
assert!(!log.has_presentation(&user.id, "not-the-real-digest")); - 154
} - 155
- 156
#[test] - 157
fn derive_messages_skips_presentation_entries_like_receipts() { - 158
let dir = tempdir().unwrap(); - 159
let with_path = dir.path().join("with.jsonl"); - 160
let without_path = dir.path().join("without.jsonl"); - 161
- 162
let mut with_log = SessionLog::create(with_path, header()).unwrap(); - 163
let mut without_log = SessionLog::create(without_path, header()).unwrap(); - 164
- 165
for log in [&mut with_log, &mut without_log] { - 166
log.append_message(MessageRecord { - 167
message: Message::user_text("show me a chart"), - 168
meta: None, - 169
}) - 170
.unwrap(); - 171
log.append_message(MessageRecord { - 172
message: Message::assistant(vec![ContentBlock::ToolUse { - 173
id: "call-1".into(), - 174
name: "emit_chart_card".into(), - 175
input: serde_json::json!({"semantic_type": "chart", "payload": chart_payload("flat")}), - 176
}]), - 177
meta: None, - 178
}) - 179
.unwrap(); - 180
log.append_message(MessageRecord { - 181
message: Message { - 182
role: Role::User, - 183
content: vec![ContentBlock::tool_result( - 184
"call-1", - 185
"Card displayed to the user.", - 186
)], - 187
}, - 188
meta: None, - 189
}) - 190
.unwrap(); - 191
} - 192
- 193
let user_entry = with_log.latest_directive_entry_id().unwrap(); - 194
with_log - 195
.append_presentation(record_for(&user_entry, "call-1", "flat")) - 196
.unwrap(); - 197
- 198
assert_eq!( - 199
with_log.derive_messages().len(), - 200
without_log.derive_messages().len(), - 201
"a Presentation entry must never change the model-visible projection" - 202
); - 203
// And it genuinely never appears verbatim anywhere in that projection. - 204
let serialized: String = with_log - 205
.derive_messages() - 206
.iter() - 207
.map(|m| m.text_content()) - 208
.collect(); - 209
assert!(!serialized.contains("identity_digest")); - 210
} - 211
- 212
#[test] - 213
fn latest_directive_entry_id_ignores_control_messages() { - 214
let dir = tempdir().unwrap(); - 215
let path = dir.path().join("directive.jsonl"); - 216
let mut log = SessionLog::create(path, header()).unwrap(); - 217
let first = log - 218
.append_message(MessageRecord { - 219
message: Message::user_text("first"), - 220
meta: None, - 221
}) - 222
.unwrap(); - 223
assert_eq!(log.latest_directive_entry_id(), Some(first.id.clone())); - 224
- 225
log.append_message(MessageRecord::control( - 226
vak_intent::control::ControlKind::GroundingCheck, - 227
"redo", - 228
)) - 229
.unwrap(); - 230
// A runtime-authored nudge must never be mistaken for the directive. - 231
assert_eq!(log.latest_directive_entry_id(), Some(first.id)); - 232
- 233
let second = log - 234
.append_message(MessageRecord { - 235
message: Message::user_text("second"), - 236
meta: None, - 237
}) - 238
.unwrap(); - 239
assert_eq!(log.latest_directive_entry_id(), Some(second.id.clone())); - 240
- 241
// A mid-turn tool-result message is `User` role and non-control, but it - 242
// is not a directive: it carries no `Text` block and it carries a - 243
// `ToolResult` block. Mistaking it for the directive breaks any - 244
// `turn_id` computed from it after the tool call — the fence-path - 245
// presentation write does exactly this - 246
// (docs/design/68-context-engine.md §10). - 247
log.append_message(MessageRecord { - 248
message: Message { - 249
role: vak_llm::Role::User, - 250
content: vec![vak_llm::ContentBlock::tool_result("call-1", "result")], - 251
}, - 252
meta: None, - 253
}) - 254
.unwrap(); - 255
assert_eq!(log.latest_directive_entry_id(), Some(second.id)); - 256
} - 257
- 258
#[test] - 259
fn non_card_evidence_since_collects_prior_non_card_tool_results_only() { - 260
let dir = tempdir().unwrap(); - 261
let path = dir.path().join("evidence.jsonl"); - 262
let mut log = SessionLog::create(path, header()).unwrap(); - 263
let user = log - 264
.append_message(MessageRecord { - 265
message: Message::user_text("what's the weather, with a chart"), - 266
meta: None, - 267
}) - 268
.unwrap(); - 269
// Evidence-gathering call. - 270
log.append_message(MessageRecord { - 271
message: Message::assistant(vec![ContentBlock::ToolUse { - 272
id: "search-1".into(), - 273
name: "web_search".into(), - 274
input: serde_json::json!({"query": "weather"}), - 275
}]), - 276
meta: None, - 277
}) - 278
.unwrap(); - 279
log.append_message(MessageRecord { - 280
message: Message { - 281
role: Role::User, - 282
content: vec![ContentBlock::tool_result("search-1", "72F and sunny")], - 283
}, - 284
meta: None, - 285
}) - 286
.unwrap(); - 287
// Card call (not evidence). - 288
log.append_message(MessageRecord { - 289
message: Message::assistant(vec![ContentBlock::ToolUse { - 290
id: "call-1".into(), - 291
name: "emit_chart_card".into(), - 292
input: serde_json::json!({"semantic_type": "chart", "payload": chart_payload("flat")}), - 293
}]), - 294
meta: None, - 295
}) - 296
.unwrap(); - 297
log.append_message(MessageRecord { - 298
message: Message { - 299
role: Role::User, - 300
content: vec![ContentBlock::tool_result( - 301
"call-1", - 302
"Card displayed to the user.", - 303
)], - 304
}, - 305
meta: None, - 306
}) - 307
.unwrap(); - 308
- 309
let is_card_tool = |name: &str| name == "emit_chart_card"; - 310
let evidence = log.non_card_evidence_since(&user.id, is_card_tool); - 311
assert_eq!(evidence, vec!["search-1".to_string()]); - 312
} - 313
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.