phronomy 0.13.0 → 0.15.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 +155 -0
- data/README.md +266 -38
- data/benchmark/bench_agent_invoke.rb +2 -3
- data/docs/decisions/004-invoke-timeout-is-not-cancellation.md +14 -67
- data/docs/decisions/011-delegate-transport-policy-to-adapters.md +82 -0
- data/docs/mcp-client.md +75 -0
- data/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -0
- data/gemfiles/mcp_1_0.gemfile +9 -0
- data/lib/phronomy/agent/agent_invocation.rb +385 -0
- data/lib/phronomy/agent/agent_invocation_registry.rb +75 -0
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +448 -0
- data/lib/phronomy/agent/approval_evaluation_request.rb +102 -0
- data/lib/phronomy/agent/async_event_api.rb +471 -0
- data/lib/phronomy/agent/base.rb +509 -420
- data/lib/phronomy/agent/context/capability/base.rb +57 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -136
- data/lib/phronomy/agent/tool_approval_request.rb +121 -0
- data/lib/phronomy/agent/tool_call_intercepted.rb +11 -15
- data/lib/phronomy/agent/tool_executor.rb +47 -69
- data/lib/phronomy/agent/tool_invocation.rb +634 -0
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +378 -0
- data/lib/phronomy/agent.rb +21 -9
- data/lib/phronomy/configuration.rb +58 -53
- 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 +507 -303
- data/lib/phronomy/engine/fsm_session.rb +181 -140
- 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/engine/task.rb +5 -10
- data/lib/phronomy/event.rb +8 -8
- data/lib/phronomy/generator_verifier.rb +253 -142
- data/lib/phronomy/invalid_async_entry_action_error.rb +9 -0
- data/lib/phronomy/invalid_async_transition_action_error.rb +11 -0
- data/lib/phronomy/invalid_async_workflow_action_error.rb +9 -0
- data/lib/phronomy/invocation_context.rb +5 -19
- data/lib/phronomy/llm_adapter/base.rb +25 -34
- data/lib/phronomy/metrics.rb +6 -3
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/testing/scheduler_helpers.rb +12 -3
- data/lib/phronomy/tools/mcp.rb +410 -81
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -182
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +55 -104
- data/lib/phronomy/workflow_runner.rb +239 -291
- data/lib/phronomy.rb +30 -23
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +63 -11
- data/lib/phronomy/agent/concerns/retryable.rb +0 -103
- data/lib/phronomy/agent/context/capability/scope_policy.rb +0 -54
- data/lib/phronomy/agent/invocation_context.rb +0 -171
- data/lib/phronomy/agent/invocation_session.rb +0 -346
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
- data/lib/phronomy/engine/concurrency/concurrency_gate.rb +0 -157
- data/lib/phronomy/engine/concurrency/gate_registry.rb +0 -51
|
@@ -4,7 +4,11 @@ module Phronomy
|
|
|
4
4
|
module LLMAdapter
|
|
5
5
|
# Abstract base class for LLM adapters.
|
|
6
6
|
#
|
|
7
|
-
# Subclasses must implement {#complete} and {#stream}.
|
|
7
|
+
# Subclasses must implement {#complete} and {#stream}. The adapter or the
|
|
8
|
+
# underlying provider client owns transport timeout, retry, backoff, and
|
|
9
|
+
# rate-limit behavior. Phronomy only supplies cooperative cancellation and
|
|
10
|
+
# isolates blocking calls in {BlockingAdapterPool}.
|
|
11
|
+
#
|
|
8
12
|
# The agent pipeline calls {#complete_async} / {#stream_async} which wrap
|
|
9
13
|
# those methods in a {BlockingAdapterPool} submission.
|
|
10
14
|
class Base
|
|
@@ -14,7 +18,7 @@ module Phronomy
|
|
|
14
18
|
#
|
|
15
19
|
# @param chat [Object] the configured chat session object
|
|
16
20
|
# @param message [String] the user message
|
|
17
|
-
# @param config [Hash]
|
|
21
|
+
# @param config [Hash] invocation config (e.g. +:cancellation_token+)
|
|
18
22
|
# @return [Object] LLM response object
|
|
19
23
|
# @raise [NotImplementedError]
|
|
20
24
|
# @api private
|
|
@@ -28,7 +32,7 @@ module Phronomy
|
|
|
28
32
|
#
|
|
29
33
|
# @param chat [Object] the configured chat session object
|
|
30
34
|
# @param message [String] the user message
|
|
31
|
-
# @param config [Hash]
|
|
35
|
+
# @param config [Hash] invocation config
|
|
32
36
|
# @yield [chunk] streaming chunk from the LLM
|
|
33
37
|
# @return [Object] LLM response object
|
|
34
38
|
# @raise [NotImplementedError]
|
|
@@ -40,16 +44,18 @@ module Phronomy
|
|
|
40
44
|
# Submits a non-streaming LLM call to {BlockingAdapterPool} and returns
|
|
41
45
|
# a {BlockingAdapterPool::PendingOperation}.
|
|
42
46
|
#
|
|
47
|
+
# Transport timeout and retry remain the responsibility of the adapter or
|
|
48
|
+
# provider client; Phronomy does not attach an additional operation timeout.
|
|
49
|
+
#
|
|
43
50
|
# @param chat [Object] configured chat session
|
|
44
51
|
# @param message [String] user message
|
|
45
|
-
# @param config [Hash]
|
|
52
|
+
# @param config [Hash] invocation config
|
|
46
53
|
# @param pool [BlockingAdapterPool] pool to submit to
|
|
47
54
|
# @return [BlockingAdapterPool::PendingOperation]
|
|
48
55
|
# @api private
|
|
49
56
|
def complete_async(chat, message, config: {}, pool: default_pool)
|
|
50
57
|
token = config[:cancellation_token]
|
|
51
|
-
|
|
52
|
-
pool.submit(timeout: timeout, cancellation_token: token) do
|
|
58
|
+
pool.submit(cancellation_token: token) do
|
|
53
59
|
complete(chat, message, config: config)
|
|
54
60
|
end
|
|
55
61
|
end
|
|
@@ -57,39 +63,24 @@ module Phronomy
|
|
|
57
63
|
# Submits a streaming LLM call to {BlockingAdapterPool} and returns
|
|
58
64
|
# a {BlockingAdapterPool::PendingOperation}.
|
|
59
65
|
#
|
|
60
|
-
#
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# LLM call finishes so the consumer's drain loop terminates naturally.
|
|
64
|
-
# This keeps user-supplied blocks off the blocking-pool worker thread.
|
|
66
|
+
# The block is invoked on the blocking-pool worker thread. Agent code must
|
|
67
|
+
# pass only a lightweight internal sink that posts a value to EventLoop;
|
|
68
|
+
# Application callbacks must never be passed directly to this method.
|
|
65
69
|
#
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
# backward compatibility).
|
|
70
|
+
# Transport timeout and retry remain the responsibility of the adapter or
|
|
71
|
+
# provider client; Phronomy does not attach an additional operation timeout.
|
|
69
72
|
#
|
|
70
|
-
# @
|
|
71
|
-
# @param message [String] user message
|
|
72
|
-
# @param config [Hash] invocation config
|
|
73
|
-
# @param pool [BlockingAdapterPool] pool to submit to
|
|
74
|
-
# @param enqueue_to [AsyncQueue, nil] when set, push chunks here instead of
|
|
75
|
-
# calling the block on the worker thread
|
|
76
|
-
# @yield [chunk] streaming chunk — only used when +enqueue_to:+ is nil
|
|
73
|
+
# @yield [chunk] streaming chunk on the worker thread
|
|
77
74
|
# @return [BlockingAdapterPool::PendingOperation]
|
|
78
75
|
# @api private
|
|
79
|
-
def stream_async(chat, message, config: {}, pool: default_pool,
|
|
76
|
+
def stream_async(chat, message, config: {}, pool: default_pool, &block)
|
|
77
|
+
raise ArgumentError, "stream_async requires a block" unless block
|
|
78
|
+
|
|
80
79
|
token = config[:cancellation_token]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
enqueue_to.push(chunk)
|
|
86
|
-
end
|
|
87
|
-
ensure
|
|
88
|
-
enqueue_to.close
|
|
89
|
-
end
|
|
90
|
-
else
|
|
91
|
-
pool.submit(timeout: timeout, cancellation_token: token) do
|
|
92
|
-
stream(chat, message, config: config, &block)
|
|
80
|
+
pool.submit(cancellation_token: token) do
|
|
81
|
+
stream(chat, message, config: config) do |chunk|
|
|
82
|
+
token&.raise_if_cancelled!("invocation cancelled during streaming")
|
|
83
|
+
block.call(chunk)
|
|
93
84
|
end
|
|
94
85
|
end
|
|
95
86
|
end
|
data/lib/phronomy/metrics.rb
CHANGED
|
@@ -20,15 +20,18 @@ module Phronomy
|
|
|
20
20
|
# @return [Hash{Symbol => Numeric}]
|
|
21
21
|
# @api public
|
|
22
22
|
def self.snapshot
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
runtime = Runtime.instance
|
|
24
|
+
pool = runtime.blocking_io
|
|
25
|
+
el = runtime.event_loop
|
|
26
|
+
task_snap = runtime.task_snapshot
|
|
26
27
|
|
|
27
28
|
{
|
|
28
29
|
blocking_pool_active: pool.active_count,
|
|
29
30
|
blocking_pool_queue_length: pool.queue_depth,
|
|
30
31
|
blocking_pool_abandoned_total: pool.abandoned_count,
|
|
31
32
|
blocking_pool_size: pool.pool_size,
|
|
33
|
+
event_loop_queue_depth: el.queue_depth,
|
|
34
|
+
event_loop_queue_max_depth: el.max_queue_depth,
|
|
32
35
|
event_loop_lag_last_ms: (el.last_lag_seconds * 1000).round(3),
|
|
33
36
|
event_loop_lag_max_ms: (el.max_lag_seconds * 1000).round(3),
|
|
34
37
|
event_loop_lag_average_ms: (el.average_lag_seconds * 1000).round(3)
|
|
@@ -2,122 +2,90 @@
|
|
|
2
2
|
|
|
3
3
|
module Phronomy
|
|
4
4
|
module MultiAgent
|
|
5
|
-
# RubyLLM::Chat subclass
|
|
5
|
+
# RubyLLM::Chat subclass supporting concurrent direct Tool execution.
|
|
6
6
|
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
11
|
-
# - +:blocking_io+ tools are offloaded to a +BlockingAdapterPool+ worker
|
|
12
|
-
# thread so they do not occupy a scheduler task slot.
|
|
13
|
-
# All results are collected before being appended to the message history,
|
|
14
|
-
# preserving deterministic message order while reducing wall-clock latency
|
|
15
|
-
# when tools are IO-bound (HTTP calls, DB queries, etc.).
|
|
16
|
-
#
|
|
17
|
-
# Single-tool responses fall through to the standard sequential path via
|
|
18
|
-
# +super+, preserving all existing edge-case behaviour (Tool::Halt,
|
|
19
|
-
# forced_tool_choice, streaming, SuspendSignal, etc.).
|
|
20
|
-
#
|
|
21
|
-
# This class is used automatically when EventLoop mode is enabled
|
|
22
|
-
# ({Phronomy.configuration.event_loop}). It is not used for direct
|
|
23
|
-
# synchronous +invoke+ calls so that the streaming callback state remains
|
|
24
|
-
# single-threaded.
|
|
7
|
+
# AgentInvocation installs +on_tool_call_batch+ and intercepts the whole batch
|
|
8
|
+
# before this class dispatches anything. The direct-chat fallback dispatches
|
|
9
|
+
# the complete batch concurrently; process protection remains the responsibility
|
|
10
|
+
# of Runtime's bounded executors and queues.
|
|
25
11
|
# @api private
|
|
26
12
|
class ParallelToolChat < RubyLLM::Chat
|
|
27
|
-
|
|
28
|
-
# @param cancellation_token [Phronomy::Concurrency::CancellationToken, nil] token observed before each batch
|
|
29
|
-
# @param opts [Hash] remaining kwargs forwarded to RubyLLM::Chat
|
|
30
|
-
# @api private
|
|
31
|
-
def initialize(max_parallel_tools: 10, cancellation_token: nil, **opts)
|
|
13
|
+
def initialize(cancellation_token: nil, **opts)
|
|
32
14
|
super(**opts)
|
|
33
|
-
@max_parallel_tools = max_parallel_tools
|
|
34
15
|
@cancellation_token = cancellation_token
|
|
35
16
|
end
|
|
36
17
|
|
|
37
|
-
# Allows the owning agent to update the token between retries.
|
|
38
|
-
# @api private
|
|
39
18
|
attr_writer :cancellation_token
|
|
40
19
|
|
|
20
|
+
# Registers an Agent-owned batch interceptor. The callback must return
|
|
21
|
+
# quickly or raise ToolCallIntercepted; it must not execute Tool bodies.
|
|
22
|
+
def on_tool_call_batch(&block)
|
|
23
|
+
@on[:tool_call_batch] = block
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
|
|
41
27
|
private
|
|
42
28
|
|
|
43
|
-
# Overrides RubyLLM::Chat#handle_tool_calls to parallelise execution
|
|
44
|
-
# when multiple tool calls are present in a single LLM response.
|
|
45
|
-
#
|
|
46
|
-
# The method preserves the three-phase protocol of the original:
|
|
47
|
-
# 1. Pre-execution callbacks (+on_new_message+, +on_tool_call+) —
|
|
48
|
-
# sequential so that the Suspendable concern's approval hook can
|
|
49
|
-
# raise +SuspendSignal+ before any tool is executed.
|
|
50
|
-
# 2. Parallel tool execution — cooperative tools via Runtime.instance.spawn
|
|
51
|
-
# (respects the configured runtime backend), blocking_io tools via BlockingAdapterPool.
|
|
52
|
-
# 3. Post-execution callbacks and message recording — sequential,
|
|
53
|
-
# in the original tool-call order.
|
|
54
|
-
#
|
|
55
|
-
# @param response [RubyLLM::Message] the LLM response containing tool calls
|
|
56
|
-
# @yield streaming block forwarded to +complete+
|
|
57
|
-
# @api private
|
|
58
29
|
def handle_tool_calls(response, &block)
|
|
59
30
|
tool_calls = response.tool_calls.values
|
|
60
|
-
|
|
61
|
-
# Single tool: delegate to the parent implementation to preserve every
|
|
62
|
-
# edge case (forced_tool_choice, streaming, Halt, SuspendSignal…).
|
|
63
31
|
return super if tool_calls.size <= 1
|
|
64
32
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
33
|
+
if @on[:tool_call_batch]
|
|
34
|
+
tool_calls.each { @on[:new_message]&.call }
|
|
35
|
+
@on[:tool_call_batch].call(tool_calls)
|
|
36
|
+
return
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Direct ParallelToolChat fallback. Agent execution never reaches this
|
|
40
|
+
# branch because AgentInvocation installs the batch interceptor first.
|
|
68
41
|
tool_calls.each do |tool_call|
|
|
69
42
|
@on[:new_message]&.call
|
|
70
43
|
@on[:tool_call]&.call(tool_call)
|
|
71
44
|
end
|
|
72
45
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
#
|
|
79
|
-
# Both Phronomy::Task and BlockingAdapterPool::PendingOperation support
|
|
80
|
-
# #wait_result, so results are collected uniformly below.
|
|
81
|
-
ct = @cancellation_token
|
|
82
|
-
max = @max_parallel_tools
|
|
83
|
-
tool_results = tool_calls.each_slice(max).flat_map do |batch|
|
|
84
|
-
if ct&.cancelled?
|
|
85
|
-
raise Phronomy::CancellationError, "invocation cancelled before tool execution"
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
# Dispatch all tools in this batch via ToolExecutor (centralised routing).
|
|
89
|
-
dispatched = batch.map do |tc|
|
|
90
|
-
tool = tools[tc.name.to_sym]
|
|
91
|
-
unless tool
|
|
92
|
-
next {tool_call: tc, awaitable: nil, result: {
|
|
93
|
-
error: "Model tried to call unavailable tool `#{tc.name}`. " \
|
|
94
|
-
"Available tools: #{tools.keys.to_json}."
|
|
95
|
-
}}
|
|
96
|
-
end
|
|
46
|
+
cancellation_token = @cancellation_token
|
|
47
|
+
if cancellation_token&.cancelled?
|
|
48
|
+
raise Phronomy::CancellationError,
|
|
49
|
+
"invocation cancelled before tool execution"
|
|
50
|
+
end
|
|
97
51
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
52
|
+
dispatched = tool_calls.map do |tool_call|
|
|
53
|
+
tool = tools[tool_call.name.to_sym]
|
|
54
|
+
unless tool
|
|
55
|
+
next {
|
|
56
|
+
tool_call: tool_call,
|
|
57
|
+
awaitable: nil,
|
|
58
|
+
result: {
|
|
59
|
+
error: "Model tried to call unavailable tool `#{tool_call.name}`. " \
|
|
60
|
+
"Available tools: #{tools.keys.to_json}."
|
|
61
|
+
}
|
|
62
|
+
}
|
|
104
63
|
end
|
|
105
64
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
65
|
+
awaitable = Phronomy::Agent::ToolExecutor.call_async(
|
|
66
|
+
tool: tool,
|
|
67
|
+
args: tool_call.arguments,
|
|
68
|
+
cancellation_token: cancellation_token
|
|
69
|
+
)
|
|
70
|
+
{tool_call: tool_call, awaitable: awaitable, result: nil}
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
tool_results = dispatched.map do |item|
|
|
74
|
+
result = item[:awaitable] ? item[:awaitable].wait_result : item[:result]
|
|
75
|
+
{tool_call: item[:tool_call], result: result}
|
|
111
76
|
end
|
|
112
77
|
|
|
113
|
-
# Phase 3 — post-execution callbacks and message recording (sequential).
|
|
114
78
|
halt_result = nil
|
|
115
79
|
tool_results.each do |item|
|
|
116
80
|
result = item[:result]
|
|
117
81
|
@on[:tool_result]&.call(result)
|
|
118
82
|
tool_payload = result.is_a?(RubyLLM::Tool::Halt) ? result.content : result
|
|
119
83
|
content = content_like?(tool_payload) ? tool_payload : tool_payload.to_s
|
|
120
|
-
message = add_message(
|
|
84
|
+
message = add_message(
|
|
85
|
+
role: :tool,
|
|
86
|
+
content: content,
|
|
87
|
+
tool_call_id: item[:tool_call].id
|
|
88
|
+
)
|
|
121
89
|
@on[:end_message]&.call(message)
|
|
122
90
|
halt_result = result if result.is_a?(RubyLLM::Tool::Halt)
|
|
123
91
|
end
|
|
@@ -126,15 +94,12 @@ module Phronomy
|
|
|
126
94
|
halt_result || complete(&block)
|
|
127
95
|
end
|
|
128
96
|
|
|
129
|
-
# Overrides RubyLLM::Chat#execute_tool to forward the cancellation token
|
|
130
|
-
# explicitly and to route the call through {ToolExecutor} so that the
|
|
131
|
-
# execution_mode decision is made in a single place.
|
|
132
97
|
def execute_tool(tool_call)
|
|
133
98
|
tool = tools[tool_call.name.to_sym]
|
|
134
99
|
unless tool
|
|
135
100
|
return {
|
|
136
101
|
error: "Model tried to call unavailable tool `#{tool_call.name}`. " \
|
|
137
|
-
|
|
102
|
+
"Available tools: #{tools.keys.to_json}."
|
|
138
103
|
}
|
|
139
104
|
end
|
|
140
105
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
# Raised when an Application stream callback fails and
|
|
5
|
+
# +stream_callback_error_policy+ is +:fail_task+.
|
|
6
|
+
#
|
|
7
|
+
# This error represents delivery failure, not Agent, LLM, Tool, or FSM
|
|
8
|
+
# execution failure. The Agent result is retained in {#result}, and the
|
|
9
|
+
# original Application exception is available through both
|
|
10
|
+
# {#original_error} and Ruby's +#cause+ chain.
|
|
11
|
+
#
|
|
12
|
+
# @api public
|
|
13
|
+
class StreamCallbackError < Error
|
|
14
|
+
# @return [Symbol] stream event type handled by the failing callback
|
|
15
|
+
attr_reader :event_type
|
|
16
|
+
|
|
17
|
+
# @return [Hash, nil] successful or suspended Agent result
|
|
18
|
+
attr_reader :result
|
|
19
|
+
|
|
20
|
+
# @return [Exception] original Application callback exception
|
|
21
|
+
attr_reader :original_error
|
|
22
|
+
|
|
23
|
+
def initialize(event_type:, original_error:, result: nil)
|
|
24
|
+
@event_type = event_type
|
|
25
|
+
@original_error = original_error
|
|
26
|
+
@result = result
|
|
27
|
+
|
|
28
|
+
super(
|
|
29
|
+
"Stream callback failed while handling #{event_type.inspect}: " \
|
|
30
|
+
"#{original_error.class}: #{original_error.message}"
|
|
31
|
+
)
|
|
32
|
+
set_backtrace(original_error.backtrace)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -46,12 +46,21 @@ module Phronomy
|
|
|
46
46
|
scheduler = Phronomy::Runtime::FakeScheduler.new
|
|
47
47
|
scheduler.clock = clock if clock
|
|
48
48
|
runtime = Phronomy::Runtime.new(scheduler: scheduler)
|
|
49
|
-
original = Phronomy::Runtime.
|
|
50
|
-
Phronomy::Runtime.
|
|
49
|
+
original = Phronomy::Runtime.default_if_initialized_for_test
|
|
50
|
+
Phronomy::Runtime.replace_default_for_test(runtime)
|
|
51
51
|
begin
|
|
52
52
|
yield scheduler, clock
|
|
53
53
|
ensure
|
|
54
|
-
|
|
54
|
+
result = nil
|
|
55
|
+
begin
|
|
56
|
+
result = runtime.shutdown
|
|
57
|
+
ensure
|
|
58
|
+
Phronomy::Runtime.restore_default_for_test(original)
|
|
59
|
+
end
|
|
60
|
+
unless result&.cleanup_complete?
|
|
61
|
+
raise Phronomy::RuntimeShutdownError,
|
|
62
|
+
"Temporary test Runtime did not shut down completely"
|
|
63
|
+
end
|
|
55
64
|
end
|
|
56
65
|
end
|
|
57
66
|
end
|