Class: Phronomy::Agent::Context::Capability::Base

Inherits:
RubyLLM::Tool
  • Object
show all
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

Examples:

class SearchKnowledgeBase < Phronomy::Agent::Context::Capability::Base
  tool_name "search_kb"               # explicit name shown to the LLM
  description "Search the internal knowledge base"
  param :query,  type: :string, desc: "Search query"
  param :lang,   type: :string, desc: "Language", required: false, enum: %w[en ja fr]
  on_error :return_empty

  def execute(query:, lang: "en")
    KnowledgeBase.search(query, lang: lang)
  end
end

Direct Known Subclasses

Tools::Agent, Tools::Mcp, Tools::VectorSearch

Class Method Summary collapse

Instance Method Summary collapse

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

Parameters:

  • value (Symbol, nil) (defaults to: nil)

    when nil, returns the current value

Returns:

  • (Symbol)

    the current execution mode (default :blocking_io)



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.

Parameters:

  • value (Integer, nil) (defaults to: :__unset__)


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.

Parameters:

  • behavior (Symbol) (defaults to: nil)

    :raise (default) — re-raise as Phronomy::ToolError, stopping the agent. :suppress — suppress the error and return a descriptive string so the LLM can recover on the next turn. :return_empty — deprecated alias for :suppress; will be removed in a future major release.



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

Parameters:

  • behavior (Symbol) (defaults to: nil)

    :return_error (default) — return a descriptive error string as the tool result so the LLM can self-correct on the next turn. :raise — raise Phronomy::ToolError, stopping the agent loop. :coerce — attempt type coercion (e.g. "42" → 42 for :integer); falls back to :return_error when coercion is not possible.



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).

Parameters:

  • name (Symbol)

    parameter name

  • enum (Array, nil) (defaults to: nil)

    allowed values

  • properties (Hash, nil) (defaults to: nil)

    nested schema for :object params

  • options (Hash)

    forwarded to RubyLLM::Tool.param



56
57
58
59
60
# File 'lib/phronomy/agent/context/capability/base.rb', line 56

def param(name, enum: nil, properties: nil, **options)
  super(name, **options)
  param_enums[name] = enum if enum
  param_schemas[name] = normalize_nested_schema(properties) if properties
end

.param_enumsHash{Symbol => Array}

Returns the enum constraints registered via .param.

Returns:

  • (Hash{Symbol => Array})


65
66
67
# File 'lib/phronomy/agent/context/capability/base.rb', line 65

def param_enums
  @param_enums ||= {}
end

.param_schemasHash{Symbol => Hash}

Returns nested schema definitions registered via .param(properties: ...). mutant:disable - neutral failure: unparser round-trip produces different source

Returns:

  • (Hash{Symbol => Hash})


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

Parameters:

  • names (Array<Symbol>)

    parameter names to redact

Returns:

  • (Array<Symbol>)

    the full list of redacted param names



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.

Parameters:

  • value (Boolean, #call) (defaults to: :__unset__)


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").

Parameters:

  • value (String, nil) (defaults to: nil)

    the exact function name the LLM will see



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_metadataObject

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

Parameters:



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.message}"
    if Phronomy.configuration.logger
      Phronomy.configuration.logger.warn(msg)
    else
      warn msg
    end
    "Tool error suppressed: #{e.message}"
  else
    raise Phronomy::ToolError, "#{self.class.name} execution failed: #{e.message}"
  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

Parameters:

Returns:

  • (#await)


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

This method is abstract.

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.

Examples:

class WeatherTool < Phronomy::Agent::Context::Capability::Base
  description "Get current weather"
  param :location, type: :string, desc: "City name"

  def execute(location:)
    WeatherService.fetch(location).to_s
  end
end

Returns:

  • (String)

    result string returned to the LLM

Raises:

  • (NotImplementedError)


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

#nameObject

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_schemaObject

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_approvalObject

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.

Returns:

  • (Boolean)


368
369
370
# File 'lib/phronomy/agent/context/capability/base.rb', line 368

def requires_approval?
  self.class.requires_approval
end

#tool_originObject

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