Skip to main content

tower_llm/
items.rs

1//! # Items (orientation)
2//!
3//! Core data structures for agent communication and execution traces: `Message`,
4//! `ToolCall`, `ModelResponse`, and `RunItem`. These types are shared across the
5//! runner, model providers, and session storage.
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use serde_json::Value;
10use uuid::Uuid;
11
12/// Represents the role of a message's author in a conversation.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum Role {
16    /// The system message, which sets the context and instructions for the agent.
17    System,
18    /// A message from the end-user.
19    User,
20    /// A message from the AI assistant.
21    Assistant,
22    /// A message containing the output of a tool.
23    Tool,
24}
25
26/// A single message in a conversation, forming the basis of interaction with
27/// the LLM.
28#[derive(Debug, Clone, Serialize, Deserialize)]
29pub struct Message {
30    /// The role of the message author.
31    pub role: Role,
32    /// The text content of the message.
33    pub content: String,
34    /// The name of the author, used in some multi-agent contexts.
35    #[serde(skip_serializing_if = "Option::is_none")]
36    pub name: Option<String>,
37    /// A unique identifier for the tool call this message is a response to.
38    #[serde(skip_serializing_if = "Option::is_none")]
39    pub tool_call_id: Option<String>,
40    /// A list of tool calls requested by the assistant.
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub tool_calls: Option<Vec<ToolCall>>,
43}
44
45impl Message {
46    /// Creates a new `Message` with the `System` role.
47    pub fn system(content: impl Into<String>) -> Self {
48        Self {
49            role: Role::System,
50            content: content.into(),
51            name: None,
52            tool_call_id: None,
53            tool_calls: None,
54        }
55    }
56
57    /// Creates a new `Message` with the `User` role.
58    pub fn user(content: impl Into<String>) -> Self {
59        Self {
60            role: Role::User,
61            content: content.into(),
62            name: None,
63            tool_call_id: None,
64            tool_calls: None,
65        }
66    }
67
68    /// Creates a new `Message` with the `Assistant` role.
69    pub fn assistant(content: impl Into<String>) -> Self {
70        Self {
71            role: Role::Assistant,
72            content: content.into(),
73            name: None,
74            tool_call_id: None,
75            tool_calls: None,
76        }
77    }
78
79    /// Creates a new `Message` from the assistant that includes tool calls.
80    pub fn assistant_with_tool_calls(
81        content: impl Into<String>,
82        tool_calls: Vec<ToolCall>,
83    ) -> Self {
84        Self {
85            role: Role::Assistant,
86            content: content.into(),
87            name: None,
88            tool_call_id: None,
89            tool_calls: Some(tool_calls),
90        }
91    }
92
93    /// Creates a new `Message` with the `Tool` role, representing a tool's output.
94    pub fn tool(content: impl Into<String>, tool_call_id: impl Into<String>) -> Self {
95        Self {
96            role: Role::Tool,
97            content: content.into(),
98            name: None,
99            tool_call_id: Some(tool_call_id.into()),
100            tool_calls: None,
101        }
102    }
103}
104
105/// Represents a request from the LLM to call a specific tool.
106#[derive(Debug, Clone, Serialize, Deserialize)]
107pub struct ToolCall {
108    /// A unique identifier for this tool call.
109    pub id: String,
110    /// The name of the tool to be executed.
111    pub name: String,
112    /// The arguments to be passed to the tool, as a JSON value.
113    pub arguments: Value,
114}
115
116/// Encapsulates a response from the LLM, which may include text content and
117/// tool calls.
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ModelResponse {
120    /// A unique identifier for the response.
121    pub id: String,
122    /// The text content of the response, if any.
123    pub content: Option<String>,
124    /// A list of tool calls requested by the model.
125    pub tool_calls: Vec<ToolCall>,
126    /// The reason why the model stopped generating the response.
127    pub finish_reason: Option<String>,
128    /// The timestamp of when the response was created.
129    pub created_at: DateTime<Utc>,
130}
131
132impl ModelResponse {
133    /// Creates a new `ModelResponse` that contains only a text message.
134    pub fn new_message(content: impl Into<String>) -> Self {
135        Self {
136            id: Uuid::new_v4().to_string(),
137            content: Some(content.into()),
138            tool_calls: vec![],
139            finish_reason: Some("stop".to_string()),
140            created_at: Utc::now(),
141        }
142    }
143
144    /// Creates a new `ModelResponse` that contains one or more tool calls.
145    pub fn new_tool_calls(tool_calls: Vec<ToolCall>) -> Self {
146        Self {
147            id: Uuid::new_v4().to_string(),
148            content: None,
149            tool_calls,
150            finish_reason: Some("tool_calls".to_string()),
151            created_at: Utc::now(),
152        }
153    }
154
155    /// Returns `true` if the response contains any tool calls.
156    pub fn has_tool_calls(&self) -> bool {
157        !self.tool_calls.is_empty()
158    }
159
160    /// Returns `true` if the response has non-empty text content.
161    pub fn has_content(&self) -> bool {
162        self.content.is_some() && !self.content.as_ref().unwrap().is_empty()
163    }
164}
165
166/// An enum representing a single, discrete step in an agent's execution trace.
167///
168/// `RunItem` is used to log the history of a conversation, which can then be
169/// stored in a session for maintaining context.
170#[derive(Debug, Clone, Serialize, Deserialize)]
171#[serde(tag = "type")]
172pub enum RunItem {
173    /// A message from the user or the assistant.
174    Message(MessageItem),
175    /// A request from the agent to call a tool.
176    ToolCall(ToolCallItem),
177    /// The output of a tool execution.
178    ToolOutput(ToolOutputItem),
179    /// A handoff of control from one agent to another.
180    Handoff(HandoffItem),
181}
182
183/// A structured representation of a message within a `RunItem`.
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct MessageItem {
186    pub id: String,
187    pub role: Role,
188    pub content: String,
189    pub created_at: DateTime<Utc>,
190}
191
192/// A structured representation of a tool call within a `RunItem`.
193#[derive(Debug, Clone, Serialize, Deserialize)]
194pub struct ToolCallItem {
195    pub id: String,
196    pub tool_name: String,
197    pub arguments: Value,
198    pub created_at: DateTime<Utc>,
199}
200
201/// A structured representation of a tool's output within a `RunItem`.
202#[derive(Debug, Clone, Serialize, Deserialize)]
203pub struct ToolOutputItem {
204    pub id: String,
205    pub tool_call_id: String,
206    pub output: Value,
207    pub error: Option<String>,
208    pub created_at: DateTime<Utc>,
209}
210
211/// A structured representation of a handoff within a `RunItem`.
212#[derive(Debug, Clone, Serialize, Deserialize)]
213pub struct HandoffItem {
214    pub id: String,
215    pub from_agent: String,
216    pub to_agent: String,
217    pub reason: Option<String>,
218    pub created_at: DateTime<Utc>,
219}
220
221/// A collection of helper functions for working with `RunItem`s.
222pub struct ItemHelpers;
223
224impl ItemHelpers {
225    /// Converts a slice of `RunItem`s into a `Vec<Message>` suitable for use as
226    /// conversation history.
227    pub fn to_messages(items: &[RunItem]) -> Vec<Message> {
228        let mut messages = Vec::new();
229        let mut pending_tool_calls: Vec<ToolCall> = Vec::new();
230
231        for (i, item) in items.iter().enumerate() {
232            match item {
233                RunItem::Message(msg) => {
234                    // If we have pending tool calls from before this message,
235                    // and this is an assistant message, attach them
236                    if msg.role == Role::Assistant && !pending_tool_calls.is_empty() {
237                        messages.push(Message {
238                            role: msg.role,
239                            content: msg.content.clone(),
240                            name: None,
241                            tool_call_id: None,
242                            tool_calls: Some(pending_tool_calls.clone()),
243                        });
244                        pending_tool_calls.clear();
245                    } else {
246                        messages.push(Message {
247                            role: msg.role,
248                            content: msg.content.clone(),
249                            name: None,
250                            tool_call_id: None,
251                            tool_calls: None,
252                        });
253                    }
254                }
255                RunItem::ToolCall(tool_call) => {
256                    // Collect tool calls that should be attached to the previous assistant message
257                    pending_tool_calls.push(ToolCall {
258                        id: tool_call.id.clone(),
259                        name: tool_call.tool_name.clone(),
260                        arguments: tool_call.arguments.clone(),
261                    });
262
263                    // Check if this is the last tool call before tool outputs
264                    // If the next item is a ToolOutput, we should create an assistant message now
265                    if i + 1 < items.len() {
266                        if let RunItem::ToolOutput(_) = &items[i + 1] {
267                            // Create an assistant message with the tool calls
268                            if !pending_tool_calls.is_empty() {
269                                messages.push(Message {
270                                    role: Role::Assistant,
271                                    content: String::new(),
272                                    name: None,
273                                    tool_call_id: None,
274                                    tool_calls: Some(pending_tool_calls.clone()),
275                                });
276                                pending_tool_calls.clear();
277                            }
278                        }
279                    }
280                }
281                RunItem::ToolOutput(output) => {
282                    let content = if let Some(error) = &output.error {
283                        format!("Error: {}", error)
284                    } else {
285                        output.output.to_string()
286                    };
287                    messages.push(Message::tool(content, &output.tool_call_id));
288                }
289                _ => {}
290            }
291        }
292
293        // If we still have pending tool calls at the end, create an assistant message for them
294        if !pending_tool_calls.is_empty() {
295            messages.push(Message {
296                role: Role::Assistant,
297                content: String::new(),
298                name: None,
299                tool_call_id: None,
300                tool_calls: Some(pending_tool_calls),
301            });
302        }
303
304        messages
305    }
306
307    /// Filters a slice of `RunItem`s by their type.
308    pub fn filter_by_type<T>(
309        items: &[RunItem],
310        filter_fn: impl Fn(&RunItem) -> Option<&T>,
311    ) -> Vec<&T> {
312        items.iter().filter_map(filter_fn).collect()
313    }
314}
315
316#[cfg(test)]
317mod tests {
318    use super::*;
319    use pretty_assertions::assert_eq;
320
321    #[test]
322    fn test_message_creation() {
323        let sys_msg = Message::system("You are a helpful assistant");
324        assert_eq!(sys_msg.role, Role::System);
325        assert_eq!(sys_msg.content, "You are a helpful assistant");
326        assert!(sys_msg.tool_call_id.is_none());
327
328        let user_msg = Message::user("Hello");
329        assert_eq!(user_msg.role, Role::User);
330        assert_eq!(user_msg.content, "Hello");
331
332        let tool_msg = Message::tool("Result", "call_123");
333        assert_eq!(tool_msg.role, Role::Tool);
334        assert_eq!(tool_msg.tool_call_id, Some("call_123".to_string()));
335    }
336
337    #[test]
338    fn test_model_response() {
339        let response = ModelResponse::new_message("Hello, how can I help?");
340        assert!(response.has_content());
341        assert!(!response.has_tool_calls());
342        assert_eq!(response.content, Some("Hello, how can I help?".to_string()));
343
344        let tool_call = ToolCall {
345            id: "call_1".to_string(),
346            name: "get_weather".to_string(),
347            arguments: serde_json::json!({"city": "Tokyo"}),
348        };
349
350        let tool_response = ModelResponse::new_tool_calls(vec![tool_call]);
351        assert!(!tool_response.has_content());
352        assert!(tool_response.has_tool_calls());
353        assert_eq!(tool_response.tool_calls.len(), 1);
354    }
355
356    #[test]
357    fn test_run_items() {
358        let msg_item = RunItem::Message(MessageItem {
359            id: "msg_1".to_string(),
360            role: Role::User,
361            content: "Hello".to_string(),
362            created_at: Utc::now(),
363        });
364
365        let tool_item = RunItem::ToolCall(ToolCallItem {
366            id: "call_1".to_string(),
367            tool_name: "calculator".to_string(),
368            arguments: serde_json::json!({"operation": "add", "a": 1, "b": 2}),
369            created_at: Utc::now(),
370        });
371
372        // Test serialization
373        let serialized = serde_json::to_string(&msg_item).unwrap();
374        assert!(serialized.contains("\"type\":\"Message\""));
375
376        let serialized_tool = serde_json::to_string(&tool_item).unwrap();
377        assert!(serialized_tool.contains("\"type\":\"ToolCall\""));
378    }
379
380    #[test]
381    fn test_item_helpers_to_messages() {
382        let items = vec![
383            RunItem::Message(MessageItem {
384                id: "1".to_string(),
385                role: Role::User,
386                content: "What's the weather?".to_string(),
387                created_at: Utc::now(),
388            }),
389            RunItem::ToolCall(ToolCallItem {
390                id: "2".to_string(),
391                tool_name: "get_weather".to_string(),
392                arguments: serde_json::json!({"city": "Paris"}),
393                created_at: Utc::now(),
394            }),
395            RunItem::ToolOutput(ToolOutputItem {
396                id: "3".to_string(),
397                tool_call_id: "2".to_string(),
398                output: serde_json::json!({"temp": 20, "condition": "sunny"}),
399                error: None,
400                created_at: Utc::now(),
401            }),
402        ];
403
404        let messages = ItemHelpers::to_messages(&items);
405        assert_eq!(messages.len(), 3); // User Message, Assistant with tool_calls, and ToolOutput
406        assert_eq!(messages[0].role, Role::User);
407        assert_eq!(messages[1].role, Role::Assistant); // Assistant message with tool_calls
408        assert!(messages[1].tool_calls.is_some());
409        assert_eq!(messages[2].role, Role::Tool);
410    }
411
412    #[test]
413    fn test_handoff_item() {
414        let handoff = HandoffItem {
415            id: "handoff_1".to_string(),
416            from_agent: "triage".to_string(),
417            to_agent: "specialist".to_string(),
418            reason: Some("User needs technical support".to_string()),
419            created_at: Utc::now(),
420        };
421
422        let item = RunItem::Handoff(handoff.clone());
423        let serialized = serde_json::to_string(&item).unwrap();
424        assert!(serialized.contains("\"type\":\"Handoff\""));
425        assert!(serialized.contains("\"from_agent\":\"triage\""));
426    }
427
428    #[test]
429    fn test_role_serialization() {
430        let role = Role::Assistant;
431        let serialized = serde_json::to_string(&role).unwrap();
432        assert_eq!(serialized, "\"assistant\"");
433
434        let deserialized: Role = serde_json::from_str("\"system\"").unwrap();
435        assert_eq!(deserialized, Role::System);
436    }
437}