Class: Phronomy::Tool::Base
- Inherits:
-
RubyLLM::Tool
- Object
- RubyLLM::Tool
- Phronomy::Tool::Base
- Defined in:
- lib/phronomy/tool/base.rb
Overview
Base class extending RubyLLM::Tool with Phronomy-specific DSL.
Additional DSL over RubyLLM::Tool:
- tool_name : explicit function name exposed to the LLM (overrides auto-conversion)
- scope : access-scope metadata (:read_only, :write, etc.)
- on_error : error-handling policy (:raise or :return_empty)
- on_schema_error : behavior when LLM passes schema-violating arguments :return_error (default), :raise, or :coerce
- requires_approval : require human approval before execution
- param :name, enum: [...] : restrict allowed values in the JSON Schema
Class Attribute Summary collapse
-
._sleep_proc ⇒ #call
private
Injectable sleep callable for testing.
Class Method Summary collapse
-
.execution_mode(value = nil) ⇒ Symbol
Sets or reads the execution mode for this tool.
-
.max_result_size(value = :__unset__) ⇒ Object
Sets a per-tool maximum result size (in characters).
-
.on_error(behavior = nil) ⇒ Object
Configures error-handling behavior when +execute+ raises an unexpected error.
-
.on_schema_error(behavior = nil) ⇒ Object
Configures how this tool responds when the LLM passes arguments that violate the declared parameter types or enum constraints.
-
.param(name, enum: nil, properties: nil, **options) ⇒ Object
Extends RubyLLM::Tool.param with optional +enum:+ and +properties:+ keywords.
-
.param_enums ⇒ Hash{Symbol => Array}
Returns the enum constraints registered via .param.
-
.param_schemas ⇒ Hash{Symbol => Hash}
Returns nested schema definitions registered via .param(properties: ...).
-
.redact_params(*names) ⇒ Array<Symbol>
Marks one or more parameter names as sensitive so their values are replaced with +"[REDACTED]"+ in log and trace output.
-
.requires_approval(value = nil) ⇒ Object
Configures whether human approval is required before executing this tool.
-
.retry_on(*exception_classes, times: 1, wait: 0, base: 1.0) ⇒ Object
Registers a retry policy for one or more exception classes.
-
.retry_policies ⇒ Array<Hash>
Returns all retry policies registered on this tool class.
-
.scope(value = nil) ⇒ Object
Sets the access scope for this tool (metadata; enforcement is the responsibility of the Workflow/Guardrail layer).
-
.tool_name(value = nil) ⇒ Object
Sets an explicit function name to expose to the LLM, bypassing RubyLLM's automatic CamelCase-to-snake_case conversion.
Instance Method Summary collapse
-
#call(args, cancellation_token: nil) ⇒ Object
Overrides RubyLLM::Tool#call to apply schema validation, the retry policy, the on_error policy, and wrap errors as ToolError.
-
#call_async(args, cancellation_token: nil) ⇒ #await
Invokes this tool asynchronously and returns a Phronomy::Task.
-
#execute(**_args) ⇒ String
abstract
Override this method to implement the tool's logic.
-
#name ⇒ Object
Returns the function name exposed to the LLM.
-
#params_schema ⇒ Object
Returns the JSON Schema for this tool's parameters.
-
#requires_approval ⇒ Object
Instance method accessor — delegates to the class-level flag.
-
#requires_approval? ⇒ Boolean
Instance method for requires_approval? (convenience accessor).
Class Attribute Details
._sleep_proc ⇒ #call
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Injectable sleep callable for testing. Defaults to Kernel#sleep.
239 240 241 |
# File 'lib/phronomy/tool/base.rb', line 239 def _sleep_proc @_sleep_proc || method(:sleep) end |
Class Method Details
.execution_mode(value = nil) ⇒ Symbol
Sets or reads the execution mode for this tool.
Execution mode is the concurrency contract declaration for the tool. In Phronomy's non-preemptive, cooperative concurrency model it controls which runtime resource is used to dispatch the tool:
| Mode | Dispatcher | Constraint |
|---|---|---|
| +:cooperative+ | +Runtime.instance.spawn+ (scheduler task) | Must not block the scheduler thread; use only for in-memory computation |
| +:blocking_io+ | BlockingAdapterPool (bounded thread pool) | Default. Safe for all blocking I/O (HTTP, DB, file) |
| +:cpu_bound+ | Falls back to +:blocking_io+ + emits a warning | No dedicated process pool yet; use +:blocking_io+ explicitly to suppress the warning |
| +:external_process+ | Falls back to +:blocking_io+ | No process manager yet |
Tools that perform network calls, file I/O, or database queries should use +:blocking_io+ (the default). Tools that only perform in-memory computation may declare +:cooperative+ for lower overhead.
120 121 122 123 124 125 126 127 128 129 |
# File 'lib/phronomy/tool/base.rb', line 120 def execution_mode(value = nil) return @execution_mode || :blocking_io if value.nil? valid = %i[cooperative blocking_io cpu_bound external_process] unless valid.include?(value) raise ArgumentError, "execution_mode must be one of #{valid.inspect}, got #{value.inspect}" end @execution_mode = value end |
.max_result_size(value = :__unset__) ⇒ Object
Sets a per-tool maximum result size (in characters). Overrides the global +Phronomy.configuration.tool_result_max_size+ when set. Set to +nil+ to inherit the global limit.
200 201 202 203 204 |
# File 'lib/phronomy/tool/base.rb', line 200 def max_result_size(value = :__unset__) return @max_result_size if value == :__unset__ @max_result_size = value end |
.on_error(behavior = nil) ⇒ Object
Configures error-handling behavior when +execute+ raises an unexpected error.
140 141 142 143 144 145 146 147 148 149 150 151 152 |
# File 'lib/phronomy/tool/base.rb', line 140 def on_error(behavior = nil) return @on_error || :raise if behavior.nil? if behavior == :return_empty msg = "[Phronomy] on_error :return_empty is deprecated; use :suppress instead" if Phronomy.configuration.logger Phronomy.configuration.logger.warn(msg) else warn msg end end @on_error = behavior end |
.on_schema_error(behavior = nil) ⇒ Object
Configures how this tool responds when the LLM passes arguments that violate the declared parameter types or enum constraints.
164 165 166 167 168 |
# File 'lib/phronomy/tool/base.rb', line 164 def on_schema_error(behavior = nil) return @on_schema_error || :return_error if behavior.nil? @on_schema_error = behavior end |
.param(name, enum: nil, properties: nil, **options) ⇒ Object
Extends RubyLLM::Tool.param with optional +enum:+ and +properties:+ keywords.
- +enum:+ restricts allowed values; injected into the JSON Schema.
- +properties:+ declares nested fields for :object type params. Each entry is a Hash mapping field name (Symbol) to a spec Hash with keys: :type (Symbol, default :string), :required (Boolean, default false), and optionally :properties (for further nesting).
55 56 57 58 59 |
# File 'lib/phronomy/tool/base.rb', line 55 def param(name, enum: nil, properties: nil, **) super(name, **) param_enums[name] = enum if enum param_schemas[name] = normalize_nested_schema(properties) if properties end |
.param_enums ⇒ Hash{Symbol => Array}
Returns the enum constraints registered via .param.
64 65 66 |
# File 'lib/phronomy/tool/base.rb', line 64 def param_enums @param_enums ||= {} end |
.param_schemas ⇒ Hash{Symbol => Hash}
Returns nested schema definitions registered via .param(properties: ...).
71 72 73 |
# File 'lib/phronomy/tool/base.rb', line 71 def param_schemas @param_schemas ||= {} end |
.redact_params(*names) ⇒ Array<Symbol>
Marks one or more parameter names as sensitive so their values are replaced with +"[REDACTED]"+ in log and trace output.
185 186 187 188 189 190 191 192 |
# File 'lib/phronomy/tool/base.rb', line 185 def redact_params(*names) if names.empty? parent = superclass.respond_to?(:redact_params) ? superclass.redact_params : [] ((@redacted_params || []) + parent).uniq else @redacted_params = ((@redacted_params || []) + names.map(&:to_sym)).uniq end end |
.requires_approval(value = nil) ⇒ Object
Configures whether human approval is required before executing this tool.
173 174 175 176 177 |
# File 'lib/phronomy/tool/base.rb', line 173 def requires_approval(value = nil) return @requires_approval || false if value.nil? @requires_approval = value end |
.retry_on(*exception_classes, times: 1, wait: 0, base: 1.0) ⇒ Object
Registers a retry policy for one or more exception classes.
When the tool raises one of the listed exception classes, it will be retried up to +times+ times with the specified wait strategy. Multiple policies can be registered and are evaluated in order.
GuardrailError is never retried regardless of this configuration.
223 224 225 226 |
# File 'lib/phronomy/tool/base.rb', line 223 def retry_on(*exception_classes, times: 1, wait: 0, base: 1.0) @retry_policies ||= [] @retry_policies << {exceptions: exception_classes, times: times, wait: wait, base: base} end |
.retry_policies ⇒ Array<Hash>
Returns all retry policies registered on this tool class.
231 232 233 |
# File 'lib/phronomy/tool/base.rb', line 231 def retry_policies @retry_policies || [] end |
.scope(value = nil) ⇒ Object
Sets the access scope for this tool (metadata; enforcement is the responsibility of the Workflow/Guardrail layer).
94 95 96 97 98 |
# File 'lib/phronomy/tool/base.rb', line 94 def scope(value = nil) return @scope if value.nil? @scope = value end |
.tool_name(value = nil) ⇒ Object
Sets an explicit function name to expose to the LLM, bypassing RubyLLM's automatic CamelCase-to-snake_case conversion. When omitted, RubyLLM's default conversion applies (e.g. WeatherTool → "weather").
37 38 39 40 41 |
# File 'lib/phronomy/tool/base.rb', line 37 def tool_name(value = nil) return @tool_name if value.nil? @tool_name = value.to_s end |
Instance Method Details
#call(args, cancellation_token: nil) ⇒ Object
Overrides RubyLLM::Tool#call to apply schema validation, the retry policy, the on_error policy, and wrap errors as ToolError.
Execution order:
- Early cancellation check (kwarg token takes precedence over thread-local).
- Schema validation (type + enum checks).
- Inject +cancellation_token:+ into args when +execute+ opts in.
- Call super(validated_args) inside a retry loop.
- On persistent failure, apply on_error policy.
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 |
# File 'lib/phronomy/tool/base.rb', line 308 def call(args, cancellation_token: nil) ct = cancellation_token ct&.raise_if_cancelled! validated_args, schema_error = validate_and_coerce(args) if schema_error case self.class.on_schema_error when :raise raise Phronomy::ToolError, "#{self.class.name} schema error: #{schema_error}" else # :return_error (default) and coerce fallback return "Schema validation failed: #{schema_error}" end end validated_args = validated_args.merge(cancellation_token: ct) if ct && execute_accepts_cancellation_token? result = with_tool_retry { super(validated_args) } truncate_result_if_needed(result) rescue Phronomy::ToolError raise rescue Phronomy::CancellationError raise rescue => e case self.class.on_error when :return_empty, :suppress msg = "[Phronomy] Tool #{self.class.name} suppressed error: #{e.class}: #{e.}" if Phronomy.configuration.logger Phronomy.configuration.logger.warn(msg) else warn msg end "Tool error suppressed: #{e.}" else raise Phronomy::ToolError, "#{self.class.name} execution failed: #{e.}" end end |
#call_async(args, cancellation_token: nil) ⇒ #await
Invokes this tool asynchronously and returns a Phronomy::Task.
Routing is governed by the class-level execution_mode setting. Delegates to Phronomy::ToolExecutor.call_async which is the single place in the framework that applies the execution-mode routing rules.
353 354 355 356 357 358 359 |
# File 'lib/phronomy/tool/base.rb', line 353 def call_async(args, cancellation_token: nil) Phronomy::ToolExecutor.call_async( tool: self, args: args, cancellation_token: cancellation_token ) end |
#execute(**_args) ⇒ String
Subclasses must implement this method.
Override this method to implement the tool's logic.
The method receives the declared param fields as keyword arguments. The return value is passed back to the LLM as the tool result.
388 389 390 |
# File 'lib/phronomy/tool/base.rb', line 388 def execute(**_args) raise NotImplementedError, "#{self.class}#execute is not implemented" end |
#name ⇒ Object
Returns the function name exposed to the LLM. Uses the class-level tool_name if set; otherwise falls back to RubyLLM's automatic conversion (CamelCase → snake_case, strips trailing "_tool").
251 252 253 |
# File 'lib/phronomy/tool/base.rb', line 251 def name self.class.tool_name || super end |
#params_schema ⇒ Object
Returns the JSON Schema for this tool's parameters. Injects "enum" entries for any param declared with enum: [...].
257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 |
# File 'lib/phronomy/tool/base.rb', line 257 def params_schema schema = super return schema if schema.nil? properties = schema.dig("properties") || schema.dig(:properties) return schema unless properties # Inject enum values for params declared with enum: [...]. unless self.class.param_enums.empty? enums = self.class.param_enums enums.each do |param_name, values| key = properties.key?(param_name.to_s) ? param_name.to_s : param_name.to_sym next unless properties[key] param_type = properties[key]["type"] properties[key]["enum"] = values.map do |v| case param_type when "integer" then v.is_a?(Integer) ? v : Integer(v.to_s) when "number" then v.is_a?(Numeric) ? v : Float(v.to_s) else v.to_s end end end end # Inject nested properties for :object params (issue #162). # Without this the LLM sees only { "type": "object" } with no field # definitions, making it unable to populate nested object params. self.class.param_schemas.each do |param_name, nested| key = properties.key?(param_name.to_s) ? param_name.to_s : param_name.to_sym next unless properties[key] properties[key]["properties"] = nested_schema_to_json_schema(nested) end schema end |
#requires_approval ⇒ Object
Instance method accessor — delegates to the class-level flag.
362 363 364 |
# File 'lib/phronomy/tool/base.rb', line 362 def requires_approval self.class.requires_approval end |
#requires_approval? ⇒ Boolean
Instance method for requires_approval? (convenience accessor).
367 368 369 |
# File 'lib/phronomy/tool/base.rb', line 367 def requires_approval? self.class.requires_approval end |