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 is_error,
94 ..
95 } => {
96 format!(
97 "tool:{}:{}:{}",
98 tool_call_id,
99 content_text(content),
100 is_error
101 )
102 }
103 },
104 AgentMessage::Extension(ext) => {
105 format!("ext:{}:{}", ext.kind, ext.data)
106 }
107 }
108}
109
110pub fn message_is_error(msg: &AgentMessage) -> bool {
112 matches!(
113 msg,
114 AgentMessage::Llm(Message::ToolResult { is_error: true, .. })
115 )
116}
117
118pub fn message_tool_call_id(msg: &AgentMessage) -> Option<&str> {
120 match msg {
121 AgentMessage::Llm(Message::ToolResult { tool_call_id, .. }) => Some(tool_call_id.as_str()),
122 _ => None,
123 }
124}
125
126pub fn message_usage(msg: &AgentMessage) -> Option<yoagent::types::Usage> {
128 match msg {
129 AgentMessage::Llm(Message::Assistant { usage, .. }) => Some(usage.clone()),
130 _ => None,
131 }
132}
133
134pub fn message_is_system_stop(msg: &AgentMessage) -> bool {
138 if !message_is_user(msg) {
139 return false;
140 }
141 let text = message_text(msg);
142 text.trim_start().starts_with("[Agent stopped:")
143}
144
145pub fn message_error(msg: &AgentMessage) -> Option<&str> {
147 match msg {
148 AgentMessage::Llm(Message::Assistant {
149 error_message: Some(e),
150 ..
151 }) => Some(e.as_str()),
152 _ => None,
153 }
154}
155
156pub fn message_is_user(msg: &AgentMessage) -> bool {
158 matches!(msg, AgentMessage::Llm(Message::User { .. }))
159}
160
161pub fn message_is_assistant(msg: &AgentMessage) -> bool {
163 matches!(msg, AgentMessage::Llm(Message::Assistant { .. }))
164}
165
166pub fn message_is_tool_result(msg: &AgentMessage) -> bool {
168 matches!(msg, AgentMessage::Llm(Message::ToolResult { .. }))
169}
170
171pub fn user_message(text: impl Into<String>) -> AgentMessage {
173 AgentMessage::Llm(Message::User {
174 content: vec![Content::Text { text: text.into() }],
175 timestamp: yoagent::types::now_ms(),
176 })
177}
178
179pub fn assistant_message(text: impl Into<String>) -> AgentMessage {
181 AgentMessage::Llm(Message::Assistant {
182 content: vec![Content::Text { text: text.into() }],
183 stop_reason: yoagent::types::StopReason::Stop,
184 model: String::new(),
185 provider: String::new(),
186 usage: yoagent::types::Usage::default(),
187 timestamp: yoagent::types::now_ms(),
188 error_message: None,
189 })
190}
191
192pub fn tool_result_message(
194 tool_call_id: impl Into<String>,
195 tool_name: impl Into<String>,
196 text: impl Into<String>,
197 is_error: bool,
198) -> AgentMessage {
199 AgentMessage::Llm(Message::ToolResult {
200 tool_call_id: tool_call_id.into(),
201 tool_name: tool_name.into(),
202 content: vec![Content::Text { text: text.into() }],
203 is_error,
204 timestamp: yoagent::types::now_ms(),
205 })
206}
207
208pub fn message_tool_call_count(msg: &AgentMessage) -> usize {
210 match msg {
211 AgentMessage::Llm(Message::Assistant { content, .. }) => content
212 .iter()
213 .filter(|c| matches!(c, Content::ToolCall { .. }))
214 .count(),
215 AgentMessage::Llm(_) => 0,
216 _ => 0,
217 }
218}
219
220pub fn message_is_extension(msg: &AgentMessage) -> bool {
224 matches!(msg, AgentMessage::Extension(_))
225}
226
227pub fn message_extension_kind(msg: &AgentMessage) -> Option<&str> {
229 match msg {
230 AgentMessage::Extension(ext) => Some(ext.kind.as_str()),
231 _ => None,
232 }
233}
234
235pub fn message_extension_text(msg: &AgentMessage) -> Option<String> {
237 match msg {
238 AgentMessage::Extension(ext) => ext
239 .data
240 .get("text")
241 .and_then(|v| v.as_str())
242 .map(|s| s.to_string()),
243 _ => None,
244 }
245}
246
247pub fn extension_message(
250 kind: impl Into<String>,
251 text: impl Into<String>,
252 display: bool,
253) -> AgentMessage {
254 AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
255 kind,
256 serde_json::json!({
257 "text": text.into(),
258 "display": display,
259 }),
260 ))
261}
262
263pub fn extension_message_with_details(
265 kind: impl Into<String>,
266 text: impl Into<String>,
267 display: bool,
268 details: serde_json::Value,
269) -> AgentMessage {
270 AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
271 kind,
272 serde_json::json!({
273 "text": text.into(),
274 "display": display,
275 "details": details,
276 }),
277 ))
278}
279
280pub fn base_model_config(model: &str) -> yoagent::provider::model::ModelConfig {
284 let mut mc = yoagent::provider::model::ModelConfig::openai_compat(
285 "https://opencode.ai/zen/go/v1",
286 model,
287 "opencode-go",
288 yoagent::provider::model::OpenAiCompat::deepseek(),
289 );
290 mc.context_window = 1_000_000;
291 mc.max_tokens = 393_216;
292 mc
293}