Skip to main content

rab/agent/
types.rs

1//! Helper functions for common operations on yoagent types.
2//!
3//! All message types are used directly from `yoagent::types`.
4
5pub use yoagent::types::{AgentMessage, Content, Message};
6
7// ── Helper functions for working with yoagent types ─────────────────
8
9/// Extract all text content from a `Vec<Content>` as a single string.
10fn 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
24/// Extract all tool calls from a `Vec<Content>`.
25pub 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
44/// Get the text content of an AgentMessage (all text parts joined).
45pub 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
56/// Compute a dedup key for a message, distinguishing messages that
57/// `message_text()` alone would conflate (e.g. two assistant messages
58/// with empty text but different tool calls).
59pub 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                // Include tool call IDs and stop_reason so two assistant
71                // messages with empty text but different tool calls get
72                // distinct keys.
73                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
110/// Check if an AgentMessage is a tool result with an error.
111pub fn message_is_error(msg: &AgentMessage) -> bool {
112    matches!(
113        msg,
114        AgentMessage::Llm(Message::ToolResult { is_error: true, .. })
115    )
116}
117
118/// Get the tool_call_id from a ToolResult message.
119pub 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
126/// Get the usage from an Assistant message.
127pub 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
134/// Check if an AgentMessage is a system-generated stop notification
135/// (e.g. execution limit reached, max tokens exceeded).
136/// The agent loop injects these as user messages starting with `[Agent stopped:`.
137pub 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
145/// Extract the error_message from an Assistant message, if present.
146pub 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
156/// Check if an AgentMessage is a User message.
157pub fn message_is_user(msg: &AgentMessage) -> bool {
158    matches!(msg, AgentMessage::Llm(Message::User { .. }))
159}
160
161/// Check if an AgentMessage is an Assistant message.
162pub fn message_is_assistant(msg: &AgentMessage) -> bool {
163    matches!(msg, AgentMessage::Llm(Message::Assistant { .. }))
164}
165
166/// Check if an AgentMessage is a ToolResult message.
167pub fn message_is_tool_result(msg: &AgentMessage) -> bool {
168    matches!(msg, AgentMessage::Llm(Message::ToolResult { .. }))
169}
170
171/// Create a simple User AgentMessage with text content.
172pub 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
179/// Create a simple Assistant AgentMessage with text content.
180pub 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
192/// Create a ToolResult AgentMessage.
193pub 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
208/// Count how many tool calls are in an AgentMessage.
209pub 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
220// ── Extension message helpers (pi-compatible custom_message) ────────
221
222/// Check if an AgentMessage is an Extension message.
223pub fn message_is_extension(msg: &AgentMessage) -> bool {
224    matches!(msg, AgentMessage::Extension(_))
225}
226
227/// Get the kind/customType from an Extension message.
228pub 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
235/// Get the text content from an Extension message's data field.
236pub 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
247/// Create an Extension message (pi-compatible custom_message).
248/// `kind` identifies the type ("info", "error", "system_stop", etc.).
249pub 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
263/// Create an Extension message with structured details.
264pub 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
280/// Create a base ModelConfig for the opencode-go provider.
281/// Sets the standard context_window (1M) and max_tokens (393216)
282/// for DeepSeek v4 models. Callers override if needed.
283pub 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}