phronomy 0.28.0 → 0.29.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/README.md +20 -5
- data/docs/application-recipes.md +229 -0
- data/docs/architecture/dx05-agent-scope.md +119 -0
- data/docs/architecture/dx05-workflow-task-state.md +134 -0
- data/docs/architecture/error-api-review.md +245 -0
- data/docs/failure-reason-codes.md +45 -0
- data/docs/features.md +4 -1
- data/docs/getting-started.md +44 -43
- data/docs/ordered-event-delivery.md +54 -0
- data/docs/workflow-completion.md +92 -0
- data/gemfiles/http_minimum.gemfile +5 -0
- data/lib/phronomy/agent/agent_not_found_error.rb +16 -0
- data/lib/phronomy/agent/agent_unloaded_error.rb +14 -0
- data/lib/phronomy/agent/api/agent.rb +13 -0
- data/lib/phronomy/agent/async_event_api.rb +111 -87
- data/lib/phronomy/agent/base.rb +74 -43
- data/lib/phronomy/agent/before_llm_input.rb +7 -1
- data/lib/phronomy/agent/execution/exact_execution.rb +11 -9
- data/lib/phronomy/agent/execution/execution_coordinator.rb +16 -13
- data/lib/phronomy/agent/execution/execution_outcome_committer.rb +1 -1
- data/lib/phronomy/agent/execution/initial_preparation.rb +1 -4
- data/lib/phronomy/agent/execution/runtime_record_encoder.rb +1 -4
- data/lib/phronomy/agent/filterable.rb +21 -15
- data/lib/phronomy/agent/lifecycle/lifetime_api.rb +76 -0
- data/lib/phronomy/agent/lifecycle/operation_lease.rb +33 -0
- data/lib/phronomy/agent/lifecycle/operation_result.rb +53 -0
- data/lib/phronomy/agent/lifecycle/ownership_registry.rb +99 -8
- data/lib/phronomy/agent/lifecycle/scope.rb +198 -0
- data/lib/phronomy/agent/lifecycle/scope_cleanup_error.rb +19 -0
- data/lib/phronomy/agent/lifecycle/unload_transition.rb +78 -0
- data/lib/phronomy/agent/recovery/recovery_coordinator/continuation.rb +3 -3
- data/lib/phronomy/agent/recovery/recovery_coordinator/installation.rb +7 -7
- data/lib/phronomy/agent/recovery/recovery_coordinator/resolution.rb +14 -18
- data/lib/phronomy/agent/recovery/recovery_coordinator.rb +5 -9
- data/lib/phronomy/agent/recovery/recovery_support.rb +2 -8
- data/lib/phronomy/agent/stream_callback_error.rb +2 -2
- data/lib/phronomy/common/error.rb +65 -1
- data/lib/phronomy/content_store/base.rb +7 -2
- data/lib/phronomy/context/assembly.rb +11 -18
- data/lib/phronomy/embeddings/authentication_error.rb +11 -0
- data/lib/phronomy/embeddings/backends/ruby_llm_embeddings.rb +5 -1
- data/lib/phronomy/embeddings/rate_limit_error.rb +11 -0
- data/lib/phronomy/execution/execution_cancellation_error.rb +2 -2
- data/lib/phronomy/execution/execution_timeout_error.rb +2 -2
- data/lib/phronomy/execution/task_result.rb +7 -2
- data/lib/phronomy/filter/filter_block_error.rb +2 -2
- data/lib/phronomy/generation/low_confidence_error.rb +2 -2
- data/lib/phronomy/integrations/ordered_event_delivery.rb +208 -0
- data/lib/phronomy/multi_agent/persistence/team_execution_repository.rb +5 -5
- data/lib/phronomy/multi_agent/team_busy_error.rb +18 -0
- data/lib/phronomy/multi_agent/team_coordinator.rb +20 -7
- data/lib/phronomy/multi_agent/team_not_found_error.rb +18 -0
- data/lib/phronomy/storage/condition_failed_error.rb +2 -2
- data/lib/phronomy/storage/unique_constraint_error.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/coordination_repositories.rb +1 -1
- data/lib/phronomy/tool/base.rb +25 -23
- data/lib/phronomy/tracing/base.rb +35 -6
- data/lib/phronomy/tracing/open_telemetry_tracer.rb +4 -29
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow/execution/workflow.rb +30 -2
- data/lib/phronomy/workflow/execution/workflow_execution_environment.rb +1 -0
- data/lib/phronomy/workflow/execution/workflow_execution_registry.rb +74 -3
- data/lib/phronomy/workflow/execution/workflow_runner.rb +48 -14
- data/lib/phronomy/workflow/execution/workflow_task_execution.rb +153 -0
- data/lib/phronomy/workflow/execution/workflow_task_reception.rb +72 -0
- data/lib/phronomy/workflow/execution/workflow_task_resume_error.rb +14 -0
- data/lib/phronomy/workflow/runtime_binding/workflow_engine_environment.rb +26 -3
- data/lib/phronomy/workflow/runtime_binding/workflow_phase_machine_builder.rb +3 -1
- data/lib/phronomy/workflow/runtime_binding/workflow_task_session.rb +76 -0
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/scripts/check_readme_ruby.rb +24 -29
- data/scripts/check_readme_runnable.rb +92 -81
- data/sig/phronomy/agent/agent_not_found_error.rbs +6 -0
- data/sig/phronomy/agent/agent_unloaded_error.rbs +6 -0
- data/sig/phronomy/agent/stream_callback_error.rbs +8 -0
- data/sig/phronomy/agent.rbs +18 -0
- data/sig/phronomy/embeddings/base.rbs +6 -0
- data/sig/phronomy/execution/execution.rbs +2 -2
- data/sig/phronomy/extensions.rbs +1 -0
- data/sig/phronomy/filter/filter_block_error.rbs +6 -0
- data/sig/phronomy/generation/low_confidence_error.rbs +6 -0
- data/sig/phronomy/integrations.rbs +14 -0
- data/sig/phronomy/multi_agent/team_busy_error.rbs +8 -0
- data/sig/phronomy/multi_agent/team_not_found_error.rbs +8 -0
- data/sig/phronomy/storage/contracts.rbs +2 -2
- data/sig/phronomy/workflow/execution/workflow_execution_environment.rbs +1 -0
- data/sig/phronomy/workflow.rbs +4 -0
- data/sig/phronomy.rbs +4 -0
- metadata +49 -2
|
@@ -93,7 +93,7 @@ module Phronomy
|
|
|
93
93
|
)
|
|
94
94
|
@agent.send(:__assert_live_agent!)
|
|
95
95
|
@agent.send(:_reject_removed_generic_identity_keys!, config)
|
|
96
|
-
result_task =
|
|
96
|
+
result_task = OperationResult.new(agent: @agent, name: "agent-#{@agent.agent_id}-#{mode}")
|
|
97
97
|
controls = Phronomy::InvocationControls.attach(result_task,
|
|
98
98
|
invocation_context: config[:invocation_context],
|
|
99
99
|
cancellation_token: config[:cancellation_token])
|
|
@@ -127,9 +127,8 @@ module Phronomy
|
|
|
127
127
|
)
|
|
128
128
|
@agent.send(:__assert_live_agent!)
|
|
129
129
|
@agent.send(:_reject_removed_generic_identity_keys!, config)
|
|
130
|
-
result_task =
|
|
131
|
-
name: "agent-approval-resume:#{execution_id}"
|
|
132
|
-
)
|
|
130
|
+
result_task = OperationResult.new(agent: @agent,
|
|
131
|
+
name: "agent-approval-resume:#{execution_id}")
|
|
133
132
|
command = ResumeCommand.new(
|
|
134
133
|
coordinator: self,
|
|
135
134
|
execution_id: execution_id.to_s.freeze,
|
|
@@ -269,7 +268,7 @@ module Phronomy
|
|
|
269
268
|
def begin_start_on_event_loop(request)
|
|
270
269
|
environment = @agent.__execution_environment
|
|
271
270
|
request.controls.check_start!
|
|
272
|
-
root = @agent.
|
|
271
|
+
root = @agent.send(:__root_for_operation)
|
|
273
272
|
if root.lifecycle_status == :closed
|
|
274
273
|
deliver_start_failure_on_event_loop(
|
|
275
274
|
request,
|
|
@@ -298,6 +297,7 @@ module Phronomy
|
|
|
298
297
|
@initial_preparation.prepare(operation)
|
|
299
298
|
end
|
|
300
299
|
submitted = true
|
|
300
|
+
request.result_task.track(task) if request.result_task.respond_to?(:track)
|
|
301
301
|
task.on_complete do |result, error|
|
|
302
302
|
ready = InitialPreparationReady.new(
|
|
303
303
|
coordinator: self,
|
|
@@ -519,7 +519,7 @@ module Phronomy
|
|
|
519
519
|
|
|
520
520
|
operation = InitialPreparation::RecoveryCommand.new(
|
|
521
521
|
execution: execution,
|
|
522
|
-
root: @agent.
|
|
522
|
+
root: @agent.send(:__root_for_operation),
|
|
523
523
|
journal_records: @agent.send(:_journal_records_snapshot)
|
|
524
524
|
)
|
|
525
525
|
task = environment.submit(on_full: :raise) do
|
|
@@ -717,7 +717,7 @@ module Phronomy
|
|
|
717
717
|
execution_id: state.execution_id,
|
|
718
718
|
fsm_session_id: fsm_session_id.to_s.freeze,
|
|
719
719
|
expected_execution_revision: state.execution.execution_revision,
|
|
720
|
-
root: @agent.
|
|
720
|
+
root: @agent.send(:__root_for_operation),
|
|
721
721
|
journal_records: @agent.send(:_journal_records_snapshot),
|
|
722
722
|
execution: state.execution,
|
|
723
723
|
base_manifest: state.base_manifest,
|
|
@@ -739,7 +739,7 @@ module Phronomy
|
|
|
739
739
|
execution_id: state.execution_id,
|
|
740
740
|
fsm_session_id: fsm_session_id.to_s.freeze,
|
|
741
741
|
expected_execution_revision: state.execution.execution_revision,
|
|
742
|
-
root: @agent.
|
|
742
|
+
root: @agent.send(:__root_for_operation),
|
|
743
743
|
execution: state.execution,
|
|
744
744
|
runtime_snapshot: invocation.runtime_snapshot,
|
|
745
745
|
tool_batch_snapshot: tool_batch_snapshot,
|
|
@@ -921,6 +921,7 @@ module Phronomy
|
|
|
921
921
|
@approval_resume_commit.commit(operation)
|
|
922
922
|
end
|
|
923
923
|
submitted = true
|
|
924
|
+
request.result_task.track(task) if request.result_task.respond_to?(:track)
|
|
924
925
|
task.on_complete do |result, error|
|
|
925
926
|
ready = ResumeCommitReady.new(
|
|
926
927
|
coordinator: self,
|
|
@@ -953,7 +954,7 @@ module Phronomy
|
|
|
953
954
|
ResumeCommitCommand.new(
|
|
954
955
|
execution_id: state.execution_id,
|
|
955
956
|
expected_execution_revision: state.execution.execution_revision,
|
|
956
|
-
root: @agent.
|
|
957
|
+
root: @agent.send(:__root_for_operation),
|
|
957
958
|
execution: state.execution,
|
|
958
959
|
approval_request_id: request.approval_request_id,
|
|
959
960
|
approved: request.approved,
|
|
@@ -1254,7 +1255,7 @@ module Phronomy
|
|
|
1254
1255
|
|
|
1255
1256
|
operation = build_terminal_operation(
|
|
1256
1257
|
execution: state.execution,
|
|
1257
|
-
root: @agent.
|
|
1258
|
+
root: @agent.send(:__root_for_operation),
|
|
1258
1259
|
journal_records: @agent.send(:_journal_records_snapshot),
|
|
1259
1260
|
invocation: invocation,
|
|
1260
1261
|
source_error: source_error,
|
|
@@ -1338,6 +1339,7 @@ module Phronomy
|
|
|
1338
1339
|
# worker boundary. TaskResult/listener delivery state stays outside it.
|
|
1339
1340
|
@outcome_committer.commit_outcome(operation)
|
|
1340
1341
|
end
|
|
1342
|
+
delivery.result_task.track(task) if delivery.result_task.respond_to?(:track)
|
|
1341
1343
|
task.on_complete do |outcome, error|
|
|
1342
1344
|
ready = TerminalCommitReady.new(
|
|
1343
1345
|
coordinator: self,
|
|
@@ -1440,7 +1442,7 @@ module Phronomy
|
|
|
1440
1442
|
if operation.execution.metadata["reservation"] || operation.execution.metadata["execution_extension"]
|
|
1441
1443
|
release_terminal_ownership(environment, operation.execution_id, state)
|
|
1442
1444
|
environment.registry.take_agent_completion_waiters(operation.execution_id, fallback: delivery.result_task).each do |task|
|
|
1443
|
-
task
|
|
1445
|
+
fail_task(task, ready.error)
|
|
1444
1446
|
end
|
|
1445
1447
|
return
|
|
1446
1448
|
end
|
|
@@ -1477,7 +1479,7 @@ module Phronomy
|
|
|
1477
1479
|
delivery = ready.delivery
|
|
1478
1480
|
outcome = ready.outcome
|
|
1479
1481
|
release_terminal_ownership(environment, operation.execution_id, state)
|
|
1480
|
-
environment.registry.take_agent_completion_waiters(operation.execution_id, fallback: delivery.result_task).each { |task| task
|
|
1482
|
+
environment.registry.take_agent_completion_waiters(operation.execution_id, fallback: delivery.result_task).each { |task| fail_task(task, outcome.error) }
|
|
1481
1483
|
end
|
|
1482
1484
|
|
|
1483
1485
|
def suspend_execution_on_event_loop(ready, state, environment)
|
|
@@ -1501,7 +1503,7 @@ module Phronomy
|
|
|
1501
1503
|
outcome.result
|
|
1502
1504
|
)
|
|
1503
1505
|
Array(state&.invocation&.config&.fetch(:phronomy_exact_observers, [])).each do |task|
|
|
1504
|
-
task
|
|
1506
|
+
fail_task(task, Phronomy::ExecutionRehydrationRequiredError.new("Execution #{operation.execution_id} requires approval"))
|
|
1505
1507
|
end
|
|
1506
1508
|
end
|
|
1507
1509
|
|
|
@@ -1695,6 +1697,7 @@ module Phronomy
|
|
|
1695
1697
|
end
|
|
1696
1698
|
if execution_error
|
|
1697
1699
|
if execution_error.is_a?(Phronomy::CancellationError)
|
|
1700
|
+
result_task.finish_operation! if result_task.respond_to?(:finish_operation!)
|
|
1698
1701
|
result_task.cancel!(execution_error)
|
|
1699
1702
|
else
|
|
1700
1703
|
fail_task(result_task, execution_error)
|
|
@@ -187,7 +187,7 @@ module Phronomy
|
|
|
187
187
|
def encode_failure(tx, operation, error)
|
|
188
188
|
current = operation.execution
|
|
189
189
|
encoded_records, call_records = encode_runtime_records(tx, operation, context_candidate: false)
|
|
190
|
-
error_ref = tx.contents.put_json(
|
|
190
|
+
error_ref = tx.contents.put_json(Phronomy::Error.diagnostic(error))
|
|
191
191
|
terminal_status = ExecutionFailure.status_for(error)
|
|
192
192
|
records = failure_records(operation, encoded_records, terminal_status, error_ref)
|
|
193
193
|
failed = current.with(
|
|
@@ -262,10 +262,7 @@ module Phronomy
|
|
|
262
262
|
translated_error = error
|
|
263
263
|
failed = next_root = appended = nil
|
|
264
264
|
@persistence.transaction do |tx|
|
|
265
|
-
error_ref = tx.contents.put_json(
|
|
266
|
-
"class" => translated_error.class.name,
|
|
267
|
-
"message" => translated_error.message
|
|
268
|
-
)
|
|
265
|
+
error_ref = tx.contents.put_json(Phronomy::Error.diagnostic(translated_error))
|
|
269
266
|
status = ExecutionFailure.status_for(translated_error)
|
|
270
267
|
records = preparation_failure_records(execution, root, status, error_ref)
|
|
271
268
|
appended = tx.journals.append(
|
|
@@ -29,10 +29,7 @@ module Phronomy
|
|
|
29
29
|
output_ref = assistant_output_ref(tx, outcome)
|
|
30
30
|
assistant_ref = assistant_message_ref(tx, outcome)
|
|
31
31
|
error_ref = if call_error
|
|
32
|
-
tx.contents.put_json(
|
|
33
|
-
"class" => call_error.class.name,
|
|
34
|
-
"message" => call_error.message
|
|
35
|
-
)
|
|
32
|
+
tx.contents.put_json(Phronomy::Error.diagnostic(call_error))
|
|
36
33
|
end
|
|
37
34
|
usage_ref = if outcome && outcome.usage.to_h.values.any? { |value| !value.nil? }
|
|
38
35
|
tx.contents.put_json(json_value(outcome.usage.to_h))
|
|
@@ -92,9 +92,11 @@ module Phronomy
|
|
|
92
92
|
# @return [self]
|
|
93
93
|
# @api public
|
|
94
94
|
def add_input_filter(filter)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
95
|
+
__with_operation do
|
|
96
|
+
@_instance_input_filters ||= []
|
|
97
|
+
@_instance_input_filters << _resolve_filter(filter)
|
|
98
|
+
self
|
|
99
|
+
end
|
|
98
100
|
end
|
|
99
101
|
|
|
100
102
|
# Registers an output filter on this instance.
|
|
@@ -105,9 +107,11 @@ module Phronomy
|
|
|
105
107
|
# @return [self]
|
|
106
108
|
# @api public
|
|
107
109
|
def add_output_filter(filter)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
__with_operation do
|
|
111
|
+
@_instance_output_filters ||= []
|
|
112
|
+
@_instance_output_filters << _resolve_filter(filter)
|
|
113
|
+
self
|
|
114
|
+
end
|
|
111
115
|
end
|
|
112
116
|
|
|
113
117
|
# Registers a tool result filter on this instance.
|
|
@@ -125,16 +129,18 @@ module Phronomy
|
|
|
125
129
|
# @return [self]
|
|
126
130
|
# @api public
|
|
127
131
|
def add_tool_result_filter(tool_class_or_filter, filter = nil)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
132
|
+
__with_operation do
|
|
133
|
+
if filter.nil?
|
|
134
|
+
# Single-argument form: apply to all tools.
|
|
135
|
+
@_instance_tool_result_filters ||= []
|
|
136
|
+
@_instance_tool_result_filters << _resolve_filter(tool_class_or_filter)
|
|
137
|
+
else
|
|
138
|
+
# Two-argument form: scoped to one tool class.
|
|
139
|
+
@_scoped_tool_result_filters ||= {}
|
|
140
|
+
(@_scoped_tool_result_filters[tool_class_or_filter] ||= []) << _resolve_filter(filter)
|
|
141
|
+
end
|
|
142
|
+
self
|
|
136
143
|
end
|
|
137
|
-
self
|
|
138
144
|
end
|
|
139
145
|
|
|
140
146
|
private
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Public admission boundaries. The explicit operation capability belongs to
|
|
6
|
+
# C04, including queued work and domain completion after logical cancellation.
|
|
7
|
+
# @api private
|
|
8
|
+
module LifetimeApi
|
|
9
|
+
# Releases only the Runtime incarnation; durable state and Store stay intact.
|
|
10
|
+
# @return [TaskResult<Boolean>]
|
|
11
|
+
# @api public
|
|
12
|
+
def unload_async
|
|
13
|
+
return Phronomy::TaskResult.completed(false) if @_phronomy_runtime_owner_state == :unloaded
|
|
14
|
+
|
|
15
|
+
__assert_agent_accessible!
|
|
16
|
+
transition, start = __execution_environment.ownership.reserve_unload(self)
|
|
17
|
+
start ? transition.start : transition.result
|
|
18
|
+
rescue => error
|
|
19
|
+
Phronomy::TaskResult.failed(error)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# A timeout stops this wait, never the accepted release.
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
# @api public
|
|
25
|
+
def unload!(timeout: nil)
|
|
26
|
+
_check_event_loop_reentrancy(:unload!, :unload_async)
|
|
27
|
+
unload_async.wait_result(timeout: timeout)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def __with_operation(async: false, read_only: false)
|
|
33
|
+
lease = __acquire_operation!(read_only: read_only)
|
|
34
|
+
followed = false
|
|
35
|
+
value = yield
|
|
36
|
+
if async && lease
|
|
37
|
+
lease.follow(value)
|
|
38
|
+
followed = true
|
|
39
|
+
end
|
|
40
|
+
value
|
|
41
|
+
rescue Phronomy::AgentUnloadedError, Phronomy::AgentBusyError, Phronomy::RuntimeShutdownError => error
|
|
42
|
+
raise unless async
|
|
43
|
+
Phronomy::TaskResult.failed(error)
|
|
44
|
+
ensure
|
|
45
|
+
lease&.release unless followed
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Accepted C04 commands carry their admission/operation capability. Their
|
|
49
|
+
# continuations read the snapshot without entering the public admission gate.
|
|
50
|
+
def __root_for_operation
|
|
51
|
+
__assert_agent_accessible!
|
|
52
|
+
@root
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def __acquire_operation!(read_only: false)
|
|
56
|
+
__assert_agent_accessible!
|
|
57
|
+
return nil if @_phronomy_runtime_owner_state == :constructing
|
|
58
|
+
|
|
59
|
+
__execution_environment.ownership.acquire_operation(self, read_only: read_only)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def __mark_unloaded!
|
|
63
|
+
@_phronomy_runtime_owner_state = :unloaded
|
|
64
|
+
@root = nil
|
|
65
|
+
@_phronomy_journal_records = [].freeze
|
|
66
|
+
@_phronomy_event_listener = nil
|
|
67
|
+
@_phronomy_execution_wiring = {}.freeze
|
|
68
|
+
@execution_coordinator = nil
|
|
69
|
+
@tool_approval_policy = @before_llm_input = nil
|
|
70
|
+
@_instance_input_filters = @_instance_output_filters = nil
|
|
71
|
+
@_instance_tool_result_filters = @_scoped_tool_result_filters = nil
|
|
72
|
+
@initial_retention = nil
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# C04 capability for accepted work. Never inferred from the current thread.
|
|
6
|
+
# @api private
|
|
7
|
+
class OperationLease
|
|
8
|
+
def initialize(registry, agent, token)
|
|
9
|
+
@registry, @agent, @token = registry, agent, token
|
|
10
|
+
@mutex = Mutex.new
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def release
|
|
14
|
+
ownership = @mutex.synchronize do
|
|
15
|
+
next unless @registry
|
|
16
|
+
current = [@registry, @agent, @token]
|
|
17
|
+
@registry = @agent = @token = nil
|
|
18
|
+
current
|
|
19
|
+
end
|
|
20
|
+
ownership&.first&.release_operation(ownership[1], ownership[2])
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def follow(task)
|
|
24
|
+
if task.respond_to?(:on_physical_complete)
|
|
25
|
+
task.on_physical_complete { release }
|
|
26
|
+
else
|
|
27
|
+
task.on_complete { |_value, _error| release }
|
|
28
|
+
end
|
|
29
|
+
task
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Agent
|
|
5
|
+
# Logical cancellation of a public handle does not release Agent ownership.
|
|
6
|
+
# Only the domain runner acknowledges the end of its accepted operation.
|
|
7
|
+
# @api private
|
|
8
|
+
class OperationResult < Phronomy::Concurrency::PhysicalCompletionTask
|
|
9
|
+
def initialize(agent:, name:)
|
|
10
|
+
super(name: name)
|
|
11
|
+
@operation_lease = agent.send(:__acquire_operation!)
|
|
12
|
+
@lifetime_mutex = Mutex.new
|
|
13
|
+
@work = 0
|
|
14
|
+
@finished = false
|
|
15
|
+
@released = false
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def finish_operation!
|
|
19
|
+
@lifetime_mutex.synchronize { @finished = true }
|
|
20
|
+
release_if_quiet
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def track(task)
|
|
24
|
+
@lifetime_mutex.synchronize { @work += 1 }
|
|
25
|
+
task.on_physical_complete do
|
|
26
|
+
@lifetime_mutex.synchronize { @work -= 1 }
|
|
27
|
+
release_if_quiet
|
|
28
|
+
end
|
|
29
|
+
task
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def settle(value = nil, error: nil)
|
|
33
|
+
finish_operation!
|
|
34
|
+
error ? fail(error) : complete(value)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def release_if_quiet
|
|
40
|
+
released = @lifetime_mutex.synchronize do
|
|
41
|
+
next unless @finished && @work.zero? && !@released
|
|
42
|
+
@released = true
|
|
43
|
+
current = @operation_lease
|
|
44
|
+
@operation_lease = nil
|
|
45
|
+
[current]
|
|
46
|
+
end
|
|
47
|
+
return unless released
|
|
48
|
+
released.first&.release
|
|
49
|
+
mark_physical_complete!
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -25,12 +25,14 @@ module Phronomy
|
|
|
25
25
|
@mutex = Mutex.new
|
|
26
26
|
@condition = ConditionVariable.new
|
|
27
27
|
@entries = {}
|
|
28
|
+
@scope_owners = {}
|
|
29
|
+
@operations = {}
|
|
28
30
|
@state = :running
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
def create(agent_id, expected_class:)
|
|
33
|
+
def create(agent_id, expected_class:, scope_owner: nil)
|
|
32
34
|
key = normalize_agent_id(agent_id)
|
|
33
|
-
token = reserve_create!(key)
|
|
35
|
+
token = reserve_create!(key, scope_owner: scope_owner)
|
|
34
36
|
|
|
35
37
|
begin
|
|
36
38
|
agent = yield(@environment)
|
|
@@ -59,7 +61,7 @@ module Phronomy
|
|
|
59
61
|
)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
|
-
def load(agent_id, expected_class:)
|
|
64
|
+
def load(agent_id, expected_class:, scope_owner: nil)
|
|
63
65
|
key = normalize_agent_id(agent_id)
|
|
64
66
|
token = nil
|
|
65
67
|
|
|
@@ -67,9 +69,14 @@ module Phronomy
|
|
|
67
69
|
existing = @mutex.synchronize do
|
|
68
70
|
ensure_running!
|
|
69
71
|
entry = @entries[key]
|
|
72
|
+
if scope_owner && entry && !@scope_owners[key].equal?(scope_owner)
|
|
73
|
+
raise Phronomy::AgentBusyError, "Agent #{key.inspect} belongs to another live owner"
|
|
74
|
+
end
|
|
70
75
|
case entry&.state
|
|
71
76
|
when :live
|
|
72
77
|
entry.agent
|
|
78
|
+
when :unloading
|
|
79
|
+
raise Phronomy::AgentBusyError, "Agent #{key.inspect} is unloading"
|
|
73
80
|
when :constructing, :purging
|
|
74
81
|
@condition.wait(@mutex)
|
|
75
82
|
:retry
|
|
@@ -78,6 +85,7 @@ module Phronomy
|
|
|
78
85
|
when nil
|
|
79
86
|
token = Object.new.freeze
|
|
80
87
|
@entries[key] = Entry.new(state: :constructing, agent: nil, token: token)
|
|
88
|
+
@scope_owners[key] = scope_owner if scope_owner
|
|
81
89
|
nil
|
|
82
90
|
# :nocov:
|
|
83
91
|
else
|
|
@@ -119,8 +127,8 @@ module Phronomy
|
|
|
119
127
|
when :recovery_required
|
|
120
128
|
raise ownership_recovery_error(key)
|
|
121
129
|
# :nocov:
|
|
122
|
-
when :purging
|
|
123
|
-
raise Phronomy::
|
|
130
|
+
when :purging, :unloading
|
|
131
|
+
raise Phronomy::AgentBusyError, "Agent #{key.inspect} is #{entry.state}"
|
|
124
132
|
# :cover:
|
|
125
133
|
end
|
|
126
134
|
end
|
|
@@ -129,13 +137,77 @@ module Phronomy
|
|
|
129
137
|
validate_expected_class!(agent, expected_class, key)
|
|
130
138
|
end
|
|
131
139
|
|
|
140
|
+
# Admission and lifetime transitions share this mutex. No Runtime or Store
|
|
141
|
+
# operation is called while it is held.
|
|
142
|
+
def acquire_operation(agent, read_only: false)
|
|
143
|
+
token = Object.new.freeze
|
|
144
|
+
@mutex.synchronize do
|
|
145
|
+
ensure_running! unless read_only && @state == :draining
|
|
146
|
+
entry = @entries[agent.agent_id]
|
|
147
|
+
if entry&.agent&.equal?(agent) && entry.state == :unloading
|
|
148
|
+
raise Phronomy::AgentBusyError, "Agent #{agent.agent_id.inspect} is unloading"
|
|
149
|
+
end
|
|
150
|
+
unless entry&.state == :live && entry.agent.equal?(agent)
|
|
151
|
+
raise Phronomy::RuntimeShutdownError, "Agent is no longer the live Runtime owner"
|
|
152
|
+
end
|
|
153
|
+
(@operations[agent] ||= {})[token] = true
|
|
154
|
+
end
|
|
155
|
+
OperationLease.new(self, agent, token)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def release_operation(agent, token)
|
|
159
|
+
@mutex.synchronize do
|
|
160
|
+
operations = @operations[agent]
|
|
161
|
+
operations&.delete(token)
|
|
162
|
+
@operations.delete(agent) if operations && operations.empty?
|
|
163
|
+
@condition.broadcast
|
|
164
|
+
end
|
|
165
|
+
true
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Returns the already accepted transition for duplicate callers.
|
|
169
|
+
def reserve_unload(agent)
|
|
170
|
+
@mutex.synchronize do
|
|
171
|
+
ensure_running!
|
|
172
|
+
entry = @entries[agent.agent_id]
|
|
173
|
+
if entry&.state == :unloading && entry.agent.equal?(agent)
|
|
174
|
+
return [entry.token, false]
|
|
175
|
+
end
|
|
176
|
+
unless entry&.state == :live && entry.agent.equal?(agent)
|
|
177
|
+
raise Phronomy::RuntimeShutdownError, "Agent is no longer the live Runtime owner"
|
|
178
|
+
end
|
|
179
|
+
assert_no_operations!(agent)
|
|
180
|
+
transition = UnloadTransition.new(agent: agent, environment: @environment, ownership: self)
|
|
181
|
+
@entries[agent.agent_id] = Entry.new(state: :unloading, agent: agent, token: transition)
|
|
182
|
+
[transition, true]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def finish_unload(agent, transition, success:)
|
|
187
|
+
@mutex.synchronize do
|
|
188
|
+
entry = @entries[agent.agent_id]
|
|
189
|
+
unless entry&.state == :unloading && entry.agent.equal?(agent) && entry.token.equal?(transition)
|
|
190
|
+
raise Phronomy::RuntimeShutdownError, "Agent unload reservation is no longer authoritative"
|
|
191
|
+
end
|
|
192
|
+
if success
|
|
193
|
+
agent.send(:__mark_unloaded!)
|
|
194
|
+
@entries.delete(agent.agent_id)
|
|
195
|
+
@scope_owners.delete(agent.agent_id)
|
|
196
|
+
else
|
|
197
|
+
@entries[agent.agent_id] = Entry.new(state: :live, agent: agent, token: nil)
|
|
198
|
+
end
|
|
199
|
+
@condition.broadcast
|
|
200
|
+
end
|
|
201
|
+
true
|
|
202
|
+
end
|
|
203
|
+
|
|
132
204
|
def owned?(agent)
|
|
133
205
|
return false unless agent
|
|
134
206
|
|
|
135
207
|
key = normalize_agent_id(agent.agent_id)
|
|
136
208
|
@mutex.synchronize do
|
|
137
209
|
entry = @entries[key]
|
|
138
|
-
entry&.state
|
|
210
|
+
%i[live unloading].include?(entry&.state) && entry.agent.equal?(agent)
|
|
139
211
|
end
|
|
140
212
|
rescue ArgumentError
|
|
141
213
|
false
|
|
@@ -151,9 +223,13 @@ module Phronomy
|
|
|
151
223
|
ensure_running!
|
|
152
224
|
entry = @entries[key]
|
|
153
225
|
unless entry&.state == :live && entry.agent.equal?(agent)
|
|
226
|
+
if entry&.state == :unloading && entry.agent.equal?(agent)
|
|
227
|
+
raise Phronomy::AgentBusyError, "Agent #{key.inspect} is unloading"
|
|
228
|
+
end
|
|
154
229
|
raise Phronomy::RuntimeShutdownError,
|
|
155
230
|
"Agent #{key.inspect} is not the live owner in this Runtime"
|
|
156
231
|
end
|
|
232
|
+
assert_no_operations!(agent)
|
|
157
233
|
@entries[key] = Entry.new(state: :purging, agent: agent, token: token)
|
|
158
234
|
end
|
|
159
235
|
agent.send(:__mark_purging!, @environment)
|
|
@@ -167,6 +243,7 @@ module Phronomy
|
|
|
167
243
|
validate_purge_entry!(entry, agent, token, key)
|
|
168
244
|
agent.send(:__mark_purged!, @environment)
|
|
169
245
|
@entries.delete(key)
|
|
246
|
+
@scope_owners.delete(key)
|
|
170
247
|
@condition.broadcast
|
|
171
248
|
end
|
|
172
249
|
true
|
|
@@ -215,7 +292,7 @@ module Phronomy
|
|
|
215
292
|
# ownership transition that is still actively changing state must settle.
|
|
216
293
|
def wait_until_idle(deadline)
|
|
217
294
|
@mutex.synchronize do
|
|
218
|
-
while @entries.values.any? { |entry| %i[constructing purging].include?(entry.state) }
|
|
295
|
+
while @entries.values.any? { |entry| %i[constructing purging unloading].include?(entry.state) }
|
|
219
296
|
remaining = deadline - monotonic_now
|
|
220
297
|
# :nocov:
|
|
221
298
|
return false if remaining <= 0
|
|
@@ -231,6 +308,8 @@ module Phronomy
|
|
|
231
308
|
@state = :terminated
|
|
232
309
|
owned = @entries.values.filter_map(&:agent).uniq
|
|
233
310
|
@entries.clear
|
|
311
|
+
@scope_owners.clear
|
|
312
|
+
@operations.clear
|
|
234
313
|
@condition.broadcast
|
|
235
314
|
owned
|
|
236
315
|
end
|
|
@@ -240,17 +319,22 @@ module Phronomy
|
|
|
240
319
|
|
|
241
320
|
private
|
|
242
321
|
|
|
243
|
-
def reserve_create!(key)
|
|
322
|
+
def reserve_create!(key, scope_owner: nil)
|
|
244
323
|
token = Object.new.freeze
|
|
245
324
|
@mutex.synchronize do
|
|
246
325
|
ensure_running!
|
|
247
326
|
if (entry = @entries[key])
|
|
248
327
|
raise ownership_recovery_error(key) if entry.state == :recovery_required
|
|
249
328
|
|
|
329
|
+
if scope_owner
|
|
330
|
+
raise Phronomy::AgentBusyError, "Agent #{key.inspect} already has a Runtime owner"
|
|
331
|
+
end
|
|
332
|
+
|
|
250
333
|
raise Phronomy::AgentAlreadyExistsError,
|
|
251
334
|
"Agent #{key.inspect} already exists in this Runtime (#{entry.state})"
|
|
252
335
|
end
|
|
253
336
|
@entries[key] = Entry.new(state: :constructing, agent: nil, token: token)
|
|
337
|
+
@scope_owners[key] = scope_owner if scope_owner
|
|
254
338
|
end
|
|
255
339
|
token
|
|
256
340
|
end
|
|
@@ -291,6 +375,7 @@ module Phronomy
|
|
|
291
375
|
# :nocov:
|
|
292
376
|
if entry&.state == :constructing && entry.token.equal?(token)
|
|
293
377
|
@entries.delete(key)
|
|
378
|
+
@scope_owners.delete(key)
|
|
294
379
|
@condition.broadcast
|
|
295
380
|
end
|
|
296
381
|
# :cover:
|
|
@@ -353,6 +438,12 @@ module Phronomy
|
|
|
353
438
|
# :cover:
|
|
354
439
|
end
|
|
355
440
|
|
|
441
|
+
def assert_no_operations!(agent)
|
|
442
|
+
return unless @operations[agent]&.any?
|
|
443
|
+
|
|
444
|
+
raise Phronomy::AgentBusyError, "Agent #{agent.agent_id.inspect} still has accepted operations"
|
|
445
|
+
end
|
|
446
|
+
|
|
356
447
|
def monotonic_now
|
|
357
448
|
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
358
449
|
end
|