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
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
|
|
5
|
+
module Phronomy
|
|
6
|
+
module Agent
|
|
7
|
+
# Mutable domain state for one Agent invocation.
|
|
8
|
+
#
|
|
9
|
+
# AgentInvocation interprets Agent-internal events. FSMSession owns the
|
|
10
|
+
# transition mechanics, while Agent::Base projects the terminal outcome to
|
|
11
|
+
# both the Application listener and the returned Task.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class AgentInvocation
|
|
15
|
+
TOOL_EVENT_TYPES = %i[
|
|
16
|
+
tool_authorized
|
|
17
|
+
tool_approval_required
|
|
18
|
+
tool_completed
|
|
19
|
+
tool_failed
|
|
20
|
+
tool_rejected
|
|
21
|
+
tool_cancelled
|
|
22
|
+
].freeze
|
|
23
|
+
|
|
24
|
+
LLM_EVENT_TYPES = %i[
|
|
25
|
+
llm_completed
|
|
26
|
+
llm_failed
|
|
27
|
+
].freeze
|
|
28
|
+
|
|
29
|
+
attr_accessor :input,
|
|
30
|
+
:messages,
|
|
31
|
+
:chat,
|
|
32
|
+
:output,
|
|
33
|
+
:usage,
|
|
34
|
+
:input_blocked,
|
|
35
|
+
:output_blocked,
|
|
36
|
+
:block_error,
|
|
37
|
+
:user_message_sent,
|
|
38
|
+
:event_listener,
|
|
39
|
+
:approval_request,
|
|
40
|
+
:rejected,
|
|
41
|
+
:error
|
|
42
|
+
|
|
43
|
+
attr_reader :id,
|
|
44
|
+
:agent,
|
|
45
|
+
:config,
|
|
46
|
+
:thread_id,
|
|
47
|
+
:approval_policy,
|
|
48
|
+
:approval_listener,
|
|
49
|
+
:pending_tool_calls,
|
|
50
|
+
:tool_invocations,
|
|
51
|
+
:session_id,
|
|
52
|
+
:phase,
|
|
53
|
+
:mode
|
|
54
|
+
|
|
55
|
+
def initialize(
|
|
56
|
+
agent:,
|
|
57
|
+
input:,
|
|
58
|
+
messages:,
|
|
59
|
+
config:,
|
|
60
|
+
approval_policy: nil,
|
|
61
|
+
approval_listener: nil,
|
|
62
|
+
event_listener: nil,
|
|
63
|
+
stream_listener: nil,
|
|
64
|
+
mode: nil,
|
|
65
|
+
id: nil
|
|
66
|
+
)
|
|
67
|
+
@agent = agent
|
|
68
|
+
@input = input
|
|
69
|
+
@messages = Array(messages)
|
|
70
|
+
@config = config
|
|
71
|
+
@thread_id = config[:thread_id]
|
|
72
|
+
@id = (id || config[:agent_invocation_id] || SecureRandom.uuid).to_s
|
|
73
|
+
invocation_context = config[:invocation_context]
|
|
74
|
+
invocation_policy = if invocation_context&.respond_to?(:approval_policy)
|
|
75
|
+
invocation_context.approval_policy
|
|
76
|
+
end
|
|
77
|
+
@approval_policy = invocation_policy || approval_policy
|
|
78
|
+
@approval_listener = approval_listener
|
|
79
|
+
@event_listener = event_listener || stream_listener
|
|
80
|
+
@mode = (mode || (stream_listener ? :stream : :invoke)).to_sym
|
|
81
|
+
|
|
82
|
+
@chat = nil
|
|
83
|
+
@output = nil
|
|
84
|
+
@usage = nil
|
|
85
|
+
@input_blocked = false
|
|
86
|
+
@output_blocked = false
|
|
87
|
+
@block_error = nil
|
|
88
|
+
@user_message_sent = false
|
|
89
|
+
@pending_tool_calls = []
|
|
90
|
+
@tool_invocations = []
|
|
91
|
+
@approval_request = nil
|
|
92
|
+
@rejected = false
|
|
93
|
+
@human_rejection = false
|
|
94
|
+
@approval_resume_in_progress = false
|
|
95
|
+
@pending_approval_resolution_ids = []
|
|
96
|
+
@error = nil
|
|
97
|
+
@session_id = nil
|
|
98
|
+
@phase = nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Compatibility aliases for existing internal callers.
|
|
102
|
+
def stream_listener
|
|
103
|
+
@event_listener
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def stream_listener=(listener)
|
|
107
|
+
@event_listener = listener
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def streaming?
|
|
111
|
+
@mode == :stream
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def set_graph_metadata(thread_id: nil, phase: nil)
|
|
115
|
+
@session_id = thread_id if thread_id
|
|
116
|
+
@phase = phase
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def pending_tool_calls=(calls)
|
|
120
|
+
@pending_tool_calls = Array(calls)
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def tool_invocations=(invocations)
|
|
124
|
+
@tool_invocations = Array(invocations)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def clear_tool_batch!
|
|
128
|
+
@pending_tool_calls = []
|
|
129
|
+
@tool_invocations = []
|
|
130
|
+
@approval_request = nil
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def handle_fsm_event(event)
|
|
134
|
+
if event.type == :llm_stream_chunk
|
|
135
|
+
deliver_event(
|
|
136
|
+
StreamEvent.new(
|
|
137
|
+
type: :token,
|
|
138
|
+
payload: {content: event.payload.fetch(:content)}
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
return true
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
if LLM_EVENT_TYPES.include?(event.type)
|
|
145
|
+
apply_llm_event(event)
|
|
146
|
+
return true
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
return false unless TOOL_EVENT_TYPES.include?(event.type)
|
|
150
|
+
|
|
151
|
+
invocation = tool_invocation(
|
|
152
|
+
event.payload&.fetch(:tool_invocation_id, nil)
|
|
153
|
+
)
|
|
154
|
+
return true unless invocation
|
|
155
|
+
|
|
156
|
+
@error ||= invocation.error if invocation.failed? || invocation.cancelled?
|
|
157
|
+
@rejected = true if invocation.rejected?
|
|
158
|
+
if @approval_resume_in_progress &&
|
|
159
|
+
@pending_approval_resolution_ids.delete(invocation.id)
|
|
160
|
+
if @pending_approval_resolution_ids.empty?
|
|
161
|
+
@approval_resume_in_progress = false
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
true
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Deprecated internal compatibility hook. FSMSession no longer calls
|
|
168
|
+
# this method; asynchronous results enter through explicit events.
|
|
169
|
+
def apply_fsm_action_result(result)
|
|
170
|
+
event_type =
|
|
171
|
+
if result.respond_to?(:error) &&
|
|
172
|
+
result.error &&
|
|
173
|
+
!result.error.is_a?(ToolCallIntercepted)
|
|
174
|
+
:llm_failed
|
|
175
|
+
else
|
|
176
|
+
:llm_completed
|
|
177
|
+
end
|
|
178
|
+
handle_fsm_event(
|
|
179
|
+
Phronomy::Event.new(
|
|
180
|
+
type: event_type,
|
|
181
|
+
target_id: @id,
|
|
182
|
+
payload: result
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
self
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def accept_tool_calls!(tool_calls)
|
|
189
|
+
@user_message_sent = true
|
|
190
|
+
@pending_tool_calls = Array(tool_calls)
|
|
191
|
+
@messages = @chat.messages
|
|
192
|
+
@pending_tool_calls.each do |tool_call|
|
|
193
|
+
deliver_event(
|
|
194
|
+
StreamEvent.new(
|
|
195
|
+
type: :tool_call,
|
|
196
|
+
payload: {tool_call: tool_call}
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
self
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def apply_llm_response!(response)
|
|
204
|
+
unless response
|
|
205
|
+
raise Phronomy::Error, "LLM operation completed without a response"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
@user_message_sent = true
|
|
209
|
+
@output = response.content
|
|
210
|
+
@usage = Phronomy::TokenUsage.from_tokens(response.tokens)
|
|
211
|
+
@messages = @chat.messages
|
|
212
|
+
@pending_tool_calls = []
|
|
213
|
+
self
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def tool_invocation(id)
|
|
217
|
+
@tool_invocations.find { |invocation| invocation.id == id.to_s }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def merge_config!(values)
|
|
221
|
+
@config.merge!(values) unless values.empty?
|
|
222
|
+
self
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def approval_context
|
|
226
|
+
return @config[:approval_context] if @config[:approval_context]
|
|
227
|
+
|
|
228
|
+
context = @config[:invocation_context]
|
|
229
|
+
return {} unless context
|
|
230
|
+
|
|
231
|
+
%i[
|
|
232
|
+
thread_id session_id user_id token_budget
|
|
233
|
+
task_id parent_task_id
|
|
234
|
+
].each_with_object({}) do |name, result|
|
|
235
|
+
if context.respond_to?(name)
|
|
236
|
+
result[name] = context.public_send(name)
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
def begin_approval_resume!(approved:)
|
|
242
|
+
@human_rejection = !approved
|
|
243
|
+
@pending_approval_resolution_ids = @tool_invocations
|
|
244
|
+
.select(&:awaiting_approval?)
|
|
245
|
+
.map(&:id)
|
|
246
|
+
@approval_resume_in_progress =
|
|
247
|
+
!@pending_approval_resolution_ids.empty?
|
|
248
|
+
self
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def prepare_approval_request!
|
|
252
|
+
@approval_request = ToolApprovalRequest.build(self)
|
|
253
|
+
self
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def record_tool_results!
|
|
257
|
+
@tool_invocations.each do |invocation|
|
|
258
|
+
@chat.add_message(
|
|
259
|
+
role: :tool,
|
|
260
|
+
content: invocation.result.to_s,
|
|
261
|
+
tool_call_id: invocation.tool_call_id
|
|
262
|
+
)
|
|
263
|
+
deliver_event(
|
|
264
|
+
StreamEvent.new(
|
|
265
|
+
type: :tool_result,
|
|
266
|
+
payload: {
|
|
267
|
+
tool_call_id: invocation.tool_call_id,
|
|
268
|
+
tool_name: invocation.tool_name,
|
|
269
|
+
tool_result: invocation.result
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
)
|
|
273
|
+
end
|
|
274
|
+
@messages = @chat.messages
|
|
275
|
+
clear_tool_batch!
|
|
276
|
+
self
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def input_passed?
|
|
280
|
+
!@input_blocked
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def input_blocked?
|
|
284
|
+
@input_blocked
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def output_passed?
|
|
288
|
+
!@output_blocked
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def output_blocked?
|
|
292
|
+
@output_blocked
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def tool_call_pending?
|
|
296
|
+
!@pending_tool_calls.empty?
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def preflight_complete?
|
|
300
|
+
!@tool_invocations.empty? &&
|
|
301
|
+
@tool_invocations.all?(&:preflight_settled?)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
def approval_required?
|
|
305
|
+
!@human_rejection &&
|
|
306
|
+
!@approval_resume_in_progress &&
|
|
307
|
+
preflight_complete? &&
|
|
308
|
+
@tool_invocations.any?(&:awaiting_approval?)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def ready_to_dispatch?
|
|
312
|
+
!@approval_resume_in_progress &&
|
|
313
|
+
preflight_complete? &&
|
|
314
|
+
@tool_invocations.none?(&:awaiting_approval?) &&
|
|
315
|
+
@tool_invocations.none?(&:rejected?) &&
|
|
316
|
+
@tool_invocations.none?(&:failed?) &&
|
|
317
|
+
@tool_invocations.none?(&:cancelled?) &&
|
|
318
|
+
@tool_invocations.any?(&:authorized?)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def tool_batch_terminal?
|
|
322
|
+
!@tool_invocations.empty? &&
|
|
323
|
+
@tool_invocations.all?(&:terminal?)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
def tool_batch_failed?
|
|
327
|
+
return false if @human_rejection || @approval_resume_in_progress
|
|
328
|
+
|
|
329
|
+
no_rejection = @tool_invocations.none?(&:rejected?)
|
|
330
|
+
preflight_failure =
|
|
331
|
+
preflight_complete? &&
|
|
332
|
+
@tool_invocations.any? do |invocation|
|
|
333
|
+
invocation.failed? || invocation.cancelled?
|
|
334
|
+
end
|
|
335
|
+
terminal_failure =
|
|
336
|
+
tool_batch_terminal? &&
|
|
337
|
+
@tool_invocations.any? do |invocation|
|
|
338
|
+
invocation.failed? || invocation.cancelled?
|
|
339
|
+
end
|
|
340
|
+
no_rejection && (preflight_failure || terminal_failure)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def tool_batch_rejected?
|
|
344
|
+
return false unless @tool_invocations.any?(&:rejected?)
|
|
345
|
+
|
|
346
|
+
@human_rejection ? tool_batch_terminal? : preflight_complete?
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def tool_batch_completed?
|
|
350
|
+
tool_batch_terminal? &&
|
|
351
|
+
@tool_invocations.all?(&:execution_completed?)
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
private
|
|
355
|
+
|
|
356
|
+
def apply_llm_event(event)
|
|
357
|
+
result = event.payload
|
|
358
|
+
unless result.is_a?(LLMOperationResult)
|
|
359
|
+
raise Phronomy::Error,
|
|
360
|
+
"Expected LLMOperationResult, got #{result.class}"
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
if event.type == :llm_failed
|
|
364
|
+
@error = result.error ||
|
|
365
|
+
Phronomy::Error.new("LLM operation failed without an error")
|
|
366
|
+
return
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
if result.error
|
|
370
|
+
if result.error.is_a?(ToolCallIntercepted)
|
|
371
|
+
accept_tool_calls!(result.error.tool_calls)
|
|
372
|
+
else
|
|
373
|
+
@error = result.error
|
|
374
|
+
end
|
|
375
|
+
else
|
|
376
|
+
apply_llm_response!(result.response)
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
def deliver_event(event)
|
|
381
|
+
@event_listener&.call(event)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# In-process registry for suspended AgentInvocation aggregates.
|
|
6
|
+
#
|
|
7
|
+
# The registry is the single in-process source for pending Human approval.
|
|
8
|
+
# Cross-process persistence remains outside the scope of this implementation.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module AgentInvocationRegistry
|
|
12
|
+
Entry = Struct.new(:invocation, :approval_request)
|
|
13
|
+
|
|
14
|
+
@entries = {}
|
|
15
|
+
@approval_index = {}
|
|
16
|
+
@mutex = Mutex.new
|
|
17
|
+
|
|
18
|
+
def self.store_suspended(invocation, approval_request)
|
|
19
|
+
@mutex.synchronize do
|
|
20
|
+
invocation_id = invocation.id
|
|
21
|
+
request_id = approval_request.id
|
|
22
|
+
if @entries.key?(invocation_id) || @approval_index.key?(request_id)
|
|
23
|
+
raise Phronomy::Error,
|
|
24
|
+
"Suspended AgentInvocation is already registered: #{invocation_id}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@entries[invocation_id] = Entry.new(
|
|
28
|
+
invocation: invocation,
|
|
29
|
+
approval_request: approval_request
|
|
30
|
+
)
|
|
31
|
+
@approval_index[request_id] = invocation_id
|
|
32
|
+
end
|
|
33
|
+
approval_request
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Atomically removes and returns one pending approval aggregate.
|
|
37
|
+
# Duplicate approval commands therefore cannot execute the Tool twice.
|
|
38
|
+
def self.consume_approval(agent_invocation_id, approval_request_id)
|
|
39
|
+
@mutex.synchronize do
|
|
40
|
+
indexed_invocation_id = @approval_index[approval_request_id.to_s]
|
|
41
|
+
return nil unless indexed_invocation_id == agent_invocation_id.to_s
|
|
42
|
+
|
|
43
|
+
entry = @entries.delete(indexed_invocation_id)
|
|
44
|
+
return nil unless entry
|
|
45
|
+
|
|
46
|
+
@approval_index.delete(entry.approval_request.id)
|
|
47
|
+
entry
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def self.lookup(agent_invocation_id)
|
|
52
|
+
@mutex.synchronize { @entries[agent_invocation_id.to_s] }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.exists?(agent_invocation_id)
|
|
56
|
+
@mutex.synchronize { @entries.key?(agent_invocation_id.to_s) }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def self.remove_terminal(agent_invocation_id)
|
|
60
|
+
@mutex.synchronize do
|
|
61
|
+
entry = @entries.delete(agent_invocation_id.to_s)
|
|
62
|
+
@approval_index.delete(entry.approval_request.id) if entry
|
|
63
|
+
entry
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def self.clear!
|
|
68
|
+
@mutex.synchronize do
|
|
69
|
+
@entries.clear
|
|
70
|
+
@approval_index.clear
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|