Skip to main content

skilllite_agent/types/
chat.rs

1//! OpenAI-compatible chat types.
2
3use serde::{Deserialize, Serialize};
4
5use super::feedback::ExecutionFeedback;
6use super::task::Task;
7
8/// A chat message in OpenAI format.
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ChatMessage {
11    pub role: String,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub content: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub tool_calls: Option<Vec<ToolCall>>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub tool_call_id: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub name: Option<String>,
20}
21
22impl ChatMessage {
23    pub fn system(content: &str) -> Self {
24        Self {
25            role: "system".to_string(),
26            content: Some(content.to_string()),
27            tool_calls: None,
28            tool_call_id: None,
29            name: None,
30        }
31    }
32
33    pub fn user(content: &str) -> Self {
34        Self {
35            role: "user".to_string(),
36            content: Some(content.to_string()),
37            tool_calls: None,
38            tool_call_id: None,
39            name: None,
40        }
41    }
42
43    pub fn assistant(content: &str) -> Self {
44        Self {
45            role: "assistant".to_string(),
46            content: Some(content.to_string()),
47            tool_calls: None,
48            tool_call_id: None,
49            name: None,
50        }
51    }
52
53    pub fn assistant_with_tool_calls(content: Option<&str>, tool_calls: Vec<ToolCall>) -> Self {
54        Self {
55            role: "assistant".to_string(),
56            content: content.map(|s| s.to_string()),
57            tool_calls: Some(tool_calls),
58            tool_call_id: None,
59            name: None,
60        }
61    }
62
63    pub fn tool_result(tool_call_id: &str, content: &str) -> Self {
64        Self {
65            role: "tool".to_string(),
66            content: Some(content.to_string()),
67            tool_calls: None,
68            tool_call_id: Some(tool_call_id.to_string()),
69            name: None,
70        }
71    }
72}
73
74/// A tool call from the LLM.
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct ToolCall {
77    pub id: String,
78    #[serde(rename = "type")]
79    pub call_type: String,
80    pub function: FunctionCall,
81}
82
83/// Function call details.
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct FunctionCall {
86    pub name: String,
87    pub arguments: String,
88}
89
90/// Supported LLM tool formats.
91/// Ported from Python `core/tools.py` ToolFormat enum.
92#[derive(Debug, Clone, PartialEq, Eq)]
93pub enum ToolFormat {
94    /// OpenAI function calling format (GPT-4, DeepSeek, Qwen, etc.)
95    OpenAI,
96    /// Claude native tool format (Anthropic SDK)
97    Claude,
98}
99
100/// OpenAI-compatible tool definition.
101#[derive(Debug, Clone, Serialize, Deserialize)]
102pub struct ToolDefinition {
103    #[serde(rename = "type")]
104    pub tool_type: String,
105    pub function: FunctionDef,
106}
107
108impl ToolDefinition {
109    /// Convert to Claude API format.
110    /// Claude expects: { name, description, input_schema }
111    pub fn to_claude_format(&self) -> serde_json::Value {
112        serde_json::json!({
113            "name": self.function.name,
114            "description": self.function.description,
115            "input_schema": self.function.parameters
116        })
117    }
118
119    /// Convert to the specified format.
120    pub fn to_format(&self, format: &ToolFormat) -> serde_json::Value {
121        match format {
122            ToolFormat::OpenAI => serde_json::to_value(self).unwrap_or_default(),
123            ToolFormat::Claude => self.to_claude_format(),
124        }
125    }
126}
127
128/// Function definition within a tool.
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct FunctionDef {
131    pub name: String,
132    pub description: String,
133    pub parameters: serde_json::Value,
134}
135
136/// Result from executing a tool.
137#[derive(Debug, Clone)]
138pub struct ToolResult {
139    pub tool_call_id: String,
140    pub tool_name: String,
141    pub content: String,
142    pub is_error: bool,
143    /// Whether this result should count toward task failure / replan heuristics.
144    pub counts_as_failure: bool,
145}
146
147impl ToolResult {
148    /// Convert to Claude API tool_result format.
149    pub fn to_claude_format(&self) -> serde_json::Value {
150        serde_json::json!({
151            "type": "tool_result",
152            "tool_use_id": self.tool_call_id,
153            "content": self.content,
154            "is_error": self.is_error
155        })
156    }
157
158    /// Convert to OpenAI API tool result message.
159    pub fn to_openai_format(&self) -> serde_json::Value {
160        serde_json::json!({
161            "role": "tool",
162            "tool_call_id": self.tool_call_id,
163            "content": self.content
164        })
165    }
166
167    /// Convert to the specified format.
168    pub fn to_format(&self, format: &ToolFormat) -> serde_json::Value {
169        match format {
170            ToolFormat::OpenAI => self.to_openai_format(),
171            ToolFormat::Claude => self.to_claude_format(),
172        }
173    }
174}
175
176/// Parse tool calls from a Claude native API response.
177/// Claude returns content blocks with type "tool_use".
178/// Ported from Python `ToolUseRequest.parse_from_claude_response`.
179pub fn parse_claude_tool_calls(content_blocks: &[serde_json::Value]) -> Vec<ToolCall> {
180    let mut calls = Vec::new();
181    for block in content_blocks {
182        if block.get("type").and_then(|v| v.as_str()) == Some("tool_use") {
183            let id = block
184                .get("id")
185                .and_then(|v| v.as_str())
186                .unwrap_or("")
187                .to_string();
188            let name = block
189                .get("name")
190                .and_then(|v| v.as_str())
191                .unwrap_or("")
192                .to_string();
193            let input = block.get("input").cloned().unwrap_or(serde_json::json!({}));
194            let arguments = serde_json::to_string(&input).unwrap_or_else(|_| "{}".to_string());
195
196            calls.push(ToolCall {
197                id,
198                call_type: "function".to_string(),
199                function: FunctionCall { name, arguments },
200            });
201        }
202    }
203    calls
204}
205
206/// Agent loop result.
207#[derive(Debug)]
208pub struct AgentResult {
209    pub response: String,
210    #[allow(dead_code)]
211    pub messages: Vec<ChatMessage>,
212    #[allow(dead_code)]
213    pub tool_calls_count: usize,
214    #[allow(dead_code)]
215    pub iterations: usize,
216    /// Task plan generated by the planner (empty if no planning was used).
217    pub task_plan: Vec<Task>,
218    /// Execution feedback for the evolution engine (EVO-1).
219    pub feedback: ExecutionFeedback,
220}
221
222impl AgentResult {
223    /// Convert to protocol-layer [`NodeResult`] for stdio_rpc/agent_chat/P2P.
224    /// `task_id` is echoed back; use a generated UUID when the caller did not provide one.
225    pub fn to_node_result(
226        &self,
227        task_id: impl Into<String>,
228    ) -> skilllite_core::protocol::NodeResult {
229        skilllite_core::protocol::NodeResult {
230            task_id: task_id.into(),
231            response: self.response.clone(),
232            task_completed: self.feedback.task_completed,
233            tool_calls: self.feedback.total_tools,
234            new_skill: None, // agent_chat: N/A. Use `skilllite evolution run --json` for NewSkill output.
235        }
236    }
237}