phronomy 0.17.0 → 0.18.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +97 -1134
  3. data/README.md +68 -1280
  4. data/benchmark/bench_regression.rb +25 -5
  5. data/docs/changelog/0.14-and-earlier.md +1137 -0
  6. data/docs/decisions/008-orchestrator-uses-os-threads.md +46 -48
  7. data/docs/decisions/010-cooperative-first-concurrency.md +155 -235
  8. data/docs/features.md +87 -0
  9. data/docs/getting-started.md +351 -0
  10. data/docs/migrations/0.15.md +35 -0
  11. data/docs/migrations/0.16.md +43 -0
  12. data/docs/runtime-and-concurrency.md +258 -0
  13. data/examples/workflows/generic_task_event_mapping.rb +14 -6
  14. data/lib/phronomy/agent/agent_invocation_session_builder.rb +2 -2
  15. data/lib/phronomy/agent/async_event_api.rb +3 -3
  16. data/lib/phronomy/agent/base.rb +35 -19
  17. data/lib/phronomy/agent/context/capability/base.rb +13 -3
  18. data/lib/phronomy/agent/execution_coordinator.rb +6 -6
  19. data/lib/phronomy/agent/shared_state.rb +2 -0
  20. data/lib/phronomy/agent/tool_executor.rb +29 -71
  21. data/lib/phronomy/agent/tool_invocation.rb +97 -47
  22. data/lib/phronomy/agent/tool_invocation_session_builder.rb +55 -161
  23. data/lib/phronomy/configuration.rb +5 -29
  24. data/lib/phronomy/diagnostics.rb +12 -41
  25. data/lib/phronomy/engine/concurrency/async_queue.rb +5 -188
  26. data/lib/phronomy/engine/concurrency/cancellation_scope.rb +6 -7
  27. data/lib/phronomy/engine/concurrency/cancellation_token.rb +48 -3
  28. data/lib/phronomy/engine/concurrency/deadline.rb +2 -3
  29. data/lib/phronomy/engine/concurrency/offload_pool.rb +696 -0
  30. data/lib/phronomy/engine/concurrency/pool_registry.rb +5 -5
  31. data/lib/phronomy/engine/event_loop.rb +89 -190
  32. data/lib/phronomy/engine/runtime/timer_queue.rb +48 -71
  33. data/lib/phronomy/engine/runtime/timer_service.rb +13 -21
  34. data/lib/phronomy/engine/runtime.rb +45 -158
  35. data/lib/phronomy/engine/task.rb +136 -277
  36. data/lib/phronomy/llm_adapter/base.rb +14 -14
  37. data/lib/phronomy/llm_adapter/ruby_llm.rb +3 -4
  38. data/lib/phronomy/llm_adapter.rb +2 -2
  39. data/lib/phronomy/metrics.rb +15 -30
  40. data/lib/phronomy/multi_agent/fan_out_invocation.rb +146 -0
  41. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +125 -0
  42. data/lib/phronomy/multi_agent/handoff.rb +1 -0
  43. data/lib/phronomy/multi_agent/orchestrator.rb +147 -99
  44. data/lib/phronomy/multi_agent/team_coordinator.rb +2 -0
  45. data/lib/phronomy/testing/eval/comparison.rb +23 -0
  46. data/lib/phronomy/testing/eval/dataset.rb +27 -0
  47. data/lib/phronomy/testing/eval/eval_case.rb +13 -0
  48. data/lib/phronomy/testing/eval/eval_result.rb +16 -0
  49. data/lib/phronomy/testing/eval/metrics.rb +43 -0
  50. data/lib/phronomy/testing/eval/runner.rb +52 -0
  51. data/lib/phronomy/testing/eval/scorer/base.rb +15 -0
  52. data/lib/phronomy/testing/eval/scorer/exact_match.rb +25 -0
  53. data/lib/phronomy/testing/eval/scorer/includes_scorer.rb +25 -0
  54. data/lib/phronomy/testing/eval/scorer/llm_judge.rb +46 -0
  55. data/lib/phronomy/testing/eval/scorer.rb +10 -0
  56. data/lib/phronomy/testing/eval.rb +9 -0
  57. data/lib/phronomy/testing/fake_clock.rb +6 -53
  58. data/lib/phronomy/testing.rb +2 -6
  59. data/lib/phronomy/tools/agent.rb +141 -6
  60. data/lib/phronomy/vector_store/async_backend.rb +21 -17
  61. data/lib/phronomy/vector_store/base.rb +2 -2
  62. data/lib/phronomy/vector_store/embeddings/base.rb +6 -5
  63. data/lib/phronomy/version.rb +1 -1
  64. data/lib/phronomy/workflow_runner.rb +2 -4
  65. data/lib/phronomy.rb +7 -4
  66. data/scripts/api_snapshot.rb +4 -4
  67. metadata +23 -31
  68. data/lib/phronomy/engine/concurrency/blocking_adapter_pool.rb +0 -561
  69. data/lib/phronomy/engine/runtime/deterministic_scheduler.rb +0 -439
  70. data/lib/phronomy/engine/runtime/fake_scheduler.rb +0 -165
  71. data/lib/phronomy/engine/runtime/runtime_metrics.rb +0 -116
  72. data/lib/phronomy/engine/runtime/scheduler.rb +0 -98
  73. data/lib/phronomy/engine/runtime/scheduler_timer_adapter.rb +0 -79
  74. data/lib/phronomy/engine/runtime/task_registry.rb +0 -95
  75. data/lib/phronomy/engine/runtime/thread_scheduler.rb +0 -30
  76. data/lib/phronomy/engine/task/backend.rb +0 -80
  77. data/lib/phronomy/engine/task/deferred_backend.rb +0 -73
  78. data/lib/phronomy/engine/task/fiber_backend.rb +0 -157
  79. data/lib/phronomy/engine/task/immediate_backend.rb +0 -89
  80. data/lib/phronomy/engine/task/mapped_backend.rb +0 -90
  81. data/lib/phronomy/engine/task/thread_backend.rb +0 -84
  82. data/lib/phronomy/engine/task_group.rb +0 -193
  83. data/lib/phronomy/eval/comparison.rb +0 -47
  84. data/lib/phronomy/eval/dataset.rb +0 -45
  85. data/lib/phronomy/eval/eval_case.rb +0 -17
  86. data/lib/phronomy/eval/eval_result.rb +0 -29
  87. data/lib/phronomy/eval/metrics.rb +0 -66
  88. data/lib/phronomy/eval/runner.rb +0 -94
  89. data/lib/phronomy/eval/scorer/base.rb +0 -22
  90. data/lib/phronomy/eval/scorer/exact_match.rb +0 -31
  91. data/lib/phronomy/eval/scorer/includes_scorer.rb +0 -32
  92. data/lib/phronomy/eval/scorer/llm_judge.rb +0 -72
  93. data/lib/phronomy/eval/scorer.rb +0 -9
  94. data/lib/phronomy/eval.rb +0 -7
  95. data/lib/phronomy/testing/fake_scheduler.rb +0 -104
  96. data/lib/phronomy/testing/scheduler_helpers.rb +0 -68
@@ -2,104 +2,81 @@
2
2
 
3
3
  module Phronomy
4
4
  class Runtime
5
- # A thread-safe timer queue backed by a single background thread.
6
- #
7
- # Replaces the pattern of spawning one +Thread.new { sleep(t); callback }+
8
- # per deadline. Any number of timers share a single background thread that
9
- # sleeps until the earliest pending deadline.
10
- #
11
- # Use {#schedule} to register a one-shot callback; call {#shutdown} when the
12
- # queue is no longer needed (e.g. on process exit) to stop the background
13
- # thread cleanly.
5
+ # Threadless monotonic timer heap driven by EventLoop.
14
6
  class TimerQueue
15
- # @param clock [#call] zero-argument callable that returns the current
16
- # monotonic time in seconds (defaults to +Process::CLOCK_MONOTONIC+).
17
- # Override in tests to inject a fake clock.
18
- # @api private
19
7
  def initialize(clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) })
20
8
  @clock = clock
21
- @heap = [] # [[fire_at, callback], ...]
9
+ @heap = []
22
10
  @mutex = Mutex.new
23
- @cond = ConditionVariable.new
24
11
  @stopped = false
25
- @thread = Thread.new { run_loop }
26
- @thread.name = "phronomy-timer-queue"
12
+ @wake = nil
13
+ end
14
+
15
+ # Installs a lightweight wake callback used when a newly scheduled timer
16
+ # may change EventLoop's current wait deadline.
17
+ def wake_with(&block)
18
+ @mutex.synchronize { @wake = block }
19
+ self
27
20
  end
28
21
 
29
- # Schedule a one-shot callback to fire after +seconds+ from now.
30
- #
31
- # @param seconds [Numeric] delay before the callback fires
32
- # @yield called (in the timer thread) when the deadline is reached
33
- # @return [self]
34
- # @api private
35
22
  def schedule(seconds:, &callback)
23
+ raise ArgumentError, "schedule requires a block" unless callback
24
+
36
25
  fire_at = @clock.call + seconds.to_f
26
+ wake = nil
37
27
  @mutex.synchronize do
38
28
  raise Phronomy::PoolShutdownError, "TimerQueue has been shut down" if @stopped
39
- insert_sorted(fire_at, callback)
40
- @cond.signal
29
+
30
+ previous_first = @heap.first&.first
31
+ @heap << [fire_at, callback]
32
+ @heap.sort_by!(&:first)
33
+ wake = @wake if previous_first.nil? || fire_at < previous_first
41
34
  end
35
+ wake&.call
42
36
  self
43
37
  end
44
38
 
45
- # Stop the background thread. Pending (un-fired) callbacks are discarded.
46
- #
47
- # @return [self]
48
- # @api private
49
- def shutdown
39
+ # Seconds until the next timer is due, nil when no timers are pending.
40
+ def seconds_until_next
50
41
  @mutex.synchronize do
51
- @stopped = true
52
- @cond.signal
42
+ return nil if @stopped || @heap.empty?
43
+ [@heap.first.first - @clock.call, 0.0].max
53
44
  end
54
- @thread.join
55
- self
56
- end
57
-
58
- # Number of pending (not yet fired) callbacks. Primarily for testing.
59
- # @return [Integer]
60
- # @api private
61
- def pending_count
62
- @mutex.synchronize { @heap.size }
63
45
  end
64
46
 
65
- private
47
+ # Executes all callbacks whose deadline is due. Must be called by EventLoop.
48
+ def fire_due
49
+ callbacks = @mutex.synchronize do
50
+ return 0 if @stopped
66
51
 
67
- def insert_sorted(fire_at, callback)
68
- @heap << [fire_at, callback]
69
- @heap.sort_by! { |(t, _)| t }
70
- end
52
+ now = @clock.call
53
+ due_count = @heap.bsearch_index { |(fire_at, _)| fire_at > now } || @heap.length
54
+ @heap.shift(due_count).map(&:last)
55
+ end
71
56
 
72
- def run_loop
73
- loop do
74
- callback = next_callback
75
- break if callback == :stopped
76
- begin
77
- callback&.call
78
- rescue => e
79
- Phronomy.configuration.logger&.error { "[TimerQueue] callback raised #{e.class}: #{e.message}" }
57
+ callbacks.each do |callback|
58
+ callback.call
59
+ rescue => error
60
+ Phronomy.configuration.logger&.error do
61
+ "[TimerQueue] callback raised #{error.class}: #{error.message}"
80
62
  end
81
63
  end
64
+ callbacks.length
82
65
  end
83
66
 
84
- def next_callback
85
- @mutex.synchronize do
86
- loop do
87
- return :stopped if @stopped
67
+ def pending_count
68
+ @mutex.synchronize { @heap.size }
69
+ end
88
70
 
89
- if @heap.empty?
90
- @cond.wait(@mutex)
91
- else
92
- now = @clock.call
93
- fire_at, = @heap.first
94
- if fire_at <= now
95
- return @heap.shift[1]
96
- else
97
- remaining = fire_at - now
98
- @cond.wait(@mutex, remaining)
99
- end
100
- end
101
- end
71
+ def shutdown
72
+ wake = @mutex.synchronize do
73
+ return self if @stopped
74
+ @stopped = true
75
+ @heap.clear
76
+ @wake
102
77
  end
78
+ wake&.call
79
+ self
103
80
  end
104
81
  end
105
82
  end
@@ -2,38 +2,30 @@
2
2
 
3
3
  module Phronomy
4
4
  class Runtime
5
- # Lazy-initialised timer service for a {Runtime} instance.
6
- #
7
- # Returns a {SchedulerTimerAdapter} when the backing scheduler is a
8
- # {DeterministicScheduler} (enabling virtual-time integration for the
9
- # `:fiber` backend), or a standard {TimerQueue} (OS-thread backed) for all
10
- # other schedulers.
11
- # @api private
5
+ # Lazy owner of the Runtime's threadless timer queue.
12
6
  class TimerService
13
- # @param scheduler [Scheduler]
14
- # @api private
15
- def initialize(scheduler)
16
- @scheduler = scheduler
7
+ def initialize
17
8
  @mutex = Mutex.new
18
9
  @timer = nil
10
+ @waker = nil
19
11
  end
20
12
 
21
- # Returns (or lazily creates) the timer queue for this runtime.
22
- # @return [TimerQueue, SchedulerTimerAdapter]
23
- # @api private
24
13
  def timer_queue
25
14
  @mutex.synchronize do
26
- @timer ||= if @scheduler.is_a?(DeterministicScheduler)
27
- SchedulerTimerAdapter.new(@scheduler)
28
- else
29
- TimerQueue.new
15
+ @timer ||= TimerQueue.new.tap do |timer|
16
+ timer.wake_with(&@waker) if @waker
30
17
  end
31
18
  end
32
19
  end
33
20
 
34
- # Shuts down the timer queue if it was started.
35
- # @return [void]
36
- # @api private
21
+ def wake_with(&block)
22
+ @mutex.synchronize do
23
+ @waker = block
24
+ @timer&.wake_with(&block)
25
+ end
26
+ self
27
+ end
28
+
37
29
  def shutdown
38
30
  @mutex.synchronize { @timer&.shutdown }
39
31
  end
@@ -1,25 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "runtime/scheduler"
4
- require_relative "runtime/thread_scheduler"
5
- require_relative "runtime/fake_scheduler"
6
- require_relative "runtime/deterministic_scheduler"
7
3
  require_relative "runtime/timer_queue"
8
- require_relative "runtime/scheduler_timer_adapter"
9
- require_relative "runtime/task_registry"
10
- require_relative "runtime/runtime_metrics"
11
4
  require_relative "runtime/shutdown_result"
12
5
  require_relative "runtime/timer_service"
13
6
 
14
7
  module Phronomy
15
- # Central authority for concurrent primitives.
8
+ # Owns the EventLoop, offloaded synchronous work, timers and shutdown lifecycle.
9
+ #
10
+ # Runtime no longer schedules arbitrary Tasks. Framework control flow belongs
11
+ # to EventLoop/FSMSession; synchronous work that must not run on the EventLoop
12
+ # belongs to OffloadPool.
16
13
  class Runtime
17
14
  @instance_mutex = Mutex.new
18
15
 
19
16
  class << self
20
17
  def instance
21
18
  instance_mutex.synchronize do
22
- @instance ||= build_default_runtime
19
+ @instance ||= new
23
20
  end
24
21
  end
25
22
 
@@ -65,54 +62,12 @@ module Phronomy
65
62
  def instance_mutex
66
63
  @instance_mutex ||= Mutex.new
67
64
  end
68
-
69
- def build_default_runtime
70
- scheduler = case Phronomy.configuration.runtime_backend
71
- when :thread
72
- ThreadScheduler.new
73
- when :immediate
74
- FakeScheduler.new
75
- when :fiber
76
- Phronomy.configuration.logger&.warn(
77
- "[phronomy] runtime_backend: :fiber uses DeterministicScheduler in autorun mode. " \
78
- "This is an EXPERIMENTAL Fiber-based cooperative scheduler. " \
79
- "Wall-clock timer integration is available via SchedulerTimerAdapter (Issues #331, #337). " \
80
- "Not recommended for production use."
81
- )
82
- DeterministicScheduler.new(autorun: true)
83
- else
84
- raise Phronomy::ConfigurationError,
85
- "unknown runtime_backend: #{Phronomy.configuration.runtime_backend.inspect}"
86
- end
87
- new(scheduler: scheduler)
88
- end
89
- end
90
-
91
- def self.in_scheduler_context?
92
- !Task.current.nil?
93
- end
94
-
95
- def self.measure_ms
96
- t0 = Process.clock_gettime(Process::CLOCK_MONOTONIC)
97
- result = yield
98
- elapsed_ms = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - t0) * 1000).round
99
- [result, elapsed_ms]
100
- end
101
-
102
- attr_reader :scheduler
103
-
104
- def state
105
- @lifecycle_mutex.synchronize { @state }
106
65
  end
107
66
 
108
- def initialize(scheduler: ThreadScheduler.new)
109
- @scheduler = scheduler
110
- @event_loop_scheduler = ThreadScheduler.new
111
- @task_registry = TaskRegistry.new
112
- @metrics = RuntimeMetrics.new
113
- @timer_service = TimerService.new(scheduler)
67
+ def initialize
68
+ @timer_service = TimerService.new
114
69
  @pool_registry = Phronomy::Concurrency::PoolRegistry.new(
115
- timer_queue_provider: -> { @timer_service.timer_queue }
70
+ timer_queue_provider: -> { timer_queue }
116
71
  )
117
72
  @lifecycle_mutex = Mutex.new
118
73
  @shutdown_mutex = Mutex.new
@@ -122,73 +77,13 @@ module Phronomy
122
77
  @shutdown_result = nil
123
78
  end
124
79
 
125
- def yield
126
- if (threshold = Phronomy.configuration.blocking_detect_threshold_ms)
127
- slice_start = Task.current_cpu_slice_start_ms
128
- if slice_start
129
- elapsed = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - slice_start
130
- if elapsed > threshold
131
- name = Task.current&.name || "unknown"
132
- Phronomy.configuration.logger&.warn(
133
- "[Phronomy] CPU-bound task detected: '#{name}' ran #{elapsed.round}ms " \
134
- "without yielding (threshold: #{threshold}ms)"
135
- )
136
- @metrics.increment_starvation
137
- end
138
- end
139
- end
140
- Task.record_yield!
141
- @scheduler.yield
142
- end
143
-
144
- def non_yield_threshold_violation_count
145
- @metrics.starvation_count
146
- end
147
-
148
- def yield_if_needed(every: 1000)
149
- self.yield if (Task.increment_yield_counter! % every).zero?
150
- end
151
-
152
- def task_group(limit: Float::INFINITY, failure_policy: :fail_fast)
153
- ensure_accepting_work!
154
- TaskGroup.new(limit: limit, failure_policy: failure_policy, runtime: self)
155
- end
156
-
157
- def spawn(name: nil, &block)
158
- ensure_accepting_work!
159
- type = _task_type(name)
160
- spawn_at = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
161
- @metrics.record_start(type)
162
-
163
- task = @scheduler.spawn(name: name, parent: Task.current) do
164
- run_start = Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
165
- @metrics.record_wait(run_start - spawn_at)
166
- begin
167
- result = block.call
168
- @metrics.record_end(type, :completed, run_start)
169
- result
170
- rescue CancellationError
171
- @metrics.record_end(type, :cancelled, run_start)
172
- raise
173
- rescue => error
174
- @metrics.record_end(type, :failed, run_start)
175
- raise error
176
- ensure
177
- current = Task.current
178
- @task_registry.deregister(current) if current
179
- end
180
- end
181
- @task_registry.register(task)
182
- task
183
- end
184
-
185
- def task_snapshot
186
- @metrics.snapshot
80
+ def state
81
+ @lifecycle_mutex.synchronize { @state }
187
82
  end
188
83
 
189
- def blocking_io(
190
- pool_size: Phronomy.configuration.blocking_io_pool_size,
191
- queue_size: Phronomy.configuration.blocking_io_queue_size
84
+ def offload(
85
+ pool_size: Phronomy.configuration.offload_pool_size,
86
+ queue_size: Phronomy.configuration.offload_queue_size
192
87
  )
193
88
  ensure_accepting_work!
194
89
  @pool_registry.default_pool(pool_size: pool_size, queue_size: queue_size)
@@ -199,8 +94,16 @@ module Phronomy
199
94
  @pool_registry.named_pool(name, size: size, queue_size: queue_size)
200
95
  end
201
96
 
97
+ # Public timer access also ensures that the EventLoop that drives timers is alive.
202
98
  def timer_queue
203
99
  ensure_accepting_work!
100
+ timer = @timer_service.timer_queue
101
+ event_loop
102
+ timer
103
+ end
104
+
105
+ # Internal EventLoop access that does not recursively initialise EventLoop.
106
+ def __timer_queue
204
107
  @timer_service.timer_queue
205
108
  end
206
109
 
@@ -208,7 +111,11 @@ module Phronomy
208
111
  @lifecycle_mutex.synchronize do
209
112
  case @state
210
113
  when :running
211
- @event_loop ||= EventLoop.new(runtime: self)
114
+ unless @event_loop
115
+ @event_loop = EventLoop.new(runtime: self)
116
+ @timer_service.wake_with { @event_loop&.wake }
117
+ end
118
+ @event_loop
212
119
  when :draining
213
120
  return @event_loop if @event_loop
214
121
 
@@ -222,12 +129,8 @@ module Phronomy
222
129
  end
223
130
 
224
131
  def event_loop_current?
225
- event_loop = @lifecycle_mutex.synchronize { @event_loop }
226
- event_loop&.current? || false
227
- end
228
-
229
- def __spawn_event_loop_service(&block)
230
- @event_loop_scheduler.spawn(name: "event-loop", parent: nil, &block)
132
+ loop_instance = @lifecycle_mutex.synchronize { @event_loop }
133
+ loop_instance&.current? || false
231
134
  end
232
135
 
233
136
  def __event_loop_failed(error)
@@ -243,7 +146,7 @@ module Phronomy
243
146
  timeout: Phronomy.configuration.event_loop_stop_grace_seconds,
244
147
  cancel_grace: timeout
245
148
  )
246
- if Phronomy::Task.current
149
+ if event_loop_current?
247
150
  raise Phronomy::RuntimeShutdownReentrancyError,
248
151
  "Runtime#shutdown must be called from an external management thread"
249
152
  end
@@ -253,29 +156,33 @@ module Phronomy
253
156
  @shutdown_mutex.synchronize do
254
157
  return @shutdown_result if @shutdown_result
255
158
 
256
- deadline = monotonic_now + timeout
257
- event_loop = @lifecycle_mutex.synchronize do
159
+ # Phase 1 — drain sessions with the full configured grace.
160
+ drain_deadline = monotonic_now + timeout
161
+ loop_instance = @lifecycle_mutex.synchronize do
258
162
  @state = :draining unless @state == :failed
259
163
  @event_loop
260
164
  end
261
- event_loop&.begin_draining
165
+ loop_instance&.begin_draining
262
166
 
263
- task_status = drain_runtime_work(event_loop, deadline)
167
+ loop_idle = !loop_instance || loop_instance.wait_until_idle(drain_deadline)
264
168
 
265
169
  @lifecycle_mutex.synchronize do
266
170
  @state = :stopping unless @state == :failed
267
171
  end
268
172
 
269
- event_loop_status = if event_loop
270
- event_loop.shutdown(deadline: deadline, cancel_grace: cancel_grace)
173
+ # Phase 2 — stop the EventLoop thread with a short independent budget.
174
+ # An idle EventLoop processes STOP and exits in < 1ms normally; the 0.2s
175
+ # budget here is only a safety net for OS scheduling jitter.
176
+ stop_deadline = monotonic_now + [cancel_grace.to_f, 0.2].max
177
+ event_loop_status = if loop_instance
178
+ loop_instance.stop_and_join(deadline: stop_deadline)
271
179
  else
272
180
  :not_started
273
181
  end
274
182
 
275
183
  subsystem_error = shutdown_pools_and_timer
276
- final_task_status = @task_registry.empty? ? :empty : task_status
277
- cleanup_complete = final_task_status == :empty &&
278
- (!event_loop || !event_loop.task_alive?) &&
184
+ cleanup_complete = loop_idle &&
185
+ (!loop_instance || !loop_instance.thread_alive?) &&
279
186
  event_loop_status != :cancel_timeout &&
280
187
  subsystem_error.nil?
281
188
 
@@ -285,11 +192,12 @@ module Phronomy
285
192
  else
286
193
  :terminated
287
194
  end
195
+
288
196
  result = ShutdownResult.new(
289
197
  runtime_outcome: runtime_outcome,
290
198
  cleanup_status: cleanup_complete ? :complete : :incomplete,
291
199
  event_loop_status: event_loop_status,
292
- task_registry_status: final_task_status,
200
+ task_registry_status: :empty,
293
201
  error: failure
294
202
  )
295
203
 
@@ -311,17 +219,6 @@ module Phronomy
311
219
  "Runtime is #{current_state}; new work is not accepted"
312
220
  end
313
221
 
314
- def drain_runtime_work(event_loop, deadline)
315
- loop do
316
- task_status = @task_registry.drain_until(deadline)
317
- return :timeout if task_status == :timeout
318
-
319
- event_loop_idle = !event_loop || event_loop.wait_until_idle(deadline)
320
- return :timeout unless event_loop_idle
321
- return :empty if @task_registry.empty?
322
- end
323
- end
324
-
325
222
  def shutdown_pools_and_timer
326
223
  error = nil
327
224
  begin
@@ -347,15 +244,5 @@ module Phronomy
347
244
  def monotonic_now
348
245
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
349
246
  end
350
-
351
- TASK_TYPE_PREFIXES = %w[agent tool workflow rag llm vector].freeze
352
- private_constant :TASK_TYPE_PREFIXES
353
-
354
- def _task_type(name)
355
- return :other if name.nil?
356
-
357
- prefix = TASK_TYPE_PREFIXES.find { |candidate| name.to_s.start_with?("#{candidate}-") }
358
- prefix ? prefix.to_sym : :other
359
- end
360
247
  end
361
248
  end