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::{