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                ..
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
104/// Check if an AgentMessage is a tool result with an error.
105pub fn message_is_error(msg: &AgentMessage) -> bool {
106    matches!(
107        msg,
108        AgentMessage::Llm(Message::ToolResult { is_error: true, .. })
109    )
110}
111
112/// Get the tool_call_id from a ToolResult message.
113pub 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
120/// Get the usage from an Assistant message.
121pub 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
128/// Check if an AgentMessage is a system-generated stop notification
129/// (e.g. execution limit reached, max tokens exceeded).
130/// The agent loop injects these as user messages starting with `[Agent stopped:`.
131pub 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
139/// Extract the error_message from an Assistant message, if present.
140pub 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
150/// Check if an AgentMessage is a User message.
151pub fn message_is_user(msg: &AgentMessage) -> bool {
152    matches!(msg, AgentMessage::Llm(Message::User { .. }))
153}
154
155/// Check if an AgentMessage is an Assistant message.
156pub fn message_is_assistant(msg: &AgentMessage) -> bool {
157    matches!(msg, AgentMessage::Llm(Message::Assistant { .. }))
158}
159
160/// Check if an AgentMessage is a ToolResult message.
161pub fn message_is_tool_result(msg: &AgentMessage) -> bool {
162    matches!(msg, AgentMessage::Llm(Message::ToolResult { .. }))
163}
164
165/// Create a simple User AgentMessage with text content.
166pub 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
173/// Create a simple Assistant AgentMessage with text content.
174pub 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
186/// Create a ToolResult AgentMessage.
187pub 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
202/// Count how many tool calls are in an AgentMessage.
203pub 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
214// ── Extension message helpers (pi-compatible custom_message) ────────
215
216/// Check if an AgentMessage is an Extension message.
217pub fn message_is_extension(msg: &AgentMessage) -> bool {
218    matches!(msg, AgentMessage::Extension(_))
219}
220
221/// Get the kind/customType from an Extension message.
222pub 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
229/// Get the text content from an Extension message's data field.
230pub 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
241/// Create an Extension message (pi-compatible custom_message).
242/// `kind` identifies the type ("info", "error", "system_stop", etc.).
243pub 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
257/// Create an Extension message with structured details.
258pub 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}