1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
//! `ConversationView` (§4.8). Read-only view over the conversation post memory-policy
//! mangling — "what the LLM saw."
use crate::message::{Content, Message};
pub struct ConversationView<'a> {
messages: &'a [Message],
}
impl<'a> ConversationView<'a> {
pub fn new(messages: &'a [Message]) -> Self {
Self { messages }
}
pub fn messages(&self) -> &[Message] {
self.messages
}
pub fn last_assistant(&self) -> Option<&Message> {
self.messages
.iter()
.rev()
.find(|m| matches!(m, Message::Assistant { .. }))
}
/// Strips tool-use/tool-result content blocks. Useful for `UserSimulator`
/// implementations that should only see human-visible conversation.
pub fn user_visible(&self) -> Vec<Message> {
self.messages
.iter()
.filter_map(|m| match m {
Message::System { .. } => None,
Message::User { content, meta } => {
let filtered: Vec<Content> = content
.iter()
.filter(|c| {
!matches!(c, Content::ToolUse { .. } | Content::ToolResult { .. })
})
.cloned()
.collect();
if filtered.is_empty() {
None
} else {
Some(Message::User {
content: filtered,
meta: meta.clone(),
})
}
}
Message::Assistant {
content,
stop_reason,
meta,
} => {
let filtered: Vec<Content> = content
.iter()
.filter(|c| {
!matches!(
c,
Content::ToolUse { .. }
| Content::ToolResult { .. }
| Content::Thinking { .. }
)
})
.cloned()
.collect();
if filtered.is_empty() {
None
} else {
Some(Message::Assistant {
content: filtered,
stop_reason: stop_reason.clone(),
meta: meta.clone(),
})
}
}
})
.collect()
}
/// Cheap heuristic — 4 chars per token. Useful only for budget estimates, not
/// for anything the LLM charges for.
pub fn token_estimate(&self) -> u32 {
let chars: usize = self
.messages
.iter()
.map(|m| match m {
Message::System { content, .. } => content.len(),
Message::User { content, .. } | Message::Assistant { content, .. } => content
.iter()
.map(|c| match c {
Content::Text { text } => text.len(),
Content::Thinking { thinking } => thinking.len(),
Content::ToolUse { input, .. } => input.to_string().len(),
Content::ToolResult { output, .. } => output
.content
.iter()
.map(|inner| match inner {
Content::Text { text } => text.len(),
_ => 0,
})
.sum(),
Content::Image(_)
| Content::Document(_)
| Content::Audio(_)
| Content::Citation(_) => 0,
})
.sum(),
})
.sum();
(chars / 4) as u32
}
}