- 1
//! Shared "current turn" boundary used by adapters that must decide which - 2
//! messages are still part of the turn in progress (thinking blocks kept - 3
//! verbatim, since the provider may require replay) versus a closed prior - 4
//! turn (thinking stripped, since no provider needs it across turns). - 5
- 6
use crate::types::{ContentBlock, Message, Role}; - 7
- 8
/// Index of the first message belonging to the current turn: the last user - 9
/// message that carries a `Text` block and no `ToolResult` block. A - 10
/// `ToolResult`-only user message is a mid-turn continuation, not a new - 11
/// directive, so it does not start a new "current turn". When no such - 12
/// message exists, the whole history is treated as current (index 0) — - 13
/// the safe default, since it never strips a signature the provider needs. - 14
pub fn current_turn_boundary(messages: &[Message]) -> usize { - 15
messages - 16
.iter() - 17
.enumerate() - 18
.rev() - 19
.find(|(_, m)| { - 20
m.role == Role::User - 21
&& m.content - 22
.iter() - 23
.any(|b| matches!(b, ContentBlock::Text { .. })) - 24
&& !m - 25
.content - 26
.iter() - 27
.any(|b| matches!(b, ContentBlock::ToolResult { .. })) - 28
}) - 29
.map(|(i, _)| i) - 30
.unwrap_or(0) - 31
} - 32
- 33
/// A copy of `m` with every `Thinking` block removed. - 34
pub fn strip_thinking(m: &Message) -> Message { - 35
Message { - 36
role: m.role, - 37
content: m - 38
.content - 39
.iter() - 40
.filter(|b| !matches!(b, ContentBlock::Thinking { .. })) - 41
.cloned() - 42
.collect(), - 43
} - 44
} - 45
- 46
#[cfg(test)] - 47
mod tests { - 48
use super::*; - 49
use crate::types::ContentBlock; - 50
- 51
#[test] - 52
fn boundary_is_the_last_fresh_directive() { - 53
let messages = vec![ - 54
Message::user_text("first question"), - 55
Message::assistant(vec![ContentBlock::text("answer 1")]), - 56
Message::user_text("second question"), - 57
Message::assistant(vec![ContentBlock::ToolUse { - 58
id: "t1".into(), - 59
name: "search".into(), - 60
input: serde_json::json!({}), - 61
}]), - 62
Message { - 63
role: Role::User, - 64
content: vec![ContentBlock::tool_result("t1", "result")], - 65
}, - 66
]; - 67
assert_eq!(current_turn_boundary(&messages), 2); - 68
} - 69
- 70
#[test] - 71
fn no_fresh_directive_defaults_to_the_whole_history() { - 72
let messages = vec![Message { - 73
role: Role::User, - 74
content: vec![ContentBlock::tool_result("t1", "result")], - 75
}]; - 76
assert_eq!(current_turn_boundary(&messages), 0); - 77
} - 78
- 79
#[test] - 80
fn strip_thinking_removes_only_thinking_blocks() { - 81
let m = Message::assistant(vec![ - 82
ContentBlock::Thinking { - 83
text: "hmm".into(), - 84
signature: Some("sig".into()), - 85
}, - 86
ContentBlock::text("visible"), - 87
]); - 88
let stripped = strip_thinking(&m); - 89
assert_eq!(stripped.content, vec![ContentBlock::text("visible")]); - 90
} - 91
} - 92
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.