@@ -100,14 +100,13 @@ def params_schema
100100
101101 def call ( args )
102102 normalized_args = normalize_args ( args )
103+ validation_error = validate_keyword_arguments ( normalized_args )
104+ return { error : "Invalid tool arguments: #{ validation_error } " } if validation_error
105+
103106 RubyLLM . logger . debug { "Tool #{ name } called with: #{ normalized_args . inspect } " }
104107 result = execute ( **normalized_args )
105108 RubyLLM . logger . debug { "Tool #{ name } returned: #{ result . inspect } " }
106109 result
107- rescue ArgumentError => e
108- raise e unless keyword_argument_error? ( e )
109-
110- { error : "Invalid tool arguments: #{ e . message } " }
111110 end
112111
113112 def execute ( ...)
@@ -127,9 +126,44 @@ def normalize_args(args)
127126 { }
128127 end
129128
130- def keyword_argument_error? ( error )
131- message = error . message . to_s
132- message . include? ( 'unknown keyword' ) || message . include? ( 'missing keyword' )
129+ def validate_keyword_arguments ( arguments )
130+ required_keywords , optional_keywords , accepts_extra_keywords = execute_keyword_signature
131+
132+ return nil if required_keywords . empty? && optional_keywords . empty?
133+
134+ argument_keys = arguments . keys
135+ missing_keyword = first_missing_keyword ( required_keywords , argument_keys )
136+ return "missing keyword: #{ keyword_label ( missing_keyword ) } " if missing_keyword
137+ return nil if accepts_extra_keywords
138+
139+ allowed_keywords = required_keywords + optional_keywords
140+ unknown_keyword = first_unknown_keyword ( argument_keys , allowed_keywords )
141+ return "unknown keyword: #{ keyword_label ( unknown_keyword ) } " if unknown_keyword
142+
143+ nil
144+ end
145+
146+ def execute_keyword_signature
147+ keyword_signature = method ( :execute ) . parameters
148+ required_keywords = keyword_signature . filter_map { |kind , name | name if kind == :keyreq }
149+ optional_keywords = keyword_signature . filter_map { |kind , name | name if kind == :key }
150+ accepts_extra_keywords = keyword_signature . any? { |kind , _ | kind == :keyrest }
151+
152+ [ required_keywords , optional_keywords , accepts_extra_keywords ]
153+ end
154+
155+ def first_missing_keyword ( required_keywords , argument_keys )
156+ ( required_keywords - argument_keys ) . first
157+ end
158+
159+ def first_unknown_keyword ( argument_keys , allowed_keywords )
160+ ( argument_keys - allowed_keywords ) . first
161+ end
162+
163+ def keyword_label ( keyword )
164+ return keyword . to_s if RUBY_ENGINE == 'jruby'
165+
166+ ":#{ keyword } "
133167 end
134168
135169 # Wraps schema handling for tool parameters, supporting JSON Schema hashes,
0 commit comments