Module: Phronomy::Agent::ToolExecutor Private
- Defined in:
- lib/phronomy/agent/tool_executor.rb
Overview
This module is part of a private API. You should avoid using this module if possible, as it may be removed or be changed in the future.
Non-goals ToolExecutor deliberately does NOT provide:
- A CPU-bound process pool. CPU-intensive tool work must be handled at the
application layer (e.g., fork, Sidekiq, separate OS processes). The
framework will not add a
ProcessPoolExecutorequivalent. - An external process manager. Spawning or supervising subprocesses is out of scope for this module.
- Additional core execution routes beyond scheduler-backed cooperative
execution and BlockingAdapterPool-backed blocking I/O isolation.
The
:cpu_boundand:external_processmodes are accepted for compatibility but both fall back to:blocking_iorouting with a one-time warning. If a genuinely new core execution route is needed, a new ADR is required. These non-goals follow from the cooperative-first, non-preemptive concurrency model (ADR-010): framework components must not assume the caller's concurrency model, and CPU/process management belongs to the application layer.
Centralises tool execution routing based on Tool::Base.execution_mode.
This is the single place in the framework that decides how a tool call is dispatched:
:cooperative— dispatched viaRuntime#spawnthrough the configured scheduler. Under the:fiberbackend this avoids an extra OS thread; under the:threadbackend it is backed byThreadScheduler(one thread per task).:blocking_io— submitted toBlockingAdapterPoolwhen the runtime provides a pool; falls back toRuntime#spawnotherwise.:cpu_bound— emits a deprecation-style warning then falls back to:blocking_iorouting (no process pool available yet).:external_process— falls back to:blocking_iorouting (no process manager available yet).
All paths return an object that responds to #await (+Phronomy::Task+ or
BlockingAdapterPool::PendingOperation), so callers can collect results
uniformly.
Class Method Summary collapse
-
.call_async(tool:, args:, cancellation_token: nil, runtime: Phronomy::Runtime.instance) ⇒ #await
private
Dispatches a single tool call asynchronously according to its
execution_modeand returns an awaitable.
Class Method Details
.call_async(tool:, args:, cancellation_token: nil, runtime: Phronomy::Runtime.instance) ⇒ #await
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.
Dispatches a single tool call asynchronously according to its
execution_mode and returns an awaitable.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
# File 'lib/phronomy/agent/tool_executor.rb', line 61 def self.call_async(tool:, args:, cancellation_token: nil, runtime: Phronomy::Runtime.instance) ct = cancellation_token mode = tool.class.execution_mode # Warn and normalise unsupported modes to :blocking_io. # Each (tool class, mode) pair emits the warning at most once per process # lifetime to avoid log flooding in high-throughput scenarios. if mode == :cpu_bound || mode == :external_process warn_key = [tool.class.name, mode] newly_warned = WARNED_MODES_MUTEX.synchronize { WARNED_MODES.add?(warn_key) } if newly_warned msg = if mode == :cpu_bound "[Phronomy] Tool #{tool.class.name} declares execution_mode :cpu_bound, " \ "which has no dedicated executor. " \ "Falling back to blocking_io (BlockingAdapterPool). " \ "Use :blocking_io explicitly to suppress this warning." else "[Phronomy] Tool #{tool.class.name} declares execution_mode :external_process, " \ "which has no dedicated process manager. " \ "Falling back to blocking_io (BlockingAdapterPool)." end if Phronomy.configuration.logger Phronomy.configuration.logger.warn(msg) else warn msg end end mode = :blocking_io end pool = begin runtime&.blocking_io rescue nil end if mode == :cooperative || pool.nil? runtime.spawn(name: "tool-#{tool.class.name.to_s.split("::").last}") do tool.call(args, cancellation_token: ct) end else # Submit directly to pool — no wrapping Task thread required. pool.submit(cancellation_token: ct) { tool.call(args, cancellation_token: ct) } end end |