[Rust Frontend] Add error context in tool parser failures (#46512)

Signed-off-by: Maria Guevara <kawaiiplush14@gmail.com>

Maria Guevara committed Jul 1, 2026 at 06:48 UTC aeb35b90f0b66e5c5f12a3c86864ea92dccbd180
12 files changed +57 -19
rust/src/parser/src/tool/deepseek_json/deepseek_v3.rs
+4 -1
@@ -238,6 +238,9 @@ mod tests {
238
239 let error = parser.parse_chunk(&input).unwrap_err();
240
241 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
241 + expect![[
242 + r#"tool parser parsing failed: near "tool<|tool▁sep|>get_weather\n```json\n{}": "#
243 + ]]
244 + .assert_eq(&error.to_report_string());
245 }
246 }
rust/src/parser/src/tool/deepseek_json/deepseek_v31.rs
+2 -1
@@ -242,6 +242,7 @@ mod tests {
242
243 let error = parser.parse_chunk(&input).unwrap_err();
244
245 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
245 + expect![[r#"tool parser parsing failed: near "<|tool▁sep|>{}": "#]]
246 + .assert_eq(&error.to_report_string());
247 }
248 }
rust/src/parser/src/tool/json/granite4.rs
+4 -2
@@ -540,8 +540,10 @@ mod tests {
540 .parse_chunk(r#"<tool_call>{"name":"f","arguments":42}</tool_call>"#)
541 .unwrap_err();
542
543 - expect!["tool parser parsing failed: invalid Granite4 arguments"]
544 - .assert_eq(&error.to_report_string());
543 + expect![[
544 + r#"tool parser parsing failed: near "42}</tool_call>": invalid Granite4 arguments"#
545 + ]]
546 + .assert_eq(&error.to_report_string());
547 }
548
549 #[test]
rust/src/parser/src/tool/json/internlm2.rs
+1 -1
@@ -335,7 +335,7 @@ mod tests {
335 let error = parser.parse_chunk(&input).unwrap_err();
336
337 expect![[r#"
338 - tool parser parsing failed: invalid InternLM2
338 + tool parser parsing failed: near "{\"name\":\"get_weather\",\"params\":{\"location\":\"Tokyo\"}}<|action_end|>": invalid InternLM2
339 expected `parameters`, `arguments`"#]]
340 .assert_eq(&error.to_report_string());
341 }
rust/src/parser/src/tool/json/llama.rs
+3 -3
@@ -336,7 +336,7 @@ mod tests {
336 .unwrap_err();
337
338 expect![[r#"
339 - tool parser parsing failed: invalid Llama JSON
339 + tool parser parsing failed: near "{\"name\":\"get_weather\",\"arguments\":{\"location\":\"Tokyo\"}}": invalid Llama JSON
340 expected `parameters`"#]]
341 .assert_eq(&error.to_report_string());
342 }
@@ -474,7 +474,7 @@ mod tests {
474 let error = parser.parse_chunk(r#"{"parameters":{},"name":"get_weather"}"#).unwrap_err();
475
476 expect![[r#"
477 - tool parser parsing failed: invalid Llama JSON
477 + tool parser parsing failed: near "{\"parameters\":{},\"name\":\"get_weather\"}": invalid Llama JSON
478 expected `name`"#]]
479 .assert_eq(&error.to_report_string());
480 }
@@ -489,7 +489,7 @@ mod tests {
489 ))
490 .unwrap_err();
491
492 - expect!["tool parser parsing failed: invalid Llama JSON"]
492 + expect![[r#"tool parser parsing failed: near " trailing": invalid Llama JSON"#]]
493 .assert_eq(&error.to_report_string());
494 }
495 }
rust/src/parser/src/tool/json/mistral.rs
+1 -1
@@ -240,7 +240,7 @@ mod tests {
240 .unwrap_err();
241
242 expect![[r#"
243 - tool parser parsing failed: invalid Mistral
243 + tool parser parsing failed: near "{\"arguments\":{},\"name\":\"get_weather\"}]": invalid Mistral
244 expected `name`"#]]
245 .assert_eq(&error.to_report_string());
246 }
rust/src/parser/src/tool/json/qwen.rs
+1 -1
@@ -280,7 +280,7 @@ mod tests {
280 .unwrap_err();
281
282 expect![[r#"
283 - tool parser parsing failed: invalid Qwen XML
283 + tool parser parsing failed: near "{\"arguments\":{},\"name\":\"get_weather\"}\n</tool_call>": invalid Qwen XML
284 expected `name`"#]]
285 .assert_eq(&error.to_report_string());
286 }
rust/src/parser/src/tool/kimi_k2.rs
+4 -1
@@ -594,6 +594,9 @@ mod tests {
594
595 let error = parser.parse_chunk(&input).unwrap_err();
596
597 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
597 + expect![[
598 + r#"tool parser parsing failed: near "get_weather<|tool_call_argument_begin|>{}": "#
599 + ]]
600 + .assert_eq(&error.to_report_string());
601 }
602 }
rust/src/parser/src/tool/minimax_m2.rs
+2 -1
@@ -593,6 +593,7 @@ mod tests {
593 let mut parser = MinimaxM2ToolParser::new(&test_tools());
594 let error = parser.parse_chunk("<minimax:tool_call><bad></minimax:tool_call>").unwrap_err();
595
596 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
596 + expect![[r#"tool parser parsing failed: near "<bad></minimax:tool_call>": "#]]
597 + .assert_eq(&error.to_report_string());
598 }
599 }
rust/src/parser/src/tool/minimax_m3.rs
+4 -1
@@ -878,7 +878,10 @@ mod tests {
878 ))
879 .unwrap_err();
880
881 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
881 + expect![[
882 + r#"tool parser parsing failed: near "]<]minimax[>[<bad>]<]minimax[>[</tool_call>": "#
883 + ]]
884 + .assert_eq(&error.to_report_string());
885 }
886
887 #[test]
rust/src/parser/src/tool/qwen_coder.rs
+3 -2
@@ -685,7 +685,8 @@ mod tests {
685 let mut parser = Qwen3CoderToolParser::new(&test_tools());
686 let error = parser.parse_chunk("<tool_call>\n<bad>\n</tool_call>").unwrap_err();
687
688 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
688 + expect![[r#"tool parser parsing failed: near "\n<bad>\n</tool_call>": "#]]
689 + .assert_eq(&error.to_report_string());
690 }
691
692 #[test]
@@ -697,7 +698,7 @@ mod tests {
698 )
699 .unwrap_err();
700
700 - expect!["tool parser parsing failed: "].assert_eq(&error.to_report_string());
701 + expect![[r#"tool parser parsing failed: near "\n<function=get_weather>\n<parameter=location>SF</function>\n</tool_call>": "#]].assert_eq(&error.to_report_string());
702 }
703
704 #[test]
rust/src/parser/src/utils.rs
+28 -4
@@ -395,9 +395,9 @@ pub fn parse_buffered_event<E>(
395 Ok(event) => event,
396 Err(ErrMode::Incomplete(_)) => return Ok(None),
397 Err(ErrMode::Backtrack(e) | ErrMode::Cut(e)) => {
398 - // TODO: enrich context for error reporting
398 + let snippet = buffer.char_indices().nth(80).map_or(buffer, |(i, _)| &buffer[..i]);
399 return Err(ToolParserError::ParsingFailed {
400 - message: e.to_string(),
400 + message: format!("near {snippet:?}: {e}"),
401 });
402 }
403 };
@@ -423,8 +423,9 @@ mod tests {
423 use winnow::stream::{Offset, Partial, Stream};
424
425 use super::{
426 - JsonObjectScanState, JsonStringScanState, MarkerScanState, json_str, partial_prefix_len,
427 - safe_text_len, safe_text_len_mul, take_json_object, take_json_string, take_until_marker,
426 + JsonObjectScanState, JsonStringScanState, MarkerScanState, json_str, parse_buffered_event,
427 + partial_prefix_len, safe_text_len, safe_text_len_mul, take_json_object, take_json_string,
428 + take_until_marker,
429 };
430
431 #[test]
@@ -832,4 +833,27 @@ mod tests {
833
834 assert!(matches!(error, ErrMode::Incomplete(_)));
835 }
836 +
837 + #[test]
838 + fn parse_buffered_event_error_includes_input_snippet() {
839 + let result = parse_buffered_event(" {\"x\":1}", |input| {
840 + take_json_object(input, &mut JsonObjectScanState::default())
841 + });
842 + let err = result.unwrap_err().to_string();
843 + assert!(err.contains("near \""), "error must include snippet");
844 + }
845 +
846 + #[test]
847 + fn parse_buffered_event_error_truncates_long_input() {
848 + let long_input = format!(" {}", "x".repeat(100));
849 + let result = parse_buffered_event(&long_input, |input| {
850 + take_json_object(input, &mut JsonObjectScanState::default())
851 + });
852 + let err = result.unwrap_err().to_string();
853 + assert!(err.contains("near \""), "error must include snippet");
854 + assert!(
855 + !err.contains(&long_input),
856 + "snippet must be truncated for long input"
857 + );
858 + }
859 }