Skip to main content

lc_agents/
types.rs

1// lc-agents/src/types.rs
2//! Agent related type definitions
3
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7/// Agent action
8///
9/// Represents an action that the Agent decides to execute (usually a tool call).
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct AgentAction {
12    /// Tool name
13    pub tool: String,
14
15    /// Tool input (string or JSON object)
16    pub tool_input: ToolInput,
17
18    /// Log information (contains the full LLM output)
19    pub log: String,
20}
21
22/// Tool input type
23///
24/// Uses internally tagged serialization to avoid `untagged` ambiguity:
25/// - String inputs are tagged with `"type": "string"`
26/// - Object inputs are tagged with `"type": "object"`
27///
28/// A `TryFrom<serde_json::Value>` implementation validates incoming untagged
29/// JSON and dispatches to the correct variant.
30#[derive(Debug, Clone, Serialize, Deserialize)]
31#[serde(tag = "type", rename_all = "snake_case")]
32pub enum ToolInput {
33    /// String input
34    String {
35        /// The string value.
36        value: String,
37    },
38
39    /// JSON object input
40    Object {
41        /// The JSON object value.
42        value: serde_json::Value,
43    },
44}
45
46impl Default for ToolInput {
47    fn default() -> Self {
48        ToolInput::String {
49            value: String::new(),
50        }
51    }
52}
53
54impl std::fmt::Display for ToolInput {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        match self {
57            ToolInput::String { value } => write!(f, "{}", value),
58            ToolInput::Object { value } => write!(
59                f,
60                "{}",
61                serde_json::to_string(value).unwrap_or_else(|_| "unknown".to_string())
62            ),
63        }
64    }
65}
66
67/// Validates untagged JSON into a `ToolInput`.
68///
69/// This handles the case where external systems send JSON without the
70/// `type` tag. A JSON string becomes `ToolInput::String`, a JSON object
71/// becomes `ToolInput::Object`, and anything else is an error.
72impl TryFrom<serde_json::Value> for ToolInput {
73    type Error = String;
74
75    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
76        match value {
77            serde_json::Value::String(s) => Ok(ToolInput::String { value: s }),
78            serde_json::Value::Object(_) => Ok(ToolInput::Object { value }),
79            other => Err(format!(
80                "ToolInput must be a string or object, got: {}",
81                other
82            )),
83        }
84    }
85}
86
87/// Agent finish state
88///
89/// Represents that the Agent has reached a final answer.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct AgentFinish {
92    /// Return values (key-value pairs)
93    pub return_values: HashMap<String, serde_json::Value>,
94
95    /// Log information (contains the full LLM output)
96    pub log: String,
97}
98
99impl AgentFinish {
100    /// Create a new AgentFinish
101    pub fn new(output: impl Into<String>, log: impl Into<String>) -> Self {
102        let mut return_values = HashMap::new();
103        return_values.insert(
104            "output".to_string(),
105            serde_json::Value::String(output.into()),
106        );
107        Self {
108            return_values,
109            log: log.into(),
110        }
111    }
112
113    /// Get the output value
114    pub fn output(&self) -> Option<&str> {
115        self.return_values.get("output").and_then(|v| v.as_str())
116    }
117}
118
119/// Agent execution step
120///
121/// Represents an executed action and its observation result.
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct AgentStep {
124    /// The executed action
125    pub action: AgentAction,
126
127    /// Observation result (tool output)
128    pub observation: String,
129}
130
131impl AgentStep {
132    /// Create a new AgentStep
133    pub fn new(action: AgentAction, observation: impl Into<String>) -> Self {
134        Self {
135            action,
136            observation: observation.into(),
137        }
138    }
139}
140
141/// Agent output
142///
143/// The plan method of the Agent may return an action or a final answer.
144#[derive(Debug, Clone, Serialize, Deserialize)]
145pub enum AgentOutput {
146    /// Execute a single action
147    Action(AgentAction),
148
149    /// Execute multiple actions in parallel
150    Actions(Vec<AgentAction>),
151
152    /// Finish (return final answer)
153    Finish(AgentFinish),
154}
155
156impl AgentOutput {
157    /// Whether this is a final answer
158    pub fn is_finish(&self) -> bool {
159        matches!(self, AgentOutput::Finish(_))
160    }
161
162    /// Whether this is an action (single or multiple)
163    pub fn is_action(&self) -> bool {
164        matches!(self, AgentOutput::Action(_) | AgentOutput::Actions(_))
165    }
166
167    /// Get a single action (if any)
168    pub fn action(&self) -> Option<&AgentAction> {
169        match self {
170            AgentOutput::Action(action) => Some(action),
171            _ => None,
172        }
173    }
174
175    /// Get all actions (single or multiple)
176    pub fn actions(&self) -> Vec<&AgentAction> {
177        match self {
178            AgentOutput::Action(action) => vec![action],
179            AgentOutput::Actions(actions) => actions.iter().collect(),
180            _ => vec![],
181        }
182    }
183
184    /// Get the finish state (if any)
185    pub fn finish(&self) -> Option<&AgentFinish> {
186        match self {
187            AgentOutput::Finish(finish) => Some(finish),
188            _ => None,
189        }
190    }
191}
192
193#[cfg(test)]
194mod tests {
195    use super::*;
196
197    fn create_action(tool: &str, input: &str) -> AgentAction {
198        AgentAction {
199            tool: tool.to_string(),
200            tool_input: ToolInput::String {
201                value: input.to_string(),
202            },
203            log: "test".to_string(),
204        }
205    }
206
207    #[test]
208    fn test_agent_output_single_action() {
209        let action = create_action("calculator", "1+2");
210        let output = AgentOutput::Action(action);
211
212        assert!(output.is_action());
213        assert!(!output.is_finish());
214        assert_eq!(output.actions().len(), 1);
215    }
216
217    #[test]
218    fn test_agent_output_multiple_actions() {
219        let actions = vec![
220            create_action("calculator", "1+2"),
221            create_action("datetime", "now"),
222        ];
223        let output = AgentOutput::Actions(actions);
224
225        assert!(output.is_action());
226        assert!(!output.is_finish());
227        assert_eq!(output.actions().len(), 2);
228        assert!(output.action().is_none());
229    }
230
231    #[test]
232    fn test_agent_output_finish() {
233        let finish = AgentFinish::new("answer".to_string(), "log".to_string());
234        let output = AgentOutput::Finish(finish);
235
236        assert!(!output.is_action());
237        assert!(output.is_finish());
238        assert_eq!(output.actions().len(), 0);
239        assert!(output.finish().is_some());
240    }
241
242    #[test]
243    fn test_agent_finish_output() {
244        let finish = AgentFinish::new("the answer is 42".to_string(), String::new());
245        assert_eq!(finish.output(), Some("the answer is 42"));
246    }
247}