phronomy 0.22.0 → 0.23.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/.mutant.yml +3 -4
- data/CHANGELOG.md +200 -10
- data/CONTRIBUTING.md +81 -9
- data/README.md +15 -6
- data/VERIFY.sh +587 -0
- data/benchmark/bench_agent_invoke.rb +2 -2
- data/benchmark/bench_context_assembler.rb +39 -68
- data/benchmark/bench_regression.rb +2 -2
- data/docs/architecture/agent-context.md +174 -0
- data/docs/architecture/before-llm-input.md +78 -0
- data/docs/architecture/context-management.md +232 -0
- data/docs/architecture/knowledge-and-rag.md +130 -0
- data/docs/architecture/multi-agent-handoff.md +152 -0
- data/docs/architecture/persistence.md +175 -0
- data/docs/architecture/removed/agent-context.md +72 -0
- data/docs/architecture/security-boundaries.md +173 -0
- data/docs/architecture/tracing.md +194 -0
- data/docs/architecture.md +82 -0
- data/docs/archive/design/archived/04_api_design.md +507 -0
- data/docs/archive/design/archived/09_guardrails.md +186 -0
- data/docs/archive/design/archived/17_rails_integration.md +175 -0
- data/docs/archive/design/historical/00_design_philosophy.md +122 -0
- data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
- data/docs/archive/design/historical/06_design_decisions.md +143 -0
- data/docs/changelog/0.14-and-earlier.md +1 -1
- data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
- data/docs/decisions/002-workflow-context-immutability.md +26 -1
- data/docs/decisions/006-no-built-in-guardrails.md +2 -1
- data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
- data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
- data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
- data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
- data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
- data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
- data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
- data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
- data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
- data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
- data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
- data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
- data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
- data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
- data/docs/decisions/README.md +172 -0
- data/docs/features.md +31 -11
- data/docs/getting-started.md +77 -45
- data/docs/migrations/0.19.md +14 -7
- data/docs/migrations/0.22.md +390 -0
- data/docs/persistence-backends.md +88 -38
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +19 -15
- data/lib/phronomy/agent/agent_invocation.rb +288 -93
- data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
- data/lib/phronomy/agent/agent_root.rb +3 -3
- data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
- data/lib/phronomy/agent/async_event_api.rb +145 -72
- data/lib/phronomy/agent/base.rb +388 -181
- data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
- data/lib/phronomy/agent/context_assembler.rb +437 -178
- data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
- data/lib/phronomy/agent/context_plan.rb +18 -13
- data/lib/phronomy/agent/context_plan_validator.rb +246 -88
- data/lib/phronomy/agent/context_policies/default.rb +123 -34
- data/lib/phronomy/agent/context_policy.rb +109 -3
- data/lib/phronomy/agent/context_policy_input.rb +244 -0
- data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
- data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
- data/lib/phronomy/agent/journal_record.rb +17 -4
- data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
- data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
- data/lib/phronomy/agent/llm_operation_result.rb +12 -7
- data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
- data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
- data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
- data/lib/phronomy/agent/recovery_support.rb +512 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
- data/lib/phronomy/agent/selection/candidate.rb +53 -0
- data/lib/phronomy/agent/selection/constraint.rb +49 -0
- data/lib/phronomy/agent/shared_state.rb +38 -1
- data/lib/phronomy/agent/tool_approval_request.rb +33 -5
- data/lib/phronomy/agent/tool_definition_set.rb +49 -3
- data/lib/phronomy/agent/tool_invocation.rb +336 -102
- data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
- data/lib/phronomy/agent.rb +20 -2
- data/lib/phronomy/agent_already_exists_error.rb +5 -0
- data/lib/phronomy/agent_purged_error.rb +5 -0
- data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
- data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
- data/lib/phronomy/engine/event_loop.rb +622 -63
- data/lib/phronomy/engine/fsm_session.rb +194 -21
- data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
- data/lib/phronomy/engine/runtime.rb +77 -20
- data/lib/phronomy/generator_verifier.rb +12 -14
- data/lib/phronomy/invocation_context.rb +9 -29
- data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
- data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
- data/lib/phronomy/multi_agent/coordinator.rb +154 -0
- data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
- data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
- data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
- data/lib/phronomy/multi_agent/handoff.rb +24 -45
- data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
- data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
- data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
- data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
- data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
- data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
- data/lib/phronomy/multi_agent/runner.rb +98 -0
- data/lib/phronomy/persistence/durable_codec.rb +646 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +210 -134
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +316 -0
- data/lib/phronomy/persistence.rb +81 -41
- data/lib/phronomy/recovery.rb +186 -0
- data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
- data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
- data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
- data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
- data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
- data/lib/phronomy/tracing/automatic.rb +176 -0
- data/lib/phronomy/tracing/base.rb +11 -2
- data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
- data/lib/phronomy/version.rb +1 -1
- data/lib/phronomy/workflow.rb +3 -6
- data/lib/phronomy/workflow_context.rb +14 -5
- data/lib/phronomy/workflow_recovery.rb +123 -0
- data/lib/phronomy/workflow_runner.rb +468 -256
- data/lib/phronomy.rb +6 -0
- data/scripts/api_snapshot.rb +12 -0
- data/sig/phronomy/agent.rbs +209 -7
- data/sig/phronomy/multi_agent.rbs +39 -0
- data/sig/phronomy/persistence.rbs +62 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +65 -17
- data/examples/workflows/agent_event_mapping.rb +0 -101
- data/examples/workflows/generic_task_event_mapping.rb +0 -66
- data/lib/phronomy/agent/activation_registry.rb +0 -28
- data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
- data/lib/phronomy/agent/context_candidate.rb +0 -47
- data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
- data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
- data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
- data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
- data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
- data/lib/phronomy/agent/context_policy_registry.rb +0 -46
- data/lib/phronomy/agent/context_request.rb +0 -35
- data/lib/phronomy/agent/context_selection_unit.rb +0 -38
- data/lib/phronomy/agent/derived_content_spec.rb +0 -34
- data/lib/phronomy/agent/runner.rb +0 -97
|
@@ -40,18 +40,28 @@ module Phronomy
|
|
|
40
40
|
]
|
|
41
41
|
}.freeze
|
|
42
42
|
|
|
43
|
-
def self.build(
|
|
44
|
-
|
|
43
|
+
def self.build(
|
|
44
|
+
tool_invocation:,
|
|
45
|
+
parent_event_sink:,
|
|
46
|
+
runtime: Phronomy::Runtime.instance
|
|
47
|
+
)
|
|
48
|
+
build_session(
|
|
49
|
+
tool_invocation: tool_invocation,
|
|
50
|
+
parent_event_sink: parent_event_sink,
|
|
51
|
+
runtime: runtime
|
|
52
|
+
)
|
|
45
53
|
end
|
|
46
54
|
|
|
47
55
|
def self.build_for_resume(
|
|
48
56
|
tool_invocation:,
|
|
57
|
+
parent_event_sink:,
|
|
49
58
|
resume_event:,
|
|
50
59
|
resume_phase:,
|
|
51
60
|
runtime: Phronomy::Runtime.instance
|
|
52
61
|
)
|
|
53
62
|
build_session(
|
|
54
63
|
tool_invocation: tool_invocation,
|
|
64
|
+
parent_event_sink: parent_event_sink,
|
|
55
65
|
runtime: runtime,
|
|
56
66
|
resume_event: resume_event,
|
|
57
67
|
resume_phase: resume_phase
|
|
@@ -60,16 +70,18 @@ module Phronomy
|
|
|
60
70
|
|
|
61
71
|
def self.build_session(
|
|
62
72
|
tool_invocation:,
|
|
73
|
+
parent_event_sink:,
|
|
63
74
|
runtime:,
|
|
64
75
|
resume_event: nil,
|
|
65
76
|
resume_phase: nil
|
|
66
77
|
)
|
|
67
|
-
|
|
78
|
+
event_sink = Phronomy::FSMSession::EventSink.new(event_loop: runtime.event_loop)
|
|
79
|
+
actions = build_entry_actions(runtime, event_sink, parent_event_sink)
|
|
68
80
|
phase_machine = build_phase_machine(actions)
|
|
69
81
|
|
|
70
82
|
Phronomy::FSMSession.new(
|
|
71
|
-
id: tool_invocation.id,
|
|
72
83
|
context: tool_invocation,
|
|
84
|
+
event_sink: event_sink,
|
|
73
85
|
entry_point: :idle,
|
|
74
86
|
phase_machine_class: phase_machine,
|
|
75
87
|
entry_actions: {},
|
|
@@ -85,18 +97,18 @@ module Phronomy
|
|
|
85
97
|
end
|
|
86
98
|
private_class_method :build_session
|
|
87
99
|
|
|
88
|
-
def self.build_entry_actions(runtime)
|
|
100
|
+
def self.build_entry_actions(runtime, event_sink, parent_event_sink)
|
|
89
101
|
{
|
|
90
102
|
validating: [method(:validating_action)],
|
|
91
|
-
authorizing: [method(:authorizing_action).curry.call(runtime)],
|
|
92
|
-
awaiting_approval: [method(:awaiting_approval_action).curry.call(
|
|
93
|
-
authorized: [method(:authorized_action).curry.call(
|
|
103
|
+
authorizing: [method(:authorizing_action).curry.call(runtime, event_sink)],
|
|
104
|
+
awaiting_approval: [method(:awaiting_approval_action).curry.call(parent_event_sink)],
|
|
105
|
+
authorized: [method(:authorized_action).curry.call(parent_event_sink)],
|
|
94
106
|
queued: [method(:queued_action)],
|
|
95
|
-
running: [method(:running_action).curry.call(runtime)],
|
|
96
|
-
completed: [method(:completed_action).curry.call(
|
|
97
|
-
failed: [method(:failed_action).curry.call(
|
|
98
|
-
rejected: [method(:rejected_action).curry.call(
|
|
99
|
-
cancelled: [method(:cancelled_action).curry.call(
|
|
107
|
+
running: [method(:running_action).curry.call(runtime, event_sink)],
|
|
108
|
+
completed: [method(:completed_action).curry.call(parent_event_sink)],
|
|
109
|
+
failed: [method(:failed_action).curry.call(parent_event_sink)],
|
|
110
|
+
rejected: [method(:rejected_action).curry.call(parent_event_sink)],
|
|
111
|
+
cancelled: [method(:cancelled_action).curry.call(parent_event_sink)]
|
|
100
112
|
}
|
|
101
113
|
end
|
|
102
114
|
private_class_method :build_entry_actions
|
|
@@ -183,24 +195,24 @@ module Phronomy
|
|
|
183
195
|
end
|
|
184
196
|
private_class_method :validating_action
|
|
185
197
|
|
|
186
|
-
def self.authorizing_action(runtime, invocation)
|
|
198
|
+
def self.authorizing_action(runtime, event_sink, invocation)
|
|
187
199
|
invocation.start_authorization(runtime: runtime) do |outcome|
|
|
188
|
-
|
|
200
|
+
post_to_session(event_sink, :authorization_completed, outcome)
|
|
189
201
|
end
|
|
190
202
|
invocation
|
|
191
203
|
end
|
|
192
204
|
private_class_method :authorizing_action
|
|
193
205
|
|
|
194
|
-
def self.awaiting_approval_action(
|
|
206
|
+
def self.awaiting_approval_action(parent_event_sink, invocation)
|
|
195
207
|
invocation.mark_awaiting_approval!
|
|
196
|
-
notify_parent(
|
|
208
|
+
notify_parent(parent_event_sink, invocation, :tool_approval_required)
|
|
197
209
|
invocation
|
|
198
210
|
end
|
|
199
211
|
private_class_method :awaiting_approval_action
|
|
200
212
|
|
|
201
|
-
def self.authorized_action(
|
|
213
|
+
def self.authorized_action(parent_event_sink, invocation)
|
|
202
214
|
invocation.mark_authorized!
|
|
203
|
-
notify_parent(
|
|
215
|
+
notify_parent(parent_event_sink, invocation, :tool_authorized)
|
|
204
216
|
invocation
|
|
205
217
|
end
|
|
206
218
|
private_class_method :authorized_action
|
|
@@ -210,61 +222,53 @@ module Phronomy
|
|
|
210
222
|
end
|
|
211
223
|
private_class_method :queued_action
|
|
212
224
|
|
|
213
|
-
def self.running_action(runtime, invocation)
|
|
225
|
+
def self.running_action(runtime, event_sink, invocation)
|
|
214
226
|
invocation.start_execution(runtime: runtime) do |outcome|
|
|
215
|
-
|
|
227
|
+
post_to_session(event_sink, :execution_completed, outcome)
|
|
216
228
|
end
|
|
217
229
|
invocation.mark_running!
|
|
218
230
|
invocation
|
|
219
231
|
end
|
|
220
232
|
private_class_method :running_action
|
|
221
233
|
|
|
222
|
-
def self.completed_action(
|
|
223
|
-
notify_parent(
|
|
234
|
+
def self.completed_action(parent_event_sink, invocation)
|
|
235
|
+
notify_parent(parent_event_sink, invocation, :tool_completed)
|
|
224
236
|
invocation
|
|
225
237
|
end
|
|
226
238
|
private_class_method :completed_action
|
|
227
239
|
|
|
228
|
-
def self.failed_action(
|
|
229
|
-
notify_parent(
|
|
240
|
+
def self.failed_action(parent_event_sink, invocation)
|
|
241
|
+
notify_parent(parent_event_sink, invocation, :tool_failed)
|
|
230
242
|
invocation
|
|
231
243
|
end
|
|
232
244
|
private_class_method :failed_action
|
|
233
245
|
|
|
234
|
-
def self.rejected_action(
|
|
246
|
+
def self.rejected_action(parent_event_sink, invocation)
|
|
235
247
|
invocation.mark_rejected!
|
|
236
|
-
notify_parent(
|
|
248
|
+
notify_parent(parent_event_sink, invocation, :tool_rejected)
|
|
237
249
|
invocation
|
|
238
250
|
end
|
|
239
251
|
private_class_method :rejected_action
|
|
240
252
|
|
|
241
|
-
def self.cancelled_action(
|
|
253
|
+
def self.cancelled_action(parent_event_sink, invocation)
|
|
242
254
|
invocation.mark_cancelled!
|
|
243
|
-
notify_parent(
|
|
255
|
+
notify_parent(parent_event_sink, invocation, :tool_cancelled)
|
|
244
256
|
invocation
|
|
245
257
|
end
|
|
246
258
|
private_class_method :cancelled_action
|
|
247
259
|
|
|
248
|
-
def self.
|
|
249
|
-
|
|
250
|
-
Phronomy::Event.new(type: event_type, target_id: id, payload: payload)
|
|
251
|
-
)
|
|
252
|
-
return if accepted
|
|
260
|
+
def self.post_to_session(event_sink, event_type, payload)
|
|
261
|
+
return if event_sink.post(event_type, payload)
|
|
253
262
|
|
|
254
263
|
Phronomy.configuration.logger&.warn(
|
|
255
|
-
"[Phronomy] Dropped #{event_type.inspect} for
|
|
264
|
+
"[Phronomy] Dropped #{event_type.inspect} for " \
|
|
265
|
+
"FSMSession #{event_sink.fsm_session_id}"
|
|
256
266
|
)
|
|
257
267
|
end
|
|
258
|
-
private_class_method :
|
|
259
|
-
|
|
260
|
-
def self.notify_parent(
|
|
261
|
-
|
|
262
|
-
Phronomy::Event.new(
|
|
263
|
-
type: event_type,
|
|
264
|
-
target_id: invocation.parent_agent_invocation_id,
|
|
265
|
-
payload: {tool_invocation_id: invocation.id}
|
|
266
|
-
)
|
|
267
|
-
)
|
|
268
|
+
private_class_method :post_to_session
|
|
269
|
+
|
|
270
|
+
def self.notify_parent(parent_event_sink, invocation, event_type)
|
|
271
|
+
parent_event_sink.post(event_type, {tool_invocation_id: invocation.id})
|
|
268
272
|
end
|
|
269
273
|
private_class_method :notify_parent
|
|
270
274
|
end
|
data/lib/phronomy/agent.rb
CHANGED
|
@@ -4,12 +4,26 @@ module Phronomy
|
|
|
4
4
|
module Agent
|
|
5
5
|
StreamEvent = Data.define(:type, :payload)
|
|
6
6
|
|
|
7
|
-
def self.run_once(
|
|
7
|
+
def self.run_once(
|
|
8
|
+
definition:,
|
|
9
|
+
input:,
|
|
10
|
+
context: nil,
|
|
11
|
+
knowledge: [],
|
|
12
|
+
on_event: nil,
|
|
13
|
+
**invoke_options,
|
|
14
|
+
&event_block
|
|
15
|
+
)
|
|
16
|
+
if on_event && event_block
|
|
17
|
+
raise ArgumentError, "Provide either on_event: or a block, not both"
|
|
18
|
+
end
|
|
19
|
+
|
|
8
20
|
persistence = Phronomy::Persistence::InMemory.new
|
|
9
21
|
agent = definition.create(
|
|
10
22
|
context: context,
|
|
11
23
|
knowledge: knowledge,
|
|
12
|
-
persistence: persistence
|
|
24
|
+
persistence: persistence,
|
|
25
|
+
on_event: on_event,
|
|
26
|
+
&event_block
|
|
13
27
|
)
|
|
14
28
|
agent.invoke(input, **invoke_options)
|
|
15
29
|
end
|
|
@@ -21,3 +35,7 @@ require_relative "agent/async_event_api"
|
|
|
21
35
|
unless Phronomy::Agent::Base < Phronomy::Agent::AsyncEventApi
|
|
22
36
|
Phronomy::Agent::Base.prepend(Phronomy::Agent::AsyncEventApi)
|
|
23
37
|
end
|
|
38
|
+
|
|
39
|
+
# Durable Agent Recovery and Agent-incarnation event binding.
|
|
40
|
+
require_relative "agent/recovery_support"
|
|
41
|
+
require_relative "agent/recovery_coordinator"
|
|
@@ -65,7 +65,9 @@ module Phronomy
|
|
|
65
65
|
@timeout = timeout
|
|
66
66
|
@cancellation_token = cancellation_token
|
|
67
67
|
@on_abandoned = on_abandoned
|
|
68
|
-
@task = Phronomy::
|
|
68
|
+
@task = Phronomy::Concurrency::PhysicalCompletionTask.deferred(
|
|
69
|
+
name: task_name
|
|
70
|
+
)
|
|
69
71
|
@settled = false
|
|
70
72
|
@timed_out = false
|
|
71
73
|
@cancelled = false
|
|
@@ -162,6 +164,7 @@ module Phronomy
|
|
|
162
164
|
else
|
|
163
165
|
@task.complete(nil)
|
|
164
166
|
end
|
|
167
|
+
@task.mark_physical_complete!
|
|
165
168
|
changed
|
|
166
169
|
end
|
|
167
170
|
|
|
@@ -224,6 +227,7 @@ module Phronomy
|
|
|
224
227
|
|
|
225
228
|
detach_submit_cancellation
|
|
226
229
|
notify_abandoned if abandoned_now
|
|
230
|
+
@task.mark_physical_complete! unless abandoned_now
|
|
227
231
|
if cancelled
|
|
228
232
|
@task.cancel!(error)
|
|
229
233
|
else
|
|
@@ -251,18 +255,26 @@ module Phronomy
|
|
|
251
255
|
|
|
252
256
|
def complete_with_value!(value)
|
|
253
257
|
changed = claim_terminal!
|
|
254
|
-
|
|
258
|
+
unless changed
|
|
259
|
+
@task.mark_physical_complete!
|
|
260
|
+
return false
|
|
261
|
+
end
|
|
255
262
|
|
|
256
263
|
detach_submit_cancellation
|
|
264
|
+
@task.mark_physical_complete!
|
|
257
265
|
@task.complete(value)
|
|
258
266
|
true
|
|
259
267
|
end
|
|
260
268
|
|
|
261
269
|
def complete_with_error!(error)
|
|
262
270
|
changed = claim_terminal!
|
|
263
|
-
|
|
271
|
+
unless changed
|
|
272
|
+
@task.mark_physical_complete!
|
|
273
|
+
return false
|
|
274
|
+
end
|
|
264
275
|
|
|
265
276
|
detach_submit_cancellation
|
|
277
|
+
@task.mark_physical_complete!
|
|
266
278
|
@task.fail(error)
|
|
267
279
|
true
|
|
268
280
|
end
|
|
@@ -581,6 +593,8 @@ module Phronomy
|
|
|
581
593
|
@completed_count += 1
|
|
582
594
|
end
|
|
583
595
|
|
|
596
|
+
operation.task.mark_physical_complete!
|
|
597
|
+
|
|
584
598
|
if abandoned
|
|
585
599
|
@logger&.warn do
|
|
586
600
|
"OffloadPool: worker finished after caller-facing timeout/cancellation settlement"
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Phronomy
|
|
4
|
+
module Concurrency
|
|
5
|
+
# Private Task specialization used when logical completion and physical
|
|
6
|
+
# completion are intentionally different boundaries.
|
|
7
|
+
#
|
|
8
|
+
# Offload cancellation/timeout may settle the caller-facing Task while a
|
|
9
|
+
# worker is still executing. EventLoop supervision must observe the later
|
|
10
|
+
# physical-completion boundary without adding domain-specific state to the
|
|
11
|
+
# public Phronomy::Task contract.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class PhysicalCompletionTask < Phronomy::Task
|
|
15
|
+
# @api private
|
|
16
|
+
def initialize(name: nil, parent: nil)
|
|
17
|
+
super
|
|
18
|
+
@physical_mutex = Mutex.new
|
|
19
|
+
@physical_complete = false
|
|
20
|
+
@physical_callbacks = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Boolean]
|
|
24
|
+
# @api private
|
|
25
|
+
def physical_complete?
|
|
26
|
+
@physical_mutex.synchronize { @physical_complete }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Registers a callback that fires only when the underlying physical work
|
|
30
|
+
# can no longer affect the owning execution.
|
|
31
|
+
#
|
|
32
|
+
# @return [self]
|
|
33
|
+
# @api private
|
|
34
|
+
def on_physical_complete(&callback)
|
|
35
|
+
raise ArgumentError, "on_physical_complete requires a block" unless callback
|
|
36
|
+
|
|
37
|
+
fire_now = @physical_mutex.synchronize do
|
|
38
|
+
if @physical_complete
|
|
39
|
+
true
|
|
40
|
+
else
|
|
41
|
+
@physical_callbacks << callback
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
deliver_physical_callback(callback) if fire_now
|
|
46
|
+
self
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Idempotently marks the physical work boundary complete.
|
|
50
|
+
#
|
|
51
|
+
# @return [self]
|
|
52
|
+
# @api private
|
|
53
|
+
def mark_physical_complete!
|
|
54
|
+
callbacks = @physical_mutex.synchronize do
|
|
55
|
+
return self if @physical_complete
|
|
56
|
+
|
|
57
|
+
@physical_complete = true
|
|
58
|
+
current = @physical_callbacks
|
|
59
|
+
@physical_callbacks = []
|
|
60
|
+
current
|
|
61
|
+
end
|
|
62
|
+
callbacks.each { |callback| deliver_physical_callback(callback) }
|
|
63
|
+
self
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Preserves the physical-completion boundary across Task#map.
|
|
67
|
+
#
|
|
68
|
+
# A mapped PhysicalCompletionTask is physically complete only after both:
|
|
69
|
+
# 1. the source Task's underlying physical work is complete; and
|
|
70
|
+
# 2. the mapping callback itself has finished running.
|
|
71
|
+
#
|
|
72
|
+
# The second condition is required because Task completion callbacks may run
|
|
73
|
+
# on the OffloadPool worker that settles the source Task. Propagating the
|
|
74
|
+
# source physical signal immediately could otherwise let EventLoop declare
|
|
75
|
+
# the owning Execution quiescent while the mapping callback is still active.
|
|
76
|
+
#
|
|
77
|
+
# Logical success/failure semantics intentionally remain the same as
|
|
78
|
+
# Phronomy::Task#map.
|
|
79
|
+
#
|
|
80
|
+
# @api public
|
|
81
|
+
def map(&block)
|
|
82
|
+
raise ArgumentError, "map requires a block" unless block
|
|
83
|
+
|
|
84
|
+
mapped = self.class.deferred(name: "#{name}-mapped", parent: parent)
|
|
85
|
+
propagation_mutex = Mutex.new
|
|
86
|
+
source_physical_complete = physical_complete?
|
|
87
|
+
mapping_complete = false
|
|
88
|
+
|
|
89
|
+
mark_mapped_physical_if_ready = lambda do
|
|
90
|
+
ready = propagation_mutex.synchronize do
|
|
91
|
+
source_physical_complete && mapping_complete
|
|
92
|
+
end
|
|
93
|
+
mapped.mark_physical_complete! if ready
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
on_physical_complete do
|
|
97
|
+
propagation_mutex.synchronize { source_physical_complete = true }
|
|
98
|
+
mark_mapped_physical_if_ready.call
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
on_complete do |value, error|
|
|
102
|
+
if error
|
|
103
|
+
propagation_mutex.synchronize { mapping_complete = true }
|
|
104
|
+
mark_mapped_physical_if_ready.call
|
|
105
|
+
mapped.fail(error)
|
|
106
|
+
next
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
begin
|
|
110
|
+
transformed = block.call(value)
|
|
111
|
+
propagation_mutex.synchronize { mapping_complete = true }
|
|
112
|
+
mark_mapped_physical_if_ready.call
|
|
113
|
+
mapped.complete(transformed)
|
|
114
|
+
rescue => mapped_error
|
|
115
|
+
propagation_mutex.synchronize { mapping_complete = true }
|
|
116
|
+
mark_mapped_physical_if_ready.call
|
|
117
|
+
mapped.fail(mapped_error)
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
mapped
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
private
|
|
125
|
+
|
|
126
|
+
def deliver_physical_callback(callback)
|
|
127
|
+
callback.call
|
|
128
|
+
rescue => error
|
|
129
|
+
Phronomy.configuration.logger&.error do
|
|
130
|
+
"[PhysicalCompletionTask] callback raised #{error.class}: #{error.message}"
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|