Module: Phronomy::ToolExecutor Private
- Defined in:
- lib/phronomy/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 +ProcessPoolExecutor+ equivalent.
- 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_bound+ and +:external_process+ modes are accepted for compatibility but both fall back to +:blocking_io+ routing 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 Phronomy::Tool::Base.execution_mode.
This is the single place in the framework that decides how a tool call is dispatched:
- +:cooperative+ — dispatched via +Runtime#spawn+ through the configured scheduler. Under the +:fiber+ backend this avoids an extra OS thread; under the +:thread+ backend it is backed by +ThreadScheduler+ (one thread per task).
- +:blocking_io+ — submitted to +BlockingAdapterPool+ when the runtime provides a pool; falls back to +Runtime#spawn+ otherwise.
- +:cpu_bound+ — emits a deprecation-style warning then falls back to +:blocking_io+ routing (no process pool available yet).
- +:external_process+ — falls back to +:blocking_io+ routing (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_mode+ and 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.
60 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 |
# File 'lib/phronomy/tool_executor.rb', line 60 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 |