#[cfg(feature = "tools")]
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
#[cfg(feature = "tools")]
use crate::error::CoreResult;
#[derive(Debug, Clone, PartialEq)]
pub struct ParsedToolCall {
pub name: String,
pub arguments: serde_json::Value,
}
pub fn parse_tool_calls(text: &str) -> Vec<ParsedToolCall> {
let mut calls = Vec::new();
for block in fenced_blocks(text) {
if matches!(block.tag.as_str(), "tool_call" | "json") {
collect_calls(block.body, &mut calls);
}
}
if calls.is_empty() {
let trimmed = text.trim();
if trimmed.starts_with('{')
&& trimmed.contains("\"name\"")
&& trimmed.contains("\"arguments\"")
{
collect_calls(trimmed, &mut calls);
}
}
calls
}
fn collect_calls(snippet: &str, out: &mut Vec<ParsedToolCall>) {
let Ok(value) = serde_json::from_str::<serde_json::Value>(snippet.trim()) else {
return;
};
match value {
serde_json::Value::Array(items) => {
for item in items {
if let Some(call) = call_from_value(&item) {
out.push(call);
}
}
}
other => {
if let Some(call) = call_from_value(&other) {
out.push(call);
}
}
}
}
fn call_from_value(value: &serde_json::Value) -> Option<ParsedToolCall> {
let name = value.get("name")?.as_str()?.trim().to_string();
if name.is_empty() {
return None;
}
let arguments = value
.get("arguments")
.cloned()
.unwrap_or_else(|| serde_json::json!({}));
Some(ParsedToolCall { name, arguments })
}
struct FencedBlock<'a> {
tag: String,
body: &'a str,
}
fn fenced_blocks(text: &str) -> Vec<FencedBlock<'_>> {
let mut blocks = Vec::new();
let bytes = text.as_bytes();
let mut search = 0;
while let Some(rel) = text[search..].find("```") {
let open = search + rel;
let after_fence = open + 3;
let line_end = text[after_fence..]
.find('\n')
.map(|i| after_fence + i)
.unwrap_or(bytes.len());
let tag = text[after_fence..line_end].trim().to_ascii_lowercase();
let body_start = (line_end + 1).min(bytes.len());
let (body_end, next) = match text[body_start..].find("```") {
Some(i) => (body_start + i, body_start + i + 3),
None => (bytes.len(), bytes.len()),
};
blocks.push(FencedBlock {
tag,
body: &text[body_start..body_end],
});
search = next;
}
blocks
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolSchema {
pub name: String,
pub description: String,
pub parameters: serde_json::Value,
}
#[cfg(feature = "tools")]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ToolOutput {
pub content: String,
#[serde(default)]
pub details: serde_json::Value,
}
#[cfg(feature = "tools")]
pub type ToolUpdateCallback = Box<dyn FnMut(&str) + Send>;
#[cfg(feature = "tools")]
#[async_trait]
pub trait Tool: Send + Sync {
fn name(&self) -> &str;
fn label(&self) -> &str;
fn description(&self) -> &str;
fn parameters_schema(&self) -> serde_json::Value;
async fn execute(
&self,
tool_call_id: &str,
args: serde_json::Value,
on_update: Option<ToolUpdateCallback>,
) -> CoreResult<ToolOutput>;
fn schema(&self) -> ToolSchema {
ToolSchema {
name: self.name().to_string(),
description: self.description().to_string(),
parameters: self.parameters_schema(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn parses_fenced_tool_call() {
let text =
"Sure.\n```tool_call\n{\"name\": \"add\", \"arguments\": {\"a\": 2, \"b\": 3}}\n```";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "add");
assert_eq!(calls[0].arguments, json!({"a": 2, "b": 3}));
}
#[test]
fn parses_array_of_calls() {
let text = "```tool_call\n[{\"name\": \"a\", \"arguments\": {}}, {\"name\": \"b\", \"arguments\": {\"x\": 1}}]\n```";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "a");
assert_eq!(calls[1].name, "b");
assert_eq!(calls[1].arguments, json!({"x": 1}));
}
#[test]
fn parses_json_tagged_fence() {
let text = "```json\n{\"name\": \"now\", \"arguments\": {}}\n```";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "now");
}
#[test]
fn parses_bare_object_whole_message() {
let text = " {\"name\": \"now\", \"arguments\": {}} ";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "now");
}
#[test]
fn missing_arguments_defaults_to_empty_object() {
let text = "```tool_call\n{\"name\": \"now\"}\n```";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].arguments, json!({}));
}
#[test]
fn collects_multiple_fenced_blocks() {
let text = "```tool_call\n{\"name\": \"a\", \"arguments\": {}}\n```\nthen\n```tool_call\n{\"name\": \"b\", \"arguments\": {}}\n```";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 2);
}
#[test]
fn skips_malformed_and_nameless() {
let text = "```tool_call\nnot json at all\n```";
assert!(parse_tool_calls(text).is_empty());
let nameless = "```tool_call\n{\"arguments\": {}}\n```";
assert!(parse_tool_calls(nameless).is_empty());
}
#[test]
fn plain_prose_yields_no_calls() {
let text = "Here is some JSON you might use: it has a name field somewhere.";
assert!(parse_tool_calls(text).is_empty());
let answer = "{\"name\": \"Ada\", \"age\": 36}";
assert!(parse_tool_calls(answer).is_empty());
}
#[test]
fn handles_unterminated_fence() {
let text = "```tool_call\n{\"name\": \"now\", \"arguments\": {}}";
let calls = parse_tool_calls(text);
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "now");
}
}