1pub use yoagent::types::{AgentMessage, Content, Message};
6
7fn content_text(content: &[Content]) -> String {
11 content
12 .iter()
13 .filter_map(|c| {
14 if let Content::Text { text } = c {
15 Some(text.as_str())
16 } else {
17 None
18 }
19 })
20 .collect::<Vec<_>>()
21 .join("")
22}
23
24pub fn content_tool_calls(content: &[Content]) -> Vec<(String, String, serde_json::Value)> {
26 content
27 .iter()
28 .filter_map(|c| {
29 if let Content::ToolCall {
30 id,
31 name,
32 arguments,
33 ..
34 } = c
35 {
36 Some((id.clone(), name.clone(), arguments.clone()))
37 } else {
38 None
39 }
40 })
41 .collect()
42}
43
44pub fn message_text(msg: &AgentMessage) -> String {
46 match msg {
47 AgentMessage::Llm(m) => match m {
48 Message::User { content, .. }
49 | Message::Assistant { content, .. }
50 | Message::ToolResult { content, .. } => content_text(content),
51 },
52 AgentMessage::Extension(ext) => ext.data.to_string(),
53 }
54}
55
56pub fn message_dedup_key(msg: &AgentMessage) -> String {
60 match msg {
61 AgentMessage::Llm(m) => match m {
62 Message::User { content, .. } => {
63 format!("user:{}", content_text(content))
64 }
65 Message::Assistant {
66 content,
67 stop_reason,
68 ..
69 } => {
70 let tc_ids: Vec<&str> = content
74 .iter()
75 .filter_map(|c| {
76 if let Content::ToolCall { id, .. } = c {
77 Some(id.as_str())
78 } else {
79 None
80 }
81 })
82 .collect();
83 format!(
84 "assistant:{}:{:?}:{:?}",
85 content_text(content),
86 tc_ids,
87 stop_reason
88 )
89 }
90 Message::ToolResult {
91 tool_call_id,
92 content,
93 ..
94 } => {
95 format!("tool:{}:{}", tool_call_id, content_text(content))
96 }
97 },
98 AgentMessage::Extension(ext) => {
99 format!("ext:{}:{}", ext.kind, ext.data)
100 }
101 }
102}
103
104pub fn message_is_error(msg: &AgentMessage) -> bool {
106 matches!(
107 msg,
108 AgentMessage::Llm(Message::ToolResult { is_error: true, .. })
109 )
110}
111
112pub fn message_tool_call_id(msg: &AgentMessage) -> Option<&str> {
114 match msg {
115 AgentMessage::Llm(Message::ToolResult { tool_call_id, .. }) => Some(tool_call_id.as_str()),
116 _ => None,
117 }
118}
119
120pub fn message_usage(msg: &AgentMessage) -> Option<yoagent::types::Usage> {
122 match msg {
123 AgentMessage::Llm(Message::Assistant { usage, .. }) => Some(usage.clone()),
124 _ => None,
125 }
126}
127
128pub fn message_is_system_stop(msg: &AgentMessage) -> bool {
132 if !message_is_user(msg) {
133 return false;
134 }
135 let text = message_text(msg);
136 text.trim_start().starts_with("[Agent stopped:")
137}
138
139pub fn message_error(msg: &AgentMessage) -> Option<&str> {
141 match msg {
142 AgentMessage::Llm(Message::Assistant {
143 error_message: Some(e),
144 ..
145 }) => Some(e.as_str()),
146 _ => None,
147 }
148}
149
150pub fn message_is_user(msg: &AgentMessage) -> bool {
152 matches!(msg, AgentMessage::Llm(Message::User { .. }))
153}
154
155pub fn message_is_assistant(msg: &AgentMessage) -> bool {
157 matches!(msg, AgentMessage::Llm(Message::Assistant { .. }))
158}
159
160pub fn message_is_tool_result(msg: &AgentMessage) -> bool {
162 matches!(msg, AgentMessage::Llm(Message::ToolResult { .. }))
163}
164
165pub fn user_message(text: impl Into<String>) -> AgentMessage {
167 AgentMessage::Llm(Message::User {
168 content: vec![Content::Text { text: text.into() }],
169 timestamp: yoagent::types::now_ms(),
170 })
171}
172
173pub fn assistant_message(text: impl Into<String>) -> AgentMessage {
175 AgentMessage::Llm(Message::Assistant {
176 content: vec![Content::Text { text: text.into() }],
177 stop_reason: yoagent::types::StopReason::Stop,
178 model: String::new(),
179 provider: String::new(),
180 usage: yoagent::types::Usage::default(),
181 timestamp: yoagent::types::now_ms(),
182 error_message: None,
183 })
184}
185
186pub fn tool_result_message(
188 tool_call_id: impl Into<String>,
189 tool_name: impl Into<String>,
190 text: impl Into<String>,
191 is_error: bool,
192) -> AgentMessage {
193 AgentMessage::Llm(Message::ToolResult {
194 tool_call_id: tool_call_id.into(),
195 tool_name: tool_name.into(),
196 content: vec![Content::Text { text: text.into() }],
197 is_error,
198 timestamp: yoagent::types::now_ms(),
199 })
200}
201
202pub fn message_tool_call_count(msg: &AgentMessage) -> usize {
204 match msg {
205 AgentMessage::Llm(Message::Assistant { content, .. }) => content
206 .iter()
207 .filter(|c| matches!(c, Content::ToolCall { .. }))
208 .count(),
209 AgentMessage::Llm(_) => 0,
210 _ => 0,
211 }
212}
213
214pub fn message_is_extension(msg: &AgentMessage) -> bool {
218 matches!(msg, AgentMessage::Extension(_))
219}
220
221pub fn message_extension_kind(msg: &AgentMessage) -> Option<&str> {
223 match msg {
224 AgentMessage::Extension(ext) => Some(ext.kind.as_str()),
225 _ => None,
226 }
227}
228
229pub fn message_extension_text(msg: &AgentMessage) -> Option<String> {
231 match msg {
232 AgentMessage::Extension(ext) => ext
233 .data
234 .get("text")
235 .and_then(|v| v.as_str())
236 .map(|s| s.to_string()),
237 _ => None,
238 }
239}
240
241pub fn extension_message(
244 kind: impl Into<String>,
245 text: impl Into<String>,
246 display: bool,
247) -> AgentMessage {
248 AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
249 kind,
250 serde_json::json!({
251 "text": text.into(),
252 "display": display,
253 }),
254 ))
255}
256
257pub fn extension_message_with_details(
259 kind: impl Into<String>,
260 text: impl Into<String>,
261 display: bool,
262 details: serde_json::Value,
263) -> AgentMessage {
264 AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
265 kind,
266 serde_json::json!({
267 "text": text.into(),
268 "display": display,
269 "details": details,
270 }),
271 ))
272}