- 1
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 2
- 3
//! A slow listener never paces the model: stream pieces it has no room for - 4
//! are dropped (each carries the message so far), so a long reasoning reply - 5
//! finishes at the provider's speed, not the listener's. - 6
//! - 7
//! Found live: the eval runner handed the loop an event channel nobody read, - 8
//! and a long reasoning reply waited on it until the step watchdog although - 9
//! the provider had finished. - 10
- 11
use std::sync::Arc; - 12
use std::time::Duration; - 13
- 14
use async_trait::async_trait; - 15
use tokio_util::sync::CancellationToken; - 16
- 17
use tempfile::tempdir; - 18
- 19
use vak_agent::{Agent, AgentConfig, TurnOutcome}; - 20
use vak_llm::stream; - 21
use vak_llm::types::{AssistantMessage, ChatRequest, ContentBlock, StopReason, Usage}; - 22
use vak_llm::{EventStream, LlmError, Provider}; - 23
use vak_session::types::{FrozenContract, SessionHeader}; - 24
use vak_session::{SessionLog, SessionPath}; - 25
- 26
/// Streams two thousand reasoning pieces, then the answer. - 27
struct LongReasoning; - 28
- 29
#[async_trait] - 30
impl Provider for LongReasoning { - 31
fn name(&self) -> &str { - 32
"long-reasoning" - 33
} - 34
- 35
async fn stream( - 36
&self, - 37
_request: ChatRequest, - 38
_cancel: CancellationToken, - 39
) -> Result<EventStream, LlmError> { - 40
let (mut sink, rx) = stream::channel(4096); - 41
tokio::spawn(async move { - 42
let mut partial = AssistantMessage::empty("test-model"); - 43
for n in 0..2000 { - 44
let delta = format!("step {n} "); - 45
partial.content = vec![ContentBlock::Thinking { - 46
text: format!( - 47
"{}{delta}", - 48
match partial.content.first() { - 49
Some(ContentBlock::Thinking { text, .. }) => text.clone(), - 50
_ => String::new(), - 51
} - 52
), - 53
signature: None, - 54
}]; - 55
sink.push(stream::StreamEvent::ThinkingDelta { - 56
delta, - 57
partial: partial.clone(), - 58
}); - 59
} - 60
sink.close_message(AssistantMessage { - 61
content: vec![ContentBlock::text("done")], - 62
stop_reason: StopReason::EndTurn, - 63
usage: Usage { - 64
input_tokens: 1, - 65
output_tokens: 1, - 66
..Default::default() - 67
}, - 68
model: "test-model".into(), - 69
response_id: None, - 70
}) - 71
.await; - 72
}); - 73
Ok(rx) - 74
} - 75
} - 76
- 77
#[tokio::test] - 78
async fn a_slow_listener_does_not_pace_a_long_reply() { - 79
let dir = tempdir().unwrap(); - 80
let home = dir.path().join("home"); - 81
std::fs::create_dir_all(&home).unwrap(); - 82
let header = SessionHeader { - 83
agent: None, - 84
session_id: "stalled".into(), - 85
created_at: chrono::Utc::now(), - 86
cwd: dir.path().to_path_buf(), - 87
parent_session_id: None, - 88
contract_id: None, - 89
work_item_id: None, - 90
conversation: None, - 91
contract: FrozenContract { - 92
app_version: "0".into(), - 93
provider: "long-reasoning".into(), - 94
model: "test-model".into(), - 95
route_ladder: Vec::new(), - 96
route_objective: String::new(), - 97
route_annotations: Vec::new(), - 98
system_prompt: "sys".into(), - 99
permission_mode: "read-only".into(), - 100
capabilities: Vec::new(), - 101
prompt_layers: Vec::new(), - 102
}, - 103
}; - 104
let log = SessionLog::create( - 105
SessionPath::new_session_file(&home, dir.path(), "stalled"), - 106
header, - 107
) - 108
.unwrap(); - 109
let mut cfg = AgentConfig::new("sys"); - 110
cfg.model = "test-model".into(); - 111
let mut agent = Agent::new(Arc::new(LongReasoning), log, cfg); - 112
- 113
// 20ms per event: pacing the two thousand pieces would take 40s. - 114
let (events, mut rx) = tokio::sync::mpsc::channel(64); - 115
tokio::spawn(async move { - 116
while rx.recv().await.is_some() { - 117
tokio::time::sleep(Duration::from_millis(20)).await; - 118
} - 119
}); - 120
let outcome = tokio::time::timeout( - 121
Duration::from_secs(10), - 122
agent.run( - 123
"think it through", - 124
&Default::default(), - 125
CancellationToken::new(), - 126
events, - 127
), - 128
) - 129
.await - 130
.expect("the reply is not paced by the listener"); - 131
match outcome { - 132
TurnOutcome::Completed { response } => assert_eq!(response.text_content(), "done"), - 133
other => panic!("expected completed, got {other:?}"), - 134
} - 135
} - 136
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.