const DSML_START: &[u8] = "<|DSML|tool_calls>".as_bytes();
const SSML_START: &[u8] = "<|SSML|tool_calls>".as_bytes();
const DSML_START_BAR: &[u8] = "<|DSML|tool_calls|>".as_bytes();
const SSML_START_BAR: &[u8] = "<|SSML|tool_calls|>".as_bytes();
const CLOSE_SCAN_HEAD: &[u8] = "</".as_bytes();
const DSML_BAR: &[u8] = "|".as_bytes();
pub(crate) const MARKER_NAMES: [&str; 2] = ["DSML", "SSML"];
pub(crate) fn tag_prefix_len(s: &[u8], closing: bool, name: &str) -> Option<usize> {
MARKER_NAMES.iter().find_map(|marker| {
tag_prefix_forms(marker, closing, name)
.into_iter()
.find_map(|form| segments_prefix_of(&form, s))
})
}
pub(crate) fn tag_prefix_partial(s: &[u8], closing: bool, name: &str) -> bool {
MARKER_NAMES.iter().any(|marker| {
tag_prefix_forms(marker, closing, name)
.iter()
.any(|form| is_prefix_of_segments(s, form))
})
}
fn tag_prefix_forms<'a>(marker: &'a str, closing: bool, name: &'a str) -> [[&'a [u8]; 6]; 2] {
let slash: &[u8] = if closing { b"/" } else { b"" };
let (marker, name) = (marker.as_bytes(), name.as_bytes());
[
[b"<", slash, DSML_BAR, marker, DSML_BAR, name],
[b"<", slash, marker, DSML_BAR, name, b""],
]
}
fn segments_prefix_of(segments: &[&[u8]], s: &[u8]) -> Option<usize> {
let mut at = 0;
for seg in segments {
if !s[at..].starts_with(seg) {
return None;
}
at += seg.len();
}
Some(at)
}
fn is_prefix_of_segments(s: &[u8], segments: &[&[u8]]) -> bool {
let mut rest = s;
for seg in segments {
if rest.len() < seg.len() {
return seg.starts_with(rest);
}
if !rest.starts_with(seg) {
return false;
}
rest = &rest[seg.len()..];
}
rest.is_empty()
}
#[must_use]
pub fn find_tool_start(s: &str) -> Option<usize> {
let mut forms: Vec<String> = vec!["<tool_calls>".to_owned()];
for m in MARKER_NAMES {
forms.push(format!("<|{m}|tool_calls>"));
forms.push(format!("<|{m}|tool_calls|>"));
forms.push(format!("<{m}|tool_calls>"));
forms.push(format!("<{m}|tool_calls|>"));
}
forms.iter().filter_map(|f| s.find(f.as_str())).min()
}
pub const TOOL_START_SCAN_HOLD: usize = 80;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ToolArg {
pub name: String,
pub value: String,
pub is_string: bool,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ToolCall {
pub name: String,
pub args: Vec<ToolArg>,
}
impl ToolCall {
pub fn arg_value(&self, name: impl AsRef<str>) -> Option<&str> {
let name = name.as_ref();
self.args
.iter()
.find(|a| a.name == name)
.map(|a| a.value.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum DsmlState {
#[default]
Search,
Structural,
ParamValue,
Done,
Error,
}
#[derive(Debug, Default)]
pub struct DsmlParser {
state: DsmlState,
search_tail: Vec<u8>,
raw: Vec<u8>,
parse_pos: usize,
current: Option<PendingCall>,
param_name: Option<String>,
param_is_string: bool,
param_value_start: usize,
param_elem: Option<String>,
param_close_prefix: bool,
calls: Vec<ToolCall>,
error: String,
}
#[derive(Debug, Default)]
struct PendingCall {
name: String,
args: Vec<ToolArg>,
}
fn is_prompt_placeholder(name: &str) -> bool {
matches!(name, "$TOOL_NAME" | "$PARAMETER_NAME" | "$PARAMETER_VALUE")
}
impl DsmlParser {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn state(&self) -> DsmlState {
self.state
}
#[must_use]
pub fn calls(&self) -> &[ToolCall] {
&self.calls
}
#[must_use]
pub fn error(&self) -> &str {
&self.error
}
#[must_use]
pub fn pending_call(&self) -> Option<ToolCall> {
self.current.as_ref().map(|c| ToolCall {
name: c.name.clone(),
args: c.args.clone(),
})
}
#[must_use]
pub fn raw(&self) -> &[u8] {
&self.raw
}
#[must_use]
pub fn param_close_prefix(&self) -> bool {
self.param_close_prefix
}
pub fn reset(&mut self) {
*self = Self::default();
}
pub fn feed(&mut self, s: impl AsRef<[u8]>) {
let s = s.as_ref();
if matches!(self.state, DsmlState::Done | DsmlState::Error) {
return;
}
for &c in s {
if self.state == DsmlState::Search {
if self.search_tail.len() == 64 {
self.search_tail.remove(0);
}
self.search_tail.push(c);
if [DSML_START, SSML_START, DSML_START_BAR, SSML_START_BAR]
.iter()
.any(|f| self.search_tail.ends_with(f))
{
self.start();
}
continue;
}
self.raw.push(c);
self.parse();
if self.state == DsmlState::ParamValue {
self.update_param_close_prefix();
} else {
self.param_close_prefix = false;
}
}
}
fn start(&mut self) {
self.state = DsmlState::Structural;
self.search_tail.clear();
self.raw.extend_from_slice(DSML_START);
self.parse_pos = DSML_START.len();
}
fn set_error(&mut self, msg: impl Into<String>) {
self.state = DsmlState::Error;
self.error = msg.into();
}
fn push_current(&mut self) {
if let Some(call) = self.current.take() {
self.calls.push(ToolCall {
name: call.name,
args: call.args,
});
}
}
fn parse(&mut self) {
loop {
match self.state {
DsmlState::ParamValue => {
let mut names: Vec<&str> = vec!["parameter"];
if let Some(elem) = self.param_elem.as_deref() {
names.push(elem);
names.push("invoke");
}
let Some((end, tag_len)) =
find_close_tag_any(&self.raw[self.param_value_start..], &names)
else {
return;
};
let value_bytes =
&self.raw[self.param_value_start..self.param_value_start + end];
let arg = ToolArg {
name: self.param_name.take().unwrap_or_default(),
value: String::from_utf8_lossy(value_bytes).into_owned(),
is_string: self.param_is_string,
};
self.current
.get_or_insert_with(Default::default)
.args
.push(arg);
self.param_close_prefix = false;
self.param_elem = None;
self.parse_pos = self.param_value_start + end + tag_len;
self.state = DsmlState::Structural;
}
DsmlState::Structural => {
while self.parse_pos < self.raw.len()
&& self.raw[self.parse_pos].is_ascii_whitespace()
{
self.parse_pos += 1;
}
if self.parse_pos >= self.raw.len() {
return;
}
let rest = &self.raw[self.parse_pos..];
if let Some(close_len) = close_tag_at(rest, "tool_calls") {
self.push_current();
self.parse_pos += close_len;
self.state = DsmlState::Done;
return;
}
if let Some(close_len) = close_tag_at(rest, "invoke") {
self.push_current();
self.parse_pos += close_len;
continue;
}
let Some(gt) = rest.iter().position(|&b| b == b'>') else {
return;
};
let tag_len = gt + 1;
let tag = String::from_utf8_lossy(&rest[..tag_len]).into_owned();
if !self.open_tag(&tag, tag_len) {
return;
}
}
_ => return,
}
}
}
fn open_tag(&mut self, tag: &str, tag_len: usize) -> bool {
if open_tag_is(tag, "tool_calls") {
self.parse_pos += tag_len;
return true;
}
if open_tag_is(tag, "invoke") {
let Some(name) = parse_attr(tag, "name") else {
self.set_error("tool invoke without name");
return false;
};
if is_prompt_placeholder(&name) {
self.set_error(format!(
"tool name is the prompt's placeholder {name}, not a real tool; substitute the actual tool name"
));
return false;
}
self.open_invoke(name, tag_len);
} else if open_tag_is(tag, "parameter") {
let Some(name) = parse_attr(tag, "name") else {
self.set_error("tool parameter without name");
return false;
};
if is_prompt_placeholder(&name) {
self.set_error(format!(
"parameter name is the prompt's placeholder {name}, not a real parameter; substitute the actual parameter name"
));
return false;
}
self.param_elem = None;
self.open_param(name, tag, tag_len);
} else if let Some(elem) = self.shorthand_param_name(tag) {
self.param_elem = Some(elem.clone());
self.open_param(elem, tag, tag_len);
} else if let Some(elem) = self.shorthand_invoke_name(tag) {
self.open_invoke(elem, tag_len);
} else {
let shown: String = tag.chars().take(80).collect();
self.set_error(format!("unexpected DSML tag: {shown}"));
return false;
}
true
}
fn open_invoke(&mut self, name: String, tag_len: usize) {
self.current = Some(PendingCall {
name,
args: Vec::new(),
});
self.parse_pos += tag_len;
}
fn open_param(&mut self, name: String, tag: &str, tag_len: usize) {
self.param_name = Some(name);
self.param_is_string = parse_attr(tag, "string").as_deref() == Some("true");
self.parse_pos += tag_len;
self.param_value_start = self.parse_pos;
self.param_close_prefix = false;
self.state = DsmlState::ParamValue;
}
fn shorthand_param_name(&self, tag: &str) -> Option<String> {
if self.current.is_none() || parse_attr(tag, "name").is_some() {
return None;
}
let elem = element_name(tag)?;
(!is_prompt_placeholder(&elem) && !Self::STRUCTURAL_ELEMS.contains(&elem.as_str()))
.then_some(elem)
}
const STRUCTURAL_ELEMS: [&'static str; 3] = ["tool_calls", "invoke", "parameter"];
fn shorthand_invoke_name(&self, tag: &str) -> Option<String> {
if self.current.is_some() || parse_attr(tag, "name").is_some() {
return None;
}
let elem = element_name(tag)?;
(!is_prompt_placeholder(&elem) && !Self::STRUCTURAL_ELEMS.contains(&elem.as_str()))
.then_some(elem)
}
fn update_param_close_prefix(&mut self) {
self.param_close_prefix = false;
if self.state != DsmlState::ParamValue || self.raw.len() <= self.param_value_start {
return;
}
let value = &self.raw[self.param_value_start..];
let Some(lt) = value.iter().rposition(|&b| b == b'<') else {
return;
};
let tail = &value[lt..];
if tail.len() > 64 || tag_prefix_len(tail, true, "").is_none() {
return;
}
let mut complete = false;
self.param_close_prefix = parameter_close_tail(tail, &mut complete) && !complete;
}
}
pub(crate) fn element_name(tag: &str) -> Option<String> {
let len = tag_prefix_len(tag.as_bytes(), false, "")?;
let name: String = tag[len..]
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || *c == '_')
.collect();
(!name.is_empty()).then_some(name)
}
fn open_tag_is(tag: &str, name: &str) -> bool {
let Some(len) = tag_prefix_len(tag.as_bytes(), false, name) else {
return false;
};
tag.as_bytes()
.get(len)
.is_some_and(|&c| c == b'>' || c.is_ascii_whitespace())
}
fn close_tag_at(s: &[u8], name: &str) -> Option<usize> {
let mut i = tag_prefix_len(s, true, name)?;
while i < s.len() && s[i].is_ascii_whitespace() {
i += 1;
}
if s[i..].starts_with(DSML_BAR) {
i += DSML_BAR.len();
}
while i < s.len() && s[i].is_ascii_whitespace() {
i += 1;
}
if s.get(i) != Some(&b'>') {
return None;
}
Some(i + 1)
}
fn find_close_tag_any(s: &[u8], names: &[&str]) -> Option<(usize, usize)> {
let mut from = 0;
while let Some(pos) = find_bytes(&s[from..], CLOSE_SCAN_HEAD) {
let at = from + pos;
if let Some(tag_len) = names.iter().find_map(|n| close_tag_at(&s[at..], n)) {
return Some((at, tag_len));
}
from = at + 1;
}
None
}
fn find_bytes(haystack: &[u8], needle: &[u8]) -> Option<usize> {
haystack.windows(needle.len()).position(|w| w == needle)
}
fn parameter_close_tail(tail: &[u8], complete: &mut bool) -> bool {
*complete = false;
if tag_prefix_partial(tail, true, "parameter") {
return true;
}
let Some(mut i) = tag_prefix_len(tail, true, "parameter") else {
return false;
};
while i < tail.len() && tail[i].is_ascii_whitespace() {
i += 1;
}
if i < tail.len() && tail.len() - i <= DSML_BAR.len() && DSML_BAR.starts_with(&tail[i..]) {
return true;
}
if tail[i..].starts_with(DSML_BAR) {
i += DSML_BAR.len();
}
while i < tail.len() {
if tail[i] == b'>' {
*complete = i == tail.len() - 1;
return *complete;
}
if !tail[i].is_ascii_whitespace() {
return false;
}
i += 1;
}
true
}
fn parse_attr(tag: &str, name: &str) -> Option<String> {
let pat = format!("{name}=\"");
let start = tag.find(&pat)? + pat.len();
let end = tag[start..].find('"')? + start;
Some(tag[start..end].to_string())
}
#[cfg(test)]
mod tests {
use super::*;
const STANZA: &str = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"read_file\">",
"<|DSML|parameter name=\"path\" string=\"true\">src/main.rs</|DSML|parameter|>",
"<|DSML|parameter name=\"offset\">42</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
fn feed_all(p: &mut DsmlParser, s: &str) {
p.feed(s.as_bytes());
}
fn feed_bytewise(p: &mut DsmlParser, s: &str) {
for b in s.as_bytes() {
p.feed([*b]);
}
}
#[test]
fn opener_tolerates_trailing_bar() {
let stanza = STANZA.replacen("<|DSML|tool_calls>", "<|DSML|tool_calls|>", 1);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, &stanza);
assert_eq!(p.state(), super::DsmlState::Done);
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "read_file");
assert_eq!(p.calls()[0].arg_value("path"), Some("src/main.rs"));
}
assert_eq!(super::find_tool_start(&stanza), Some(0));
}
#[test]
fn shorthand_parameter_element_is_executed() {
let stanza = concat!(
"<|DSML|tool_calls|>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|command string=\"true\">cd /tmp && ls</|DSML|invoke>",
"</|DSML|invoke>",
"</|DSML|tool_calls|>",
);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, stanza);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "bash");
assert_eq!(p.calls()[0].arg_value("command"), Some("cd /tmp && ls"));
assert!(p.calls()[0].args[0].is_string);
}
}
#[test]
fn shorthand_invoke_element_is_executed() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|edit>",
"<|DSML|parameter name=\"path\" string=\"true\">/tmp/a.rs</|DSML|parameter>",
"<|DSML|parameter name=\"old\" string=\"true\">one</|DSML|parameter>",
"<|DSML|parameter name=\"new\" string=\"true\">two</|DSML|parameter>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, stanza);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "edit");
assert_eq!(p.calls()[0].arg_value("path"), Some("/tmp/a.rs"));
assert_eq!(p.calls()[0].arg_value("old"), Some("one"));
assert_eq!(p.calls()[0].arg_value("new"), Some("two"));
}
}
#[test]
fn a_repeated_wrapper_opener_is_skipped_not_named() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\" string=\"true\">ls -la</|DSML|parameter>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, stanza);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls().len(), 1, "{:?}", p.calls());
assert_eq!(p.calls()[0].name, "bash");
assert_eq!(p.calls()[0].arg_value("command"), Some("ls -la"));
}
let no_invoke = concat!(
"<|DSML|tool_calls>",
"<|DSML|tool_calls>",
"<|DSML|parameter name=\"command\" string=\"true\">ls</|DSML|parameter>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, no_invoke);
assert!(
p.calls().iter().all(|c| c.name != "tool_calls"),
"the wrapper name must never become a tool: {:?}",
p.calls()
);
}
}
#[test]
fn the_folded_attribute_shape_errors_without_fabricating_a_call() {
let stanza = concat!(
"<|DSML|tool_calls>\n",
"<|DSML|tool_calls>\n",
"<|DSML|tool name=\"edit\">\n",
"<|DSML|tool path=\"/tmp/lib.rs\">\n",
"<|DSML|tool old=\"true\">OLD TEXT</|DSML|tool>\n",
"</|DSML|invoke>\n",
"</|DSML|tool_calls>",
);
for feed in [feed_all as fn(&mut DsmlParser, &str), feed_bytewise] {
let mut p = super::DsmlParser::new();
feed(&mut p, stanza);
assert_eq!(p.state(), super::DsmlState::Error, "{:?}", p.calls());
assert!(
p.error().starts_with("unexpected DSML tag:"),
"{}",
p.error()
);
assert!(
p.calls().iter().all(|c| c.name != "tool_calls"),
"{:?}",
p.calls()
);
assert!(
p.calls().iter().all(|c| c.arg_value("old") != Some("true")),
"`old=\"true\"` is a string flag, never the old text: {:?}",
p.calls()
);
}
}
#[test]
fn structural_elements_are_never_names() {
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls><|DSML|tool_calls>\
<|DSML|parameter name=\"command\">ls</|DSML|parameter>",
);
assert!(
p.calls().iter().all(|c| c.name != "tool_calls"),
"{:?}",
p.calls()
);
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls><|DSML|invoke name=\"bash\"><|DSML|tool_calls>\
<|DSML|parameter name=\"command\" string=\"true\">ls</|DSML|parameter>\
</|DSML|invoke></|DSML|tool_calls>",
);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "bash");
assert!(
p.calls()[0].args.iter().all(|a| a.name != "tool_calls"),
"{:?}",
p.calls()[0].args
);
assert_eq!(p.calls()[0].arg_value("command"), Some("ls"));
for (text, want) in [
(
"<|DSML|tool_calls><|DSML|invoke>",
"tool invoke without name",
),
(
"<|DSML|tool_calls><|DSML|invoke name=\"bash\"><|DSML|parameter>",
"tool parameter without name",
),
] {
let mut p = super::DsmlParser::new();
feed_all(&mut p, text);
assert_eq!(p.state(), super::DsmlState::Error, "{text}");
assert_eq!(p.error(), want, "{text}");
}
}
#[test]
fn bare_elements_are_tool_then_parameter_names() {
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls>\
<|DSML|bash>\
<|DSML|command string=\"true\">ls -la</|DSML|command>\
</|DSML|invoke>\
</|DSML|tool_calls>",
);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "bash");
assert_eq!(p.calls()[0].arg_value("command"), Some("ls -la"));
}
#[test]
fn shorthand_parameter_closed_by_its_own_element() {
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls>\
<|DSML|invoke name=\"read\">\
<|DSML|path string=\"true\">src/main.rs</|DSML|path>\
</|DSML|invoke>\
</|DSML|tool_calls>",
);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls()[0].arg_value("path"), Some("src/main.rs"));
}
#[test]
fn canonical_parameter_value_is_not_truncated_by_a_foreign_close_tag() {
let content = "docs mentioning </|DSML|invoke> and </|DSML|command> inline";
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
&format!(
"<|DSML|tool_calls>\
<|DSML|invoke name=\"write\">\
<|DSML|parameter name=\"content\" string=\"true\">{content}</|DSML|parameter|>\
</|DSML|invoke|>\
</|DSML|tool_calls|>"
),
);
assert_eq!(p.state(), super::DsmlState::Done, "{}", p.error());
assert_eq!(p.calls()[0].arg_value("content"), Some(content));
}
#[test]
fn unknown_element_with_a_name_attribute_still_errors() {
let mut p = super::DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls><|DSML|invoke name=\"bash\">\
<|DSML|argument name=\"command\">ls</|DSML|argument>",
);
assert_eq!(p.state(), super::DsmlState::Error);
assert!(
p.error().starts_with("unexpected DSML tag:"),
"{}",
p.error()
);
}
#[test]
fn unexpected_tag_outside_an_invoke_keeps_the_plain_error() {
let mut p = super::DsmlParser::new();
feed_all(&mut p, "<|DSML|tool_calls><b>");
assert_eq!(p.state(), super::DsmlState::Error);
assert_eq!(p.error(), "unexpected DSML tag: <b>");
}
#[test]
fn placeholder_tool_name_is_named_as_such() {
let mut p = super::DsmlParser::new();
p.feed("<|DSML|tool_calls><|DSML|invoke name=\"$TOOL_NAME\">".as_bytes());
assert_eq!(p.state(), super::DsmlState::Error);
assert_eq!(
p.error(),
"tool name is the prompt's placeholder $TOOL_NAME, not a real tool; substitute the actual tool name"
);
}
#[test]
fn placeholder_parameter_name_is_named_as_such() {
let mut p = super::DsmlParser::new();
p.feed(
"<|DSML|tool_calls><|DSML|invoke name=\"bash\">\
<|DSML|parameter name=\"$PARAMETER_NAME\" string=\"true\">x"
.as_bytes(),
);
assert_eq!(p.state(), super::DsmlState::Error);
assert_eq!(
p.error(),
"parameter name is the prompt's placeholder $PARAMETER_NAME, not a real parameter; substitute the actual parameter name"
);
}
#[test]
fn dollar_inside_a_name_is_not_a_placeholder() {
for name in ["we$rd", "$path"] {
let mut p = super::DsmlParser::new();
p.feed(
format!(
"<|DSML|tool_calls><|DSML|invoke name=\"{name}\">\
</|DSML|invoke|></|DSML|tool_calls|>"
)
.as_bytes(),
);
assert_eq!(p.state(), super::DsmlState::Done, "error: {}", p.error());
assert_eq!(p.calls()[0].name, name);
}
}
#[test]
fn ssml_alias_parses_like_dsml() {
let ssml = STANZA.replace("DSML", "SSML");
let mixed = STANZA.replacen("DSML", "SSML", 2);
for text in [ssml.as_str(), mixed.as_str()] {
for mut p in [DsmlParser::new(), DsmlParser::new()] {
feed_all(&mut p, text);
assert_eq!(p.state(), DsmlState::Done, "{text:?}");
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].name, "read_file");
assert_eq!(p.calls()[0].arg_value("path"), Some("src/main.rs"));
assert_eq!(p.calls()[0].arg_value("offset"), Some("42"));
assert!(!p.raw().is_empty());
}
let mut p = DsmlParser::new();
feed_bytewise(&mut p, text);
assert_eq!(p.state(), DsmlState::Done, "bytewise {text:?}");
assert_eq!(p.calls()[0].arg_value("path"), Some("src/main.rs"));
}
}
#[test]
fn other_marker_names_do_not_open_a_stanza() {
let mut p = DsmlParser::new();
feed_all(&mut p, &STANZA.replace("DSML", "XSML"));
assert_eq!(p.state(), DsmlState::Search);
assert!(p.calls().is_empty());
}
#[test]
fn parses_full_stanza() {
let mut p = DsmlParser::new();
feed_all(&mut p, STANZA);
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(p.calls().len(), 1);
let call = &p.calls()[0];
assert_eq!(call.name, "read_file");
assert_eq!(call.arg_value("path"), Some("src/main.rs"));
assert_eq!(call.arg_value("offset"), Some("42"));
assert_eq!(call.arg_value("missing"), None);
assert!(call.args[0].is_string);
assert!(!call.args[1].is_string);
}
#[test]
fn parses_bytewise_identically() {
let mut p = DsmlParser::new();
feed_bytewise(&mut p, STANZA);
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].arg_value("path"), Some("src/main.rs"));
}
#[test]
fn find_tool_start_matches_every_accepted_wrapper_form() {
for form in [
"<|DSML|tool_calls>",
"<DSML|tool_calls>",
"<|SSML|tool_calls>",
"<tool_calls>",
] {
let text = format!("prose {form} rest");
assert_eq!(
super::find_tool_start(&text),
Some("prose ".len()),
"{form}"
);
}
}
#[test]
fn find_tool_start_ignores_partial_and_bare_invoke() {
assert_eq!(super::find_tool_start("<"), None);
assert_eq!(super::find_tool_start("<|DSML|tool_call"), None);
assert_eq!(super::find_tool_start("<|DSML|invoke name=\"a\">"), None);
}
#[test]
fn skips_leading_prose_before_marker() {
let mut p = DsmlParser::new();
feed_all(&mut p, "Some thinking text first. ");
assert_eq!(p.state(), DsmlState::Search);
feed_all(&mut p, STANZA);
assert_eq!(p.state(), DsmlState::Done);
}
#[test]
fn incomplete_input_stays_pending() {
let mut p = DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls><|DSML|invoke name=\"bash\"><|DSML|parameter name=\"command\">ls -la",
);
assert_eq!(p.state(), DsmlState::ParamValue);
assert!(p.calls().is_empty());
}
#[test]
fn close_tag_variants_accepted() {
let s = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"t\">",
"<|DSML|parameter name=\"a\">v</|DSML|parameter >",
"</|DSML|invoke | >",
"</|DSML|tool_calls>",
);
let mut p = DsmlParser::new();
feed_all(&mut p, s);
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(p.calls()[0].arg_value("a"), Some("v"));
}
#[test]
fn literal_close_bytes_in_param_value_do_not_terminate_it() {
let html = "<div>hi</div></p> see </parameter> too";
let s = format!(
concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"write\">",
"<|DSML|parameter name=\"content\" string=\"true\">{html}</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
),
html = html
);
let mut p = DsmlParser::new();
feed_all(&mut p, &s);
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(p.calls().len(), 1);
assert_eq!(p.calls()[0].arg_value("content"), Some(html));
}
#[test]
fn multiple_invokes() {
let s = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"a\"></|DSML|invoke|>",
"<|DSML|invoke name=\"b\"></|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
let mut p = DsmlParser::new();
feed_all(&mut p, s);
assert_eq!(p.state(), DsmlState::Done);
let names: Vec<_> = p.calls().iter().map(|c| c.name.as_str()).collect();
assert_eq!(names, ["a", "b"]);
}
#[test]
fn invoke_without_name_errors() {
let mut p = DsmlParser::new();
feed_all(&mut p, "<|DSML|tool_calls><|DSML|invoke>");
assert_eq!(p.state(), DsmlState::Error);
assert_eq!(p.error(), "tool invoke without name");
}
#[test]
fn unexpected_tag_errors() {
let mut p = DsmlParser::new();
feed_all(&mut p, "<|DSML|tool_calls><b>");
assert_eq!(p.state(), DsmlState::Error);
assert!(p.error().starts_with("unexpected DSML tag:"));
}
#[test]
fn param_value_may_contain_angle_brackets() {
let s = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"write\">",
"<|DSML|parameter name=\"content\">if a < b { x > y }</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
let mut p = DsmlParser::new();
feed_all(&mut p, s);
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(
p.calls()[0].arg_value("content"),
Some("if a < b { x > y }")
);
}
#[test]
fn param_close_prefix_tracks_partial_close_tag() {
let mut p = DsmlParser::new();
feed_all(
&mut p,
"<|DSML|tool_calls><|DSML|invoke name=\"t\"><|DSML|parameter name=\"a\">v",
);
assert!(!p.param_close_prefix());
feed_all(&mut p, "</|DSML|parameter");
assert!(p.param_close_prefix());
feed_all(&mut p, "|>");
assert!(!p.param_close_prefix());
assert_eq!(p.state(), DsmlState::Structural);
}
#[test]
fn reset_returns_to_search() {
let mut p = DsmlParser::new();
feed_all(&mut p, STANZA);
p.reset();
assert_eq!(p.state(), DsmlState::Search);
assert!(p.calls().is_empty());
feed_all(&mut p, STANZA);
assert_eq!(p.state(), DsmlState::Done);
}
#[test]
fn ignores_input_after_done() {
let mut p = DsmlParser::new();
feed_all(&mut p, STANZA);
feed_all(&mut p, "trailing garbage <b>");
assert_eq!(p.state(), DsmlState::Done);
assert_eq!(p.calls().len(), 1);
}
#[test]
fn inner_tags_tolerate_the_dropped_leading_bar() {
let mut p = super::DsmlParser::new();
p.feed(
"<|DSML|tool_calls><DSML|invoke name=\"bash\">\
<DSML|parameter name=\"command\" string=\"true\">ls</DSML|parameter|>\
</DSML|invoke|></|DSML|tool_calls|>"
.as_bytes(),
);
assert_eq!(p.state(), super::DsmlState::Done, "error: {}", p.error());
let calls = p.calls();
assert_eq!(calls.len(), 1);
assert_eq!(calls[0].name, "bash");
assert_eq!(calls[0].arg_value("command"), Some("ls"));
}
#[test]
fn canonical_inner_tags_still_parse() {
let mut p = super::DsmlParser::new();
p.feed(
"<|DSML|tool_calls><|DSML|invoke name=\"bash\">\
<|DSML|parameter name=\"command\" string=\"true\">ls</|DSML|parameter|>\
</|DSML|invoke|></|DSML|tool_calls|>"
.as_bytes(),
);
assert_eq!(p.state(), super::DsmlState::Done, "error: {}", p.error());
assert_eq!(p.calls()[0].arg_value("command"), Some("ls"));
}
}