phronomy 0.12.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +115 -0
- data/README.md +54 -5
- data/docs/mcp-client.md +75 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/base.rb +17 -17
- data/lib/phronomy/agent/context/capability/base.rb +9 -2
- data/lib/phronomy/agent/invocation_session.rb +11 -5
- data/lib/phronomy/agent/phase_machine_builder.rb +5 -4
- data/lib/phronomy/agent/tool_executor.rb +9 -2
- data/lib/phronomy/configuration.rb +15 -46
- data/lib/phronomy/diagnostics.rb +1 -1
- data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +230 -118
- data/lib/phronomy/engine/concurrency/cancellation_token.rb +5 -1
- data/lib/phronomy/engine/concurrency/pool_registry.rb +8 -3
- data/lib/phronomy/engine/event_loop.rb +319 -272
- data/lib/phronomy/engine/fsm_session.rb +17 -14
- data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +1 -1
- data/lib/phronomy/engine/runtime/shutdown_result.rb +62 -0
- data/lib/phronomy/engine/runtime/task_registry.rb +62 -15
- data/lib/phronomy/engine/runtime.rb +247 -57
- data/lib/phronomy/metrics.rb +4 -3
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +361 -321
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +13 -9
- data/lib/phronomy/workflow_context.rb +1 -2
- data/lib/phronomy/workflow_runner.rb +22 -12
- data/lib/phronomy.rb +24 -19
- metadata +59 -4
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
data/lib/phronomy/version.rb
CHANGED
|
@@ -76,6 +76,10 @@ module Phronomy
|
|
|
76
76
|
# waits for the async worker thread to post a state_completed event back.
|
|
77
77
|
attr_accessor :async_pending
|
|
78
78
|
|
|
79
|
+
# Invocation-local routing. FSMSession sets these on each machine
|
|
80
|
+
# instance; the compiled Class captures no Runtime-specific object.
|
|
81
|
+
attr_accessor :event_loop, :timer_queue_provider, :session_id
|
|
82
|
+
|
|
79
83
|
state_machine :phase, initial: entry do
|
|
80
84
|
all_states.each { |s| state s }
|
|
81
85
|
|
|
@@ -190,16 +194,16 @@ module Phronomy
|
|
|
190
194
|
# @api private
|
|
191
195
|
def dispatch_task_in_event_loop(machine, result, state_name, timeout_secs)
|
|
192
196
|
machine.async_pending = true
|
|
193
|
-
|
|
197
|
+
session_id = machine.session_id || machine.context.thread_id
|
|
194
198
|
if timeout_secs
|
|
195
|
-
|
|
199
|
+
machine.timer_queue_provider.call.schedule(seconds: timeout_secs) do
|
|
196
200
|
next if result.done?
|
|
197
201
|
|
|
198
|
-
|
|
202
|
+
machine.event_loop.post(
|
|
199
203
|
Phronomy::Event.new(
|
|
200
204
|
type: :error,
|
|
201
205
|
target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID,
|
|
202
|
-
payload: {session_id:
|
|
206
|
+
payload: {session_id: session_id, result: Phronomy::ActionTimeoutError.new(
|
|
203
207
|
"Action in state #{state_name.inspect} timed out after #{timeout_secs}s"
|
|
204
208
|
)}
|
|
205
209
|
)
|
|
@@ -208,17 +212,17 @@ module Phronomy
|
|
|
208
212
|
end
|
|
209
213
|
result.on_complete do |task_result, error|
|
|
210
214
|
if error
|
|
211
|
-
|
|
212
|
-
Phronomy::Event.new(type: :error, target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID, payload: {session_id:
|
|
215
|
+
machine.event_loop.post(
|
|
216
|
+
Phronomy::Event.new(type: :error, target_id: Phronomy::EventLoop::SYSTEM_CHANNEL_ID, payload: {session_id: session_id, result: error})
|
|
213
217
|
)
|
|
214
218
|
next
|
|
215
219
|
end
|
|
216
220
|
ev = if task_result.is_a?(Phronomy::WorkflowContext)
|
|
217
|
-
Phronomy::Event.new(type: :action_completed, target_id:
|
|
221
|
+
Phronomy::Event.new(type: :action_completed, target_id: session_id, payload: task_result)
|
|
218
222
|
else
|
|
219
|
-
Phronomy::Event.new(type: :state_completed, target_id:
|
|
223
|
+
Phronomy::Event.new(type: :state_completed, target_id: session_id, payload: nil)
|
|
220
224
|
end
|
|
221
|
-
|
|
225
|
+
machine.event_loop.post(ev)
|
|
222
226
|
end
|
|
223
227
|
end
|
|
224
228
|
|
|
@@ -167,10 +167,9 @@ module Phronomy
|
|
|
167
167
|
# @api private
|
|
168
168
|
# mutant:disable - multiple genuine equivalent mutations: defined?(Phronomy::EventLoop)&& removal is genuine because EventLoop is always loaded in the killfork environment; true&& is genuine (truthy guard); EventLoop.current? resolves to Phronomy::EventLoop.current? within the Phronomy module; WorkflowContextOwnershipError resolves to Phronomy::WorkflowContextOwnershipError within the module; raise without message or with nil message is genuine (spec checks exception class, not message text)
|
|
169
169
|
def _assert_write_permitted!
|
|
170
|
-
return unless defined?(Phronomy::EventLoop)
|
|
171
170
|
# Allow mutations when executing synchronously (e.g. Workflow#stream via run_workflow).
|
|
172
171
|
return if Thread.current[:phronomy_sync_execution]
|
|
173
|
-
return if Phronomy::
|
|
172
|
+
return if Phronomy::Runtime.in_event_loop_context?
|
|
174
173
|
|
|
175
174
|
raise Phronomy::WorkflowContextOwnershipError,
|
|
176
175
|
"WorkflowContext fields may only be mutated from the EventLoop dispatch " \
|
|
@@ -99,14 +99,19 @@ module Phronomy
|
|
|
99
99
|
# @return [Phronomy::Task]
|
|
100
100
|
# @api private
|
|
101
101
|
def invoke_deferred(input, config: {})
|
|
102
|
+
runtime = Phronomy::Runtime.instance
|
|
103
|
+
event_loop = runtime.event_loop
|
|
102
104
|
state, thread_id, recursion_limit, store = _build_initial_context(input, config)
|
|
103
105
|
result_task = Phronomy::Task.deferred(name: "workflow-async:#{thread_id}")
|
|
104
|
-
|
|
105
|
-
|
|
106
|
+
session = build_session_for(
|
|
107
|
+
context: state,
|
|
108
|
+
recursion_limit: recursion_limit,
|
|
109
|
+
runtime: runtime
|
|
110
|
+
)
|
|
106
111
|
if store && config[:thread_id]
|
|
107
112
|
# Wrap so that state is persisted when the task resolves.
|
|
108
113
|
persist_task = Phronomy::Task.deferred(name: "workflow-async-persist:#{thread_id}")
|
|
109
|
-
|
|
114
|
+
event_loop.register(session, completion: persist_task)
|
|
110
115
|
persist_task.on_complete do |result, error|
|
|
111
116
|
store.save(thread_id, {fields: result.to_h, phase: result.phase.to_s}) unless error
|
|
112
117
|
if error
|
|
@@ -118,7 +123,7 @@ module Phronomy
|
|
|
118
123
|
end
|
|
119
124
|
end
|
|
120
125
|
else
|
|
121
|
-
|
|
126
|
+
event_loop.register(session, completion: result_task)
|
|
122
127
|
end
|
|
123
128
|
result_task
|
|
124
129
|
end
|
|
@@ -202,7 +207,8 @@ module Phronomy
|
|
|
202
207
|
end
|
|
203
208
|
|
|
204
209
|
# Builds an FSMSession for the given context. Used in EventLoop mode.
|
|
205
|
-
def build_session_for(context:, recursion_limit:,
|
|
210
|
+
def build_session_for(context:, recursion_limit:, runtime: Phronomy::Runtime.instance,
|
|
211
|
+
resume_event: nil, resume_phase: nil)
|
|
206
212
|
Phronomy::FSMSession.new(
|
|
207
213
|
id: context.thread_id,
|
|
208
214
|
context: context,
|
|
@@ -214,24 +220,28 @@ module Phronomy
|
|
|
214
220
|
external_events: @external_events,
|
|
215
221
|
phase_machine_class: @phase_machine_class,
|
|
216
222
|
recursion_limit: recursion_limit,
|
|
223
|
+
event_loop: runtime.event_loop,
|
|
224
|
+
timer_queue_provider: -> { runtime.timer_queue },
|
|
217
225
|
action_timeouts: @action_timeouts,
|
|
218
226
|
resume_event: resume_event,
|
|
219
227
|
resume_phase: resume_phase
|
|
220
228
|
)
|
|
221
229
|
end
|
|
222
230
|
|
|
223
|
-
# Executes the workflow via the
|
|
231
|
+
# Executes the workflow via the default Runtime-owned EventLoop.
|
|
224
232
|
# Blocks the calling thread on a completion queue until the workflow
|
|
225
233
|
# finishes, halts at a wait state, or raises an error.
|
|
226
234
|
def run_via_event_loop(context, recursion_limit:, resume_event: nil, resume_phase: nil)
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
Phronomy::EventLoop.instance.start
|
|
235
|
+
runtime = Phronomy::Runtime.instance
|
|
236
|
+
event_loop = runtime.event_loop
|
|
230
237
|
session = build_session_for(
|
|
231
|
-
context: context,
|
|
232
|
-
|
|
238
|
+
context: context,
|
|
239
|
+
recursion_limit: recursion_limit,
|
|
240
|
+
runtime: runtime,
|
|
241
|
+
resume_event: resume_event,
|
|
242
|
+
resume_phase: resume_phase
|
|
233
243
|
)
|
|
234
|
-
completion_queue =
|
|
244
|
+
completion_queue = event_loop.register(session)
|
|
235
245
|
result = completion_queue.pop
|
|
236
246
|
raise result if result.is_a?(Exception)
|
|
237
247
|
result
|
data/lib/phronomy.rb
CHANGED
|
@@ -77,6 +77,13 @@ module Phronomy
|
|
|
77
77
|
# @see Phronomy::Runtime.in_scheduler_context?
|
|
78
78
|
class SchedulerReentrancyError < Error; end
|
|
79
79
|
|
|
80
|
+
# Raised when work is submitted to a Runtime whose shutdown has begun, or
|
|
81
|
+
# when a Runtime cannot be reset because owned resources are still alive.
|
|
82
|
+
class RuntimeShutdownError < Error; end
|
|
83
|
+
|
|
84
|
+
# Raised when Runtime#shutdown is invoked from inside a Phronomy::Task.
|
|
85
|
+
class RuntimeShutdownReentrancyError < RuntimeShutdownError; end
|
|
86
|
+
|
|
80
87
|
# Raised by {Phronomy::GeneratorVerifier#invoke} when +raise_if_untrusted: true+
|
|
81
88
|
# and the pipeline's combined confidence score falls below the configured threshold.
|
|
82
89
|
#
|
|
@@ -175,28 +182,26 @@ module Phronomy
|
|
|
175
182
|
@configuration = original
|
|
176
183
|
end
|
|
177
184
|
|
|
178
|
-
#
|
|
179
|
-
#
|
|
185
|
+
# Shuts down and clears the process-wide default Runtime, then resets
|
|
186
|
+
# global configuration. Intended for test suites only.
|
|
180
187
|
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
183
|
-
# Call once before/after each example to ensure test isolation.
|
|
188
|
+
# Runtime execution failure and resource cleanup are separate. The
|
|
189
|
+
# singleton is cleared when cleanup completed, even if execution failed.
|
|
184
190
|
#
|
|
185
|
-
# @
|
|
186
|
-
#
|
|
191
|
+
# @param timeout [Numeric] maximum graceful wait for Runtime tasks and
|
|
192
|
+
# EventLoop shutdown
|
|
193
|
+
# @return [Phronomy::Runtime::ShutdownResult]
|
|
187
194
|
# @api public
|
|
188
|
-
def reset_runtime!
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
@configuration = Configuration.new
|
|
199
|
-
@configuration.event_loop_stop_grace_seconds = prev_grace if prev_grace
|
|
195
|
+
def reset_runtime!(timeout: configuration.event_loop_stop_grace_seconds)
|
|
196
|
+
previous_grace = @configuration&.event_loop_stop_grace_seconds
|
|
197
|
+
result = Runtime.reset_default!(timeout: timeout)
|
|
198
|
+
|
|
199
|
+
new_configuration = Configuration.new
|
|
200
|
+
if previous_grace
|
|
201
|
+
new_configuration.event_loop_stop_grace_seconds = previous_grace
|
|
202
|
+
end
|
|
203
|
+
@configuration = new_configuration
|
|
204
|
+
result
|
|
200
205
|
end
|
|
201
206
|
end
|
|
202
207
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: phronomy
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.14.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Raizo T.C.S
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-27 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ruby_llm
|
|
@@ -64,6 +64,60 @@ dependencies:
|
|
|
64
64
|
- - "~>"
|
|
65
65
|
- !ruby/object:Gem::Version
|
|
66
66
|
version: '0.6'
|
|
67
|
+
- !ruby/object:Gem::Dependency
|
|
68
|
+
name: mcp
|
|
69
|
+
requirement: !ruby/object:Gem::Requirement
|
|
70
|
+
requirements:
|
|
71
|
+
- - "~>"
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: '1.0'
|
|
74
|
+
type: :runtime
|
|
75
|
+
prerelease: false
|
|
76
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - "~>"
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '1.0'
|
|
81
|
+
- !ruby/object:Gem::Dependency
|
|
82
|
+
name: faraday
|
|
83
|
+
requirement: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '2'
|
|
88
|
+
- - "<"
|
|
89
|
+
- !ruby/object:Gem::Version
|
|
90
|
+
version: '3'
|
|
91
|
+
type: :runtime
|
|
92
|
+
prerelease: false
|
|
93
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
94
|
+
requirements:
|
|
95
|
+
- - ">="
|
|
96
|
+
- !ruby/object:Gem::Version
|
|
97
|
+
version: '2'
|
|
98
|
+
- - "<"
|
|
99
|
+
- !ruby/object:Gem::Version
|
|
100
|
+
version: '3'
|
|
101
|
+
- !ruby/object:Gem::Dependency
|
|
102
|
+
name: event_stream_parser
|
|
103
|
+
requirement: !ruby/object:Gem::Requirement
|
|
104
|
+
requirements:
|
|
105
|
+
- - ">="
|
|
106
|
+
- !ruby/object:Gem::Version
|
|
107
|
+
version: '1'
|
|
108
|
+
- - "<"
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '2'
|
|
111
|
+
type: :runtime
|
|
112
|
+
prerelease: false
|
|
113
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - ">="
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '1'
|
|
118
|
+
- - "<"
|
|
119
|
+
- !ruby/object:Gem::Version
|
|
120
|
+
version: '2'
|
|
67
121
|
description: Phronomy is a Ruby AI agent framework that provides composable building
|
|
68
122
|
blocks — Agents, Workflows, Tools, Filters, and Tracing — for building AI agents
|
|
69
123
|
in Ruby. Powered by RubyLLM for LLM abstraction.
|
|
@@ -101,6 +155,8 @@ files:
|
|
|
101
155
|
- docs/decisions/009-state-store-abstraction.md
|
|
102
156
|
- docs/decisions/010-cooperative-first-concurrency.md
|
|
103
157
|
- docs/decisions/011-build-context-as-single-llm-input-authority.md
|
|
158
|
+
- docs/mcp-client.md
|
|
159
|
+
- gemfiles/mcp_1_0.gemfile
|
|
104
160
|
- lib/phronomy.rb
|
|
105
161
|
- lib/phronomy/agent.rb
|
|
106
162
|
- lib/phronomy/agent/base.rb
|
|
@@ -129,9 +185,7 @@ files:
|
|
|
129
185
|
- lib/phronomy/engine/concurrency/blocking_adapter_pool.rb
|
|
130
186
|
- lib/phronomy/engine/concurrency/cancellation_scope.rb
|
|
131
187
|
- lib/phronomy/engine/concurrency/cancellation_token.rb
|
|
132
|
-
- lib/phronomy/engine/concurrency/concurrency_gate.rb
|
|
133
188
|
- lib/phronomy/engine/concurrency/deadline.rb
|
|
134
|
-
- lib/phronomy/engine/concurrency/gate_registry.rb
|
|
135
189
|
- lib/phronomy/engine/concurrency/pool_registry.rb
|
|
136
190
|
- lib/phronomy/engine/event_loop.rb
|
|
137
191
|
- lib/phronomy/engine/fsm_session.rb
|
|
@@ -141,6 +195,7 @@ files:
|
|
|
141
195
|
- lib/phronomy/engine/runtime/runtime_metrics.rb
|
|
142
196
|
- lib/phronomy/engine/runtime/scheduler.rb
|
|
143
197
|
- lib/phronomy/engine/runtime/scheduler_timer_adapter.rb
|
|
198
|
+
- lib/phronomy/engine/runtime/shutdown_result.rb
|
|
144
199
|
- lib/phronomy/engine/runtime/task_registry.rb
|
|
145
200
|
- lib/phronomy/engine/runtime/thread_scheduler.rb
|
|
146
201
|
- lib/phronomy/engine/runtime/timer_queue.rb
|
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Concurrency
|
|
5
|
-
# A counting semaphore that enforces a concurrency cap across a named
|
|
6
|
-
# resource category (e.g. agent tasks, tool tasks, LLM calls).
|
|
7
|
-
#
|
|
8
|
-
# When +max_concurrent+ is +nil+ the gate is a no-op and all callers
|
|
9
|
-
# pass through immediately without acquiring a slot.
|
|
10
|
-
#
|
|
11
|
-
# Backpressure behaviour when the gate is full is controlled by the
|
|
12
|
-
# +on_full:+ keyword:
|
|
13
|
-
# +:reject+ — raise {Phronomy::BackpressureError} immediately
|
|
14
|
-
# +:wait+ — block the calling fiber/thread until a slot is free
|
|
15
|
-
# +:timeout+ — like +:wait+ but raises {Phronomy::BackpressureError}
|
|
16
|
-
# after +timeout:+ seconds if no slot becomes available
|
|
17
|
-
#
|
|
18
|
-
# @example
|
|
19
|
-
# gate = Phronomy::Concurrency::ConcurrencyGate.new(max_concurrent: 5, name: :agent)
|
|
20
|
-
# gate.acquire(on_full: :reject) do
|
|
21
|
-
# run_agent_task
|
|
22
|
-
# end
|
|
23
|
-
class ConcurrencyGate
|
|
24
|
-
# @param max_concurrent [Integer, nil] concurrency cap; nil = unlimited
|
|
25
|
-
# @param name [Symbol, String, nil] human-readable label used in error messages
|
|
26
|
-
# @api private
|
|
27
|
-
def initialize(max_concurrent:, name: nil)
|
|
28
|
-
@max = max_concurrent
|
|
29
|
-
@name = name
|
|
30
|
-
@mutex = Mutex.new
|
|
31
|
-
@cond = ConditionVariable.new
|
|
32
|
-
@count = 0
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
# Returns the configured cap (or nil when unlimited).
|
|
36
|
-
attr_reader :max
|
|
37
|
-
|
|
38
|
-
# Returns the name label.
|
|
39
|
-
attr_reader :name
|
|
40
|
-
|
|
41
|
-
# Returns the number of slots currently in use.
|
|
42
|
-
def current_count
|
|
43
|
-
@mutex.synchronize { @count }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Acquires a slot, executes +block+, then releases the slot.
|
|
47
|
-
# When the gate is unlimited (max is nil) the block runs directly.
|
|
48
|
-
#
|
|
49
|
-
# @param on_full [:reject, :wait, :timeout] backpressure strategy
|
|
50
|
-
# @param timeout [Numeric, nil] seconds before +:timeout+ gives up
|
|
51
|
-
# @yield
|
|
52
|
-
# @return block return value
|
|
53
|
-
# @raise [Phronomy::BackpressureError] when +:reject+ or +:timeout+ fires
|
|
54
|
-
# @api private
|
|
55
|
-
def acquire(on_full: :wait, timeout: nil, &block)
|
|
56
|
-
return block.call if @max.nil?
|
|
57
|
-
|
|
58
|
-
_acquire_slot(on_full: on_full, timeout: timeout)
|
|
59
|
-
begin
|
|
60
|
-
block.call
|
|
61
|
-
ensure
|
|
62
|
-
_release_slot
|
|
63
|
-
end
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
|
|
68
|
-
def _acquire_slot(on_full:, timeout:)
|
|
69
|
-
scheduler = Phronomy::Runtime::Scheduler.current
|
|
70
|
-
if scheduler
|
|
71
|
-
_acquire_slot_coop(scheduler, on_full: on_full, timeout: timeout)
|
|
72
|
-
else
|
|
73
|
-
_acquire_slot_threaded(on_full: on_full, timeout: timeout)
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def _acquire_slot_coop(scheduler, on_full:, timeout:)
|
|
78
|
-
# In cooperative mode all tasks run on the same thread, so no mutex needed.
|
|
79
|
-
deadline = timeout ? (scheduler.virtual_time + timeout) : nil
|
|
80
|
-
@coop_signal ||= scheduler.new_signal
|
|
81
|
-
|
|
82
|
-
loop do
|
|
83
|
-
if @count < @max
|
|
84
|
-
@count += 1
|
|
85
|
-
return
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
case on_full
|
|
89
|
-
when :reject
|
|
90
|
-
raise Phronomy::BackpressureError,
|
|
91
|
-
"ConcurrencyGate[#{@name}] at capacity (#{@max}); " \
|
|
92
|
-
"increase max_concurrent_#{@name}_tasks or retry later"
|
|
93
|
-
when :timeout
|
|
94
|
-
if deadline && scheduler.virtual_time >= deadline
|
|
95
|
-
raise Phronomy::BackpressureError,
|
|
96
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
97
|
-
end
|
|
98
|
-
scheduler.wait_for_signal(@coop_signal)
|
|
99
|
-
if deadline && scheduler.virtual_time >= deadline
|
|
100
|
-
raise Phronomy::BackpressureError,
|
|
101
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
102
|
-
end
|
|
103
|
-
else # :wait
|
|
104
|
-
scheduler.wait_for_signal(@coop_signal)
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def _acquire_slot_threaded(on_full:, timeout:)
|
|
110
|
-
deadline = timeout ? (Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout) : nil
|
|
111
|
-
|
|
112
|
-
@mutex.synchronize do
|
|
113
|
-
loop do
|
|
114
|
-
if @count < @max
|
|
115
|
-
@count += 1
|
|
116
|
-
return
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
case on_full
|
|
120
|
-
when :reject
|
|
121
|
-
raise Phronomy::BackpressureError,
|
|
122
|
-
"ConcurrencyGate[#{@name}] at capacity (#{@max}); " \
|
|
123
|
-
"increase max_concurrent_#{@name}_tasks or retry later"
|
|
124
|
-
when :timeout
|
|
125
|
-
remaining = deadline ? (deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)) : nil
|
|
126
|
-
if remaining && remaining <= 0
|
|
127
|
-
raise Phronomy::BackpressureError,
|
|
128
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
129
|
-
end
|
|
130
|
-
@cond.wait(@mutex, remaining || nil)
|
|
131
|
-
# re-check deadline after wakeup
|
|
132
|
-
if deadline && Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
133
|
-
raise Phronomy::BackpressureError,
|
|
134
|
-
"ConcurrencyGate[#{@name}] timed out waiting for a free slot (cap: #{@max})"
|
|
135
|
-
end
|
|
136
|
-
else # :wait
|
|
137
|
-
@cond.wait(@mutex)
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
end
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
def _release_slot
|
|
144
|
-
scheduler = Phronomy::Runtime::Scheduler.current
|
|
145
|
-
if scheduler && @coop_signal
|
|
146
|
-
@count -= 1
|
|
147
|
-
scheduler.raise_signal(@coop_signal)
|
|
148
|
-
else
|
|
149
|
-
@mutex.synchronize do
|
|
150
|
-
@count -= 1
|
|
151
|
-
@cond.signal
|
|
152
|
-
end
|
|
153
|
-
end
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
end
|
|
157
|
-
end
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Concurrency
|
|
5
|
-
# Lazy cache of {ConcurrencyGate} instances, keyed by resource name.
|
|
6
|
-
#
|
|
7
|
-
# Gate concurrency caps are read from {Phronomy::Configuration} when a gate
|
|
8
|
-
# is first accessed; subsequent calls return the cached instance. Call
|
|
9
|
-
# {#reset} to drop the cache and force a rebuild on the next access.
|
|
10
|
-
# @api private
|
|
11
|
-
class GateRegistry
|
|
12
|
-
GATE_CONFIG_MAP = {
|
|
13
|
-
agent: :max_concurrent_agent_tasks,
|
|
14
|
-
tool: :max_concurrent_tool_tasks,
|
|
15
|
-
workflow: :max_concurrent_workflow_tasks,
|
|
16
|
-
llm: :max_concurrent_llm_calls,
|
|
17
|
-
vector: :max_concurrent_vector_searches
|
|
18
|
-
}.freeze
|
|
19
|
-
private_constant :GATE_CONFIG_MAP
|
|
20
|
-
|
|
21
|
-
def initialize
|
|
22
|
-
@mutex = Mutex.new
|
|
23
|
-
@gates = {}
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Returns (or lazily creates) the gate for +name+.
|
|
27
|
-
# @param name [Symbol]
|
|
28
|
-
# @return [ConcurrencyGate]
|
|
29
|
-
# @api private
|
|
30
|
-
def get(name)
|
|
31
|
-
@mutex.synchronize { @gates[name] ||= _build(name) }
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Drops the cached gate for +name+ so the next {#get} rebuilds it.
|
|
35
|
-
# @param name [Symbol]
|
|
36
|
-
# @return [void]
|
|
37
|
-
# @api private
|
|
38
|
-
def reset(name)
|
|
39
|
-
@mutex.synchronize { @gates.delete(name) }
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
private
|
|
43
|
-
|
|
44
|
-
def _build(name)
|
|
45
|
-
config_key = GATE_CONFIG_MAP[name]
|
|
46
|
-
max = config_key ? Phronomy.configuration.public_send(config_key) : nil
|
|
47
|
-
ConcurrencyGate.new(max_concurrent: max, name: name)
|
|
48
|
-
end
|
|
49
|
-
end
|
|
50
|
-
end
|
|
51
|
-
end
|