1use crate::models::MessagePhase;
2use crate::models::WebSearchAction;
3use crate::protocol::AgentMessageEvent;
4use crate::protocol::AgentReasoningEvent;
5use crate::protocol::AgentReasoningRawContentEvent;
6use crate::protocol::ContextCompactedEvent;
7use crate::protocol::EventMsg;
8use crate::protocol::UserMessageEvent;
9use crate::protocol::WebSearchEndEvent;
10use crate::user_input::ByteRange;
11use crate::user_input::TextElement;
12use crate::user_input::UserInput;
13use schemars::JsonSchema;
14use serde::Deserialize;
15use serde::Serialize;
16use ts_rs::TS;
17
18#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
19#[serde(tag = "type")]
20#[ts(tag = "type")]
21pub enum TurnItem {
22 UserMessage(UserMessageItem),
23 AgentMessage(AgentMessageItem),
24 Plan(PlanItem),
25 Reasoning(ReasoningItem),
26 WebSearch(WebSearchItem),
27 ContextCompaction(ContextCompactionItem),
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
31pub struct UserMessageItem {
32 pub id: String,
33 pub content: Vec<UserInput>,
34}
35
36#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
37#[serde(tag = "type")]
38#[ts(tag = "type")]
39pub enum AgentMessageContent {
40 Text { text: String },
41}
42
43#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
44pub struct AgentMessageItem {
50 pub id: String,
51 pub content: Vec<AgentMessageContent>,
52 #[serde(default, skip_serializing_if = "Option::is_none")]
57 #[ts(optional)]
58 pub phase: Option<MessagePhase>,
59}
60
61#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
62pub struct PlanItem {
63 pub id: String,
64 pub text: String,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
68pub struct ReasoningItem {
69 pub id: String,
70 pub summary_text: Vec<String>,
71 #[serde(default)]
72 pub raw_content: Vec<String>,
73}
74
75#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema, PartialEq)]
76pub struct WebSearchItem {
77 pub id: String,
78 pub query: String,
79 pub action: WebSearchAction,
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize, TS, JsonSchema)]
83pub struct ContextCompactionItem {
84 pub id: String,
85}
86
87impl ContextCompactionItem {
88 pub fn new() -> Self {
89 Self {
90 id: uuid::Uuid::new_v4().to_string(),
91 }
92 }
93
94 pub fn as_legacy_event(&self) -> EventMsg {
95 EventMsg::ContextCompacted(ContextCompactedEvent {})
96 }
97}
98
99impl Default for ContextCompactionItem {
100 fn default() -> Self {
101 Self::new()
102 }
103}
104
105impl UserMessageItem {
106 pub fn new(content: &[UserInput]) -> Self {
107 Self {
108 id: uuid::Uuid::new_v4().to_string(),
109 content: content.to_vec(),
110 }
111 }
112
113 pub fn as_legacy_event(&self) -> EventMsg {
114 EventMsg::UserMessage(UserMessageEvent {
117 message: self.message(),
118 images: Some(self.image_urls()),
119 local_images: self.local_image_paths(),
120 text_elements: self.text_elements(),
121 })
122 }
123
124 pub fn message(&self) -> String {
125 self.content
126 .iter()
127 .map(|c| match c {
128 UserInput::Text { text, .. } => text.clone(),
129 _ => String::new(),
130 })
131 .collect::<Vec<String>>()
132 .join("")
133 }
134
135 pub fn text_elements(&self) -> Vec<TextElement> {
136 let mut out = Vec::new();
137 let mut offset = 0usize;
138 for input in &self.content {
139 if let UserInput::Text {
140 text,
141 text_elements,
142 } = input
143 {
144 for elem in text_elements {
147 let byte_range = ByteRange {
148 start: offset + elem.byte_range.start,
149 end: offset + elem.byte_range.end,
150 };
151 out.push(TextElement::new(
152 byte_range,
153 elem.placeholder(text).map(str::to_string),
154 ));
155 }
156 offset += text.len();
157 }
158 }
159 out
160 }
161
162 pub fn image_urls(&self) -> Vec<String> {
163 self.content
164 .iter()
165 .filter_map(|c| match c {
166 UserInput::Image { image_url } => Some(image_url.clone()),
167 _ => None,
168 })
169 .collect()
170 }
171
172 pub fn local_image_paths(&self) -> Vec<std::path::PathBuf> {
173 self.content
174 .iter()
175 .filter_map(|c| match c {
176 UserInput::LocalImage { path } => Some(path.clone()),
177 _ => None,
178 })
179 .collect()
180 }
181}
182
183impl AgentMessageItem {
184 pub fn new(content: &[AgentMessageContent]) -> Self {
185 Self {
186 id: uuid::Uuid::new_v4().to_string(),
187 content: content.to_vec(),
188 phase: None,
189 }
190 }
191
192 pub fn as_legacy_events(&self) -> Vec<EventMsg> {
193 self.content
196 .iter()
197 .map(|c| match c {
198 AgentMessageContent::Text { text } => EventMsg::AgentMessage(AgentMessageEvent {
199 message: text.clone(),
200 }),
201 })
202 .collect()
203 }
204}
205
206impl ReasoningItem {
207 pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
208 let mut events = Vec::new();
209 for summary in &self.summary_text {
210 events.push(EventMsg::AgentReasoning(AgentReasoningEvent {
211 text: summary.clone(),
212 }));
213 }
214
215 if show_raw_agent_reasoning {
216 for entry in &self.raw_content {
217 events.push(EventMsg::AgentReasoningRawContent(
218 AgentReasoningRawContentEvent {
219 text: entry.clone(),
220 },
221 ));
222 }
223 }
224
225 events
226 }
227}
228
229impl WebSearchItem {
230 pub fn as_legacy_event(&self) -> EventMsg {
231 EventMsg::WebSearchEnd(WebSearchEndEvent {
232 call_id: self.id.clone(),
233 query: self.query.clone(),
234 action: self.action.clone(),
235 })
236 }
237}
238
239impl TurnItem {
240 pub fn id(&self) -> String {
241 match self {
242 TurnItem::UserMessage(item) => item.id.clone(),
243 TurnItem::AgentMessage(item) => item.id.clone(),
244 TurnItem::Plan(item) => item.id.clone(),
245 TurnItem::Reasoning(item) => item.id.clone(),
246 TurnItem::WebSearch(item) => item.id.clone(),
247 TurnItem::ContextCompaction(item) => item.id.clone(),
248 }
249 }
250
251 pub fn as_legacy_events(&self, show_raw_agent_reasoning: bool) -> Vec<EventMsg> {
252 match self {
253 TurnItem::UserMessage(item) => vec![item.as_legacy_event()],
254 TurnItem::AgentMessage(item) => item.as_legacy_events(),
255 TurnItem::Plan(_) => Vec::new(),
256 TurnItem::WebSearch(item) => vec![item.as_legacy_event()],
257 TurnItem::Reasoning(item) => item.as_legacy_events(show_raw_agent_reasoning),
258 TurnItem::ContextCompaction(item) => vec![item.as_legacy_event()],
259 }
260 }
261}
262