ask-tools 0.4.0 → 0.6.0

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: 0e0b16a246eee9282788e5ec82210cacc6a507d83591645aca2ce87d17e0e129
4
- data.tar.gz: 546cf192269daacbbf48e7e1fe4a188ad42d3e5f8e9e50ccaeb3d96ca24803be
3
+ metadata.gz: aff9f4752095dc3224f21ed08a1bc6768ff211af1f48dcfc51bb1b7c41cab0e3
4
+ data.tar.gz: acccd4fc9f40e57520e51c55cb71faf302dfd05fa4c7ca4159ffacf4da8b8d19
5
5
  SHA512:
6
- metadata.gz: 9b6cab4332f8f76e36ccfe1024976578d739258125cdad49567cc9eb0ab21334bff1b28a5c61aee3bdaf21d375e9ac5d78ae2f248f717b1d6c942a0d75b8a51c
7
- data.tar.gz: 9a08ac4432817ceb9917f1490a2e1b5c0905609feac8181b9de4404097c1b366b185cd653315e116cf658a655ee48089dec662ce1e5b611024feb0fcde4f2660
6
+ metadata.gz: 5f37ccab34db3b16d504eb980830dae8ae1acb27c9de568fd3654a4da8604efcf5e27d6b917f280a74c4e6d3815862d54ebcd4bb6e9d2395ecbde0c3aa5730f3
7
+ data.tar.gz: ca97d4e8122b3d3ae161318f23780b0539c64d450d746a05e88fe56ab32a791ed3d161568ad8288d3d8ef4913a0b7e3bc7812e9c90ab68bb87df1630634bd20c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,59 @@
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
+
33
+ ## [0.5.0] — 2026-08-03
34
+
35
+ ### Changed
36
+
37
+ - **`Ask::Result` now comes from ask-core.** The duplicated `Ask::Result`
38
+ definition in this gem is removed; ask-tools depends on ask-core
39
+ (>= 0.9.0), which owns the single result type for the whole ecosystem with
40
+ both the foundational API (`success`/`failure`/`aborted`/`blocked`) and the
41
+ tool API (`ok`/`error`/`output`/`ok?`/`error_message`). Previously the two
42
+ gems' incompatible constructors meant whichever loaded last broke the
43
+ other's factories (`Ask::Result.success` raised `ArgumentError` in any app
44
+ loading both).
45
+
46
+ The tool API is unchanged:
47
+
48
+ ```ruby
49
+ Ask::Result.ok(data: "hello").output # => "hello"
50
+ Ask::Result.error(message: "fail").error # => "fail"
51
+ ```
52
+
53
+ ### Tested
54
+
55
+ - 71 tests, 159 assertions, 0 failures.
56
+
1
57
  ## [0.4.0] — 2026-07-26
2
58
 
3
59
  ### Added
data/README.md CHANGED
@@ -1,23 +1,15 @@
1
1
  # ask-tools
2
2
 
3
- The foundational gem for the ask-rb ecosystem. Defines `Ask::Tool` — the base class every tool inherits from — along with `Ask::Result` (standardized return value), tool discovery/registration, and a scaffold generator.
3
+ [![Gem Version](https://badge.fury.io/rb/ask-tools.svg)](https://badge.fury.io/rb/ask-tools)
4
4
 
5
- This gem does **not** ship any executable tools. It only provides the contract that tool gems (e.g., `ask-tools-shell`, `ask-tools-filesystem`) implement.
5
+ The tool framework for the ask-rb ecosystem. Defines `Ask::Tool` (the base class every tool inherits from), `Ask::Result` (standardized return value), and the `Ask::Tools` registry. This gem ships no executable tools; tool gems such as ask-tools-shell implement them.
6
6
 
7
7
  ## Installation
8
8
 
9
- Add this line to your `Gemfile`:
10
-
11
9
  ```ruby
12
10
  gem "ask-tools"
13
11
  ```
14
12
 
15
- Or install it directly:
16
-
17
- ```bash
18
- gem install ask-tools
19
- ```
20
-
21
13
  ## Quick Start
22
14
 
23
15
  ```ruby
@@ -32,174 +24,60 @@ class Greeter < Ask::Tool
32
24
  end
33
25
  end
34
26
 
35
- # Use it
36
- tool = Greeter.new
37
- tool.name # => "greeter"
38
- tool.description # => "Greets a person by name"
39
-
40
- result = tool.call(name: "World")
41
- result.ok? # => true
42
- result.output # => "Hello, World!"
27
+ result = Greeter.new.call(name: "World")
28
+ result.ok? # => true
29
+ result.output # => "Hello, World!"
43
30
  ```
44
31
 
45
- ## API Reference
46
-
47
- ### `Ask::Tool` — Base Class
48
-
49
- Subclass `Ask::Tool` to define a tool that an LLM can call.
50
-
51
- #### Class DSL
52
-
53
- | Method | Description |
54
- |--------|-------------|
55
- | `description(text)` | Sets or retrieves the tool's human-readable description. Alias: `desc` |
56
- | `param(name, type:, desc:, required:)` | Declares a parameter. `type` must be a valid JSON Schema type (`:string`, `:integer`, `:number`, `:boolean`, `:array`, `:object`) |
57
-
58
- #### Instance Methods
59
-
60
- | Method | Returns | Description |
61
- |--------|---------|-------------|
62
- | `name` | `String` | Auto-derived from the class name: CamelCase → snake_case, strips `_tool` suffix |
63
- | `description` | `String?, nil` | The tool's description |
64
- | `parameters` | `Hash{Symbol => Parameter}` | Declared parameter definitions |
65
- | `call(args = {})` | `Ask::Result` | Normalizes args (symbolizes keys), validates required params, delegates to `execute`. Catches `Halt` and `StandardError` |
66
- | `execute(**args)` | `Ask::Result` | **Override this.** Implement the tool's logic. |
67
- | `params_schema` | `Hash?, nil` | JSON Schema hash for LLM function-calling APIs. Returns `nil` when no params declared |
68
- | `tool_definition` | `Hash` | Full tool definition hash with `:name`, `:description`, and `:input_schema` |
69
-
70
- #### Error Handling
32
+ ## Essential API
71
33
 
72
- - **`Ask::Tool::Halt`** — Raise this inside `execute` to signal the conversation loop should stop after this tool's result. `call` returns an `Ask::Result` with `metadata[:halted] = true`.
73
- - **`StandardError`** — Any other exception raised in `execute` is caught by `call` and returned as an error `Ask::Result`.
34
+ ### Ask::Tool
74
35
 
75
- ### `Ask::Result` Return Value
36
+ | Method | Purpose |
37
+ |---|---|
38
+ | `description "..."` (alias `desc`) | Set the tool description |
39
+ | `param :name, type: :string, desc: "...", required: true` | Declare a parameter. `type` must be a JSON Schema type (`:string`, `:integer`, `:number`, `:boolean`, `:array`, `:object`) |
40
+ | `name "custom_tool"` | Set a custom tool name (default: derived from the class name, CamelCase to snake_case, `_tool` suffix stripped) |
41
+ | `params do ... end` | Declare parameters with the ask-schema DSL |
76
42
 
77
- A value object representing the outcome of a tool execution.
43
+ Override `execute(**args)` with the tool logic. `call(args)` normalizes input (JSON strings and hash keys), validates required parameters, and returns an `Ask::Result`. Raising `Ask::Tool::Halt` inside `execute` yields a success result with `metadata[:halted] = true`; any other exception becomes a failure result.
78
44
 
79
- #### Factory Methods
45
+ ### Ask::Result
80
46
 
81
47
  ```ruby
82
- # Successful result
83
48
  Ask::Result.ok(data: "output", metadata: { key: "val" })
84
-
85
- # Failed result
86
49
  Ask::Result.error(message: "Something went wrong", metadata: { code: 500 })
50
+ Ask::Result.failure("Something went wrong")
51
+
52
+ result.ok? # => true
53
+ result.output # => "output"
54
+ result.error # => nil
55
+ result.metadata # => { key: "val" }
56
+ result.to_s # => "output"
57
+ result.to_h # => { ok: true, output: "output", error: nil, metadata: { key: "val" } }
87
58
  ```
88
59
 
89
- #### Attributes
90
-
91
- | Attribute | Type | Description |
92
- |-----------|------|-------------|
93
- | `ok?` / `ok` | `Boolean` | Whether the tool completed successfully |
94
- | `output` | `Object?, nil` | Output data (success) |
95
- | `error` | `String?, nil` | Error message (failure) |
96
- | `metadata` | `Hash` | Arbitrary metadata |
97
-
98
- #### Instance Methods
99
-
100
- | Method | Returns | Description |
101
- |--------|---------|-------------|
102
- | `to_s` | `String` | Returns `output.to_s` for success, `error` for failure |
103
- | `to_h` | `Hash` | Serialized hash with `:ok`, `:output`, `:error`, `:metadata` |
104
- | `inspect` | `String` | Human-readable representation |
105
-
106
- ### `Ask::Tool::Parameter` — Parameter Definition
107
-
108
- Internal value object describing a declared parameter. Accessible via `Tool.parameters[name]`.
109
-
110
- | Attribute | Type | Description |
111
- |-----------|------|-------------|
112
- | `name` | `Symbol` | Parameter name |
113
- | `type` | `String` | JSON Schema type string |
114
- | `description` | `String?, nil` | Human-readable description |
115
- | `required` / `required?` | `Boolean` | Whether the parameter is mandatory |
116
-
117
- ### `Ask::Tools` — Registry & Discovery
118
-
119
- Central registry for tool classes.
120
-
121
- | Method | Returns | Description |
122
- |--------|---------|-------------|
123
- | `.register(tool_class)` | `void` | Manually register a tool class |
124
- | `.all` | `Array<Tool>` | Instantiated list of all registered tools |
125
- | `.discover` | `Array<Class>` | Auto-discover loaded `Ask::Tool` subclasses via `ObjectSpace` |
126
- | `.[](name)` | `Tool?, nil` | Find a registered tool by its derived name |
127
- | `.clear` | `void` | Remove all registered tools |
128
- | `.count` | `Integer` | Number of registered tool classes |
129
-
130
- ```ruby
131
- # Manual registration
132
- Ask::Tools.register(MyTool)
133
-
134
- # Auto-discover all loaded Ask::Tool subclasses
135
- Ask::Tools.discover
136
-
137
- # Find by name
138
- tool = Ask::Tools["my_tool"]
139
- tool.call(input: "hello")
140
-
141
- # List all
142
- Ask::Tools.all.each { |t| puts t.name }
143
- ```
144
-
145
- ## Defining a Custom Tool
60
+ ### Ask::Tools registry
146
61
 
147
- ```ruby
148
- class SearchTool < Ask::Tool
149
- description "Searches a knowledge base"
150
- param :query, type: :string, desc: "Search query", required: true
151
- param :limit, type: :integer, desc: "Max results", required: false
152
-
153
- def execute(query:, limit: 10)
154
- results = perform_search(query, limit)
155
- Ask::Result.ok(data: results)
156
- rescue SearchError => e
157
- Ask::Result.error(message: e.message)
158
- end
62
+ | Method | Purpose |
63
+ |---|---|
64
+ | `Ask::Tools.register(ToolClass)` | Register a tool class manually |
65
+ | `Ask::Tools.all` | Instances of all registered tools |
66
+ | `Ask::Tools.discover` | Auto-register loaded `Ask::Tool` subclasses via ObjectSpace |
67
+ | `Ask::Tools["name"]` | Find a tool instance by derived name |
68
+ | `Ask::Tools.clear` / `Ask::Tools.count` | Reset and count the registry |
159
69
 
160
- private
70
+ ## Full documentation
161
71
 
162
- def perform_search(query, limit)
163
- # ... implementation
164
- end
165
- end
166
- ```
72
+ The full ask-rb documentation lives at https://ask-rb.github.io/ask-docs. [ask-tools in depth](https://ask-rb.github.io/ask-docs/core/tools) covers the tool contract, parameter schemas, and custom tool examples. API reference: https://ask-rb.github.io/ask-docs/reference/api.
167
73
 
168
74
  ## Development
169
75
 
170
- ```bash
171
- # Install dependencies
172
- bundle install
173
-
174
- # Run tests
175
- bundle exec rake test
176
-
177
- # Build the gem
178
- gem build ask-tools.gemspec
179
76
  ```
180
-
181
- ## Testing
182
-
183
- ask-tools uses **Minitest** with **Mocha** for mocking.
184
-
185
- ```bash
186
- # Run the full test suite
77
+ bundle install
187
78
  bundle exec rake test
188
79
  ```
189
80
 
190
- ## Release Process
191
-
192
- 1. Update `CHANGELOG.md`
193
- 2. Update `lib/ask/version.rb` if needed
194
- 3. Build the gem: `gem build ask-tools.gemspec`
195
- 4. Push to GitHub Packages: `gem push ask-tools-*.gem`
196
-
197
81
  ## License
198
82
 
199
- MIT — see [LICENSE](LICENSE).
200
-
201
- ## Links
202
-
203
- - **Source:** https://github.com/ask-rb/ask-tools
204
- - **Issues:** https://github.com/ask-rb/ask-tools/issues
205
- - **Docs:** https://github.com/ask-rb/ask-docs
83
+ MIT
@@ -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)
@@ -149,6 +194,17 @@ module Ask
149
194
  self.class.parameters
150
195
  end
151
196
 
197
+ # @return [Boolean] whether calling this tool requires human approval
198
+ def approval_required?
199
+ self.class.approval_required
200
+ end
201
+
202
+ # @return [Boolean] whether this tool may be auto-approved under a
203
+ # session-level auto-approval rule
204
+ def auto_approvable?
205
+ self.class.auto_approvable
206
+ end
207
+
152
208
  def call(args = {}, abort_controller = nil)
153
209
  normalized = normalize_args(args)
154
210
  validation = validate(normalized)
data/lib/ask/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Ask
4
4
  module Tools
5
- VERSION = "0.4.0"
5
+ VERSION = "0.6.0"
6
6
  end
7
7
  end
data/lib/ask-tools.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "ask/version"
4
4
  require_relative "ask/tools"
5
- require_relative "ask/tools/result"
5
+ require "ask" # ask-core: the shared Ask::Result lives here
6
6
  require_relative "ask/tools/tool"
7
7
 
8
8
  module Ask
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.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kaka Ruto
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: ask-core
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.9.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.9.0
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: ask-schema
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -65,7 +79,8 @@ dependencies:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
81
  version: '13.0'
68
- description: Defines Ask::Tool (base class), Ask::Result, and tool discovery.
82
+ description: Defines Ask::Tool (base class), tool discovery, and the Ask::Result returned
83
+ by tools.
69
84
  email:
70
85
  - kaka@myrrlabs.com
71
86
  executables: []
@@ -77,7 +92,6 @@ files:
77
92
  - README.md
78
93
  - lib/ask-tools.rb
79
94
  - lib/ask/tools.rb
80
- - lib/ask/tools/result.rb
81
95
  - lib/ask/tools/tool.rb
82
96
  - lib/ask/version.rb
83
97
  homepage: https://github.com/ask-rb/ask-tools
@@ -1,98 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Ask
4
- # Standardized return value for tool execution.
5
- #
6
- # Every tool's #execute method should return an Ask::Result.
7
- # Use the factory methods +.ok+ and +.error+ for common cases.
8
- #
9
- # Ask::Result.ok(data: "hello world")
10
- # Ask::Result.error(message: "something went wrong")
11
- #
12
- class Result
13
- # @return [Boolean] whether the tool completed successfully
14
- attr_reader :ok
15
-
16
- # @return [Object, nil] the output data when the tool succeeded
17
- attr_reader :output
18
-
19
- # @return [String, nil] the error message when the tool failed
20
- attr_reader :error
21
-
22
- # @return [Hash] arbitrary metadata attached to the result
23
- attr_reader :metadata
24
-
25
- alias ok? ok
26
-
27
- # @return [Boolean] whether the tool failed
28
- def error?
29
- !ok
30
- end
31
-
32
- # @return [String, nil] the error message (alias for +error+)
33
- alias error_message error
34
-
35
- def initialize(ok:, output: nil, error: nil, metadata: {})
36
- @ok = ok
37
- @output = output
38
- @error = error
39
- @metadata = metadata
40
- end
41
-
42
- # Create a successful result.
43
- #
44
- # @param data [Object] the tool's output
45
- # @param metadata [Hash] optional metadata
46
- # @return [Ask::Result]
47
- def self.ok(data:, metadata: {})
48
- new(ok: true, output: data, error: nil, metadata: metadata)
49
- end
50
-
51
- # Create a failed result (positional message form, used by Tool#call).
52
- #
53
- # @param message [String] description of the failure
54
- # @param metadata [Hash] optional metadata
55
- # @return [Ask::Result]
56
- def self.failure(message, metadata: {})
57
- new(ok: false, output: nil, error: message, metadata: metadata)
58
- end
59
-
60
- # Create a failed result.
61
- #
62
- # @param message [String] description of the failure
63
- # @param metadata [Hash] optional metadata
64
- # @return [Ask::Result]
65
- def self.error(message:, metadata: {})
66
- new(ok: false, output: nil, error: message, metadata: metadata)
67
- end
68
-
69
- # Human-readable representation.
70
- # Returns the output for success or the error message for failure.
71
- #
72
- # @return [String]
73
- def to_s
74
- ok? ? output.to_s : error.to_s
75
- end
76
-
77
- # Hash representation suitable for serialization.
78
- #
79
- # @return [Hash]
80
- def to_h
81
- {
82
- ok: ok,
83
- output: output,
84
- error: error,
85
- metadata: metadata
86
- }
87
- end
88
-
89
- # @return [String] inspect string
90
- def inspect
91
- if ok?
92
- "#<Ask::Result ok=true output=#{output.inspect}>"
93
- else
94
- "#<Ask::Result ok=false error=#{error.inspect}>"
95
- end
96
- end
97
- end
98
- end