- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
use std::collections::VecDeque; - 4
use std::path::Path; - 5
use std::sync::{Arc, Mutex}; - 6
- 7
use tokio_util::sync::CancellationToken; - 8
- 9
use vak_core::Core; - 10
use vak_llm::stream; - 11
use vak_llm::types::{AssistantMessage, ChatRequest, ContentBlock, Usage}; - 12
use vak_llm::{EventStream, LlmError, Provider}; - 13
use vak_session::types::{FrozenContract, MessageRecord, SessionHeader}; - 14
use vak_session::{SessionLog, SessionPath}; - 15
- 16
struct Scripted { - 17
responses: Mutex<VecDeque<AssistantMessage>>, - 18
} - 19
- 20
#[async_trait::async_trait] - 21
impl Provider for Scripted { - 22
fn name(&self) -> &str { - 23
"scripted" - 24
} - 25
- 26
async fn stream( - 27
&self, - 28
_request: ChatRequest, - 29
_cancel: CancellationToken, - 30
) -> Result<EventStream, LlmError> { - 31
let next = self.responses.lock().unwrap().pop_front(); - 32
let (mut sink, rx) = stream::channel(64); - 33
match next { - 34
Some(m) => { - 35
sink.push(stream::StreamEvent::Start { partial: m.clone() }); - 36
sink.close_message(m).await; - 37
} - 38
None => sink.close_error(LlmError::Parse("exhausted".into())).await, - 39
} - 40
Ok(rx) - 41
} - 42
} - 43
- 44
fn text(t: &str) -> AssistantMessage { - 45
AssistantMessage { - 46
content: vec![ContentBlock::text(t)], - 47
stop_reason: vak_llm::types::StopReason::EndTurn, - 48
usage: Usage { - 49
input_tokens: 7, - 50
output_tokens: 3, - 51
..Default::default() - 52
}, - 53
model: "test-model".into(), - 54
response_id: None, - 55
} - 56
} - 57
- 58
fn tool_call(id: &str, name: &str, input: serde_json::Value) -> AssistantMessage { - 59
AssistantMessage { - 60
content: vec![ContentBlock::ToolUse { - 61
id: id.into(), - 62
name: name.into(), - 63
input, - 64
}], - 65
stop_reason: vak_llm::types::StopReason::ToolUse, - 66
usage: Usage::default(), - 67
model: "test-model".into(), - 68
response_id: None, - 69
} - 70
} - 71
- 72
fn header_for(id: &str, cwd: &Path) -> SessionHeader { - 73
SessionHeader { - 74
agent: Some(vak_session::types::AgentIdentity { - 75
id: "vak".into(), - 76
revision: 1, - 77
name: "Vak".into(), - 78
character: "vak".into(), - 79
personality: String::new(), - 80
animation: "subtle".into(), - 81
voice: "default".into(), - 82
behaviour: String::new(), - 83
responsibilities: String::new(), - 84
instructions: String::new(), - 85
}), - 86
session_id: id.to_string(), - 87
created_at: chrono::Utc::now(), - 88
cwd: cwd.to_path_buf(), - 89
parent_session_id: None, - 90
contract_id: None, - 91
work_item_id: None, - 92
conversation: Some(vak_session::types::ConversationContext::local( - 93
format!("conversation:{id}"), - 94
"local", - 95
)), - 96
contract: FrozenContract { - 97
app_version: "test".into(), - 98
provider: "scripted".into(), - 99
model: "m".into(), - 100
route_ladder: Vec::new(), - 101
route_objective: String::new(), - 102
route_annotations: Vec::new(), - 103
system_prompt: String::new(), - 104
permission_mode: "workspace-write".into(), - 105
capabilities: Vec::new(), - 106
prompt_layers: Vec::new(), - 107
}, - 108
} - 109
} - 110
- 111
#[tokio::test(flavor = "multi_thread", worker_threads = 4)] - 112
async fn session_search_tool_is_available_and_logged() { - 113
let dir = tempfile::tempdir().unwrap(); - 114
let home = dir.path().join("home"); - 115
let cwd = dir.path().to_path_buf(); - 116
- 117
// A past session with retrievable knowledge. - 118
let past_path = SessionPath::new_session_file(&home, &cwd, "11111111-past"); - 119
let mut past = SessionLog::create(past_path, header_for("11111111-past", &cwd)).unwrap(); - 120
past.append_message(MessageRecord { - 121
message: vak_llm::Message { - 122
role: vak_llm::Role::User, - 123
content: vec![ContentBlock::text( - 124
"we decided the deploy script must pause before rollback windows", - 125
)], - 126
}, - 127
meta: None, - 128
}) - 129
.unwrap(); - 130
drop(past); // release the ledger lock - 131
- 132
// Live run: the scripted model reaches for memory, then answers. - 133
vak_config::paths::isolate_home_for_tests(); - 134
let core = Core::new(cwd.clone()).unwrap(); - 135
core.set_sessions_home(home.clone()); - 136
core.set_permission_mode(vak_config::PermissionMode::FullAccess); - 137
core.set_provider_instance(Arc::new(Scripted { - 138
responses: Mutex::new(VecDeque::from(vec![ - 139
tool_call( - 140
"t1", - 141
"session_search", - 142
serde_json::json!({"query": "deploy script"}), - 143
), - 144
text("Found it: the deploy script pauses before rollbacks."), - 145
])), - 146
})); - 147
std::mem::forget(dir); - 148
- 149
// Direct probe of the search function on identical inputs. - 150
let probe = vak_session::search(&home, &cwd, "deploy script", 8, &Default::default()).unwrap(); - 151
eprintln!("PROBE hits={}", probe.len()); - 152
for h in &probe { - 153
eprintln!( - 154
" -> {} score={} snippet={}", - 155
h.session_id, h.score, h.snippet - 156
); - 157
} - 158
- 159
let live = core.start_session().await.unwrap(); - 160
let live_id = live.header().unwrap().session_id.clone(); - 161
let (events_tx, _events_rx) = tokio::sync::mpsc::channel(256); - 162
let result = core - 163
.run_turn_with( - 164
live, - 165
"what did we decide about deploys?", - 166
CancellationToken::new(), - 167
None, - 168
None, - 169
None, - 170
events_tx, - 171
) - 172
.await - 173
.unwrap(); - 174
assert!(matches!(result.0, vak_agent::TurnOutcome::Completed { .. })); - 175
let log = result.1; - 176
- 177
// Invariant 1: the search output must be reconstructable from the - 178
// ledger — it lives in the ToolResult block's content. - 179
let chain = log.message_chain(); - 180
let mut saw_search_output = false; - 181
for (_, m) in &chain { - 182
for b in &m.content { - 183
if let ContentBlock::ToolResult { - 184
content, is_error, .. - 185
} = b - 186
{ - 187
eprintln!("TOOL_RESULT[{is_error}]: {content}"); - 188
assert!(!is_error, "search must not error: {content}"); - 189
if content.contains("hit(s)") { - 190
saw_search_output = true; - 191
assert!( - 192
content.contains("11111111-past"), - 193
"snippet cites the source session: {content}" - 194
); - 195
assert!( - 196
!content.contains(&live_id), - 197
"current session must be excluded from results" - 198
); - 199
} - 200
} - 201
} - 202
} - 203
assert!( - 204
saw_search_output, - 205
"tool result with hits must be logged on the chain" - 206
); - 207
- 208
// And the model's answer reflects what memory returned. - 209
let answered = chain - 210
.iter() - 211
.any(|(_, m)| m.text_content().contains("pauses before rollbacks")); - 212
assert!(answered, "final answer on the chain"); - 213
- 214
// The tool appears in the frozen contract. - 215
let capabilities = &log.header().unwrap().contract.capabilities; - 216
assert!( - 217
capabilities.iter().any(|capability| { - 218
capability.kind == vak_session::CapabilityKind::Tool - 219
&& capability.name == "session_search" - 220
}), - 221
"session_search in frozen contract: {capabilities:?}" - 222
); - 223
} - 224
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.