pub use yoagent::types::{AgentMessage, Content, Message};
fn content_text(content: &[Content]) -> String {
content
.iter()
.filter_map(|c| {
if let Content::Text { text } = c {
Some(text.as_str())
} else {
None
}
})
.collect::<Vec<_>>()
.join("")
}
pub fn content_tool_calls(content: &[Content]) -> Vec<(String, String, serde_json::Value)> {
content
.iter()
.filter_map(|c| {
if let Content::ToolCall {
id,
name,
arguments,
..
} = c
{
Some((id.clone(), name.clone(), arguments.clone()))
} else {
None
}
})
.collect()
}
pub fn message_text(msg: &AgentMessage) -> String {
match msg {
AgentMessage::Llm(m) => match m {
Message::User { content, .. }
| Message::Assistant { content, .. }
| Message::ToolResult { content, .. } => content_text(content),
},
AgentMessage::Extension(ext) => ext.data.to_string(),
}
}
pub fn message_dedup_key(msg: &AgentMessage) -> String {
match msg {
AgentMessage::Llm(m) => match m {
Message::User { content, .. } => {
format!("user:{}", content_text(content))
}
Message::Assistant {
content,
stop_reason,
..
} => {
let tc_ids: Vec<&str> = content
.iter()
.filter_map(|c| {
if let Content::ToolCall { id, .. } = c {
Some(id.as_str())
} else {
None
}
})
.collect();
format!(
"assistant:{}:{:?}:{:?}",
content_text(content),
tc_ids,
stop_reason
)
}
Message::ToolResult {
tool_call_id,
content,
is_error,
..
} => {
format!(
"tool:{}:{}:{}",
tool_call_id,
content_text(content),
is_error
)
}
},
AgentMessage::Extension(ext) => {
format!("ext:{}:{}", ext.kind, ext.data)
}
}
}
pub fn message_is_error(msg: &AgentMessage) -> bool {
matches!(
msg,
AgentMessage::Llm(Message::ToolResult { is_error: true, .. })
)
}
pub fn message_tool_call_id(msg: &AgentMessage) -> Option<&str> {
match msg {
AgentMessage::Llm(Message::ToolResult { tool_call_id, .. }) => Some(tool_call_id.as_str()),
_ => None,
}
}
pub fn message_usage(msg: &AgentMessage) -> Option<yoagent::types::Usage> {
match msg {
AgentMessage::Llm(Message::Assistant { usage, .. }) => Some(usage.clone()),
_ => None,
}
}
pub fn message_is_system_stop(msg: &AgentMessage) -> bool {
if !message_is_user(msg) {
return false;
}
let text = message_text(msg);
text.trim_start().starts_with("[Agent stopped:")
}
pub fn message_error(msg: &AgentMessage) -> Option<&str> {
match msg {
AgentMessage::Llm(Message::Assistant {
error_message: Some(e),
..
}) => Some(e.as_str()),
_ => None,
}
}
pub fn message_is_user(msg: &AgentMessage) -> bool {
matches!(msg, AgentMessage::Llm(Message::User { .. }))
}
pub fn message_is_assistant(msg: &AgentMessage) -> bool {
matches!(msg, AgentMessage::Llm(Message::Assistant { .. }))
}
pub fn message_is_tool_result(msg: &AgentMessage) -> bool {
matches!(msg, AgentMessage::Llm(Message::ToolResult { .. }))
}
pub fn user_message(text: impl Into<String>) -> AgentMessage {
AgentMessage::Llm(Message::User {
content: vec![Content::Text { text: text.into() }],
timestamp: yoagent::types::now_ms(),
})
}
pub fn assistant_message(text: impl Into<String>) -> AgentMessage {
AgentMessage::Llm(Message::Assistant {
content: vec![Content::Text { text: text.into() }],
stop_reason: yoagent::types::StopReason::Stop,
model: String::new(),
provider: String::new(),
usage: yoagent::types::Usage::default(),
timestamp: yoagent::types::now_ms(),
error_message: None,
})
}
pub fn tool_result_message(
tool_call_id: impl Into<String>,
tool_name: impl Into<String>,
text: impl Into<String>,
is_error: bool,
) -> AgentMessage {
AgentMessage::Llm(Message::ToolResult {
tool_call_id: tool_call_id.into(),
tool_name: tool_name.into(),
content: vec![Content::Text { text: text.into() }],
is_error,
timestamp: yoagent::types::now_ms(),
})
}
pub fn message_tool_call_count(msg: &AgentMessage) -> usize {
match msg {
AgentMessage::Llm(Message::Assistant { content, .. }) => content
.iter()
.filter(|c| matches!(c, Content::ToolCall { .. }))
.count(),
AgentMessage::Llm(_) => 0,
_ => 0,
}
}
pub fn message_is_extension(msg: &AgentMessage) -> bool {
matches!(msg, AgentMessage::Extension(_))
}
pub fn message_extension_kind(msg: &AgentMessage) -> Option<&str> {
match msg {
AgentMessage::Extension(ext) => Some(ext.kind.as_str()),
_ => None,
}
}
pub fn message_extension_text(msg: &AgentMessage) -> Option<String> {
match msg {
AgentMessage::Extension(ext) => ext
.data
.get("text")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
_ => None,
}
}
pub fn extension_message(
kind: impl Into<String>,
text: impl Into<String>,
display: bool,
) -> AgentMessage {
AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
kind,
serde_json::json!({
"text": text.into(),
"display": display,
}),
))
}
pub fn extension_message_with_details(
kind: impl Into<String>,
text: impl Into<String>,
display: bool,
details: serde_json::Value,
) -> AgentMessage {
AgentMessage::Extension(yoagent::types::ExtensionMessage::new(
kind,
serde_json::json!({
"text": text.into(),
"display": display,
"details": details,
}),
))
}
pub fn base_model_config(model: &str) -> yoagent::provider::model::ModelConfig {
let mut mc = yoagent::provider::model::ModelConfig::openai_compat(
"https://opencode.ai/zen/go/v1",
model,
"opencode-go",
yoagent::provider::model::OpenAiCompat::deepseek(),
);
mc.context_window = 1_000_000;
mc.max_tokens = 393_216;
mc
}