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 +4 -4
- data/CHANGELOG.md +32 -0
- data/lib/ask/tools/tool.rb +115 -10
- data/lib/ask/version.rb +1 -1
- metadata +4 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b7722a402e1411950d63a055707c676b7c68af27550284f56a7d3cb5b316af78
|
|
4
|
+
data.tar.gz: 19012fdeba501720ae891cc7b62b1a797482170a3ccbfef5cee59218d8446c59
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
data/lib/ask/tools/tool.rb
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
|
|
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
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.
|
|
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:
|
|
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:
|
|
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.
|
|
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: []
|