use crate::dsml::{
DsmlParser, DsmlState, MARKER_NAMES, ToolCall, tag_prefix_len, tag_prefix_partial,
};
pub const IN_THINK_PROHIBITION: &str =
"Tool calls are not allowed inside <think></think>; finish thinking before emitting DSML.";
const DSML_START: &[u8] = "<|DSML|tool_calls>".as_bytes();
const CANONICAL_INVOKE: &[u8] = "<|DSML|invoke".as_bytes();
const DSML_BAR: &[u8] = "|".as_bytes();
const THINK_OPEN: &[u8] = b"<think>";
const THINK_CLOSE: &[u8] = b"</think>";
fn logging_enabled_for(opt_out: Option<&std::ffi::OsStr>) -> bool {
opt_out.is_none_or(|v| v != "1")
}
fn tool_error_logging_enabled() -> bool {
if cfg!(test) {
return false;
}
logging_enabled_for(std::env::var_os("PLANK_NO_TOOL_ERROR_LOG").as_deref())
}
fn log_tool_error(reason: &str, raw: &[u8]) {
use std::io::Write;
if !tool_error_logging_enabled() {
return;
}
let Some(home) = std::env::var_os("HOME").filter(|h| !h.is_empty()) else {
return;
};
let dir = std::path::PathBuf::from(home).join(".plank");
if std::fs::create_dir_all(&dir).is_err() {
return;
}
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
let snippet = String::from_utf8_lossy(raw);
if let Ok(mut f) = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(dir.join("tool-call-errors.log"))
{
let record = format!("[{secs}] {reason}\n---\n{snippet}\n===\n");
let _ = f.write_all(record.as_bytes());
}
}
pub trait RenderSink {
fn visible_text(&mut self, text: &str);
fn think_text(&mut self, text: &str);
fn tool_text(&mut self, text: &str) {
self.visible_text(text);
}
fn error_text(&mut self, text: &str) {
self.tool_text(text);
}
}
impl RenderSink for Box<dyn RenderSink> {
fn visible_text(&mut self, text: &str) {
(**self).visible_text(text);
}
fn think_text(&mut self, text: &str) {
(**self).think_text(text);
}
fn tool_text(&mut self, text: &str) {
(**self).tool_text(text);
}
fn error_text(&mut self, text: &str) {
(**self).error_text(text);
}
}
impl RenderSink for Box<dyn RenderSink + Send> {
fn visible_text(&mut self, text: &str) {
(**self).visible_text(text);
}
fn think_text(&mut self, text: &str) {
(**self).think_text(text);
}
fn tool_text(&mut self, text: &str) {
(**self).tool_text(text);
}
fn error_text(&mut self, text: &str) {
(**self).error_text(text);
}
}
#[derive(Debug, Clone, Default)]
pub struct CollectSink(pub std::sync::Arc<std::sync::Mutex<String>>);
impl CollectSink {
#[must_use]
pub fn take(&self) -> String {
let mut guard = self
.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
std::mem::take(&mut *guard)
}
fn push(&mut self, text: &str) {
self.0
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
.push_str(text);
}
}
impl RenderSink for CollectSink {
fn visible_text(&mut self, text: &str) {
self.push(text);
}
fn think_text(&mut self, _text: &str) {}
fn tool_text(&mut self, text: &str) {
self.push(text);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum ParamKind {
#[default]
Normal,
Path,
Content,
DiffOld,
DiffNew,
BashCommand,
}
fn param_kind_for(tool: &str, param: &str) -> ParamKind {
match (tool, param) {
("bash", "command") => ParamKind::BashCommand,
("edit", "old") => ParamKind::DiffOld,
("edit", "new") => ParamKind::DiffNew,
(_, "path" | "file" | "filename") => ParamKind::Path,
(_, "content" | "text") => ParamKind::Content,
_ => ParamKind::Normal,
}
}
fn tool_prefix(name: &str) -> Option<&'static str> {
match name {
"bash" => Some("$ "),
"read" => Some("read "),
"write" => Some("write "),
"edit" => Some("edit "),
"search" => Some("search "),
"google_search" => Some("google "),
"visit_page" => Some("visit "),
name if name.starts_with("mcp_") => Some("mcp "),
_ => None,
}
}
fn diff_prefix(kind: ParamKind) -> Option<&'static str> {
match kind {
ParamKind::DiffOld => Some("- "),
ParamKind::DiffNew => Some("+ "),
_ => None,
}
}
fn parse_bool_default(s: &str, default: bool) -> bool {
if s.is_empty() {
return default;
}
if s.eq_ignore_ascii_case("true") || s.eq_ignore_ascii_case("yes") || s == "1" {
return true;
}
if s.eq_ignore_ascii_case("false") || s.eq_ignore_ascii_case("no") || s == "0" {
return false;
}
default
}
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())
}
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 dsml_start_match(tail: &[u8], complete: &mut bool, implicit_invoke: &mut bool) -> bool {
*complete = false;
*implicit_invoke = false;
let forms = MARKER_NAMES.iter().flat_map(|m| {
[
(format!("<|{m}|tool_calls>"), false),
(format!("<|{m}|tool_calls|>"), false),
(format!("<{m}|tool_calls>"), false),
(format!("<{m}|tool_calls|>"), false),
(format!("<|{m}|invoke"), true),
(format!("<{m}|invoke"), true),
]
});
for (form, implicit) in forms {
let form = form.as_bytes();
if tail.len() <= form.len() && form[..tail.len()] == *tail {
*complete = tail.len() == form.len();
*implicit_invoke = implicit;
return true;
}
}
false
}
#[derive(Debug, Default)]
struct MarkerDetector {
tail: Vec<u8>,
}
impl MarkerDetector {
const CAP: usize = 32;
fn feed(&mut self, c: u8) -> bool {
if self.tail.len() == Self::CAP {
self.tail.remove(0);
}
self.tail.push(c);
MARKER_NAMES.iter().any(|m| {
[
format!("|{m}|"),
format!("|{m}|"),
format!("<{m}|"),
format!("</{m}|"),
]
.iter()
.any(|n| self.tail.ends_with(n.as_bytes()))
})
}
}
const GENERIC_PSEUDO_OPENERS: [&str; 3] = ["<tool_call>", "<function_call>", "<invoke "];
#[derive(Debug, Default)]
struct PseudoToolDetector {
line: Vec<u8>,
in_fence: bool,
tool_names: Vec<String>,
fired: bool,
}
impl PseudoToolDetector {
const CAP: usize = 96;
fn set_tool_names(&mut self, names: Vec<String>) {
self.tool_names = names;
}
fn reset_line(&mut self) {
self.line.clear();
}
fn feed(&mut self, c: u8, in_think: bool) -> Option<String> {
if c == b'\n' {
let hit = self.match_line(in_think);
if self.line.starts_with(b"```") {
self.in_fence = !self.in_fence;
}
self.line.clear();
return hit;
}
if self.line.len() < Self::CAP {
self.line.push(c);
}
None
}
fn finish_line(&mut self, in_think: bool) -> Option<String> {
let hit = self.match_line(in_think);
self.line.clear();
hit
}
fn match_line(&mut self, in_think: bool) -> Option<String> {
if in_think || self.fired || self.in_fence {
return None;
}
let trimmed = self.line.trim_ascii();
if !trimmed.starts_with(b"<") {
return None;
}
let text = std::str::from_utf8(trimmed).ok()?;
let matched = GENERIC_PSEUDO_OPENERS.iter().any(|opener| {
if opener.ends_with(' ') {
text.starts_with(*opener) && is_lone_tag(text)
} else {
text == *opener
}
}) || self.tool_names.iter().any(|name| {
text.strip_prefix('<')
.and_then(|t| t.strip_suffix('>'))
.is_some_and(|inner| inner == name)
});
if !matched {
return None;
}
self.fired = true;
Some(text.to_string())
}
}
fn is_lone_tag(text: &str) -> bool {
text.ends_with('>') && !text[..text.len() - 1].contains('>')
}
#[derive(Debug, Default)]
enum DsmlScan {
#[default]
Between,
Tag(Vec<u8>),
Value,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug, Default)]
struct ToolViz {
active: bool,
tool_announced: bool,
param_active: bool,
at_line_start: bool,
param_kind: ParamKind,
tool_name: String,
param_name: String,
param_end_tail: Vec<u8>,
read_style: bool,
read_prefix_rendered: bool,
read_line_rendered: bool,
read_path: String,
read_start: String,
read_max: String,
read_whole: String,
code_param_active: bool,
write_path: String,
write_is_create: bool,
}
impl ToolViz {
const END_TAIL_CAP: usize = 64;
}
#[derive(Debug, Clone, Copy)]
pub struct Finished<'a> {
pub calls: &'a [ToolCall],
pub error: Option<&'a str>,
pub dsml_in_think: bool,
pub in_think_rejected: bool,
pub ended_in_think: bool,
}
#[allow(clippy::struct_excessive_bools)]
#[derive(Debug)]
pub struct StreamRenderer<S> {
sink: S,
parser: DsmlParser,
viz: ToolViz,
scan: DsmlScan,
in_think: bool,
dsml_active: bool,
dsml_ignored: bool,
pending: Vec<u8>,
dsml_start_tail: Vec<u8>,
plain_dsml: MarkerDetector,
think_dsml: MarkerDetector,
pseudo_tool: PseudoToolDetector,
dsml_in_think: bool,
dsml_in_think_reported: bool,
think_close_swallowed: bool,
in_think_rejected: bool,
pseudo_tool_fired: bool,
post_think_gap: bool,
stream_error: Option<String>,
last_output_newline: bool,
calls: Vec<ToolCall>,
vis_carry: Vec<u8>,
think_carry: Vec<u8>,
preflight: Preflight,
preflight_error: Option<String>,
show_tool_calls: bool,
show_thinking: bool,
thinking_tool_calls: bool,
replay: bool,
}
type PreflightFn = Box<dyn FnMut(&ToolCall) -> Result<(), String>>;
#[derive(Default)]
struct Preflight(Option<PreflightFn>);
impl std::fmt::Debug for Preflight {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(if self.0.is_some() {
"Preflight(set)"
} else {
"Preflight(unset)"
})
}
}
const DSML_START_TAIL_CAP: usize = 64;
impl<S: RenderSink> StreamRenderer<S> {
pub fn new(sink: S) -> Self {
Self {
sink,
parser: DsmlParser::new(),
viz: ToolViz::default(),
scan: DsmlScan::Between,
in_think: false,
dsml_active: false,
dsml_ignored: false,
pending: Vec::new(),
dsml_start_tail: Vec::new(),
plain_dsml: MarkerDetector::default(),
think_dsml: MarkerDetector::default(),
pseudo_tool: PseudoToolDetector::default(),
dsml_in_think: false,
dsml_in_think_reported: false,
think_close_swallowed: false,
in_think_rejected: false,
pseudo_tool_fired: false,
post_think_gap: false,
stream_error: None,
last_output_newline: true,
calls: Vec::new(),
vis_carry: Vec::new(),
think_carry: Vec::new(),
preflight: Preflight(None),
preflight_error: None,
show_tool_calls: true,
show_thinking: true,
thinking_tool_calls: false,
replay: false,
}
}
pub fn set_replay(&mut self, replay: bool) {
self.replay = replay;
}
pub fn set_tool_names(&mut self, names: Vec<String>) {
self.pseudo_tool.set_tool_names(names);
}
pub fn set_show_tool_calls(&mut self, show: bool) {
self.show_tool_calls = show;
}
pub fn set_show_thinking(&mut self, show: bool) {
self.show_thinking = show;
}
pub fn set_thinking_tool_calls(&mut self, allow: bool) {
self.thinking_tool_calls = allow;
}
pub fn set_preflight(&mut self, f: impl FnMut(&ToolCall) -> Result<(), String> + 'static) {
self.preflight = Preflight(Some(Box::new(f)));
}
#[must_use]
pub fn preflight_error(&self) -> Option<&str> {
self.preflight_error.as_deref()
}
pub fn begin_in_think(&mut self) {
self.in_think = true;
}
pub fn push(&mut self, text: impl AsRef<str>) {
self.stream_text(text.as_ref().as_bytes(), false);
}
pub fn finish(&mut self) {
self.stream_text(b"", true);
self.flush_carry();
self.flush_pseudo_tool();
}
#[must_use]
pub fn finished(&self) -> Finished<'_> {
let in_think = (self.in_think_rejected || self.dsml_ignored) && !self.pseudo_tool_fired;
let error = self
.stream_error
.as_deref()
.filter(|_| !in_think)
.or_else(|| in_think.then_some(IN_THINK_PROHIBITION))
.or_else(|| (self.parser.state() == DsmlState::Error).then(|| self.parser.error()))
.or_else(|| {
matches!(
self.parser.state(),
DsmlState::Structural | DsmlState::ParamValue
)
.then_some("incomplete DSML tool call")
});
Finished {
calls: &self.calls,
error,
dsml_in_think: self.dsml_in_think,
in_think_rejected: in_think,
ended_in_think: self.in_think,
}
}
#[must_use]
pub fn wants_greedy_sampling(&self) -> bool {
if matches!(self.parser.state(), DsmlState::Error | DsmlState::Done) {
return false;
}
if self.dsml_start_tail.len() > 1 {
return true;
}
if !self.dsml_active {
return false;
}
match self.parser.state() {
DsmlState::Structural => true,
DsmlState::ParamValue => self.parser.param_close_prefix(),
_ => false,
}
}
pub fn sink(&self) -> &S {
&self.sink
}
pub fn sink_mut(&mut self) -> &mut S {
&mut self.sink
}
pub fn into_sink(self) -> S {
self.sink
}
fn flush_stream(sink_write: impl FnOnce(&mut S, &str), sink: &mut S, carry: &mut Vec<u8>) {
if carry.is_empty() {
return;
}
match std::str::from_utf8(carry) {
Ok(s) => {
sink_write(sink, s);
carry.clear();
}
Err(e) if e.error_len().is_none() && e.valid_up_to() > 0 => {
let tail = carry.split_off(e.valid_up_to());
sink_write(sink, std::str::from_utf8(carry).unwrap_or_default());
*carry = tail;
}
Err(e) if e.error_len().is_none() => {}
Err(_) => {
let s = String::from_utf8_lossy(carry).into_owned();
sink_write(sink, &s);
carry.clear();
}
}
}
fn emit_visible_bytes(&mut self, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
if self.viz.active && !self.show_tool_calls {
return;
}
self.last_output_newline = bytes.last() == Some(&b'\n');
self.vis_carry.extend_from_slice(bytes);
let write = if self.viz.active {
S::tool_text as fn(&mut S, &str)
} else {
S::visible_text
};
Self::flush_stream(write, &mut self.sink, &mut self.vis_carry);
}
fn emit_think_bytes(&mut self, bytes: &[u8]) {
if !self.show_thinking {
return;
}
self.think_carry.extend_from_slice(bytes);
Self::flush_stream(S::think_text, &mut self.sink, &mut self.think_carry);
}
fn emit_preview_bytes(&mut self, bytes: &[u8]) {
if bytes.is_empty() {
return;
}
self.last_output_newline = bytes.last() == Some(&b'\n');
self.think_carry.extend_from_slice(bytes);
Self::flush_stream(S::think_text, &mut self.sink, &mut self.think_carry);
}
fn viz_preview_puts(&mut self, s: &str) {
self.emit_preview_bytes(s.as_bytes());
}
fn viz_is_write_preview(&self) -> bool {
self.viz.tool_name == "write" && self.viz.param_kind == ParamKind::Content
}
fn flush_carry(&mut self) {
for (write, carry) in [
(S::visible_text as fn(&mut S, &str), &mut self.vis_carry),
(S::think_text, &mut self.think_carry),
] {
if !carry.is_empty() {
let s = String::from_utf8_lossy(carry).into_owned();
write(&mut self.sink, &s);
carry.clear();
}
}
}
fn write_char(&mut self, c: u8) {
if self.in_think {
self.emit_think_bytes(&[c]);
} else {
self.emit_visible_bytes(&[c]);
}
}
fn viz_puts(&mut self, s: &str) {
self.emit_visible_bytes(s.as_bytes());
}
fn viz_error_puts(&mut self, s: &str) {
if s.is_empty() {
return;
}
self.last_output_newline = s.ends_with('\n');
self.sink.error_text(s);
}
fn viz_newline_if_open(&mut self) {
if !self.last_output_newline {
self.viz_puts("\n");
}
}
fn viz_start(&mut self) {
let line_open = !self.last_output_newline;
self.viz = ToolViz {
active: true,
at_line_start: true,
..ToolViz::default()
};
self.scan = DsmlScan::Between;
if line_open {
self.viz_puts("\n");
}
}
fn viz_line_prefix(&mut self) {
self.viz_newline_if_open();
self.viz_puts("🛠️ ");
self.viz.at_line_start = false;
}
fn viz_tool(&mut self, name: &str) {
if self.viz.tool_announced && self.viz.tool_name == name {
return;
}
if self.viz.tool_announced {
self.viz_newline_if_open();
}
self.viz.tool_name = name.to_string();
self.viz.tool_announced = true;
self.viz.read_style = name == "read";
self.viz_line_prefix();
if self.viz.read_style {
self.viz_puts("Reading ");
self.viz.read_prefix_rendered = true;
return;
}
if let Some(prefix) = tool_prefix(name) {
self.viz_puts(prefix);
} else {
let owned = name.to_string();
self.viz_puts(&owned);
self.viz_puts(" ");
}
}
fn viz_read_value_byte(&mut self, c: u8) {
let field = match self.viz.param_name.as_str() {
"path" => &mut self.viz.read_path,
"start_line" => &mut self.viz.read_start,
"max_lines" => &mut self.viz.read_max,
"whole" => &mut self.viz.read_whole,
_ => return,
};
field.push(c as char);
if self.viz.param_name == "path" && self.viz.read_prefix_rendered {
self.emit_visible_bytes(&[c]);
}
}
fn viz_render_read(&mut self) {
if !self.viz.read_style || self.viz.read_line_rendered {
return;
}
if !self.viz.read_prefix_rendered {
self.viz_line_prefix();
self.viz_puts("Reading ");
let path = if self.viz.read_path.is_empty() {
"<unknown>".to_string()
} else {
self.viz.read_path.clone()
};
self.viz_puts(&path);
} else if self.viz.read_path.is_empty() {
self.viz_puts("<unknown>");
}
let whole = parse_bool_default(&self.viz.read_whole, false);
let range = if whole && (self.viz.read_start.is_empty() || self.viz.read_start == "1") {
" (whole file)".to_string()
} else if whole {
format!(" {}:EOF", self.viz.read_start)
} else {
let start = if self.viz.read_start.is_empty() {
"1"
} else {
&self.viz.read_start
};
let max = if self.viz.read_max.is_empty() {
"500"
} else {
&self.viz.read_max
};
format!(" {start}:{max}")
};
self.viz_puts(&range);
self.viz_puts("...\n");
self.viz.read_line_rendered = true;
}
fn viz_param_is_code_body(&self) -> bool {
match self.viz.tool_name.as_str() {
"write" => self.viz.param_kind == ParamKind::Content,
"edit" => matches!(
self.viz.param_kind,
ParamKind::DiffOld | ParamKind::DiffNew | ParamKind::Content
),
_ => false,
}
}
fn viz_code_prefix(&mut self) {
if !self.viz.at_line_start {
return;
}
if let Some(prefix) = diff_prefix(self.viz.param_kind) {
self.viz_puts(prefix);
self.viz.at_line_start = false;
}
}
fn viz_code_begin(&mut self) {
self.viz.code_param_active = true;
if matches!(self.viz.param_kind, ParamKind::DiffOld | ParamKind::DiffNew) {
self.viz_code_prefix();
}
}
fn viz_code_end(&mut self) {
if !self.viz.code_param_active {
return;
}
self.viz.code_param_active = false;
self.viz.at_line_start = true;
}
fn viz_code_byte(&mut self, c: u8) {
if self.viz_is_write_preview() {
if self.viz.write_is_create {
self.emit_preview_bytes(&[c]);
}
self.viz.at_line_start = c == b'\n';
return;
}
self.viz_code_prefix();
self.emit_visible_bytes(&[c]);
self.viz.at_line_start = c == b'\n';
}
fn viz_param_begin(&mut self, name: &str) {
self.viz.param_name = name.to_string();
self.viz.param_kind = param_kind_for(&self.viz.tool_name, name);
self.viz.param_active = true;
self.viz.param_end_tail.clear();
if self.viz.read_style {
return;
}
match self.viz.param_kind {
ParamKind::DiffOld | ParamKind::DiffNew => {
self.viz_newline_if_open();
self.viz.at_line_start = true;
self.viz_code_begin();
}
ParamKind::Content => {
self.viz_newline_if_open();
if self.viz.tool_name == "write" {
self.viz.write_is_create = !std::path::Path::new(&self.viz.write_path).exists();
if self.viz.write_is_create && !self.show_tool_calls {
let path = if self.viz.write_path.is_empty() {
"<file>".to_string()
} else {
self.viz.write_path.clone()
};
self.viz_preview_puts(&format!("write {path}\n"));
}
} else {
let label = format!("{name}:\n");
self.viz_puts(&label);
}
self.viz.at_line_start = true;
if self.viz_param_is_code_body() {
self.viz_code_begin();
}
}
ParamKind::BashCommand => {}
ParamKind::Normal | ParamKind::Path => {
if !self.viz.at_line_start {
self.viz_puts(" ");
}
let label = format!("{name}=");
self.viz_puts(&label);
}
}
}
fn viz_param_end(&mut self) {
self.viz.param_end_tail.clear();
if self.viz.code_param_active {
self.viz_code_end();
}
self.viz.param_active = false;
self.viz.param_name.clear();
self.scan = DsmlScan::Between;
}
fn viz_param_raw_byte(&mut self, c: u8) {
if self.viz.read_style {
self.viz_read_value_byte(c);
return;
}
if self.viz.code_param_active {
self.viz_code_byte(c);
return;
}
if matches!(self.viz.param_kind, ParamKind::DiffOld | ParamKind::DiffNew) {
self.viz_code_begin();
self.viz_code_byte(c);
return;
}
if self.viz.tool_name == "write" && self.viz.param_kind == ParamKind::Path {
self.viz.write_path.push(c as char);
}
self.emit_visible_bytes(&[c]);
self.viz.at_line_start = c == b'\n';
}
fn viz_param_value_byte(&mut self, c: u8) {
if !self.viz.param_end_tail.is_empty() || c == b'<' {
if self.viz.param_end_tail.len() == ToolViz::END_TAIL_CAP {
let held = std::mem::take(&mut self.viz.param_end_tail);
for b in held {
self.viz_param_raw_byte(b);
}
if c != b'<' {
self.viz_param_raw_byte(c);
return;
}
}
self.viz.param_end_tail.push(c);
let mut complete = false;
if parameter_close_tail(&self.viz.param_end_tail, &mut complete) {
if complete {
self.viz_param_end();
}
return;
}
let held = std::mem::take(&mut self.viz.param_end_tail);
for b in held {
self.viz_param_raw_byte(b);
}
return;
}
self.viz_param_raw_byte(c);
}
fn viz_invoke_end(&mut self) {
if !self.viz.tool_announced || self.viz.param_active {
return;
}
self.viz_render_read();
self.viz_newline_if_open();
self.viz.read_style = false;
self.viz.read_prefix_rendered = false;
self.viz.read_line_rendered = false;
self.viz.read_path.clear();
self.viz.read_start.clear();
self.viz.read_max.clear();
self.viz.read_whole.clear();
self.viz.tool_announced = false;
}
fn viz_finish(&mut self, status: Option<&str>) {
if !self.viz.active {
return;
}
if self.viz.param_active {
self.viz_param_end();
}
if status.is_none() {
self.viz_render_read();
}
if let Some(status) = status {
self.viz_newline_if_open();
let owned = status.to_string();
self.viz_error_puts(&owned);
}
self.viz_newline_if_open();
self.viz.active = false;
}
fn viz_drop_invalid_dsml(&mut self) {
if !self.viz.active {
return;
}
if self.viz.param_active {
self.viz.param_active = false;
self.viz.param_end_tail.clear();
self.viz.param_name.clear();
}
self.viz_newline_if_open();
}
fn scan_dsml_byte(&mut self, c: u8) {
match &mut self.scan {
DsmlScan::Between => {
if c == b'<' {
self.scan = DsmlScan::Tag(vec![c]);
}
}
DsmlScan::Tag(tag) => {
tag.push(c);
if c == b'>' {
let tag = std::mem::take(tag);
self.scan = DsmlScan::Between;
self.scan_dsml_tag(&tag);
}
}
DsmlScan::Value => self.viz_param_value_byte(c),
}
}
fn scan_dsml_tag(&mut self, tag: &[u8]) {
let tag = String::from_utf8_lossy(tag).into_owned();
let b = tag.as_bytes();
if tag_prefix_len(b, true, "invoke").is_some() {
self.viz_invoke_end();
} else if tag_prefix_len(b, false, "invoke").is_some() {
let name = parse_attr(&tag, "name").unwrap_or_else(|| "tool".to_string());
self.viz_tool(&name);
} else if tag_prefix_len(b, false, "parameter").is_some()
&& let Some(name) = parse_attr(&tag, "name")
{
self.viz_param_begin(&name);
self.scan = DsmlScan::Value;
} else if parse_attr(&tag, "name").is_none()
&& let Some(elem) = crate::dsml::element_name(&tag)
{
if self.viz.tool_announced {
self.viz_param_begin(&elem);
self.scan = DsmlScan::Value;
} else {
self.viz_tool(&elem);
}
}
}
fn feed_dsml_byte(&mut self, c: u8) {
let was_param = self.parser.state() == DsmlState::ParamValue;
self.parser.feed([c]);
if !self.dsml_ignored {
self.scan_dsml_byte(c);
if was_param && self.parser.state() != DsmlState::ParamValue {
self.preflight_closed_param();
}
}
match self.parser.state() {
DsmlState::Done => {
self.think_close_swallowed = false;
if self.rejects_in_think() {
self.reject_in_think_stanza(
"tool calling is not allowed inside <think></think>",
);
} else {
self.calls = self.parser.calls().to_vec();
self.viz_finish(None);
self.dsml_active = false;
}
}
DsmlState::Error => {
self.resolve_swallowed_think_close();
if self.rejects_in_think() {
self.reject_in_think_stanza("malformed tool call inside <think></think>");
} else {
let err = if self.parser.error().is_empty() {
"parse error"
} else {
self.parser.error()
};
log_tool_error(err, self.parser.raw());
let status = format!("[invalid tool call: {err}]\n");
self.viz_drop_invalid_dsml();
self.viz_finish(Some(&status));
self.dsml_active = false;
}
}
_ => {}
}
}
fn preflight_closed_param(&mut self) {
if self.preflight_error.is_some() {
return;
}
let Preflight(Some(check)) = &mut self.preflight else {
return;
};
let Some(call) = self.parser.pending_call() else {
return;
};
if call.name != "edit" || call.args.last().is_none_or(|a| a.name != "old") {
return;
}
if let Err(err) = check(&call) {
self.preflight_error = Some(format!(
"edit old selector failed before new was generated: {err}"
));
}
}
fn start_dsml(&mut self) {
self.dsml_active = true;
self.dsml_ignored = self.rejects_in_think();
if self.in_think {
self.dsml_in_think = true;
}
self.dsml_start_tail.clear();
self.post_think_gap = false;
self.parser.feed(DSML_START);
self.scan = DsmlScan::Between;
if !self.dsml_ignored {
self.viz_start();
}
}
fn rejects_in_think(&self) -> bool {
self.in_think && !self.thinking_tool_calls
}
fn reject_in_think_stanza(&mut self, msg: &str) {
self.dsml_ignored = true;
self.viz_drop_invalid_dsml();
self.finish_ignored_dsml(msg);
}
fn think_close_is_control(&self) -> bool {
!self.dsml_active || self.parser.state() != DsmlState::ParamValue
}
fn resolve_swallowed_think_close(&mut self) {
if !std::mem::take(&mut self.think_close_swallowed) || !self.in_think {
return;
}
self.in_think = false;
self.pseudo_tool.reset_line();
self.plain_dsml = MarkerDetector::default();
if self.dsml_active && self.dsml_ignored {
self.dsml_ignored = false;
self.viz_start();
}
}
fn finish_ignored_dsml(&mut self, msg: &str) {
let raw = if self.parser.raw().is_empty() {
self.dsml_start_tail.clone()
} else {
self.parser.raw().to_vec()
};
log_tool_error(msg, &raw);
self.resolve_swallowed_think_close();
self.dsml_in_think = true;
self.in_think_rejected = true;
self.dsml_in_think_reported = true;
self.viz_newline_if_open();
let line = format!("[tool call ignored: {msg}]\n");
self.viz_error_puts(&line);
self.parser.reset();
self.dsml_active = false;
self.dsml_ignored = false;
}
fn malformed_dsml(&mut self, msg: &str) {
if self.stream_error.is_some() {
return;
}
self.stream_error = Some(msg.to_string());
log_tool_error(msg, self.parser.raw());
self.viz_newline_if_open();
let line = format!("[invalid tool call: {msg}]\n");
self.viz_error_puts(&line);
}
fn pseudo_tool_call(&mut self, opener: &str) {
self.pseudo_tool_fired = true;
self.malformed_dsml(&format!(
"{opener} is not a tool call; tools are invoked with the DSML syntax in the system prompt"
));
}
fn output_frozen(&self) -> bool {
self.stream_error.is_some() || self.parser.state() == DsmlState::Error
}
fn note_thinking_dsml_byte(&mut self, c: u8) {
if !self.in_think || self.dsml_in_think {
return;
}
if self.think_dsml.feed(c) {
self.dsml_in_think = true;
}
}
fn note_plain_dsml_byte(&mut self, c: u8) {
if self.output_frozen() || self.dsml_active || self.in_think || self.in_think_rejected {
return;
}
if self.plain_dsml.feed(c) {
self.malformed_dsml("DSML markup outside a valid tool_calls block");
}
}
fn note_pseudo_tool_byte(&mut self, c: u8) {
if self.output_frozen() || self.dsml_active || self.replay {
return;
}
if let Some(opener) = self.pseudo_tool.feed(c, self.in_think) {
self.pseudo_tool_call(&opener);
}
}
fn flush_pseudo_tool(&mut self) {
if self.output_frozen() || self.dsml_active || self.replay {
return;
}
if let Some(opener) = self.pseudo_tool.finish_line(self.in_think) {
self.pseudo_tool_call(&opener);
}
}
fn flush_start_tail(&mut self) {
if self.dsml_start_tail.is_empty() {
return;
}
self.post_think_gap = false;
let held = std::mem::take(&mut self.dsml_start_tail);
for b in held {
self.write_char(b);
self.note_plain_dsml_byte(b);
self.note_pseudo_tool_byte(b);
if self.output_frozen() {
break;
}
}
}
fn normal_byte(&mut self, c: u8) {
if self.output_frozen() {
return;
}
self.note_thinking_dsml_byte(c);
if self.post_think_gap && matches!(c, b' ' | b'\t' | b'\r' | b'\n') {
return;
}
if !self.dsml_start_tail.is_empty() || c == b'<' {
if self.dsml_start_tail.len() < DSML_START_TAIL_CAP {
self.dsml_start_tail.push(c);
}
let (mut complete, mut implicit_invoke) = (false, false);
if dsml_start_match(&self.dsml_start_tail, &mut complete, &mut implicit_invoke) {
if complete {
self.start_dsml();
if implicit_invoke {
for &b in CANONICAL_INVOKE {
self.feed_dsml_byte(b);
}
}
}
return;
}
if self.dsml_start_tail.len() > 1 && self.dsml_start_tail.last() == Some(&b'<') {
self.post_think_gap = false;
let held = std::mem::take(&mut self.dsml_start_tail);
for &b in &held[..held.len() - 1] {
self.write_char(b);
self.note_plain_dsml_byte(b);
self.note_pseudo_tool_byte(b);
if self.output_frozen() {
return;
}
}
self.dsml_start_tail.push(b'<');
return;
}
self.flush_start_tail();
return;
}
self.post_think_gap = false;
self.write_char(c);
self.note_plain_dsml_byte(c);
self.note_pseudo_tool_byte(c);
}
fn stream_text(&mut self, text: &[u8], finish: bool) {
let mut buf = std::mem::take(&mut self.pending);
buf.extend_from_slice(text);
let mut i = 0;
while i < buf.len() {
let rem = &buf[i..];
if !self.dsml_active && rem.starts_with(THINK_OPEN) {
self.flush_start_tail();
self.post_think_gap = false;
self.in_think = true;
self.plain_dsml = MarkerDetector::default();
i += THINK_OPEN.len();
continue;
}
if rem.starts_with(THINK_CLOSE) && self.think_close_is_control() {
if self.dsml_active {
self.in_think = false;
if self.dsml_ignored {
self.dsml_ignored = false;
self.viz_start();
}
} else {
self.flush_start_tail();
self.in_think = false;
self.pseudo_tool.reset_line();
self.viz_newline_if_open();
self.emit_visible_bytes(b"\n");
self.post_think_gap = true;
}
self.plain_dsml = MarkerDetector::default();
i += THINK_CLOSE.len();
continue;
}
if rem.starts_with(THINK_CLOSE) {
self.think_close_swallowed = true;
}
if !finish
&& rem[0] == b'<'
&& self.think_close_is_control()
&& (is_partial_prefix(rem, THINK_OPEN) || is_partial_prefix(rem, THINK_CLOSE))
{
self.pending = rem.to_vec();
break;
}
let c = rem[0];
if self.dsml_active {
self.feed_dsml_byte(c);
} else {
self.normal_byte(c);
}
i += 1;
}
if finish {
self.flush_start_tail();
self.post_think_gap = false;
if self.dsml_active {
self.resolve_swallowed_think_close();
if self.rejects_in_think() {
self.reject_in_think_stanza(
"tool calling is not allowed inside <think></think>",
);
} else {
self.viz_finish(Some(if self.preflight_error.is_some() {
"[tool call stopped: edit old selector failed]\n"
} else {
"[tool call interrupted]\n"
}));
self.dsml_active = false;
}
}
}
}
}
fn is_partial_prefix(bytes: &[u8], prefix: &[u8]) -> bool {
bytes.len() < prefix.len() && prefix[..bytes.len()] == *bytes
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Default)]
struct Cap {
visible: String,
think: String,
errors: String,
}
impl RenderSink for Cap {
fn visible_text(&mut self, text: &str) {
self.visible.push_str(text);
}
fn error_text(&mut self, text: &str) {
self.errors.push_str(text);
self.visible.push_str(text);
}
fn think_text(&mut self, text: &str) {
self.think.push_str(text);
}
}
fn run_chunked(text: &str) -> StreamRenderer<Cap> {
let mut sr = StreamRenderer::new(Cap::default());
sr.push(text);
sr.finish();
sr
}
fn pseudo_tool_renderer() -> StreamRenderer<Cap> {
let mut sr = StreamRenderer::new(Cap::default());
sr.set_tool_names(vec!["task".to_string(), "read".to_string()]);
sr
}
#[test]
fn pseudo_tool_block_after_think_is_reported() {
let mut sr = pseudo_tool_renderer();
sr.push("<think>planning</think>");
sr.push("<task>\ntask_context: \"add headers\"\n</task>");
sr.finish();
assert!(
sr.finished().error.is_some(),
"hallucinated call produced no error for the model to correct from"
);
}
#[test]
fn pseudo_tool_block_inside_think_is_ignored() {
let mut sr = pseudo_tool_renderer();
sr.push("<think>\n<task>\n</task>\n</think>");
sr.push("done");
sr.finish();
assert!(sr.finished().error.is_none());
}
#[test]
fn stray_fence_close_leaking_from_think_does_not_disarm_the_detector() {
let mut sr = pseudo_tool_renderer();
sr.push("<think>```rust\nfoo\n</think>```\n<task>\nx\n</task>");
sr.finish();
assert!(
sr.finished().error.is_some(),
"a fence opened inside <think> disarmed the detector for the rest of the stream"
);
}
#[test]
fn pseudo_tool_name_in_prose_or_fence_is_ignored() {
let mut sr = pseudo_tool_renderer();
sr.push("the <task> element is XML, not DSML\n");
sr.push("```xml\n<task>\n</task>\n```\n");
sr.finish();
assert!(sr.finished().error.is_none());
}
#[test]
fn pseudo_tool_error_wins_over_the_in_think_prohibition() {
let mut sr = pseudo_tool_renderer();
sr.push(concat!(
"<think><|DSML|tool_calls><|DSML|invoke name=\"bash\">",
"</|DSML|invoke|></|DSML|tool_calls|></think>\n",
"<task>\nx\n</task>",
));
sr.finish();
let fin = sr.finished();
assert_eq!(
fin.error,
Some(
"<task> is not a tool call; tools are invoked with the DSML syntax in the system prompt"
),
"pseudo-tool hit must not be masked by the in-think prohibition"
);
assert!(
!fin.in_think_rejected,
"in-think framing would wrap the answer-region error in the wrong advice"
);
}
#[test]
fn leaked_in_think_stanza_alone_still_reports_the_prohibition() {
let mut sr = pseudo_tool_renderer();
sr.push(concat!(
"<think><|DSML|tool_calls><|DSML|invoke name=\"bash\">",
"</|DSML|invoke|></|DSML|tool_calls|></think>\n",
"all done\n",
));
sr.finish();
let fin = sr.finished();
assert_eq!(fin.error, Some(IN_THINK_PROHIBITION));
assert!(fin.in_think_rejected);
}
#[test]
fn prose_continuing_after_a_tool_tag_does_not_fire() {
let mut sr = pseudo_tool_renderer();
sr.push("<read> is how you spell it, unlike the others.\nand more text\n");
sr.finish();
assert!(sr.finished().error.is_none(), "{:?}", sr.finished().error);
assert!(
sr.sink().visible.contains("and more text"),
"answer truncated: {:?}",
sr.sink().visible
);
}
#[test]
fn bare_tool_tag_on_its_own_line_fires() {
let mut sr = pseudo_tool_renderer();
sr.push("here goes\n<read>\npath: /tmp/x\n</read>\n");
sr.finish();
assert!(sr.finished().error.is_some());
}
#[test]
fn bare_tool_tag_as_final_line_without_newline_fires() {
let mut sr = pseudo_tool_renderer();
sr.push("here goes\n<task>");
sr.finish();
assert_eq!(
sr.finished().error,
Some(
"<task> is not a tool call; tools are invoked with the DSML syntax in the system prompt"
)
);
}
#[test]
fn pseudo_tool_message_quotes_the_matched_line() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<invoke name=\"bash\">\n");
sr.finish();
assert_eq!(
sr.finished().error,
Some(
"<invoke name=\"bash\"> is not a tool call; tools are invoked with the DSML syntax in the system prompt"
)
);
}
#[test]
fn generic_tool_call_wrapper_is_reported() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<tool_call>\n{\"name\": \"read\"}\n</tool_call>");
sr.finish();
assert!(sr.finished().error.is_some());
}
#[test]
fn pseudo_tool_block_is_not_reported_during_replay() {
let mut sr = pseudo_tool_renderer();
sr.set_replay(true);
sr.push("<task>\ntask_context: \"add headers\"\n</task>");
sr.finish();
assert!(sr.finished().error.is_none());
}
#[test]
fn real_dsml_call_is_untouched_by_the_pseudo_detector() {
let mut sr = pseudo_tool_renderer();
sr.push(
"<|DSML|tool_calls><|DSML|invoke name=\"read\">\
<|DSML|parameter name=\"path\" string=\"true\">/tmp/x</|DSML|parameter|>\
</|DSML|invoke|></|DSML|tool_calls|>",
);
sr.finish();
let fin = sr.finished();
assert!(fin.error.is_none(), "error: {:?}", fin.error);
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].name, "read");
}
#[test]
fn write_content_previews_dim_even_with_banners_off() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"write\">",
"<|DSML|parameter name=\"path\">src/foo.rs</|DSML|parameter>",
"<|DSML|parameter name=\"content\">fn main() {}\n</|DSML|parameter>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
);
let mut sr = StreamRenderer::new(Cap::default());
sr.set_show_tool_calls(false);
sr.push(stanza);
sr.finish();
let think = &sr.sink().think;
assert!(think.contains("write src/foo.rs"), "header: {think:?}");
assert!(think.contains("fn main() {}"), "content preview: {think:?}");
assert!(
!sr.sink().visible.contains("fn main()"),
"content not on the visible channel: {:?}",
sr.sink().visible
);
assert_eq!(sr.finished().calls.len(), 1, "call still parsed");
}
#[test]
fn show_tool_calls_false_suppresses_the_banner_but_keeps_visible_text() {
let stanza = concat!(
"answer before. ",
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">ls -la</|DSML|parameter>",
"</|DSML|invoke>",
"</|DSML|tool_calls>",
);
let shown = run_chunked(stanza);
assert!(
shown.sink().visible.contains("🛠️"),
"{:?}",
shown.sink().visible
);
let mut sr = StreamRenderer::new(Cap::default());
sr.set_show_tool_calls(false);
sr.push(stanza);
sr.finish();
let vis = &sr.sink().visible;
assert!(vis.contains("answer before."), "prose kept: {vis:?}");
assert!(!vis.contains("🛠️"), "banner suppressed: {vis:?}");
assert!(!vis.contains("ls -la"), "params suppressed: {vis:?}");
assert_eq!(sr.finished().calls.len(), 1, "call still parsed");
}
#[test]
fn show_thinking_false_suppresses_thinking_but_keeps_visible_text() {
let text = "<think>secret reasoning</think>visible answer";
let shown = run_chunked(text);
assert_eq!(shown.sink().think, "secret reasoning");
assert_eq!(shown.sink().visible.trim(), "visible answer");
let mut sr = StreamRenderer::new(Cap::default());
sr.set_show_thinking(false);
sr.push(text);
sr.finish();
assert_eq!(sr.sink().think, "", "thinking suppressed");
assert_eq!(sr.sink().visible.trim(), "visible answer", "prose kept");
}
fn run_charwise(text: &str) -> StreamRenderer<Cap> {
let mut sr = StreamRenderer::new(Cap::default());
for ch in text.chars() {
sr.push(ch.to_string());
}
sr.finish();
sr
}
const BASH_STANZA: &str = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">ls -la</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
#[test]
fn begin_in_think_routes_thinking_then_answer() {
let mut sr = StreamRenderer::new(Cap::default());
sr.begin_in_think();
sr.push("weighing options</think>Final answer.");
sr.finish();
assert!(sr.sink().think.contains("weighing options"));
assert!(sr.sink().visible.contains("Final answer."));
assert!(!sr.sink().visible.contains("weighing options"));
assert!(!sr.sink().visible.contains("think"));
}
#[test]
fn provider_explicit_think_tags_route_correctly() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<think>reasoning here</think>");
sr.push("visible answer");
sr.finish();
assert!(sr.sink().think.contains("reasoning here"));
assert!(!sr.sink().think.contains("visible answer"));
assert!(sr.sink().visible.contains("visible answer"));
let mut sr = StreamRenderer::new(Cap::default());
sr.push("just an answer, no thinking");
sr.finish();
assert_eq!(sr.sink().visible, "just an answer, no thinking");
assert_eq!(sr.sink().think, "");
}
#[test]
fn ssml_misspelling_is_accepted_as_an_alias() {
let text = concat!(
"Let me look at the documents module.\n",
"<|SSML|tool_calls>\n",
"<|SSML|invoke name=\"bash\">\n",
"<|SSML|parameter name=\"command\" string=\"true\">cat documents.rs",
"</|SSML|parameter>\n",
"</|SSML|invoke>\n",
"</|SSML|tool_calls>",
);
for sr in [run_chunked(text), run_charwise(text)] {
let vis = &sr.sink().visible;
assert!(vis.contains("🛠️ $ cat documents.rs"), "{vis:?}");
assert!(!vis.contains("SSML"), "{vis:?}");
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].name, "bash");
assert_eq!(fin.calls[0].arg_value("command"), Some("cat documents.rs"));
assert!(fin.error.is_none(), "{:?}", fin.error);
}
}
#[test]
fn dsml_and_ssml_tags_mix_within_one_stanza() {
let text = concat!(
"<|DSML|tool_calls>",
"<|SSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">ls -la</|SSML|parameter>",
"</|DSML|invoke>",
"</|SSML|tool_calls>",
);
for sr in [run_chunked(text), run_charwise(text)] {
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].arg_value("command"), Some("ls -la"));
assert!(fin.error.is_none(), "{:?}", fin.error);
}
}
#[test]
fn unrelated_marker_names_are_still_plain_text() {
let text = "<|XSML|tool_calls><|XSML|invoke name=\"bash\">";
for sr in [run_chunked(text), run_charwise(text)] {
assert!(sr.finished().calls.is_empty());
assert_eq!(sr.sink().visible, text);
}
}
#[test]
fn prose_passes_through() {
for sr in [run_chunked("Hello, world."), run_charwise("Hello, world.")] {
assert_eq!(sr.sink().visible, "Hello, world.");
assert_eq!(sr.sink().think, "");
assert!(sr.finished().calls.is_empty());
assert!(sr.finished().error.is_none());
}
}
#[test]
fn bash_stanza_hides_dsml_and_shows_banner() {
let text = format!("Let me look.\n{BASH_STANZA}");
for sr in [run_chunked(&text), run_charwise(&text)] {
let vis = &sr.sink().visible;
assert!(vis.starts_with("Let me look.\n"), "{vis:?}");
assert!(vis.contains("🛠️ $ ls -la"), "{vis:?}");
assert!(!vis.contains("DSML"), "{vis:?}");
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].name, "bash");
assert_eq!(fin.calls[0].arg_value("command"), Some("ls -la"));
assert!(fin.error.is_none());
}
}
#[test]
fn read_banner_shows_path_and_range() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"read\">",
"<|DSML|parameter name=\"path\" string=\"true\">src/main.rs</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
let vis = &sr.sink().visible;
assert!(vis.contains("🛠️ Reading src/main.rs 1:500...\n"), "{vis:?}");
assert!(!vis.contains("DSML"), "{vis:?}");
}
}
#[test]
fn read_banner_whole_file() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"read\">",
"<|DSML|parameter name=\"path\" string=\"true\">a.c</|DSML|parameter|>",
"<|DSML|parameter name=\"whole\">true</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
let sr = run_chunked(stanza);
assert!(
sr.sink()
.visible
.contains("🛠️ Reading a.c (whole file)...\n"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn edit_diff_uses_minus_plus_prefixes() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"edit\">",
"<|DSML|parameter name=\"path\" string=\"true\">a.rs</|DSML|parameter|>",
"<|DSML|parameter name=\"old\">let a = 1;</|DSML|parameter|>",
"<|DSML|parameter name=\"new\">let a = 2;</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
let vis = &sr.sink().visible;
assert!(vis.contains("🛠️ edit path=a.rs"), "{vis:?}");
assert!(vis.contains("- let a = 1;"), "{vis:?}");
assert!(vis.contains("+ let a = 2;"), "{vis:?}");
assert!(!vis.contains("DSML"), "{vis:?}");
assert_eq!(sr.finished().calls[0].arg_value("new"), Some("let a = 2;"));
}
}
#[test]
fn partial_marker_false_alarm_is_flushed() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<|DSM");
assert_eq!(sr.sink().visible, "");
sr.push("ok");
sr.finish();
assert_eq!(sr.sink().visible, "<|DSMok");
assert!(sr.finished().error.is_none());
}
#[test]
fn partial_marker_flushed_at_stream_end() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("done <|DSML|tool_c");
sr.finish();
assert!(
sr.sink().visible.starts_with("done <|DSML|"),
"{:?}",
sr.sink().visible
);
assert!(
sr.sink().visible.contains("[invalid tool call: "),
"{:?}",
sr.sink().visible
);
}
#[test]
fn think_text_routes_to_think_sink() {
let sr = run_chunked("<think>pondering</think>Answer.");
assert_eq!(sr.sink().think, "pondering");
assert!(
sr.sink().visible.ends_with("Answer."),
"{:?}",
sr.sink().visible
);
assert!(!sr.sink().visible.contains("pondering"));
}
#[test]
fn think_tag_split_across_pushes() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<th");
sr.push("ink>hidden</th");
sr.push("ink>shown");
sr.finish();
assert_eq!(sr.sink().think, "hidden");
assert!(
sr.sink().visible.ends_with("shown"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn dsml_inside_think_is_ignored_and_reported() {
let text = format!("<think>{BASH_STANZA}</think>ok");
for sr in [run_chunked(&text), run_charwise(&text)] {
let fin = sr.finished();
assert!(fin.dsml_in_think);
assert!(fin.calls.is_empty(), "{:?}", fin.calls);
assert!(
sr.sink().visible.contains(
"[tool call ignored: tool calling is not allowed inside <think></think>]"
),
"{:?}",
sr.sink().visible
);
assert!(!sr.sink().think.contains("DSML"), "{:?}", sr.sink().think);
}
}
fn run_allowing_in_think(text: &str) -> StreamRenderer<Cap> {
let mut sr = StreamRenderer::new(Cap::default());
sr.set_thinking_tool_calls(true);
sr.push(text);
sr.finish();
sr
}
fn run_allowing_in_think_charwise(text: &str) -> StreamRenderer<Cap> {
let mut sr = StreamRenderer::new(Cap::default());
sr.set_thinking_tool_calls(true);
for ch in text.chars() {
sr.push(ch.to_string());
}
sr.finish();
sr
}
#[test]
fn an_unfinished_in_think_stanza_reports_placement_not_syntax() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<think>let me look<|DSML|tool_calls><|DSML|invoke name=\"bash\">");
sr.finish();
let fin = sr.finished();
assert!(fin.calls.is_empty(), "{:?}", fin.calls);
assert!(fin.in_think_rejected, "rejected for placement");
assert_eq!(fin.error, Some(IN_THINK_PROHIBITION));
assert!(fin.ended_in_think);
}
#[test]
fn a_stanza_opened_in_think_but_closed_after_it_is_dispatched() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<think>the correct format is:\n\n<|DSML|tool_calls>\n</think>\n\n");
sr.push("<|DSML|invoke name=\"bash\">");
sr.push("<|DSML|parameter name=\"command\">ls -la</|DSML|parameter|>");
sr.push("</|DSML|invoke|></|DSML|tool_calls|>");
sr.finish();
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1, "{:?}", fin.calls);
assert_eq!(fin.calls[0].arg_value("command"), Some("ls -la"));
assert!(!fin.in_think_rejected, "the call closed outside thinking");
assert!(fin.error.is_none(), "{:?}", fin.error);
assert!(
!sr.sink().visible.contains("[tool call ignored:"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn dsml_mentioned_while_thinking_without_a_stop_token_is_not_rejected() {
for text in [
"<think>each arg is <|DSML|parameter name=\"x\" string=\"true\">v</|DSML|parameter|> inside the invoke</think>the answer",
"<think>and it ends with </|DSML|tool_calls|> of course</think>the answer",
] {
let sr = run_chunked(text);
let fin = sr.finished();
assert!(fin.calls.is_empty(), "{:?}", fin.calls);
assert!(!fin.in_think_rejected, "nothing completed; {text}");
assert_eq!(fin.error, None, "{text}");
assert!(fin.dsml_in_think, "{text}");
assert!(sr.sink().visible.contains("the answer"), "{text}");
}
}
#[test]
fn think_close_inside_a_parameter_value_is_payload_not_a_control_token() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<think>writing it up</think>");
sr.push("<|DSML|tool_calls><|DSML|invoke name=\"write\">");
sr.push("<|DSML|parameter name=\"content\">close it with </think> when done");
sr.push("</|DSML|parameter|></|DSML|invoke|></|DSML|tool_calls|>");
sr.finish();
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1, "{:?}", fin.calls);
assert_eq!(
fin.calls[0].arg_value("content"),
Some("close it with </think> when done")
);
assert!(fin.error.is_none(), "{:?}", fin.error);
}
#[test]
fn a_stanza_wholly_inside_think_is_rejected_without_a_banner() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push(format!("<think>thinking{BASH_STANZA}</think>done"));
sr.finish();
let fin = sr.finished();
assert!(fin.calls.is_empty(), "{:?}", fin.calls);
assert!(fin.in_think_rejected);
assert_eq!(fin.error, Some(IN_THINK_PROHIBITION));
assert!(
!sr.sink().visible.contains("🛠️"),
"no banner for a call that never happened: {:?}",
sr.sink().visible
);
}
#[test]
fn shorthand_invoke_renders_a_banner() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<|DSML|tool_calls><|DSML|bash>");
sr.push("<|DSML|parameter name=\"command\" string=\"true\">ls -la</|DSML|parameter|>");
sr.push("</|DSML|invoke|></|DSML|tool_calls|>");
sr.finish();
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1, "{:?}", fin.calls);
assert_eq!(fin.calls[0].name, "bash");
assert!(
sr.sink().visible.contains("🛠️ $ ls -la"),
"{:?}",
sr.sink().visible
);
assert!(
!sr.sink().visible.contains("DSML"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn shorthand_parameter_renders_under_its_element_name() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<|DSML|tool_calls><|DSML|invoke name=\"bash\">");
sr.push("<|DSML|command string=\"true\">ls -la</|DSML|invoke>");
sr.push("</|DSML|invoke|></|DSML|tool_calls|>");
sr.finish();
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1, "{:?}", fin.calls);
assert_eq!(fin.calls[0].arg_value("command"), Some("ls -la"));
assert!(
sr.sink().visible.contains("🛠️ $ ls -la"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn a_completed_in_think_stanza_reports_placement() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push(format!("<think>thinking{BASH_STANZA}"));
sr.finish();
let fin = sr.finished();
assert!(fin.calls.is_empty(), "the call must not be dispatched");
assert!(fin.in_think_rejected);
assert_eq!(fin.error, Some(IN_THINK_PROHIBITION));
}
#[test]
fn allowing_in_think_calls_reports_no_placement_error() {
let text = format!("<think>{BASH_STANZA}</think>ok");
let sr = run_allowing_in_think(&text);
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1);
assert!(!fin.in_think_rejected, "nothing was rejected");
assert!(fin.error.is_none(), "{:?}", fin.error);
assert!(fin.dsml_in_think, "the marker was still seen in thinking");
}
#[test]
fn dsml_inside_think_is_executed_when_allowed() {
let text = format!("<think>{BASH_STANZA}</think>ok");
for sr in [
run_allowing_in_think(&text),
run_allowing_in_think_charwise(&text),
] {
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1, "{:?}", fin.calls);
assert_eq!(fin.calls[0].name, "bash");
assert_eq!(fin.calls[0].arg_value("command"), Some("ls -la"));
assert!(fin.error.is_none(), "{:?}", fin.error);
assert!(
!sr.sink().visible.contains("[tool call ignored:"),
"{:?}",
sr.sink().visible
);
assert!(
sr.sink().visible.contains("🛠️ $ ls -la"),
"{:?}",
sr.sink().visible
);
assert!(
!sr.sink().visible.contains("DSML"),
"{:?}",
sr.sink().visible
);
assert!(!sr.sink().think.contains("DSML"), "{:?}", sr.sink().think);
}
}
#[test]
fn ended_in_think_reports_an_open_block() {
let sr = run_allowing_in_think(&format!("<think>let me look{BASH_STANZA}"));
assert!(sr.finished().ended_in_think);
let closed = run_allowing_in_think(&format!("<think>done</think>{BASH_STANZA}"));
assert!(!closed.finished().ended_in_think);
assert!(!run_chunked("plain answer").finished().ended_in_think);
let stanza_then_close =
run_allowing_in_think(&format!("<think>a{BASH_STANZA}</think>tail"));
assert!(!stanza_then_close.finished().ended_in_think);
}
#[test]
fn interrupted_stanza_reports_status() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("<|DSML|tool_calls><|DSML|invoke name=\"bash\">");
sr.push("<|DSML|parameter name=\"command\">sleep 1");
sr.finish();
let vis = &sr.sink().visible;
assert!(vis.contains("🛠️ $ sleep 1"), "{vis:?}");
assert!(vis.contains("[tool call interrupted]\n"), "{vis:?}");
assert!(sr.finished().calls.is_empty());
}
#[test]
fn incomplete_stanza_reports_incomplete_error() {
let sr = run_chunked(concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">ls",
));
assert_eq!(sr.finished().error, Some("incomplete DSML tool call"));
assert!(sr.finished().calls.is_empty());
}
#[test]
fn greedy_sampling_tracks_dsml_state() {
let mut sr = StreamRenderer::new(Cap::default());
sr.push("hello ");
assert!(!sr.wants_greedy_sampling(), "prose");
sr.push("<|DS");
assert!(sr.wants_greedy_sampling(), "DSML-shaped held prefix");
sr.push("ML|tool_calls><|DSML|invoke name=\"bash\">");
assert!(sr.wants_greedy_sampling(), "structural markup");
sr.push("<|DSML|parameter name=\"command\">ls -la");
assert!(!sr.wants_greedy_sampling(), "free-form parameter value");
sr.push("</|DSML|parameter");
assert!(sr.wants_greedy_sampling(), "close tag streaming");
sr.push("|></|DSML|invoke|></|DSML|tool_calls|>");
assert!(!sr.wants_greedy_sampling(), "stanza done");
}
#[test]
fn edit_old_preflight_failure_is_reported_midstream() {
let mut sr = StreamRenderer::new(Cap::default());
sr.set_preflight(|call| {
assert_eq!(call.name, "edit");
assert_eq!(call.arg_value("path"), Some("src/a.rs"));
Err("old text is not a unique match".to_string())
});
sr.push(concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"edit\">",
"<|DSML|parameter name=\"path\">src/a.rs</|DSML|parameter|>",
"<|DSML|parameter name=\"old\">nope</|DSML|parameter|>",
));
assert_eq!(
sr.preflight_error(),
Some(
"edit old selector failed before new was generated: \
old text is not a unique match"
)
);
sr.finish();
assert!(
sr.sink()
.errors
.contains("[tool call stopped: edit old selector failed]"),
"{:?}",
sr.sink().errors
);
}
#[test]
fn edit_old_preflight_pass_leaves_stream_clean() {
let mut sr = StreamRenderer::new(Cap::default());
sr.set_preflight(|_| Ok(()));
sr.push(concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"edit\">",
"<|DSML|parameter name=\"path\">src/a.rs</|DSML|parameter|>",
"<|DSML|parameter name=\"old\">a</|DSML|parameter|>",
"<|DSML|parameter name=\"new\">b</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
));
sr.finish();
assert!(sr.preflight_error().is_none());
assert_eq!(sr.finished().calls.len(), 1);
assert!(sr.finished().error.is_none());
}
#[test]
fn malformed_stanza_suppresses_raw_and_reports_error() {
let sr = run_chunked("<|DSML|tool_calls><b>");
let vis = &sr.sink().visible;
assert!(
sr.sink()
.errors
.contains("[invalid tool call: unexpected DSML tag: <b>]"),
"{:?}",
sr.sink().errors
);
assert!(!vis.contains("tool_calls"), "{vis:?}");
assert!(sr.finished().error.is_some());
}
#[test]
fn loose_dsml_marker_is_flagged() {
let sr = run_chunked("junk |DSML| junk");
assert!(
sr.sink()
.visible
.contains("[invalid tool call: DSML markup outside a valid tool_calls block]"),
"{:?}",
sr.sink().visible
);
assert_eq!(
sr.finished().error,
Some("DSML markup outside a valid tool_calls block")
);
}
#[test]
fn a_quoted_marker_in_think_does_not_disarm_the_loose_marker_validator() {
let sr = run_chunked("<think>the |DSML| marker opens a call</think>junk |DSML| junk");
assert_eq!(
sr.finished().error,
Some("DSML markup outside a valid tool_calls block"),
"{:?}",
sr.sink().visible
);
}
#[test]
fn a_think_close_swallowed_by_an_unfinished_stanza_still_ends_thinking() {
let sr = run_chunked(concat!(
"<think>",
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"write\">",
"<|DSML|parameter name=\"content\">x</think>",
));
let fin = sr.finished();
assert!(!fin.ended_in_think, "in_think stayed stuck after </think>");
assert!(
!fin.in_think_rejected,
"a call cut off after thinking closed is not an in-think call"
);
assert!(
sr.sink().visible.contains("[tool call interrupted]"),
"{:?}",
sr.sink()
);
}
#[test]
fn a_think_close_inside_a_valid_payload_stays_payload_text() {
let sr = run_chunked(concat!(
"<think>",
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">echo </think></|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
));
let fin = sr.finished();
assert!(fin.ended_in_think, "the thinking block never closed");
assert_eq!(fin.calls.len(), 0, "an in-think stanza is not dispatched");
}
#[test]
fn implicit_invoke_opener_is_accepted() {
let stanza = concat!(
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\">pwd</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
assert!(
sr.sink().visible.contains("🛠️ $ pwd"),
"{:?}",
sr.sink().visible
);
let fin = sr.finished();
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].arg_value("command"), Some("pwd"));
}
}
#[test]
fn opener_with_trailing_bar_is_accepted() {
let stanza = concat!(
"<|DSML|tool_calls|>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|parameter name=\"command\" string=\"true\">pwd</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
let fin = sr.finished();
assert_eq!(fin.error, None);
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].name, "bash");
assert_eq!(fin.calls[0].arg_value("command"), Some("pwd"));
}
}
#[test]
fn shorthand_parameter_element_dispatches() {
let stanza = concat!(
"<|DSML|tool_calls|>",
"<|DSML|invoke name=\"bash\">",
"<|DSML|command string=\"true\">ls</|DSML|invoke>",
"</|DSML|invoke>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
let fin = sr.finished();
assert_eq!(fin.error, None);
assert_eq!(fin.calls.len(), 1);
assert_eq!(fin.calls[0].name, "bash");
assert_eq!(fin.calls[0].arg_value("command"), Some("ls"));
}
}
#[test]
fn write_content_streams_without_label() {
let stanza = concat!(
"<|DSML|tool_calls>",
"<|DSML|invoke name=\"write\">",
"<|DSML|parameter name=\"path\" string=\"true\">x.txt</|DSML|parameter|>",
"<|DSML|parameter name=\"content\">line one\nline two</|DSML|parameter|>",
"</|DSML|invoke|>",
"</|DSML|tool_calls|>",
);
for sr in [run_chunked(stanza), run_charwise(stanza)] {
let vis = &sr.sink().visible;
assert!(vis.contains("🛠️ write path=x.txt"), "{vis:?}");
assert!(!vis.contains("line one"), "{vis:?}");
assert!(
sr.sink().think.contains("line one\nline two"),
"{:?}",
sr.sink().think
);
assert!(!vis.contains("content:"), "{vis:?}");
assert!(!vis.contains("DSML"), "{vis:?}");
}
}
#[test]
fn post_think_whitespace_gap_is_swallowed() {
let sr = run_chunked("<think>x</think>\n\n Answer");
assert!(
sr.sink().visible.ends_with("Answer"),
"{:?}",
sr.sink().visible
);
assert!(!sr.sink().visible.contains("\n\n Answer"));
}
#[test]
fn charwise_and_chunked_agree() {
let text = format!("hi <not dsml> there\n{BASH_STANZA}");
let a = run_chunked(&text);
let b = run_charwise(&text);
assert_eq!(a.sink().visible, b.sink().visible);
assert_eq!(a.finished().calls, b.finished().calls);
}
#[test]
fn tool_error_logging_honors_the_opt_out_env_var() {
use std::ffi::OsStr;
assert!(!super::logging_enabled_for(Some(OsStr::new("1"))));
assert!(super::logging_enabled_for(None));
assert!(super::logging_enabled_for(Some(OsStr::new("0"))));
}
}