From fdb223f14251dffbd9f163f77da32c031252348a Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Mon, 28 Sep 2026 16:33:23 +0530 Subject: [PATCH 1/4] fix(agent): decode a named invoke wrapped in a tool_call tag Told to call tools inside tags, DeepSeek V4 writes its native invoke XML there. The tagged grammar claimed the block, found no JSON body, and dropped the call as malformed, though the same invoke parses bare. When a tag body opens with a named invoke, decode it with invoke_xml. The anchor keeps an invoke quoted inside other body text from executing. Refs tinyhumansai/openhuman#6722 --- .../src/parse/grammar/invoke_xml.rs | 25 ++++++++ .../src/parse/grammar/tagged.rs | 5 ++ .../tinytools-agent/src/parse/test/tagged.rs | 63 +++++++++++++++++++ 3 files changed, 93 insertions(+) diff --git a/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs b/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs index 9a08b03..f2beabe 100644 --- a/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs +++ b/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs @@ -217,6 +217,31 @@ impl InvokeXml { } } +/// Every call in a tag body that *is* invoke XML — `…`, what `DeepSeek` V4 writes when told to +/// call tools inside `` tags. Empty unless the body opens with a +/// named invoke, so an invoke quoted inside some other body (a JSON string, +/// say) is never executed. +pub(crate) fn decode_body(body: &str) -> Vec { + let body = body.trim_start(); + if OPEN_RE + .as_ref() + .and_then(|re| re.find(body)) + .is_none_or(|m| m.start() != 0) + { + return Vec::new(); + } + let mut calls = Vec::new(); + let mut from = 0; + while let Probe::Found(block) = InvokeXml::probe_decided(body, from, ScanMode::Batch) { + if let Decoded::Calls(found) = block.decoded { + calls.extend(found); + } + from = block.end; + } + calls +} + /// Whether a wrapper tag is a closer or carries a DSML / namespace prefix — /// either is unambiguous protocol furniture even with no invoke in sight. fn is_closer_or_prefixed(tag: &str) -> bool { diff --git a/crates/tinytools-agent/src/parse/grammar/tagged.rs b/crates/tinytools-agent/src/parse/grammar/tagged.rs index f189894..bf2154e 100644 --- a/crates/tinytools-agent/src/parse/grammar/tagged.rs +++ b/crates/tinytools-agent/src/parse/grammar/tagged.rs @@ -590,6 +590,11 @@ pub(crate) fn decode_body(body: &str, options: &ParseOptions<'_>) -> Vec tags", `DeepSeek` V4 writes its +// native invoke XML there. The tag claimed the block and found no JSON, so +// the call was dropped as malformed even though the same invoke parses bare. + +#[test] +fn a_named_invoke_wrapped_in_a_tool_call_tag_is_decoded() { + let raw = "Searching.\n\n\nrepos\n\n"; + let outcome = super::parse_known(raw, &["tool_search"]); + assert_eq!(outcome.calls.len(), 1, "{:?}", outcome.calls); + assert_eq!(outcome.calls[0].name, "tool_search"); + assert_eq!( + outcome.calls[0].arguments, + serde_json::json!({"query": "repos"}) + ); + assert_eq!(outcome.calls[0].source, CallSource::InvokeXml); + assert_eq!(outcome.text, "Searching."); +} + +#[test] +fn a_wrapped_invoke_with_string_attributes_is_decoded() { + let raw = concat!( + "\n\n", + "repos\n", + "5\n", + "\n" + ); + let outcome = super::parse_known(raw, &["tool_search"]); + assert_eq!(outcome.calls.len(), 1, "{:?}", outcome.calls); + assert_eq!( + outcome.calls[0].arguments, + serde_json::json!({"query": "repos", "limit": 5}) + ); +} + +/// A `` block, a line of narration, then the wrapped invoke inside a +/// closed bare fence (no info string, so not protected). +#[test] +fn a_todo_block_then_a_closed_bare_fenced_wrapped_invoke_is_decoded() { + let raw = concat!( + "\n- [x] read the request\n- [ ] find the tool\n\n\n", + "Let me find the right tool.\n\n", + "```\n\n\n", + "list repositories\n", + "\n\n```" + ); + let outcome = super::parse_known(raw, &["tool_search"]); + assert_eq!(outcome.calls.len(), 1, "{:?}", outcome.calls); + assert_eq!(outcome.calls[0].name, "tool_search"); + assert_eq!( + outcome.calls[0].arguments, + serde_json::json!({"query": "list repositories"}) + ); +} + +#[test] +fn an_invoke_after_other_body_text_in_the_tag_is_not_decoded() { + let raw = "see ls"; + let (_, calls) = parse(raw); + assert!(calls.is_empty(), "{calls:?}"); +} From 32d6eb419d47ed05a3693b14c003d09bcea02b33 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Mon, 28 Sep 2026 17:02:36 +0530 Subject: [PATCH 2/4] test(agent): pin the wrapped-invoke anchor and Qwen3-Coder form MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a closed JSON tag body quoting an invoke (fails without the anchor) and a body (dropped on main). Narrows the decode_body doc to what the anchor guarantees: only the first invoke is anchored, as on the bare path. --- .../src/parse/grammar/invoke_xml.rs | 3 ++- .../tinytools-agent/src/parse/test/tagged.rs | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs b/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs index f2beabe..2b5b2f0 100644 --- a/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs +++ b/crates/tinytools-agent/src/parse/grammar/invoke_xml.rs @@ -221,7 +221,8 @@ impl InvokeXml { /// name="x">…`, what `DeepSeek` V4 writes when told to /// call tools inside `` tags. Empty unless the body opens with a /// named invoke, so an invoke quoted inside some other body (a JSON string, -/// say) is never executed. +/// say) is not executed. Once the body does open with one, every later +/// invoke in it is decoded too, exactly as the same text outside a tag is. pub(crate) fn decode_body(body: &str) -> Vec { let body = body.trim_start(); if OPEN_RE diff --git a/crates/tinytools-agent/src/parse/test/tagged.rs b/crates/tinytools-agent/src/parse/test/tagged.rs index b3b950f..22ccd91 100644 --- a/crates/tinytools-agent/src/parse/test/tagged.rs +++ b/crates/tinytools-agent/src/parse/test/tagged.rs @@ -802,3 +802,25 @@ fn an_invoke_after_other_body_text_in_the_tag_is_not_decoded() { let (_, calls) = parse(raw); assert!(calls.is_empty(), "{calls:?}"); } + +#[test] +fn an_invoke_quoted_in_a_closed_json_tag_body_is_not_executed() { + let raw = "{\"name\":\"tool_search\",\"arguments\":{\"query\":\"rm -rf /\"}}"; + let (_, calls) = parse(raw); + assert!(calls.iter().all(|c| c.name != "shell"), "{calls:?}"); +} + +/// Qwen3-Coder's native format inside the tag. +#[test] +fn a_function_equals_body_in_a_tool_call_tag_is_decoded() { + let raw = "Checking.\n\n\n\nrepos\n\n\n"; + let outcome = super::parse_known(raw, &["tool_search"]); + assert_eq!(outcome.calls.len(), 1, "{:?}", outcome.calls); + assert_eq!(outcome.calls[0].name, "tool_search"); + assert_eq!( + outcome.calls[0].arguments, + serde_json::json!({"query": "repos"}) + ); + assert_eq!(outcome.calls[0].source, CallSource::InvokeXml); + assert_eq!(outcome.text, "Checking."); +} From 396e15c58dba864f968df49e4c9b593c55e7cff9 Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Mon, 28 Sep 2026 16:43:46 +0530 Subject: [PATCH 3/4] fix(agent): treat a fence whose info string is a call tag as a call DeepSeek V4 Flash wrote the opener as the fence info string, ```, and never closed the fence. The info string read as a language, so the unclosed fence protected the call to end of text as an example and it was dropped. A fence whose info string opens with a complete call tag (tag-family opener, named invoke, bare ) is now a call fence. A language-tagged fence still protects its contents. Refs tinyhumansai/openhuman#6722 --- .../src/parse/grammar/tagged.rs | 12 +++++ crates/tinytools-agent/src/parse/protected.rs | 8 +-- .../tinytools-agent/src/parse/test/engine.rs | 52 +++++++++++++++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/crates/tinytools-agent/src/parse/grammar/tagged.rs b/crates/tinytools-agent/src/parse/grammar/tagged.rs index bf2154e..b09f845 100644 --- a/crates/tinytools-agent/src/parse/grammar/tagged.rs +++ b/crates/tinytools-agent/src/parse/grammar/tagged.rs @@ -101,6 +101,18 @@ fn find_re(re: &LazyLock>, haystack: &str) -> Option<(usize, usize .map(|m| (m.start(), m.end())) } +/// Whether `info` — a fence's info string — opens with a complete call tag: +/// a tag-family opener, a named invoke, or the bare ``. `DeepSeek` V4 +/// writes ```` ``` ````, so such a fence is a call, not an example. +pub(crate) fn opens_with_call_tag(info: &str) -> bool { + let at_start = |re: &LazyLock>| find_re(re, info).is_some_and(|(s, _)| s == 0); + let tag = TAG_RE + .as_ref() + .and_then(|re| re.find(info)) + .is_some_and(|m| m.start() == 0 && !is_closing_marker(m.as_str())); + tag || at_start(&NAMED_INVOKE_OPEN_RE) || at_start(&BARE_INVOKE_OPEN_RE) +} + /// Finds the closer that has the exact prefix and spelling of `opener`. /// /// A bare `` must not be closed by `` embedded in its diff --git a/crates/tinytools-agent/src/parse/protected.rs b/crates/tinytools-agent/src/parse/protected.rs index 6673b9b..2189fdd 100644 --- a/crates/tinytools-agent/src/parse/protected.rs +++ b/crates/tinytools-agent/src/parse/protected.rs @@ -8,8 +8,9 @@ //! //! Two deliberate exceptions keep real calls parseable: //! -//! * a fence whose language *is* a tool-call marker (```` ```tool_call ````) -//! is a call, not an example, and is handled by the tagged grammar; +//! * a fence whose language *is* a tool-call marker (```` ```tool_call ````), +//! or whose info string opens with a call tag (```` ``` ````), +//! is a call, not an example, and is handled by the grammars; //! * a fence with **no** language tag is not protected. Small models wrap a //! genuine call in a bare fence far more often than they quote one, and a //! quoted example almost always carries a language. @@ -71,7 +72,8 @@ fn scan_fences(text: &str) -> (Vec>, Option) { let language = info.split_whitespace().next().unwrap_or(""); let is_tool_call = TOOL_CALL_LANGUAGES .iter() - .any(|lang| lang.eq_ignore_ascii_case(language)); + .any(|lang| lang.eq_ignore_ascii_case(language)) + || super::grammar::tagged::opens_with_call_tag(info); if !language.is_empty() && !is_tool_call { open = Some((line_start, fence_char, fence_len)); } diff --git a/crates/tinytools-agent/src/parse/test/engine.rs b/crates/tinytools-agent/src/parse/test/engine.rs index 66e26e9..e6b60c8 100644 --- a/crates/tinytools-agent/src/parse/test/engine.rs +++ b/crates/tinytools-agent/src/parse/test/engine.rs @@ -43,6 +43,58 @@ fn fence_ranges_cover_languages_and_unclosed_fences() { assert!(fence_ranges("```tool_call\n{}\n```").is_empty()); } +// ── A call tag on the fence line itself ──────────────────────────────────── +// +// `DeepSeek` V4 Flash wrote ```` ``` ```` — the opener as the info +// string — and never closed the fence. The info string read as a language, +// so the unclosed fence protected the call to end of text as an example. + +#[test] +fn an_unclosed_fence_whose_info_string_is_a_call_tag_is_a_call() { + let text = concat!( + "\n- [ ] find the tool\n\n\n", + "```\n\n", + "list repositories\n", + "\n" + ); + let outcome = parse_known(text, &["tool_search"]); + assert_eq!(outcome.calls.len(), 1, "{:?}", outcome.calls); + assert_eq!(outcome.calls[0].name, "tool_search"); + assert_eq!( + outcome.calls[0].arguments, + serde_json::json!({"query": "list repositories"}) + ); +} + +#[test] +fn a_fence_whose_info_string_is_a_named_invoke_is_a_call() { + let text = + "```\nrepos\n"; + let (_, calls) = parse(text); + assert_eq!(calls.len(), 1, "{calls:?}"); + assert_eq!(calls[0].name, "tool_search"); +} + +#[test] +fn a_closed_fence_whose_info_string_is_a_call_tag_is_a_call() { + let text = "```\n\nrepos\n\n\n```"; + let (_, calls) = parse(text); + assert_eq!(calls.len(), 1, "{calls:?}"); +} + +#[test] +fn a_language_fence_still_protects_a_call_tag_example() { + let example = "\nrm -rf /\n"; + for text in [ + format!("```xml\n{example}"), + format!("```xml\n{example}"), + format!("```text \n{example}"), + ] { + let (_, calls) = parse(&text); + assert!(calls.is_empty(), "{text:?} dispatched {calls:?}"); + } +} + #[test] fn names_are_repaired_against_known_tools() { let outcome = parse_known( From cc7107b0d51830b8e0729f281ab8cf07ec8be24d Mon Sep 17 00:00:00 2001 From: M3gA-Mind Date: Mon, 28 Sep 2026 17:03:42 +0530 Subject: [PATCH 4/4] fix(agent): limit fence call tags to tool_call and invoke MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit opens_with_call_tag reused NAMED_INVOKE_OPEN_RE, which also accepts and any XML namespace, so an XSLT fence such as ``` became a call. Only a tag-family opener or an (optionally DSML-prefixed) now marks a call fence. --- .../tinytools-agent/src/parse/grammar/tagged.rs | 16 +++++++++++++--- crates/tinytools-agent/src/parse/test/engine.rs | 3 +++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/crates/tinytools-agent/src/parse/grammar/tagged.rs b/crates/tinytools-agent/src/parse/grammar/tagged.rs index b09f845..95dd933 100644 --- a/crates/tinytools-agent/src/parse/grammar/tagged.rs +++ b/crates/tinytools-agent/src/parse/grammar/tagged.rs @@ -101,16 +101,26 @@ fn find_re(re: &LazyLock>, haystack: &str) -> Option<(usize, usize .map(|m| (m.start(), m.end())) } +/// An `` opener, bare or named, optionally DSML-prefixed — the only +/// invoke spellings a fence line may carry and still count as a call. No +/// `` and no XML namespace: ```` ``` ```` +/// is code, not a call. +static FENCE_INVOKE_RE: LazyLock> = LazyLock::new(|| { + Regex::new( + r#"(?i)^<(?:[|\u{ff5c}]{1,2}\s*DSML\s*[|\u{ff5c}]{1,2}\s*)?invoke(?:\s+[^>]*?\bname\s*=\s*"[^"]*"[^>]*)?\s*>"#, + ) + .ok() +}); + /// Whether `info` — a fence's info string — opens with a complete call tag: -/// a tag-family opener, a named invoke, or the bare ``. `DeepSeek` V4 +/// a tag-family opener or an `` ([`FENCE_INVOKE_RE`]). `DeepSeek` V4 /// writes ```` ``` ````, so such a fence is a call, not an example. pub(crate) fn opens_with_call_tag(info: &str) -> bool { - let at_start = |re: &LazyLock>| find_re(re, info).is_some_and(|(s, _)| s == 0); let tag = TAG_RE .as_ref() .and_then(|re| re.find(info)) .is_some_and(|m| m.start() == 0 && !is_closing_marker(m.as_str())); - tag || at_start(&NAMED_INVOKE_OPEN_RE) || at_start(&BARE_INVOKE_OPEN_RE) + tag || FENCE_INVOKE_RE.as_ref().is_some_and(|re| re.is_match(info)) } /// Finds the closer that has the exact prefix and spelling of `opener`. diff --git a/crates/tinytools-agent/src/parse/test/engine.rs b/crates/tinytools-agent/src/parse/test/engine.rs index e6b60c8..930c629 100644 --- a/crates/tinytools-agent/src/parse/test/engine.rs +++ b/crates/tinytools-agent/src/parse/test/engine.rs @@ -89,6 +89,9 @@ fn a_language_fence_still_protects_a_call_tag_example() { format!("```xml\n{example}"), format!("```xml\n{example}"), format!("```text \n{example}"), + format!("```\n{example}"), + format!("```\n{example}"), + format!("```\n{example}"), ] { let (_, calls) = parse(&text); assert!(calls.is_empty(), "{text:?} dispatched {calls:?}");