From 0e8915669ac2291fe43122394c198626df89574c Mon Sep 17 00:00:00 2001 From: wsp Date: Wed, 16 Sep 2026 19:25:06 +0800 Subject: [PATCH] fix(tools): accept integral float arguments Models sometimes encode integer tool arguments as values such as 100.0, which previously caused integer extraction to fall back or reject the call. Centralize signed and unsigned integer parsing so integral floating-point values are accepted while fractional and invalid values remain rejected. Apply the parser to affected file, search, UI, and computer-use arguments and cover the behavior with focused tests. --- .../implementations/computer_use_locate.rs | 5 +- .../tools/implementations/file_read_tool.rs | 3 +- .../implementations/generative_ui_tool.rs | 5 +- .../tools/implementations/grep_tool.rs | 19 ++++--- .../core/src/agentic/tools/integer_parser.rs | 53 +++++++++++++++++++ .../assembly/core/src/agentic/tools/mod.rs | 2 + 6 files changed, 76 insertions(+), 11 deletions(-) create mode 100644 src/crates/assembly/core/src/agentic/tools/integer_parser.rs diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/computer_use_locate.rs b/src/crates/assembly/core/src/agentic/tools/implementations/computer_use_locate.rs index 3d41c8b92b..edbc379ea6 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/computer_use_locate.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/computer_use_locate.rs @@ -6,6 +6,7 @@ use crate::agentic::tools::computer_use_host::{ }; use crate::agentic::tools::framework::{ToolResult, ToolUseContext}; use crate::agentic::tools::implementations::computer_use_tool::computer_use_augment_result_json; +use crate::agentic::tools::parse_u64_value; use crate::service::config::global::GlobalConfigManager; use crate::util::errors::{OpenBitFunError, OpenBitFunResult}; use serde_json::{json, Value}; @@ -60,7 +61,7 @@ pub(crate) async fn execute_computer_use_locate( .map(|s| s.to_string()), max_depth: input .get("max_depth") - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) .map(|v| v as u32), filter_combine: input .get("filter_combine") @@ -72,7 +73,7 @@ pub(crate) async fn execute_computer_use_locate( .map(|s| s.to_string()), node_idx: input .get("node_idx") - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) .map(|v| v as u32), app_state_digest: input .get("app_state_digest") diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/file_read_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/file_read_tool.rs index c785ef4b16..0497a13784 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/file_read_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/file_read_tool.rs @@ -6,6 +6,7 @@ use crate::agentic::tools::framework::{ use crate::agentic::tools::miniapp_context_runtime::{ is_virtual_context_path, requires_virtual_context_path, virtual_context_file, }; +use crate::agentic::tools::parse_u64_value; use crate::agentic::tools::review_read_receipt_runtime::{ file_revision, get_review_read_coverage, record_review_read_receipt, review_read_receipts_enabled, @@ -611,7 +612,7 @@ Usage: let limit = input .get("limit") - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) .unwrap_or(self.default_max_lines_to_read as u64) as usize; let resolved = context.resolve_tool_path(file_path)?; diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs index 38a3d12ba2..429a87a4e4 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/generative_ui_tool.rs @@ -3,6 +3,7 @@ use crate::agentic::tools::framework::{ Tool, ToolExposure, ToolResult, ToolUseContext, ValidationResult, }; +use crate::agentic::tools::parse_i64_value; use crate::service::config::get_global_config_service; use crate::util::errors::OpenBitFunResult; use async_trait::async_trait; @@ -434,8 +435,8 @@ Input rules: .get("widget_code") .and_then(|v| v.as_str()) .unwrap_or(""); - let width = input.get("width").and_then(|v| v.as_i64()).unwrap_or(960); - let height = input.get("height").and_then(|v| v.as_i64()).unwrap_or(640); + let width = input.get("width").and_then(parse_i64_value).unwrap_or(960); + let height = input.get("height").and_then(parse_i64_value).unwrap_or(640); let modules = input .get("modules") .and_then(|v| v.as_array()) diff --git a/src/crates/assembly/core/src/agentic/tools/implementations/grep_tool.rs b/src/crates/assembly/core/src/agentic/tools/implementations/grep_tool.rs index 5d49d63b22..30ad740d32 100644 --- a/src/crates/assembly/core/src/agentic/tools/implementations/grep_tool.rs +++ b/src/crates/assembly/core/src/agentic/tools/implementations/grep_tool.rs @@ -3,6 +3,7 @@ use crate::agentic::tools::framework::{Tool, ToolResult, ToolUseContext}; use crate::agentic::tools::miniapp_context_runtime::{ is_virtual_context_path, requires_virtual_context_path, virtual_context_files_for_search, }; +use crate::agentic::tools::parse_u64_value; use crate::agentic::tools::ToolPathOperation; use crate::service::search::{ get_global_workspace_search_service, remote_workspace_search_service_for_path, @@ -64,7 +65,7 @@ impl GrepTool { fn explicit_head_limit(input: &Value) -> Option> { input .get("head_limit") - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) .map(|value| { if value == 0 { None @@ -112,7 +113,7 @@ impl GrepTool { fn resolve_offset(input: &Value) -> usize { input .get("offset") - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) .map(|value| value as usize) .unwrap_or(0) } @@ -207,10 +208,16 @@ impl GrepTool { let context_c = input .get("context") .or_else(|| input.get("-C")) - .and_then(|v| v.as_u64()) + .and_then(parse_u64_value) + .map(|v| v as usize); + let before_context = input + .get("-B") + .and_then(parse_u64_value) + .map(|v| v as usize); + let after_context = input + .get("-A") + .and_then(parse_u64_value) .map(|v| v as usize); - let before_context = input.get("-B").and_then(|v| v.as_u64()).map(|v| v as usize); - let after_context = input.get("-A").and_then(|v| v.as_u64()).map(|v| v as usize); let head_limit = Self::resolve_head_limit(input); let offset = Self::resolve_offset(input); let glob_patterns = Self::parse_glob_patterns(input.get("glob").and_then(|v| v.as_str())); @@ -266,7 +273,7 @@ impl GrepTool { ["-A", "-B", "-C", "context"] .iter() .filter_map(|key| input.get(*key)) - .filter_map(|value| value.as_u64()) + .filter_map(parse_u64_value) .any(|lines| lines > 0) } diff --git a/src/crates/assembly/core/src/agentic/tools/integer_parser.rs b/src/crates/assembly/core/src/agentic/tools/integer_parser.rs new file mode 100644 index 0000000000..343c66d938 --- /dev/null +++ b/src/crates/assembly/core/src/agentic/tools/integer_parser.rs @@ -0,0 +1,53 @@ +use serde_json::Value; + +/// Parse an unsigned integer from model-provided JSON, accepting integral +/// floating-point representations such as `100.0`. +pub fn parse_u64_value(value: &Value) -> Option { + value.as_u64().or_else(|| { + value + .as_f64() + .filter(|number| { + number.is_finite() + && *number >= 0.0 + && number.fract() == 0.0 + && *number <= u64::MAX as f64 + }) + .map(|number| number as u64) + }) +} + +/// Parse a signed integer from model-provided JSON, accepting integral +/// floating-point representations such as `-2.0`. +pub fn parse_i64_value(value: &Value) -> Option { + value.as_i64().or_else(|| { + value + .as_f64() + .filter(|number| { + number.is_finite() + && number.fract() == 0.0 + && *number >= i64::MIN as f64 + && *number <= i64::MAX as f64 + }) + .map(|number| number as i64) + }) +} + +#[cfg(test)] +mod tests { + use super::{parse_i64_value, parse_u64_value}; + use serde_json::json; + + #[test] + fn accepts_integral_float_values() { + assert_eq!(parse_u64_value(&json!(100.0)), Some(100)); + assert_eq!(parse_i64_value(&json!(-2.0)), Some(-2)); + } + + #[test] + fn rejects_fractional_and_invalid_values() { + assert_eq!(parse_u64_value(&json!(100.5)), None); + assert_eq!(parse_u64_value(&json!(-1.0)), None); + assert_eq!(parse_i64_value(&json!(1.5)), None); + assert_eq!(parse_i64_value(&json!("2")), None); + } +} diff --git a/src/crates/assembly/core/src/agentic/tools/mod.rs b/src/crates/assembly/core/src/agentic/tools/mod.rs index a6ddee778c..4963d47488 100644 --- a/src/crates/assembly/core/src/agentic/tools/mod.rs +++ b/src/crates/assembly/core/src/agentic/tools/mod.rs @@ -13,6 +13,7 @@ pub mod framework; pub mod frontend_workbench_host; pub mod image_context; pub mod implementations; +mod integer_parser; pub mod manifest_resolver; #[cfg(feature = "tools-miniapp")] pub(crate) mod miniapp_context_runtime; @@ -37,6 +38,7 @@ pub(crate) mod tool_context_runtime; pub(crate) mod tool_result_storage; pub mod user_input_manager; pub mod workspace_paths; +pub use integer_parser::{parse_i64_value, parse_u64_value}; pub use openbitfun_agent_tools::input_validator; pub use framework::{