Skip to main content

oharness_core/
view.rs

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