Skip to content

Commit 1c58197

Browse files
authored
Merge pull request #45 from tinyhumansai/fix/glm-fenced-examples
Ignore fenced examples in GLM fallback
2 parents 8f18f7f + c95677f commit 1c58197

3 files changed

Lines changed: 63 additions & 3 deletions

File tree

‎crates/tinytools-agent/src/parse/mod.rs‎

Lines changed: 36 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,49 @@ pub fn parse_text(text: &str, options: &ParseOptions<'_>) -> ParseOutcome {
7676
let diagnostics = scan.diagnostics;
7777

7878
if calls.is_empty() {
79-
let joined = parts.join("\n");
80-
let (cleaned, glm_calls) = grammar::glm::parse_and_strip(&joined);
79+
let (cleaned, glm_calls) = parse_unprotected_glm(text, &scan.kept);
8180
if !glm_calls.is_empty() {
8281
calls = glm_calls;
83-
return finalize(cleaned.trim().to_string(), calls, diagnostics, options);
82+
return finalize(cleaned, calls, diagnostics, options);
8483
}
8584
}
8685
finalize(parts.join("\n"), calls, diagnostics, options)
8786
}
8887

88+
/// Parses GLM calls outside fences and keeps fenced examples in the narrative.
89+
fn parse_unprotected_glm(text: &str, kept: &[Range<usize>]) -> (String, Vec<ParsedToolCall>) {
90+
let protected = protected::fence_ranges(text);
91+
let mut parts = Vec::new();
92+
let mut calls = Vec::new();
93+
for range in kept {
94+
let mut cursor = range.start;
95+
let mut cleaned = String::new();
96+
for fence in &protected {
97+
if fence.end <= cursor || fence.start >= range.end {
98+
continue;
99+
}
100+
if cursor < fence.start {
101+
let (part, parsed) = grammar::glm::parse_and_strip(&text[cursor..fence.start]);
102+
cleaned.push_str(&part);
103+
calls.extend(parsed);
104+
}
105+
let end = fence.end.min(range.end);
106+
cleaned.push_str(&text[cursor.max(fence.start)..end]);
107+
cursor = end;
108+
}
109+
if cursor < range.end {
110+
let (part, parsed) = grammar::glm::parse_and_strip(&text[cursor..range.end]);
111+
cleaned.push_str(&part);
112+
calls.extend(parsed);
113+
}
114+
let cleaned = cleaned.trim();
115+
if !cleaned.is_empty() {
116+
parts.push(cleaned.to_string());
117+
}
118+
}
119+
(parts.join("\n"), calls)
120+
}
121+
89122
/// Name resolution and the diagnostics it produces.
90123
fn finalize(
91124
text: String,

‎crates/tinytools-agent/src/parse/test/glm.rs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ fn glm_lines_parse_when_nothing_else_matched() {
1111
assert_eq!(calls[0].name, "shell");
1212
}
1313

14+
#[test]
15+
fn fenced_example_survives_a_real_glm_call() {
16+
let input = "Before\n```text\nshell/command>example\n```\nAfter\nshell/command>real";
17+
let (text, calls) = parse(input);
18+
assert_eq!(calls.len(), 1);
19+
assert_eq!(calls[0].name, "shell");
20+
assert_eq!(calls[0].arguments, serde_json::json!({"command": "real"}));
21+
assert!(text.contains("```text\nshell/command>example\n```"));
22+
assert!(text.contains("Before"));
23+
assert!(text.contains("After"));
24+
assert!(!text.contains("shell/command>real"));
25+
}
26+
1427
#[test]
1528
fn glm_parser_covers_json_payloads_invalid_urls_and_plain_commands() {
1629
let calls = parse_glm_style_tool_calls(concat!(

‎crates/tinytools-agent/src/parse/test/markup.rs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,19 @@
33
44
use crate::parse::contains_call_markup;
55

6+
#[test]
7+
fn glm_fallback_keeps_fenced_text_within_a_kept_range() {
8+
let input = "discard\nintro\n```text\nshell/command>example\n```\nshell/command>real";
9+
let start = input.find("intro").unwrap();
10+
let kept = start..input.len();
11+
let (text, calls) = super::super::parse_unprotected_glm(input, std::slice::from_ref(&kept));
12+
assert_eq!(calls.len(), 1);
13+
assert_eq!(calls[0].arguments, serde_json::json!({"command": "real"}));
14+
assert!(text.starts_with("intro\n```text\nshell/command>example\n```"));
15+
assert!(!text.contains("discard"));
16+
assert!(!text.contains("shell/command>real"));
17+
}
18+
619
/// What `DeepSeek` V4 returned in place of a summary when its request declared
720
/// no tools (captured from the `OpenHuman` harness benchmark).
821
const DSML: &str = "<||DSML|| calls>\n<||DSML|| invoke name=\"shell\">\n\
@@ -46,5 +59,6 @@ fn prose_a_bare_json_answer_and_a_quoted_example_are_not_markup() {
4659
assert!(!contains_call_markup(
4760
"The format is:\n```xml\n<invoke name=\"shell\"><parameter name=\"command\">ls</parameter></invoke>\n```"
4861
));
62+
assert!(!contains_call_markup("```text\nshell/command>ls\n```"));
4963
assert!(!contains_call_markup(""));
5064
}

0 commit comments

Comments
 (0)