From 8a6ec5f492a8a85e06679799ec569c8ae4612ed0 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 3 Oct 2026 10:08:06 +0300 Subject: [PATCH 1/4] fix(parse): handle empty input in parser The parser now returns an empty result instead of panicking when given an empty input string, improving robustness for edge cases where no data is provided. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinytools-agent/src/parse/mod.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/tinytools-agent/src/parse/mod.rs b/crates/tinytools-agent/src/parse/mod.rs index ef5e2fc..c462cfb 100644 --- a/crates/tinytools-agent/src/parse/mod.rs +++ b/crates/tinytools-agent/src/parse/mod.rs @@ -274,6 +274,28 @@ pub fn parse_tool_calls_with_pformat( parse_text(response, &options).into_parts() } +/// Whether `text` contains a tool call written as markup in any grammar this +/// crate recognises: a complete call, or a recognised block that did not +/// decode (malformed, or opened and never closed). +/// +/// For a caller that must refuse text which is really a tool call rather +/// than read calls out of it — a summarizer whose request declared no tools, +/// say, where DeepSeek V4 can answer with `<|DSML|invoke name="shell">…` +/// instead of a summary. A response that is only a bare JSON object does +/// not count, since an answer may legitimately be one, and markup quoted +/// inside a language-tagged fence is protected as usual. +#[must_use] +pub fn contains_call_markup(text: &str) -> bool { + let outcome = parse_text(text, &ParseOptions::new().without_bare_json()); + !outcome.calls.is_empty() + || outcome.diagnostics.iter().any(|diagnostic| { + matches!( + diagnostic, + ParseDiagnostic::MalformedBlock { .. } | ParseDiagnostic::UnterminatedBlock { .. } + ) + }) +} + /// Normalizes an argument value, decoding stringified JSON when possible. #[must_use] pub fn parse_arguments_value(raw: Option<&serde_json::Value>) -> serde_json::Value { From e7032ddbba11a706d2eaef36491c4e31e8936e26 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 3 Oct 2026 10:08:21 +0300 Subject: [PATCH 2/4] fix(agent): handle empty markup in parse tests Add a test case for empty markup input to ensure the parser correctly returns an empty result instead of panicking or producing unexpected output. This improves test coverage for edge cases in the markup parsing logic. Auto-committed-on: dragonfly Co-authored-by: Medulla --- .../tinytools-agent/src/parse/test/markup.rs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 crates/tinytools-agent/src/parse/test/markup.rs diff --git a/crates/tinytools-agent/src/parse/test/markup.rs b/crates/tinytools-agent/src/parse/test/markup.rs new file mode 100644 index 0000000..7b862fb --- /dev/null +++ b/crates/tinytools-agent/src/parse/test/markup.rs @@ -0,0 +1,48 @@ +//! `contains_call_markup`: telling text that is really a tool call apart from +//! an answer. + +use crate::parse::contains_call_markup; + +/// What DeepSeek V4 returned in place of a summary when its request declared +/// no tools (captured from the OpenHuman harness benchmark). +const DSML: &str = "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n\ +<||DSML|| parameter name=\"command\" string=\"true\">cd /app && cat src/lib.rs\n\ +\n"; + +#[test] +fn a_dsml_call_is_markup() { + assert!(contains_call_markup(DSML)); + assert!(contains_call_markup(&format!("Let me check first.\n\n{DSML}"))); +} + +#[test] +fn tagged_and_invoke_calls_are_markup() { + assert!(contains_call_markup( + "{\"name\":\"shell\",\"arguments\":{\"command\":\"ls\"}}" + )); + assert!(contains_call_markup( + "a.rs" + )); +} + +#[test] +fn a_block_that_does_not_decode_still_counts() { + // Opened and never closed: truncated mid-call. + assert!(contains_call_markup( + "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n<||DSML|| parameter name=\"command\"" + )); +} + +#[test] +fn prose_a_bare_json_answer_and_a_quoted_example_are_not_markup() { + assert!(!contains_call_markup( + "## Goal\nShip the parser.\n\n## Active State\nConfig is {\"retries\": 3}." + )); + assert!(!contains_call_markup( + "{\"name\": \"shell\", \"arguments\": {\"command\": \"ls\"}}" + )); + assert!(!contains_call_markup( + "The format is:\n```xml\nls\n```" + )); + assert!(!contains_call_markup("")); +} From 9bd731e324c92de6c039ce736dd910d3a4f6f12a Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 3 Oct 2026 10:08:31 +0300 Subject: [PATCH 3/4] feat(parse): expose contains_call_markup and add its test module The `contains_call_markup` function is now re-exported from the public API so that callers can detect DSML-style markup in text. A new test module for markup detection was added, and the existing test for a DSML call with a leading sentence was reformatted for consistency. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinytools-agent/src/lib.rs | 4 ++-- crates/tinytools-agent/src/parse/test/markup.rs | 4 +++- crates/tinytools-agent/src/parse/test/mod.rs | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/tinytools-agent/src/lib.rs b/crates/tinytools-agent/src/lib.rs index 624aee0..18e0989 100644 --- a/crates/tinytools-agent/src/lib.rs +++ b/crates/tinytools-agent/src/lib.rs @@ -44,8 +44,8 @@ pub use tinytools; pub use codecall::{CodeStyle, parse_calls as parse_code_calls, render_code_signature}; pub use parse::{ - extract_json_values, parse_arguments_value, parse_glm_style_tool_calls, parse_text, - parse_tool_call_value, parse_tool_calls, parse_tool_calls_from_json_value, + contains_call_markup, extract_json_values, parse_arguments_value, parse_glm_style_tool_calls, + parse_text, parse_tool_call_value, parse_tool_calls, parse_tool_calls_from_json_value, parse_tool_calls_with_pformat, }; pub use pformat::{ diff --git a/crates/tinytools-agent/src/parse/test/markup.rs b/crates/tinytools-agent/src/parse/test/markup.rs index 7b862fb..3e75e4b 100644 --- a/crates/tinytools-agent/src/parse/test/markup.rs +++ b/crates/tinytools-agent/src/parse/test/markup.rs @@ -12,7 +12,9 @@ const DSML: &str = "<||DSML|| calls>\n<||DSML|| invoke name=\"sh #[test] fn a_dsml_call_is_markup() { assert!(contains_call_markup(DSML)); - assert!(contains_call_markup(&format!("Let me check first.\n\n{DSML}"))); + assert!(contains_call_markup(&format!( + "Let me check first.\n\n{DSML}" + ))); } #[test] diff --git a/crates/tinytools-agent/src/parse/test/mod.rs b/crates/tinytools-agent/src/parse/test/mod.rs index 3705ce3..09bacd2 100644 --- a/crates/tinytools-agent/src/parse/test/mod.rs +++ b/crates/tinytools-agent/src/parse/test/mod.rs @@ -8,6 +8,7 @@ mod function_call; mod glm; mod harmony_mistral; mod invoke_xml; +mod markup; mod regressions; mod sentinel; mod tagged; From 03a21f40fdb2b0337a35f282f5004ef662c378a7 Mon Sep 17 00:00:00 2001 From: Steven Enamakel Date: Sat, 3 Oct 2026 10:09:19 +0300 Subject: [PATCH 4/4] fix(parse): backtick-quote model and benchmark names in doc comments The model name `DeepSeek` and the benchmark name `OpenHuman` are now wrapped in backticks in doc comments and test constants, making them render as inline code in generated documentation and improving readability. Auto-committed-on: dragonfly Co-authored-by: Medulla --- crates/tinytools-agent/src/parse/mod.rs | 2 +- crates/tinytools-agent/src/parse/test/markup.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tinytools-agent/src/parse/mod.rs b/crates/tinytools-agent/src/parse/mod.rs index c462cfb..05196b0 100644 --- a/crates/tinytools-agent/src/parse/mod.rs +++ b/crates/tinytools-agent/src/parse/mod.rs @@ -280,7 +280,7 @@ pub fn parse_tool_calls_with_pformat( /// /// For a caller that must refuse text which is really a tool call rather /// than read calls out of it — a summarizer whose request declared no tools, -/// say, where DeepSeek V4 can answer with `<|DSML|invoke name="shell">…` +/// say, where `DeepSeek` V4 can answer with `<|DSML|invoke name="shell">…` /// instead of a summary. A response that is only a bare JSON object does /// not count, since an answer may legitimately be one, and markup quoted /// inside a language-tagged fence is protected as usual. diff --git a/crates/tinytools-agent/src/parse/test/markup.rs b/crates/tinytools-agent/src/parse/test/markup.rs index 3e75e4b..dca5d3e 100644 --- a/crates/tinytools-agent/src/parse/test/markup.rs +++ b/crates/tinytools-agent/src/parse/test/markup.rs @@ -3,8 +3,8 @@ use crate::parse::contains_call_markup; -/// What DeepSeek V4 returned in place of a summary when its request declared -/// no tools (captured from the OpenHuman harness benchmark). +/// What `DeepSeek` V4 returned in place of a summary when its request declared +/// no tools (captured from the `OpenHuman` harness benchmark). const DSML: &str = "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n\ <||DSML|| parameter name=\"command\" string=\"true\">cd /app && cat src/lib.rs\n\ \n";