Class: Phronomy::Tools::Agent

Inherits:
Agent::Context::Capability::Base show all
Defined in:
lib/phronomy/tools/agent.rb

Overview

Wraps a Phronomy::Agent::Base subclass as a callable Tool.

Agent-backed Tools are logically asynchronous rather than offloaded synchronous operations. Their ToolInvocation starts the child Agent and then returns to EventLoop immediately; the Tool completion handle settles when the child Agent FSMSession finishes. An OffloadPool worker is therefore never consumed merely to wait for another Agent.

Class Method Summary collapse

Instance Method Summary collapse

Methods inherited from Agent::Context::Capability::Base

approval_facts, #approval_metadata, #call, description, #execute, execution_mode, max_result_size, #name, on_error, on_schema_error, param, param_enums, param_schemas, parameters, #params_schema, params_schema_definition, provider_params, redact_params, requires_approval, #requires_approval, #requires_approval?, tool_name, #tool_origin

Class Method Details

.from_agent(agent_class, tool_name: nil, description: nil) ⇒ Object

Raises:

  • (ArgumentError)


18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/phronomy/tools/agent.rb', line 18

def from_agent(agent_class, tool_name: nil, description: nil)
  raise ArgumentError, "agent_class must be a Class" unless agent_class.is_a?(Class)
  unless agent_class <= Phronomy::Agent::Base
    raise ArgumentError,
      "agent_class must inherit from Phronomy::Agent::Base"
  end

  # Fail at Tool definition time rather than on the first Tool call.
  agent_class.agent_definition

  klass = Class.new(self)
  effective_name = tool_name || derive_name(agent_class)
  effective_desc = description || "Delegates to #{agent_class.name || "an agent"}"

  klass.tool_name(effective_name)
  klass.description(effective_desc)

  # Preserve the synchronous Tool API for top-level callers. ToolInvocation
  # never uses this path for Agent-backed Tools; it calls #call_async.
  klass.define_method(:execute) do |input:, cancellation_token: nil|
    invoke_options = {}
    if cancellation_token
      invoke_options[:config] = {cancellation_token: cancellation_token}
    end
    result = Phronomy::Agent.run_once(
      definition: agent_class,
      input: input,
      **invoke_options
    )
    result[:output].to_s
  end

  # Internal asynchronous execution protocol used by Agent#call_async.
  # The child Agent owns its own FSMSession/EventLoop lifecycle; this
  # method only returns its completion handle and performs a short map.
  klass.define_method(:execute_async) do |input:, cancellation_token: nil, config: {}|
    persistence = Phronomy::Persistence::InMemory.new
    agent = agent_class.create(persistence: persistence)
    task_config = (config || {}).dup
    if cancellation_token && !task_config[:cancellation_token]
      task_config[:cancellation_token] = cancellation_token
    end

    agent.invoke_async(input, config: task_config).map do |result|
      result[:output].to_s
    end
  end
  klass.send(:private, :execute_async)
  klass
end

Instance Method Details

#call_async(args, cancellation_token: nil, config: {}) ⇒ Object

Agent-backed Tools have an asynchronous implementation that does not use ToolExecutor/OffloadPool. Validation and Tool error policy still match Capability::Base#call.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
# File 'lib/phronomy/tools/agent.rb', line 87

def call_async(
  args,
  cancellation_token: nil,
  config: {}
)
  cancellation_token&.raise_if_cancelled!
  validated_args, schema_error = send(:validate_and_coerce, args)

  if schema_error
    return schema_error_task(schema_error)
  end

  source = execute_async(
    **(validated_args || {}),
    cancellation_token: cancellation_token,
    config: config || {}
  )
  unless source.respond_to?(:on_complete)
    raise Phronomy::ToolError,
      "#{self.class.name} asynchronous execution must return a completion handle"
  end

  result_task = Phronomy::Task.deferred(name: "agent-tool-#{name}")
  source.on_complete do |result, error|
    if error
      settle_async_error(result_task, error)
      next
    end

    begin
      result_task.complete(send(:truncate_result_if_needed, result))
    rescue => result_error
      settle_async_error(result_task, result_error)
    end
  end
  result_task
rescue Phronomy::ToolError, Phronomy::CancellationError => error
  failed_task(error)
rescue => error
  result_task = Phronomy::Task.deferred(name: "agent-tool-#{name}")
  settle_async_error(result_task, error)
  result_task
end