ask-tools 0.5.0 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 549044a60c0528c6cd72c66bdcada359b56e106a169237d17661841f751a090f
4
- data.tar.gz: e315d06d73942a45c0cf0835069e403f221d89322ed2f456ea1ac7879db21ab8
3
+ metadata.gz: b7722a402e1411950d63a055707c676b7c68af27550284f56a7d3cb5b316af78
4
+ data.tar.gz: 19012fdeba501720ae891cc7b62b1a797482170a3ccbfef5cee59218d8446c59
5
5
  SHA512:
6
- metadata.gz: 4145513e16e503594071295d6e244ce6afad3fa55b83dbe33d88620226e0f4e152ec8837560fb85d89a5c992e19ce345fcc93775396af7e9471517cbc3c9ca91
7
- data.tar.gz: 19a7e533b4d2614ff2842cbcac201393c79bc7d4fafe63e1b3c72592b63bddc887f6d517b726aabad51bbc19d916df4b06d688dd9818172c5f0ba40c4c525443
6
+ metadata.gz: 18cc40a7fe9902a71b658aa857693f1734d25456dc1c03d38334596e1b840e80b605f0401a3237a7009423903c890a577a2829c12a59dfea2debc22d832823b4
7
+ data.tar.gz: 5a9c6578a61fed0c87210332be3efe8d3d98a1fa2a16bfa0546bba6a0de8b713c1ab5eacdb6bedc8883451a9f911ca6eee0a12733cdb37af90f21cbfe2060a20
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [0.6.0] — 2026-08-06
2
+
3
+ ### Added
4
+
5
+ - **`Ask::Tool.approval_required` — declare that a tool needs human approval.**
6
+ Combined with `Ask::Agent::ApprovalQueue` (ask-agent), calls to the tool are
7
+ queued instead of executed — the agent gets a pending result and continues,
8
+ and the tool only runs after a human approves it. Defaults to false; the
9
+ flag is not inherited by subclasses.
10
+
11
+ ```ruby
12
+ class SendEmail < Ask::Tool
13
+ approval_required true
14
+ def execute(to:, body:) ... end
15
+ end
16
+ ```
17
+
18
+ - **`Ask::Tool.auto_approvable` — declare that a tool may be auto-approved.**
19
+ A per-action verdict only: the session's user-enabled rule is still the
20
+ binding gate (dual signal). A tool that requires approval but is NOT marked
21
+ auto-approvable always queues for human review.
22
+
23
+ ```ruby
24
+ class Ping < Ask::Tool
25
+ approval_required true
26
+ auto_approvable true
27
+ def execute ... end
28
+ end
29
+ ```
30
+
31
+ - Instance predicates `#approval_required?` and `#auto_approvable?`.
32
+
1
33
  ## [0.5.0] — 2026-08-03
2
34
 
3
35
  ### Changed
@@ -21,6 +21,8 @@ module Ask
21
21
  subclass.instance_variable_set(:@parameters, {})
22
22
  subclass.instance_variable_set(:@params_schema_definition, nil)
23
23
  subclass.instance_variable_set(:@tool_name, nil)
24
+ subclass.instance_variable_set(:@approval_required, nil)
25
+ subclass.instance_variable_set(:@auto_approvable, nil)
24
26
  end
25
27
 
26
28
  def description(text = nil)
@@ -41,6 +43,49 @@ module Ask
41
43
  end
42
44
  end
43
45
 
46
+ # Declare that calling this tool requires human approval.
47
+ #
48
+ # The tool is still registered and described to the LLM normally, but
49
+ # when an agent session runs with an approval queue enabled, calls to
50
+ # it are queued instead of executed — the agent gets a pending result,
51
+ # and the tool only runs after a human approves it.
52
+ #
53
+ # Called with no argument returns the current value (default false).
54
+ #
55
+ # @example
56
+ # class SendEmail < Ask::Tool
57
+ # approval_required true
58
+ # def execute(to:, body:) ... end
59
+ # end
60
+ #
61
+ # @param value [Boolean, nil]
62
+ # @return [Boolean]
63
+ def approval_required(value = :_no_arg_given)
64
+ if value == :_no_arg_given
65
+ @approval_required == true
66
+ else
67
+ @approval_required = !!value
68
+ end
69
+ end
70
+
71
+ # Declare that this tool may be auto-approved when the session's
72
+ # approval policy has auto-approval enabled for it. This is a
73
+ # per-action verdict only — the session-level user rule is still the
74
+ # binding gate. A tool that requires approval but is NOT marked
75
+ # auto-approvable always queues for human review.
76
+ #
77
+ # Called with no argument returns the current value (default false).
78
+ #
79
+ # @param value [Boolean, nil]
80
+ # @return [Boolean]
81
+ def auto_approvable(value = :_no_arg_given)
82
+ if value == :_no_arg_given
83
+ @auto_approvable == true
84
+ else
85
+ @auto_approvable = !!value
86
+ end
87
+ end
88
+
44
89
  def param(name, type:, desc: nil, description: nil, required: true)
45
90
  type = type.to_s.downcase.to_sym
46
91
  validate_param_type!(type, name)
@@ -54,8 +99,32 @@ module Ask
54
99
  @params_schema_definition = schema || block
55
100
  end
56
101
 
102
+ # The tool's declared parameters — or, when none are declared,
103
+ # inferred from the execute signature so a tool is never silently
104
+ # uncallable: a tool whose execute takes keyword arguments but
105
+ # declares no params would otherwise reject every call ("unknown
106
+ # parameters"). Inference is a fallback, never an override: an
107
+ # explicit `params`/`param` declaration wins.
57
108
  def parameters
58
109
  @parameters ||= {}
110
+ if @parameters.empty? && @params_schema_definition.nil? && !@parameters_inferred
111
+ infer_parameters_from_execute
112
+ end
113
+ @parameters
114
+ end
115
+
116
+ # Derive parameters from `def execute(project_id:, title:, ...)`:
117
+ # required keywords become required string parameters. Ruby types
118
+ # aren't introspectable, so everything infers as string (JSON
119
+ # numbers coerce); tools that need real types declare `params`.
120
+ def infer_parameters_from_execute
121
+ @parameters_inferred = true
122
+ instance_method(:execute).parameters.each do |kind, name|
123
+ next unless %i[keyreq key opt].include?(kind)
124
+ next if name.nil? || name == :_abort_controller
125
+
126
+ @parameters[name] = Parameter.new(name: name, type: "string", required: kind == :keyreq)
127
+ end
59
128
  end
60
129
 
61
130
  def provider_params
@@ -66,7 +135,7 @@ module Ask
66
135
  @params_schema ||= begin
67
136
  if @params_schema_definition
68
137
  deep_stringify_keys(resolve_params_schema(@params_schema_definition))
69
- elsif @parameters && @parameters.any?
138
+ elsif parameters.any?
70
139
  build_schema_from_params
71
140
  else
72
141
  nil
@@ -149,6 +218,17 @@ module Ask
149
218
  self.class.parameters
150
219
  end
151
220
 
221
+ # @return [Boolean] whether calling this tool requires human approval
222
+ def approval_required?
223
+ self.class.approval_required
224
+ end
225
+
226
+ # @return [Boolean] whether this tool may be auto-approved under a
227
+ # session-level auto-approval rule
228
+ def auto_approvable?
229
+ self.class.auto_approvable
230
+ end
231
+
152
232
  def call(args = {}, abort_controller = nil)
153
233
  normalized = normalize_args(args)
154
234
  validation = validate(normalized)
@@ -189,8 +269,42 @@ module Ask
189
269
  "#<#{self.class.name} name=#{name.inspect}>"
190
270
  end
191
271
 
272
+ # Validate normalized (symbol-keyed) arguments. Returns nil when
273
+ # valid, or an actionable message — one that names what was expected
274
+ # — so the model (or a repair pass) can correct the call instead of
275
+ # guessing. Public: ask-agent's tool-call repair validates through
276
+ # this. Tools with a declared `params` block validate against the
277
+ # resolved schema (required keys + unknown keys when strict).
278
+ def validate(normalized)
279
+ return validate_against_schema(normalized) if params_schema_definition
280
+
281
+ expected = self.class.parameters.keys
282
+ missing = expected.select { |name| self.class.parameters[name].required && !normalized.key?(name) }
283
+ return "missing required parameters: #{missing.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless missing.empty?
284
+
285
+ unknown = normalized.keys - expected
286
+ return "unknown parameters: #{unknown.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless unknown.empty?
287
+
288
+ nil
289
+ end
290
+
192
291
  private
193
292
 
293
+ def validate_against_schema(normalized)
294
+ schema = params_schema || {}
295
+ expected = Array(schema["required"])
296
+ missing = expected - normalized.keys.map(&:to_s)
297
+ return "missing required parameters: #{missing.map(&:inspect).join(', ')} — expected: #{expected.map(&:inspect).join(', ')}" unless missing.empty?
298
+
299
+ properties = schema["properties"] || {}
300
+ if schema["additionalProperties"] == false
301
+ unknown = normalized.keys.map(&:to_s) - properties.keys
302
+ return "unknown parameters: #{unknown.map(&:inspect).join(', ')} — expected: #{properties.keys.map(&:inspect).join(', ')}" unless unknown.empty?
303
+ end
304
+
305
+ nil
306
+ end
307
+
194
308
  def params_schema_definition
195
309
  self.class.instance_variable_get(:@params_schema_definition)
196
310
  end
@@ -238,15 +352,6 @@ module Ask
238
352
  args.respond_to?(:transform_keys) ? args.transform_keys(&:to_sym) : {}
239
353
  end
240
354
 
241
- def validate(normalized)
242
- return nil if params_schema_definition
243
- missing = self.class.parameters.select { |_, p| p.required && !normalized.key?(p.name) }
244
- return "missing required parameters: #{missing.keys.map(&:inspect).join(', ')}" unless missing.empty?
245
- unknown = normalized.keys - self.class.parameters.keys
246
- return "unknown parameters: #{unknown.map(&:inspect).join(', ')}" unless unknown.empty?
247
- nil
248
- end
249
-
250
355
  VALID_JSON_SCHEMA_TYPES = %i[string integer number boolean array object].freeze
251
356
 
252
357
  class Parameter
data/lib/ask/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Tools
5
- VERSION = "0.5.0"
5
+ VERSION = "0.6.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ask-tools
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: '0.1'
32
+ version: 0.1.2
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: '0.1'
39
+ version: 0.1.2
40
40
  - !ruby/object:Gem::Dependency
41
41
  name: minitest
42
42
  requirement: !ruby/object:Gem::Requirement
@@ -115,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
115
115
  - !ruby/object:Gem::Version
116
116
  version: '0'
117
117
  requirements: []
118
- rubygems_version: 4.0.3
118
+ rubygems_version: 4.0.18
119
119
  specification_version: 4
120
120
  summary: Tool framework for the ask-rb ecosystem
121
121
  test_files: []