#[derive(Debug, Clone, PartialEq)]
pub enum ToolCallAction {
Message { content: String },
Silence,
Exec {
command: String,
workdir: Option<String>,
timeout_secs: Option<u64>,
},
ReadFile { path: String },
SkillCreate {
skill_name: String,
description: String,
body: String,
},
}
pub struct ResponseParser;
impl ResponseParser {
pub fn strip_thinking(content: &str) -> String {
let mut result = content.to_string();
while let Some(start) = result.find("<think") {
if let Some(tag_end) = result[start..].find('>') {
let content_start = start + tag_end + 1;
if let Some(end) = result[content_start..].find("</think reasoning>") {
result = format!(
"{}{}",
&result[..start],
&result[content_start + end + "</think reasoning>".len()..]
);
continue;
}
if let Some(end) = result[content_start..].find("</think") {
let remainder = &result[content_start + end..];
if let Some(close_pos) = remainder.find('>') {
result = format!(
"{}{}",
&result[..start],
&result[content_start + end + close_pos + 1..]
);
continue;
}
}
result = result[..start].to_string();
}
break;
}
result.trim().to_string()
}
pub fn parse_tool_calls(tool_calls: &[serde_json::Value]) -> Option<ToolCallAction> {
let tc = tool_calls.first()?;
let function = tc.get("function")?;
let name = function.get("name")?.as_str()?;
let arguments = function
.get("arguments")
.and_then(|a| a.as_str())
.unwrap_or("{}");
match name {
"respond" => {
let args: serde_json::Value = serde_json::from_str(arguments).ok()?;
let silent = args.get("silent").and_then(|v| v.as_bool()).unwrap_or(false);
if silent {
return Some(ToolCallAction::Silence);
}
let text = args
.get("text")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if text.is_empty() {
return Some(ToolCallAction::Silence);
}
Some(ToolCallAction::Message { content: text })
}
"exec" => {
let args: serde_json::Value = serde_json::from_str(arguments).ok()?;
let command = args.get("command")?.as_str()?.to_string();
if command.is_empty() {
return None;
}
let workdir = args.get("workdir").and_then(|v| v.as_str()).map(String::from);
let timeout_secs = args.get("timeout").and_then(|v| v.as_u64());
Some(ToolCallAction::Exec {
command,
workdir,
timeout_secs,
})
}
"read_file" => {
let args: serde_json::Value = serde_json::from_str(arguments).ok()?;
let path = args.get("path")?.as_str()?.to_string();
if path.is_empty() {
return None;
}
Some(ToolCallAction::ReadFile { path })
}
"create_skill" => {
let args: serde_json::Value = serde_json::from_str(arguments).ok()?;
let skill_name = args.get("skill_name")?.as_str()?.to_string();
if skill_name.is_empty() {
return None;
}
let description = args.get("description").and_then(|v| v.as_str()).unwrap_or("").to_string();
let body = args.get("body").and_then(|v| v.as_str()).unwrap_or("").to_string();
Some(ToolCallAction::SkillCreate {
skill_name,
description,
body,
})
}
_ => None,
}
}
pub fn strip_leaked_tool_calls(content: &str) -> String {
let tool_names = ["respond", "exec", "read_file", "create_skill"];
let lines: Vec<&str> = content
.lines()
.filter(|line| {
let trimmed = line.trim();
if trimmed.is_empty() {
return false; }
for name in &tool_names {
if trimmed.starts_with(name) {
let rest = trimmed[name.len()..].trim_start();
if rest.starts_with('(') {
return false; }
}
}
true
})
.collect();
lines.join("\n").trim().to_string()
}
pub fn text_fallback(content: &str) -> ToolCallAction {
let cleaned = Self::strip_thinking(content);
let cleaned = Self::strip_leaked_tool_calls(&cleaned);
if cleaned.is_empty() {
ToolCallAction::Silence
} else {
ToolCallAction::Message { content: cleaned }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_strip_thinking_basic() {
let input = "<think reasoning>Hello</think reasoning>World";
assert_eq!(ResponseParser::strip_thinking(input), "World");
}
#[test]
fn test_strip_thinking_multiline() {
let input = "<think reasoning>\nLine 1\nLine 2\n</think reasoning>\nActual response";
assert_eq!(ResponseParser::strip_thinking(input), "Actual response");
}
#[test]
fn test_strip_thinking_no_tag() {
let input = "Just a normal response";
assert_eq!(
ResponseParser::strip_thinking(input),
"Just a normal response"
);
}
#[test]
fn test_strip_thinking_empty_after_strip() {
let input = "<think reasoning>internal</think reasoning>";
assert_eq!(ResponseParser::strip_thinking(input), "");
}
#[test]
fn test_strip_thinking_variant_no_space() {
let input = "<think reasoning>Hello\n</think reasoning>\n\nWorld";
assert_eq!(ResponseParser::strip_thinking(input), "World");
}
#[test]
fn test_strip_thinking_variant_with_space() {
let input = "<think >Hello</think >World";
assert_eq!(ResponseParser::strip_thinking(input), "World");
}
#[test]
fn test_strip_thinking_variant_with_type() {
let input = "<think type=\"deep\">Hello</think type=\"deep\">World";
assert_eq!(ResponseParser::strip_thinking(input), "World");
}
#[test]
fn test_strip_thinking_multiple_blocks() {
let input = "Start <think>think1</think> middle <think>think2</think> end";
assert_eq!(ResponseParser::strip_thinking(input), "Start middle end");
}
#[test]
fn test_parse_tool_calls_respond() {
let arguments_str = r#"{"text": "Hello!"}"#;
let tool_calls = vec![serde_json::json!({
"id": "call_1",
"type": "function",
"function": {
"name": "respond",
"arguments": arguments_str
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
assert_eq!(
action,
ToolCallAction::Message {
content: "Hello!".to_string()
}
);
}
#[test]
fn test_parse_tool_calls_silence() {
let tool_calls = vec![serde_json::json!({
"id": "call_1",
"type": "function",
"function": {
"name": "respond",
"arguments": "{\"silent\": true}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
assert_eq!(action, ToolCallAction::Silence);
}
#[test]
fn test_parse_tool_calls_exec() {
let tool_calls = vec![serde_json::json!({
"id": "call_exec_1",
"type": "function",
"function": {
"name": "exec",
"arguments": "{\"command\": \"rg 'fn main' /workspace --type rust -n\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
match action {
ToolCallAction::Exec { command, .. } => {
assert_eq!(command, "rg 'fn main' /workspace --type rust -n");
}
_ => panic!("Expected Exec, got {:?}", action),
}
}
#[test]
fn test_parse_tool_calls_exec_with_workdir() {
let tool_calls = vec![serde_json::json!({
"id": "call_exec_2",
"type": "function",
"function": {
"name": "exec",
"arguments": "{\"command\": \"ls -la\", \"workdir\": \"/workspace/src\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
match action {
ToolCallAction::Exec {
command, workdir, ..
} => {
assert_eq!(command, "ls -la");
assert_eq!(workdir, Some("/workspace/src".to_string()));
}
_ => panic!("Expected Exec"),
}
}
#[test]
fn test_parse_tool_calls_exec_empty_command() {
let tool_calls = vec![serde_json::json!({
"id": "call_exec_3",
"type": "function",
"function": {
"name": "exec",
"arguments": "{\"command\": \"\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls);
assert!(action.is_none());
}
#[test]
fn test_parse_tool_calls_read_file() {
let tool_calls = vec![serde_json::json!({
"id": "call_read_1",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"/workspace/skills/code_search/SKILL.md\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
match action {
ToolCallAction::ReadFile { path } => {
assert_eq!(path, "/workspace/skills/code_search/SKILL.md");
}
_ => panic!("Expected ReadFile, got {:?}", action),
}
}
#[test]
fn test_parse_tool_calls_read_file_empty_path() {
let tool_calls = vec![serde_json::json!({
"id": "call_read_2",
"type": "function",
"function": {
"name": "read_file",
"arguments": "{\"path\": \"\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls);
assert!(action.is_none());
}
#[test]
fn test_parse_tool_calls_skill_create() {
let tool_calls = vec![serde_json::json!({
"id": "call_3",
"type": "function",
"function": {
"name": "create_skill",
"arguments": "{\"skill_name\": \"checker\", \"description\": \"Check things\", \"body\": \"# Checker\\nBody\"}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls).unwrap();
match action {
ToolCallAction::SkillCreate {
skill_name,
description,
body,
} => {
assert_eq!(skill_name, "checker");
assert_eq!(description, "Check things");
assert!(body.contains("Checker"));
}
_ => panic!("Expected SkillCreate"),
}
}
#[test]
fn test_parse_tool_calls_empty() {
let action = ResponseParser::parse_tool_calls(&[]);
assert!(action.is_none());
}
#[test]
fn test_parse_tool_calls_unknown_function() {
let tool_calls = vec![serde_json::json!({
"id": "call_x",
"type": "function",
"function": {
"name": "unknown_func",
"arguments": "{}"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls);
assert!(action.is_none());
}
#[test]
fn test_parse_tool_calls_malformed_json() {
let tool_calls = vec![serde_json::json!({
"id": "call_1",
"type": "function",
"function": {
"name": "respond",
"arguments": "not valid json"
}
})];
let action = ResponseParser::parse_tool_calls(&tool_calls);
assert!(action.is_none());
}
#[test]
fn test_text_fallback_with_content() {
let action = ResponseParser::text_fallback("Hello world!");
assert_eq!(
action,
ToolCallAction::Message {
content: "Hello world!".to_string()
}
);
}
#[test]
fn test_text_fallback_empty() {
let action = ResponseParser::text_fallback("");
assert_eq!(action, ToolCallAction::Silence);
}
#[test]
fn test_text_fallback_with_thinking() {
let action = ResponseParser::text_fallback("<think>reasoning</think>Actual response");
assert_eq!(
action,
ToolCallAction::Message {
content: "Actual response".to_string()
}
);
}
#[test]
fn test_text_fallback_whitespace_only() {
let action = ResponseParser::text_fallback(" \n\t ");
assert_eq!(action, ToolCallAction::Silence);
}
#[test]
fn test_strip_leaked_respond_silent() {
let action = ResponseParser::text_fallback("respond(silent=true)");
assert_eq!(action, ToolCallAction::Silence);
}
#[test]
fn test_strip_leaked_respond_mixed() {
let action = ResponseParser::text_fallback("Hello!\nrespond(silent=true)");
assert_eq!(
action,
ToolCallAction::Message { content: "Hello!".to_string() }
);
}
#[test]
fn test_strip_leaked_exec() {
let action = ResponseParser::text_fallback("exec(\"ls -la\")");
assert_eq!(action, ToolCallAction::Silence);
}
#[test]
fn test_strip_leaked_preserves_normal_text() {
let action = ResponseParser::text_fallback("This is a normal response about executing code.");
assert_eq!(
action,
ToolCallAction::Message { content: "This is a normal response about executing code.".to_string() }
);
}
}