phronomy 0.14.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 +65 -0
- data/README.md +236 -57
- 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/examples/workflows/agent_event_mapping.rb +104 -0
- data/examples/workflows/generic_task_event_mapping.rb +58 -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 +500 -411
- data/lib/phronomy/agent/context/capability/base.rb +51 -119
- data/lib/phronomy/agent/llm_operation_result.rb +23 -0
- data/lib/phronomy/agent/phase_machine_builder.rb +75 -137
- 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 +42 -6
- data/lib/phronomy/engine/event_loop.rb +269 -112
- data/lib/phronomy/engine/fsm_session.rb +180 -142
- 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 +2 -0
- data/lib/phronomy/multi_agent/parallel_tool_chat.rb +54 -89
- data/lib/phronomy/stream_callback_error.rb +35 -0
- data/lib/phronomy/tools/mcp.rb +25 -0
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/phase_machine_builder.rb +129 -186
- data/lib/phronomy/workflow.rb +122 -261
- data/lib/phronomy/workflow_context.rb +54 -102
- data/lib/phronomy/workflow_runner.rb +238 -300
- data/lib/phronomy.rb +6 -4
- data/scripts/check_readme_runnable.rb +4 -1
- metadata +18 -7
- 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 -352
- data/lib/phronomy/agent/suspended_session_registry.rb +0 -54
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "securerandom"
|
|
4
|
-
|
|
5
|
-
module Phronomy
|
|
6
|
-
module Agent
|
|
7
|
-
# Factory that builds a Phronomy::FSMSession configured for a single
|
|
8
|
-
# Agent#invoke execution.
|
|
9
|
-
#
|
|
10
|
-
# This is the Agent counterpart to WorkflowRunner — it assembles the
|
|
11
|
-
# FSMSession with the correct phase machine class, entry actions, and
|
|
12
|
-
# context, then hands it to EventLoop for execution.
|
|
13
|
-
#
|
|
14
|
-
# == Usage
|
|
15
|
-
#
|
|
16
|
-
# session = Agent::InvocationSession.build(
|
|
17
|
-
# agent: my_agent,
|
|
18
|
-
# input: "What is Ruby?",
|
|
19
|
-
# messages: [],
|
|
20
|
-
# config: { thread_id: "t-1" }
|
|
21
|
-
# )
|
|
22
|
-
# completion_queue = Phronomy::Runtime.instance.event_loop.register(session)
|
|
23
|
-
# ctx = completion_queue.pop
|
|
24
|
-
#
|
|
25
|
-
# == Streaming mode
|
|
26
|
-
#
|
|
27
|
-
# Pass +mode: :stream+ and an +on_event:+ block to receive token/tool events.
|
|
28
|
-
# The state graph is identical; only the +:calling_llm+ entry action differs.
|
|
29
|
-
#
|
|
30
|
-
# @api private
|
|
31
|
-
class InvocationSession
|
|
32
|
-
# States that have an automatic transition after their action completes.
|
|
33
|
-
AUTO_STATE_SET = {
|
|
34
|
-
idle: true,
|
|
35
|
-
filtering_input: true,
|
|
36
|
-
building_context: true,
|
|
37
|
-
calling_llm: true,
|
|
38
|
-
executing_tool: true,
|
|
39
|
-
output_filtering: true
|
|
40
|
-
}.freeze
|
|
41
|
-
|
|
42
|
-
# All declared action states (terminals excluded).
|
|
43
|
-
DECLARED_STATES = %i[
|
|
44
|
-
idle filtering_input building_context calling_llm
|
|
45
|
-
executing_tool awaiting_approval output_filtering
|
|
46
|
-
completed blocked
|
|
47
|
-
].freeze
|
|
48
|
-
|
|
49
|
-
# Builds a Phronomy::FSMSession for the given agent invocation.
|
|
50
|
-
#
|
|
51
|
-
# @param agent [Phronomy::Agent::Base]
|
|
52
|
-
# @param input [String, Hash]
|
|
53
|
-
# @param messages [Array]
|
|
54
|
-
# @param config [Hash]
|
|
55
|
-
# @param mode [:invoke, :stream]
|
|
56
|
-
# @param on_event [Proc, nil] stream event callback (stream mode only)
|
|
57
|
-
# @return [Phronomy::FSMSession]
|
|
58
|
-
# @api private
|
|
59
|
-
def self.build(agent:, input:, messages:, config:, mode: :invoke, on_event: nil,
|
|
60
|
-
runtime: Phronomy::Runtime.instance)
|
|
61
|
-
ctx = Agent::InvocationContext.new(
|
|
62
|
-
agent: agent,
|
|
63
|
-
input: input,
|
|
64
|
-
messages: messages,
|
|
65
|
-
config: config
|
|
66
|
-
)
|
|
67
|
-
|
|
68
|
-
actions = (mode == :stream && on_event) ?
|
|
69
|
-
build_stream_entry_actions(agent, on_event) :
|
|
70
|
-
build_entry_actions(agent)
|
|
71
|
-
|
|
72
|
-
# Calculate recursion_limit for the FSM:
|
|
73
|
-
# Base states: idle→filtering_input→building_context→calling_llm→
|
|
74
|
-
# output_filtering→completed = 6 transitions
|
|
75
|
-
# Each tool call loop: calling_llm→executing_tool→calling_llm = 2 transitions
|
|
76
|
-
# Safety margin: +4
|
|
77
|
-
iterations = agent.class.max_iterations || 10
|
|
78
|
-
fsm_recursion_limit = 6 + (iterations * 2) + 4
|
|
79
|
-
|
|
80
|
-
# Entry actions are registered as after_transition callbacks in the
|
|
81
|
-
# phase machine class. Pass empty hash to FSMSession (it uses @entry_actions
|
|
82
|
-
# only for the entry_point state, which has no action for :idle).
|
|
83
|
-
phase_machine = Agent::PhaseMachineBuilder.new(entry_actions: actions).build
|
|
84
|
-
session_id = config[:thread_id] || SecureRandom.uuid
|
|
85
|
-
|
|
86
|
-
Phronomy::FSMSession.new(
|
|
87
|
-
id: session_id,
|
|
88
|
-
context: ctx,
|
|
89
|
-
entry_point: :idle,
|
|
90
|
-
phase_machine_class: phase_machine,
|
|
91
|
-
entry_actions: {},
|
|
92
|
-
auto_state_set: AUTO_STATE_SET,
|
|
93
|
-
declared_states: DECLARED_STATES,
|
|
94
|
-
wait_state_names: %i[awaiting_approval],
|
|
95
|
-
external_events: {
|
|
96
|
-
approve: [{from: :awaiting_approval, to: :executing_tool, guard: nil}],
|
|
97
|
-
reject: [{from: :awaiting_approval, to: :blocked, guard: nil}]
|
|
98
|
-
},
|
|
99
|
-
recursion_limit: fsm_recursion_limit,
|
|
100
|
-
event_loop: runtime.event_loop,
|
|
101
|
-
timer_queue_provider: -> { runtime.timer_queue }
|
|
102
|
-
)
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
# Builds a FSMSession that resumes an existing InvocationContext
|
|
106
|
-
# from a wait state (e.g. :awaiting_approval) using an external event.
|
|
107
|
-
#
|
|
108
|
-
# @param agent [Phronomy::Agent::Base]
|
|
109
|
-
# @param context [Phronomy::Agent::InvocationContext] suspended context
|
|
110
|
-
# @param resume_event [Symbol] e.g. :approve or :reject
|
|
111
|
-
# @param resume_phase [Symbol] the wait state to resume from
|
|
112
|
-
# @return [Phronomy::FSMSession]
|
|
113
|
-
# @api private
|
|
114
|
-
def self.build_for_resume(agent:, context:, resume_event:, resume_phase:,
|
|
115
|
-
runtime: Phronomy::Runtime.instance)
|
|
116
|
-
actions = build_entry_actions(agent)
|
|
117
|
-
phase_machine = Agent::PhaseMachineBuilder.new(entry_actions: actions).build
|
|
118
|
-
|
|
119
|
-
iterations = agent.class.max_iterations || 10
|
|
120
|
-
fsm_recursion_limit = 6 + (iterations * 2) + 4
|
|
121
|
-
|
|
122
|
-
Phronomy::FSMSession.new(
|
|
123
|
-
id: context.session_id || SecureRandom.uuid,
|
|
124
|
-
context: context,
|
|
125
|
-
entry_point: :idle,
|
|
126
|
-
phase_machine_class: phase_machine,
|
|
127
|
-
entry_actions: {},
|
|
128
|
-
auto_state_set: AUTO_STATE_SET,
|
|
129
|
-
declared_states: DECLARED_STATES,
|
|
130
|
-
wait_state_names: %i[awaiting_approval],
|
|
131
|
-
external_events: {
|
|
132
|
-
approve: [{from: :awaiting_approval, to: :executing_tool, guard: nil}],
|
|
133
|
-
reject: [{from: :awaiting_approval, to: :blocked, guard: nil}]
|
|
134
|
-
},
|
|
135
|
-
recursion_limit: fsm_recursion_limit,
|
|
136
|
-
event_loop: runtime.event_loop,
|
|
137
|
-
timer_queue_provider: -> { runtime.timer_queue },
|
|
138
|
-
resume_event: resume_event,
|
|
139
|
-
resume_phase: resume_phase
|
|
140
|
-
)
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
# ---------------------------------------------------------------------------
|
|
144
|
-
# Entry action builders
|
|
145
|
-
# ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
# @api private
|
|
148
|
-
def self.build_entry_actions(agent)
|
|
149
|
-
{
|
|
150
|
-
# :idle has no action — FSMSession auto-transitions to :filtering_input
|
|
151
|
-
filtering_input: [method(:filtering_input_action).curry.call(agent)],
|
|
152
|
-
building_context: [method(:building_context_action).curry.call(agent)],
|
|
153
|
-
calling_llm: [method(:calling_llm_action).curry.call(agent)],
|
|
154
|
-
executing_tool: [method(:executing_tool_action).curry.call(agent)],
|
|
155
|
-
output_filtering: [method(:output_filtering_action).curry.call(agent)]
|
|
156
|
-
}
|
|
157
|
-
end
|
|
158
|
-
private_class_method :build_entry_actions
|
|
159
|
-
|
|
160
|
-
# @api private
|
|
161
|
-
def self.build_stream_entry_actions(agent, on_event)
|
|
162
|
-
build_entry_actions(agent).merge(
|
|
163
|
-
calling_llm: [method(:calling_llm_stream_action).curry.call(agent, on_event)]
|
|
164
|
-
)
|
|
165
|
-
end
|
|
166
|
-
private_class_method :build_stream_entry_actions
|
|
167
|
-
|
|
168
|
-
# ----------------------------------------------------------------
|
|
169
|
-
# Individual entry action implementations
|
|
170
|
-
# ----------------------------------------------------------------
|
|
171
|
-
|
|
172
|
-
def self.filtering_input_action(agent, ctx)
|
|
173
|
-
begin
|
|
174
|
-
ctx.input = agent.send(:run_input_filters!, ctx.input)
|
|
175
|
-
rescue Phronomy::FilterBlockError => e
|
|
176
|
-
ctx.input_blocked = true
|
|
177
|
-
ctx.block_error = e
|
|
178
|
-
end
|
|
179
|
-
ctx
|
|
180
|
-
end
|
|
181
|
-
private_class_method :filtering_input_action
|
|
182
|
-
|
|
183
|
-
def self.building_context_action(agent, ctx)
|
|
184
|
-
ctx.chat = agent.send(:build_chat)
|
|
185
|
-
context = agent.send(
|
|
186
|
-
:build_context,
|
|
187
|
-
ctx.input,
|
|
188
|
-
messages: ctx.messages,
|
|
189
|
-
thread_id: ctx.thread_id,
|
|
190
|
-
config: ctx.config,
|
|
191
|
-
budget: agent.send(:build_token_budget),
|
|
192
|
-
instruction: agent.send(:build_instructions, ctx.input),
|
|
193
|
-
tools: agent.class.tools + agent.send(:_handoff_tools)
|
|
194
|
-
)
|
|
195
|
-
agent.send(:_apply_context_to_chat, ctx.chat, context)
|
|
196
|
-
# Run before-completion hooks (e.g. memory injection) once per invocation.
|
|
197
|
-
agent.send(:run_before_completion_hooks!, ctx.chat, ctx.config)
|
|
198
|
-
# Register the tool-call interceptor so every tool call routes through
|
|
199
|
-
# :executing_tool in the FSM instead of executing inside RubyLLM's loop.
|
|
200
|
-
ctx.chat.on_tool_call do |tool_call|
|
|
201
|
-
raise Phronomy::Agent::ToolCallIntercepted.new(tool_call)
|
|
202
|
-
end
|
|
203
|
-
ctx
|
|
204
|
-
end
|
|
205
|
-
private_class_method :building_context_action
|
|
206
|
-
|
|
207
|
-
def self.calling_llm_action(agent, ctx)
|
|
208
|
-
# Returns a Task.deferred — no extra OS thread is created.
|
|
209
|
-
# The BlockingAdapterPool worker thread completes the LLM call and
|
|
210
|
-
# resolves result_task via on_complete, which then triggers the
|
|
211
|
-
# FSMSession's dispatch_task_in_event_loop on_complete callback to
|
|
212
|
-
# post :action_completed back to the EventLoop.
|
|
213
|
-
user_message = ctx.user_message_sent ? nil : agent.send(:extract_message, ctx.input)
|
|
214
|
-
agent.send(:check_cancellation!, ctx.config, "invocation cancelled before LLM call")
|
|
215
|
-
adapter = Phronomy.configuration.llm_adapter
|
|
216
|
-
op = adapter.complete_async(ctx.chat, user_message, config: ctx.config)
|
|
217
|
-
result_task = Phronomy::Task.deferred(name: "agent-llm:#{ctx.thread_id}")
|
|
218
|
-
op.on_complete do |response, error|
|
|
219
|
-
if error.is_a?(Phronomy::Agent::ToolCallIntercepted)
|
|
220
|
-
ctx.user_message_sent = true
|
|
221
|
-
ctx.pending_tool_call = error.tool_call
|
|
222
|
-
ctx.tool_call_pending = true
|
|
223
|
-
ctx.messages = ctx.chat.messages
|
|
224
|
-
result_task.backend.unblock(ctx, nil)
|
|
225
|
-
result_task.transition!(:completed, value: ctx)
|
|
226
|
-
elsif error
|
|
227
|
-
result_task.backend.unblock(nil, error)
|
|
228
|
-
result_task.transition!(:failed, error: error)
|
|
229
|
-
else
|
|
230
|
-
ctx.user_message_sent = true
|
|
231
|
-
ctx.output = response.content
|
|
232
|
-
ctx.usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
233
|
-
ctx.messages = ctx.chat.messages
|
|
234
|
-
ctx.tool_call_pending = false
|
|
235
|
-
result_task.backend.unblock(ctx, nil)
|
|
236
|
-
result_task.transition!(:completed, value: ctx)
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
result_task
|
|
240
|
-
end
|
|
241
|
-
private_class_method :calling_llm_action
|
|
242
|
-
|
|
243
|
-
def self.calling_llm_stream_action(agent, on_event, ctx)
|
|
244
|
-
user_message = ctx.user_message_sent ? nil : agent.send(:extract_message, ctx.input)
|
|
245
|
-
# Streaming requires a background thread because chunk_queue.pop is a
|
|
246
|
-
# blocking drain loop that must not run on the EventLoop thread.
|
|
247
|
-
# The on_complete pattern used in calling_llm_action cannot be applied
|
|
248
|
-
# here because tokens must be delivered incrementally via on_event
|
|
249
|
-
# before the final response arrives. This spawn is therefore
|
|
250
|
-
# intentional and classified under ADR-010 Rule 2 (blocking loop).
|
|
251
|
-
Phronomy::Runtime.instance.spawn(name: "agent-llm-stream:#{ctx.thread_id}") do
|
|
252
|
-
adapter = Phronomy.configuration.llm_adapter
|
|
253
|
-
chunk_queue = Phronomy::Concurrency::AsyncQueue.new(
|
|
254
|
-
max_size: Phronomy.configuration.stream_queue_max_size
|
|
255
|
-
)
|
|
256
|
-
pending = adapter.stream_async(
|
|
257
|
-
ctx.chat, user_message,
|
|
258
|
-
config: ctx.config,
|
|
259
|
-
enqueue_to: chunk_queue
|
|
260
|
-
)
|
|
261
|
-
loop do
|
|
262
|
-
chunk = chunk_queue.pop
|
|
263
|
-
break if chunk.nil?
|
|
264
|
-
on_event.call(Phronomy::Agent::StreamEvent.new(
|
|
265
|
-
type: :token, payload: {content: chunk.content}
|
|
266
|
-
))
|
|
267
|
-
end
|
|
268
|
-
response = pending.blocking_wait
|
|
269
|
-
ctx.user_message_sent = true
|
|
270
|
-
ctx.output = response.content
|
|
271
|
-
ctx.usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
272
|
-
ctx.messages = ctx.chat.messages
|
|
273
|
-
ctx.tool_call_pending = false
|
|
274
|
-
ctx
|
|
275
|
-
end
|
|
276
|
-
end
|
|
277
|
-
private_class_method :calling_llm_stream_action
|
|
278
|
-
|
|
279
|
-
def self.executing_tool_action(agent, ctx)
|
|
280
|
-
tc = ctx.pending_tool_call
|
|
281
|
-
tool_instance = ctx.chat.tools[tc.name.to_sym]
|
|
282
|
-
|
|
283
|
-
unless tool_instance
|
|
284
|
-
# Tool not found — inject an error result and continue the LLM loop.
|
|
285
|
-
ctx.chat.add_message(
|
|
286
|
-
role: :tool,
|
|
287
|
-
content: "Tool not found.",
|
|
288
|
-
tool_call_id: tc.id
|
|
289
|
-
)
|
|
290
|
-
ctx.pending_tool_call = nil
|
|
291
|
-
ctx.tool_call_pending = false
|
|
292
|
-
ctx.approval_required = false
|
|
293
|
-
return ctx
|
|
294
|
-
end
|
|
295
|
-
|
|
296
|
-
if tool_instance.requires_approval && !ctx.sync_approval_handler
|
|
297
|
-
if ctx.approved
|
|
298
|
-
# Human approved via Agent.approve — execute the tool and continue.
|
|
299
|
-
ctx.approved = false # consume the approval flag
|
|
300
|
-
else
|
|
301
|
-
# No sync handler and not yet approved — suspend for HITL.
|
|
302
|
-
ctx.approval_required = true
|
|
303
|
-
return ctx
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# Dispatch the tool off the EventLoop thread via ToolExecutor, which
|
|
308
|
-
# routes based on the tool's execution_mode class attribute:
|
|
309
|
-
# :blocking_io (default) → BlockingAdapterPool (bounded thread pool)
|
|
310
|
-
# :cooperative → Runtime.instance.spawn (scheduler task)
|
|
311
|
-
# Wrap the awaitable in Task.deferred so FSMSession recognises it as
|
|
312
|
-
# an async action and sets async_pending = true.
|
|
313
|
-
tc_id = tc.id
|
|
314
|
-
tc_args = tc.arguments
|
|
315
|
-
tc_name = tc.name
|
|
316
|
-
ct = ctx.config[:cancellation_token]
|
|
317
|
-
awaitable = tool_instance.call_async(tc_args, cancellation_token: ct, config: ctx.config)
|
|
318
|
-
result_task = Phronomy::Task.deferred(name: "tool-exec:#{tc_name}")
|
|
319
|
-
awaitable.on_complete do |result, error|
|
|
320
|
-
if error
|
|
321
|
-
result_task.backend.unblock(nil, error)
|
|
322
|
-
result_task.transition!(:failed, error: error)
|
|
323
|
-
else
|
|
324
|
-
ctx.chat.add_message(
|
|
325
|
-
role: :tool,
|
|
326
|
-
content: result.to_s,
|
|
327
|
-
tool_call_id: tc_id
|
|
328
|
-
)
|
|
329
|
-
ctx.pending_tool_call = nil
|
|
330
|
-
ctx.tool_call_pending = false
|
|
331
|
-
ctx.approval_required = false
|
|
332
|
-
result_task.backend.unblock(ctx, nil)
|
|
333
|
-
result_task.transition!(:completed, value: ctx)
|
|
334
|
-
end
|
|
335
|
-
end
|
|
336
|
-
result_task
|
|
337
|
-
end
|
|
338
|
-
private_class_method :executing_tool_action
|
|
339
|
-
|
|
340
|
-
def self.output_filtering_action(agent, ctx)
|
|
341
|
-
begin
|
|
342
|
-
ctx.output = agent.send(:run_output_filters!, ctx.output)
|
|
343
|
-
rescue Phronomy::FilterBlockError => e
|
|
344
|
-
ctx.output_blocked = true
|
|
345
|
-
ctx.block_error = e
|
|
346
|
-
end
|
|
347
|
-
ctx
|
|
348
|
-
end
|
|
349
|
-
private_class_method :output_filtering_action
|
|
350
|
-
end
|
|
351
|
-
end
|
|
352
|
-
end
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Phronomy
|
|
4
|
-
module Agent
|
|
5
|
-
# In-process registry for Agent invocations suspended at :awaiting_approval.
|
|
6
|
-
#
|
|
7
|
-
# When an agent invocation halts waiting for human approval, the
|
|
8
|
-
# InvocationContext is stored here keyed by session_id.
|
|
9
|
-
# Agent::Base.approve / Agent::Base.reject look up and remove the context
|
|
10
|
-
# to build a resume session.
|
|
11
|
-
#
|
|
12
|
-
# Thread-safe. Each process has one shared instance via module methods.
|
|
13
|
-
# Cross-process persistence is out of scope (future SessionStore feature).
|
|
14
|
-
#
|
|
15
|
-
# @api private
|
|
16
|
-
module SuspendedSessionRegistry
|
|
17
|
-
@sessions = {}
|
|
18
|
-
@mutex = Mutex.new
|
|
19
|
-
|
|
20
|
-
# Stores a suspended context under the given session_id.
|
|
21
|
-
# @param session_id [String]
|
|
22
|
-
# @param ctx [Phronomy::Agent::InvocationContext]
|
|
23
|
-
# @return [void]
|
|
24
|
-
# @api private
|
|
25
|
-
def self.store(session_id, ctx)
|
|
26
|
-
@mutex.synchronize { @sessions[session_id] = ctx }
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Retrieves and removes the suspended context for session_id.
|
|
30
|
-
# Returns nil when no matching session exists.
|
|
31
|
-
# @param session_id [String]
|
|
32
|
-
# @return [Phronomy::Agent::InvocationContext, nil]
|
|
33
|
-
# @api private
|
|
34
|
-
def self.fetch(session_id)
|
|
35
|
-
@mutex.synchronize { @sessions.delete(session_id) }
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Returns true when a session is suspended under the given id.
|
|
39
|
-
# @param session_id [String]
|
|
40
|
-
# @return [Boolean]
|
|
41
|
-
# @api private
|
|
42
|
-
def self.exists?(session_id)
|
|
43
|
-
@mutex.synchronize { @sessions.key?(session_id) }
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
# Clears all suspended sessions. Intended for test teardown only.
|
|
47
|
-
# @return [void]
|
|
48
|
-
# @api private
|
|
49
|
-
def self.clear!
|
|
50
|
-
@mutex.synchronize { @sessions.clear }
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
end
|
|
54
|
-
end
|