Class: Phronomy::Agent::Context::Capability::Base
- Inherits:
-
RubyLLM::Tool
- Object
- RubyLLM::Tool
- Phronomy::Agent::Context::Capability::Base
- Defined in:
- lib/phronomy/agent/context/capability/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)
- 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 : Boolean/callable Tool-side approval default
- approval_facts : callable exposing semantic facts to Agent policy
- param :name, enum: [...] : restrict allowed values in the JSON Schema
Direct Known Subclasses
Class Method Summary collapse
-
.approval_facts(&block) ⇒ Object
Registers a Tool-specific semantic fact extractor for authorization.
-
.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
executeraises 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:andproperties: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 = :__unset__, &block) ⇒ Object
Configures the Tool-side default for approval.
-
.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
-
#approval_metadata ⇒ Object
Display-safe transport/origin metadata for approval requests.
-
#call(args, cancellation_token: nil) ⇒ Object
Overrides RubyLLM::Tool#call to apply schema validation, the on_error policy, and wrap errors as ToolError.
-
#call_async(args, cancellation_token: nil, config: {}) ⇒ #await
Invokes this tool asynchronously and returns a 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).
-
#tool_origin ⇒ Object
Origin metadata consumed by ToolInvocation authorization policy.
Class Method Details
.approval_facts(&block) ⇒ Object
Registers a Tool-specific semantic fact extractor for authorization. The block receives validated immutable arguments and read-only context.
192 193 194 195 196 197 198 199 200 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 192 def approval_facts(&block) if block @approval_facts = block elsif instance_variable_defined?(:@approval_facts) @approval_facts elsif superclass.respond_to?(:approval_facts) superclass.approval_facts end end |
.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 |
Concurrency::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.
mutant:disable
114 115 116 117 118 119 120 121 122 123 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 114 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.
224 225 226 227 228 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 224 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.
134 135 136 137 138 139 140 141 142 143 144 145 146 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 134 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.
mutant:disable - neutral failure: unparser round-trip produces different source
159 160 161 162 163 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 159 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).
56 57 58 59 60 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 56 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.
65 66 67 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 65 def param_enums @param_enums ||= {} end |
.param_schemas ⇒ Hash{Symbol => Hash}
Returns nested schema definitions registered via .param(properties: ...). mutant:disable - neutral failure: unparser round-trip produces different source
73 74 75 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 73 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.
mutant:disable
209 210 211 212 213 214 215 216 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 209 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 = :__unset__, &block) ⇒ Object
Configures the Tool-side default for approval. A callable receives ApprovalEvaluationRequest and must return true or false. It is evaluated on the Runtime authorization pool, not in Tool#call.
170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 170 def requires_approval(value = :__unset__, &block) if block unless value == :__unset__ raise ArgumentError, "pass either a value or a block to requires_approval" end @requires_approval = block elsif value == :__unset__ return @requires_approval if instance_variable_defined?(:@requires_approval) return superclass.requires_approval if superclass.respond_to?(:requires_approval) false else unless value == true || value == false || value.respond_to?(:call) raise ArgumentError, "requires_approval must be true, false, or callable" end @requires_approval = value end 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").
38 39 40 41 42 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 38 def tool_name(value = nil) return @tool_name if value.nil? @tool_name = value.to_s end |
Instance Method Details
#approval_metadata ⇒ Object
Display-safe transport/origin metadata for approval requests.
380 381 382 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 380 def {} end |
#call(args, cancellation_token: nil) ⇒ Object
Overrides RubyLLM::Tool#call to apply schema validation, the on_error policy, and wrap errors as ToolError.
Execution order:
1. Early cancellation check (kwarg token takes precedence over thread-local).
2. Schema validation (type + enum checks).
3. Inject +cancellation_token:+ into args when +execute+ opts in.
4. Call super(validated_args) exactly once.
5. On failure, apply on_error policy.
mutant:disable
304 305 306 307 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 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 304 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 = 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, config: {}) ⇒ #await
Invokes this tool asynchronously and returns a Task.
Routing is governed by the class-level execution_mode setting. Delegates to ToolExecutor.call_async which is the single place in the framework that applies the execution-mode routing rules.
mutant:disable
350 351 352 353 354 355 356 357 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 350 def call_async(args, cancellation_token: nil, config: {}) Phronomy::Agent::ToolExecutor.call_async( tool: self, args: args, cancellation_token: cancellation_token, config: config ) 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.
401 402 403 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 401 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"). mutant:disable - neutral failure: unparser round-trip produces different source
235 236 237 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 235 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: [...]. mutant:disable - genuine equivalent mutations:
1. `|| schema.dig(:properties)`: dead code because RubyLLM::Tool always returns a
string-keyed hash; schema.dig(:properties) is always nil in practice.
2. `return schema unless properties` guard: dead code when schema is non-nil because
RubyLLM::Tool always includes a "properties" key when parameters are declared.
246 247 248 249 250 251 252 253 254 255 256 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 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 246 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) when "boolean" unless v == true || v == false raise ArgumentError, "boolean enum values must be true or false (got: #{v.inspect})" end v 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.
360 361 362 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 360 def requires_approval self.class.requires_approval end |
#requires_approval? ⇒ Boolean
Instance method for requires_approval? (convenience accessor). mutant:disable - genuine equivalent: self.requires_approval delegates to self.class.requires_approval via the instance method defined above, so both expressions produce the same value.
368 369 370 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 368 def requires_approval? self.class.requires_approval end |
#tool_origin ⇒ Object
Origin metadata consumed by ToolInvocation authorization policy.
374 375 376 |
# File 'lib/phronomy/agent/context/capability/base.rb', line 374 def tool_origin :local end |