mod bare;
mod fenced_json;
mod native_json;
mod streaming;
mod syntax;
mod tagged;
#[cfg(test)]
pub(crate) use bare::parse_bare_calls_in_body;
pub(crate) use fenced_json::parse_fenced_json_tool_calls;
#[cfg(test)]
pub(crate) use native_json::parse_native_json_tool_calls;
pub(crate) use streaming::StreamingToolCallDetector;
pub(crate) use syntax::ident_length;
pub(crate) use syntax::unescape_heredoc_body;
pub(crate) use syntax::unwrap_fully_wrapping_heredoc;
pub(crate) use syntax::{scan_heredoc, HeredocError};
pub(crate) use tagged::parse_text_tool_calls_with_tools;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum TextToolFormat {
Tagged,
FencedJson,
}
impl TextToolFormat {
pub(crate) fn from_option(tool_format: &str) -> Self {
match tool_format {
"json" => TextToolFormat::FencedJson,
_ => TextToolFormat::Tagged,
}
}
}
pub(crate) fn parse_text_tool_calls_in_format(
text: &str,
tools_val: Option<&crate::value::VmValue>,
format: TextToolFormat,
) -> TextToolParseResult {
match format {
TextToolFormat::Tagged => parse_text_tool_calls_with_tools(text, tools_val),
TextToolFormat::FencedJson => parse_fenced_json_tool_calls(text),
}
}
pub(crate) fn parse_text_tool_argument_payload(
text: &str,
name: &str,
) -> Result<serde_json::Value, String> {
let trimmed = text.trim();
if trimmed.is_empty() {
return Ok(serde_json::json!({}));
}
match syntax::parse_object_literal_from(trimmed, name) {
Ok((arguments, consumed)) if trimmed[consumed..].trim().is_empty() => Ok(arguments),
Ok((_arguments, consumed)) => Err(format!(
"trailing bytes after object literal argument at byte {consumed}"
)),
Err(object_error) => {
if let Some(name_len) = syntax::ident_length(trimmed.as_bytes()) {
if trimmed.as_bytes().get(name_len) == Some(&b'(') {
let call_name = trimmed[..name_len].to_string();
match syntax::parse_ts_call_from(trimmed, call_name) {
Ok((arguments, consumed)) if trimmed[consumed..].trim().is_empty() => {
return Ok(arguments);
}
Ok((_arguments, consumed)) => {
return Err(format!(
"trailing bytes after tool-call expression at byte {consumed}"
));
}
Err(call_error) => {
return Err(format!("{object_error}; {call_error}"));
}
}
}
}
Err(object_error)
}
}
}
pub(crate) struct TextToolParseResult {
pub calls: Vec<serde_json::Value>,
pub errors: Vec<String>,
pub prose: String,
pub user_response: Option<String>,
pub violations: Vec<String>,
pub done_marker: Option<String>,
pub canonical: String,
}
#[cfg(test)]
mod tests {
use super::parse_text_tool_argument_payload;
#[test]
fn text_tool_argument_payload_parses_object_literal_heredoc() {
let parsed = parse_text_tool_argument_payload(
r#"{ action: "create", path: "src/main.rs", content: <<EOF
fn main() {
println!("hello");
}
EOF
}"#,
"edit",
)
.expect("object literal payload parses");
assert_eq!(parsed["action"], serde_json::json!("create"));
assert_eq!(parsed["path"], serde_json::json!("src/main.rs"));
assert!(
parsed["content"]
.as_str()
.is_some_and(|content| content.contains("println!(\"hello\")")),
"content should come from the heredoc body: {parsed:?}"
);
}
#[test]
fn text_tool_argument_payload_parses_wrapped_call() {
let parsed = parse_text_tool_argument_payload(
r#"edit({ action: "replace_range", path: "src/lib.rs", range_start: 1, range_end: 2 })"#,
"edit",
)
.expect("wrapped call payload parses");
assert_eq!(parsed["action"], serde_json::json!("replace_range"));
assert_eq!(parsed["path"], serde_json::json!("src/lib.rs"));
assert_eq!(parsed["range_start"], serde_json::json!(1));
}
#[test]
fn text_tool_argument_payload_rejects_trailing_bytes() {
let error = parse_text_tool_argument_payload(
r#"{ action: "create", path: "src/main.rs" } trailing"#,
"edit",
)
.expect_err("trailing bytes should fail");
assert!(
error.contains("trailing bytes"),
"unexpected error: {error}"
);
}
}