Class: Ask::Tool

Inherits:
Object
  • Object
show all
Defined in:
lib/ask/tools/tool.rb

Defined Under Namespace

Classes: Halt, Parameter

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.approval_required(value = :_no_arg_given) ⇒ Boolean

Declare that calling this tool requires human approval.

The tool is still registered and described to the LLM normally, but when an agent session runs with an approval queue enabled, calls to it are queued instead of executed — the agent gets a pending result, and the tool only runs after a human approves it.

Called with no argument returns the current value (default false).

Examples:

class SendEmail < Ask::Tool
  approval_required true
  def execute(to:, body:) ... end
end

Parameters:

  • value (Boolean, nil) (defaults to: :_no_arg_given)

Returns:

  • (Boolean)


63
64
65
66
67
68
69
# File 'lib/ask/tools/tool.rb', line 63

def approval_required(value = :_no_arg_given)
  if value == :_no_arg_given
    @approval_required == true
  else
    @approval_required = !!value
  end
end

.auto_approvable(value = :_no_arg_given) ⇒ Boolean

Declare that this tool may be auto-approved when the session's approval policy has auto-approval enabled for it. This is a per-action verdict only — the session-level user rule is still the binding gate. A tool that requires approval but is NOT marked auto-approvable always queues for human review.

Called with no argument returns the current value (default false).

Parameters:

  • value (Boolean, nil) (defaults to: :_no_arg_given)

Returns:

  • (Boolean)


81
82
83
84
85
86
87
# File 'lib/ask/tools/tool.rb', line 81

def auto_approvable(value = :_no_arg_given)
  if value == :_no_arg_given
    @auto_approvable == true
  else
    @auto_approvable = !!value
  end
end

.build_schema_from_paramsObject



146
147
148
149
150
151
152
153
154
155
# File 'lib/ask/tools/tool.rb', line 146

def build_schema_from_params
  properties = parameters.to_h do |_name, param|
    schema = { type: param.type }
    schema[:description] = param.description if param.description
    schema[:items] = { type: "string" } if param.type == "array"
    [param.name.to_s, schema]
  end
  required = parameters.select { |_, p| p.required }.keys.map(&:to_s)
  { type: "object", properties: properties, required: required, additionalProperties: false }
end

.deep_stringify_keys(obj) ⇒ Object



169
170
171
172
173
174
175
# File 'lib/ask/tools/tool.rb', line 169

def deep_stringify_keys(obj)
  case obj
  when Hash then obj.each_with_object({}) { |(k, v), h| h[k.to_s] = deep_stringify_keys(v) }
  when Array then obj.map { |v| deep_stringify_keys(v) }
  else obj
  end
end

.descObject



32
33
34
35
# File 'lib/ask/tools/tool.rb', line 32

def description(text = nil)
  return @description unless text
  @description = text
end

.description(text = nil) ⇒ Object



28
29
30
31
# File 'lib/ask/tools/tool.rb', line 28

def description(text = nil)
  return @description unless text
  @description = text
end

.infer_parameters_from_executeObject

Derive parameters from def execute(project_id:, title:, ...): required keywords become required string parameters. Ruby types aren't introspectable, so everything infers as string (JSON numbers coerce); tools that need real types declare params.



120
121
122
123
124
125
126
127
128
# File 'lib/ask/tools/tool.rb', line 120

def infer_parameters_from_execute
  @parameters_inferred = true
  instance_method(:execute).parameters.each do |kind, name|
    next unless %i[keyreq key opt].include?(kind)
    next if name.nil? || name == :_abort_controller

    @parameters[name] = Parameter.new(name: name, type: "string", required: kind == :keyreq)
  end
end

.inherited(subclass) ⇒ Object



17
18
19
20
21
22
23
24
25
26
# File 'lib/ask/tools/tool.rb', line 17

def inherited(subclass)
  super
  @parameters = {} if @parameters.nil?
  subclass.instance_variable_set(:@description, nil)
  subclass.instance_variable_set(:@parameters, {})
  subclass.instance_variable_set(:@params_schema_definition, nil)
  subclass.instance_variable_set(:@tool_name, nil)
  subclass.instance_variable_set(:@approval_required, nil)
  subclass.instance_variable_set(:@auto_approvable, nil)
end

.name(custom = :_no_arg_given) ⇒ Object

Declare a custom tool name. Called with no argument returns the class name (via Module#name). Called with a string stores a custom name for the instance. Example: name "my_custom_tool"



38
39
40
41
42
43
44
# File 'lib/ask/tools/tool.rb', line 38

def name(custom = :_no_arg_given)
  if custom == :_no_arg_given
    super()  # Module#name, returns the Ruby class path
  else
    @tool_name = custom
  end
end

.param(name, type:, desc: nil, description: nil, required: true) ⇒ Object



89
90
91
92
93
94
95
96
# File 'lib/ask/tools/tool.rb', line 89

def param(name, type:, desc: nil, description: nil, required: true)
  type = type.to_s.downcase.to_sym
  validate_param_type!(type, name)
  parameters[name] = Parameter.new(
    name: name, type: map_type(type),
    description: desc || description, required: required
  )
end

.parametersObject

The tool's declared parameters — or, when none are declared, inferred from the execute signature so a tool is never silently uncallable: a tool whose execute takes keyword arguments but declares no params would otherwise reject every call ("unknown parameters"). Inference is a fallback, never an override: an explicit params/param declaration wins.



108
109
110
111
112
113
114
# File 'lib/ask/tools/tool.rb', line 108

def parameters
  @parameters ||= {}
  if @parameters.empty? && @params_schema_definition.nil? && !@parameters_inferred
    infer_parameters_from_execute
  end
  @parameters
end

.params(schema = nil, &block) ⇒ Object



98
99
100
# File 'lib/ask/tools/tool.rb', line 98

def params(schema = nil, &block)
  @params_schema_definition = schema || block
end

.params_schemaObject



134
135
136
137
138
139
140
141
142
143
144
# File 'lib/ask/tools/tool.rb', line 134

def params_schema
  @params_schema ||= begin
    if @params_schema_definition
      deep_stringify_keys(resolve_params_schema(@params_schema_definition))
    elsif parameters.any?
      build_schema_from_params
    else
      nil
    end
  end
end

.provider_paramsObject



130
131
132
# File 'lib/ask/tools/tool.rb', line 130

def provider_params
  @provider_params ||= {}
end

.resolve_params_schema(definition) ⇒ Object



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/ask/tools/tool.rb', line 157

def resolve_params_schema(definition)
  case definition
  when Proc
    schema_class = Ask::Schema.create(&definition)
    schema_class.new.to_json_schema.dig(:schema)
  when Hash then definition
  when ->(d) { d.respond_to?(:to_json_schema) }
    definition.to_json_schema.dig(:schema)
  else nil
  end
end

Instance Method Details

#approval_required?Boolean

Returns whether calling this tool requires human approval.

Returns:

  • (Boolean)

    whether calling this tool requires human approval



222
223
224
# File 'lib/ask/tools/tool.rb', line 222

def approval_required?
  self.class.approval_required
end

#auto_approvable?Boolean

Returns whether this tool may be auto-approved under a session-level auto-approval rule.

Returns:

  • (Boolean)

    whether this tool may be auto-approved under a session-level auto-approval rule



228
229
230
# File 'lib/ask/tools/tool.rb', line 228

def auto_approvable?
  self.class.auto_approvable
end

#call(args = {}, abort_controller = nil) ⇒ Object



232
233
234
235
236
237
238
239
240
241
242
243
# File 'lib/ask/tools/tool.rb', line 232

def call(args = {}, abort_controller = nil)
normalized = normalize_args(args)
validation = validate(normalized)
return Ask::Result.failure(validation) if validation
normalized[:_abort_controller] = abort_controller if abort_controller
execute_kwargs = normalized.reject { |k, _| k == :_abort_controller || k == :abort_controller }
execute(**execute_kwargs)
rescue Halt => e
  Ask::Result.ok(data: e.content, metadata: { halted: true })
rescue StandardError => e
  Ask::Result.failure("#{self.class.name.split('::').last} raised #{e.class}: #{e.message}")
end

#descriptionObject



213
214
215
# File 'lib/ask/tools/tool.rb', line 213

def description
  self.class.description
end

#execute(**args) ⇒ Object

Raises:

  • (NotImplementedError)


245
246
247
# File 'lib/ask/tools/tool.rb', line 245

def execute(**args)
  raise NotImplementedError, "#{self.class} must implement #execute(**args)"
end

#inspectObject



268
269
270
# File 'lib/ask/tools/tool.rb', line 268

def inspect
  "#<#{self.class.name} name=#{name.inspect}>"
end

#nameObject



199
200
201
202
203
204
205
206
207
208
209
210
211
# File 'lib/ask/tools/tool.rb', line 199

def name
  custom = self.class.instance_variable_get(:@tool_name)
  return custom if custom

  klass_name = self.class.name.to_s.split("::").last || ""
  normalized = klass_name.dup.force_encoding("UTF-8").unicode_normalize(:nfkd)
  normalized.encode("ASCII", replace: "")
            .gsub(/[^a-zA-Z0-9_-]/, "-")
            .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
            .gsub(/([a-z\d])([A-Z])/, '\1_\2')
            .downcase
            .delete_suffix("_tool")
end

#parametersObject



217
218
219
# File 'lib/ask/tools/tool.rb', line 217

def parameters
  self.class.parameters
end

#params_schemaObject



249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/ask/tools/tool.rb', line 249

def params_schema
  return @params_schema if defined?(@params_schema)
  @params_schema = begin
    if params_schema_definition
      deep_stringify_keys(resolve_params_schema(params_schema_definition))
    elsif parameters.any?
      build_schema_from_params
    else
      nil
    end
  end
end

#provider_paramsObject



195
196
197
# File 'lib/ask/tools/tool.rb', line 195

def provider_params
  self.class.provider_params
end

#tool_definitionObject



262
263
264
265
266
# File 'lib/ask/tools/tool.rb', line 262

def tool_definition
  defn = { name: name, description: description }
  defn[:input_schema] = params_schema if params_schema
  defn
end

#validate(normalized) ⇒ Object

Validate normalized (symbol-keyed) arguments. Returns nil when valid, or an actionable message — one that names what was expected — so the model (or a repair pass) can correct the call instead of guessing. Public: ask-agent's tool-call repair validates through this. Tools with a declared params block validate against the resolved schema (required keys + unknown keys when strict).



278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/ask/tools/tool.rb', line 278

def validate(normalized)
  return validate_against_schema(normalized) if params_schema_definition

  expected = self.class.parameters.keys
  missing = expected.select { |name| self.class.parameters[name].required && !normalized.key?(name) }
  return "missing required parameters: #{missing.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless missing.empty?

  unknown = normalized.keys - expected
  return "unknown parameters: #{unknown.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless unknown.empty?

  nil
end