phronomy 0.22.0 → 0.24.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 +215 -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 +147 -0
- data/docs/architecture/persistence.md +186 -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 +89 -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 +114 -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/028-preparing-recovery-replay-contract.md +106 -0
- data/docs/decisions/029-semantic-completion-and-application-effect-boundary.md +220 -0
- data/docs/decisions/030-agent-handoff-domain-and-durable-responsibility.md +235 -0
- data/docs/decisions/031-durable-multi-agent-coordination.md +301 -0
- data/docs/decisions/README.md +176 -0
- data/docs/design/durable-semantic-coordination/CHANGELOG_V2_REVISION_2.md +33 -0
- data/docs/design/durable-semantic-coordination/CONTINUATION_DECISION_REFACTOR.md +191 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_DESIGN_V2.md +862 -0
- data/docs/design/durable-semantic-coordination/IMPLEMENTATION_REPORT.md +106 -0
- data/docs/design/durable-semantic-coordination/RECOVERY_CONTRACT_CLARIFICATIONS.md +179 -0
- data/docs/design/durable-semantic-coordination/RESPONSIBILITY_BOUNDARY_REVIEW.md +302 -0
- data/docs/features.md +65 -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/migrations/durable-semantic-coordination-v2.md +65 -0
- data/docs/persistence-backends.md +129 -40
- data/docs/runtime-and-concurrency.md +227 -33
- data/examples/README.md +13 -0
- data/lib/phronomy/agent/agent_execution.rb +21 -17
- 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 +162 -72
- data/lib/phronomy/agent/base.rb +416 -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/exact_execution.rb +153 -0
- data/lib/phronomy/agent/execution_cancellation.rb +25 -0
- data/lib/phronomy/agent/execution_coordinator.rb +2417 -572
- data/lib/phronomy/agent/handoff.rb +43 -0
- data/lib/phronomy/agent/handoff_capability_factory.rb +45 -0
- data/lib/phronomy/agent/handoff_context.rb +120 -0
- data/lib/phronomy/agent/handoff_execution_coordinator.rb +143 -0
- data/lib/phronomy/agent/handoff_policy.rb +143 -0
- data/lib/phronomy/agent/handoff_projection.rb +207 -0
- data/lib/phronomy/agent/handoff_request.rb +45 -0
- data/lib/phronomy/agent/handoff_runner.rb +178 -0
- data/lib/phronomy/agent/handoff_state.rb +43 -0
- 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 +174 -0
- data/lib/phronomy/agent/recovery_coordinator/installation.rb +381 -0
- data/lib/phronomy/agent/recovery_coordinator/resolution.rb +511 -0
- data/lib/phronomy/agent/recovery_coordinator.rb +217 -0
- data/lib/phronomy/agent/recovery_support.rb +504 -0
- data/lib/phronomy/agent/ruby_llm_materializer.rb +16 -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 +339 -103
- 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/team_ownership_registry.rb +77 -0
- data/lib/phronomy/engine/runtime.rb +92 -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/durable_subagent_coordinator.rb +134 -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/orchestrator.rb +71 -26
- data/lib/phronomy/multi_agent/team_coordinator.rb +473 -125
- data/lib/phronomy/multi_agent/team_execution.rb +44 -0
- data/lib/phronomy/multi_agent/team_root.rb +41 -0
- data/lib/phronomy/persistence/durable_codec.rb +706 -0
- data/lib/phronomy/persistence/durable_record.rb +117 -0
- data/lib/phronomy/persistence/in_memory.rb +471 -133
- data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
- data/lib/phronomy/persistence/repository_facades.rb +535 -0
- data/lib/phronomy/persistence.rb +174 -40
- 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 +2 -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/testing/persistence_contract/coordination_repositories.rb +137 -0
- data/lib/phronomy/testing/persistence_contract.rb +5 -0
- data/lib/phronomy/tools/agent.rb +1 -1
- 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/handoff.rbs +41 -0
- data/sig/phronomy/multi_agent.rbs +35 -0
- data/sig/phronomy/persistence.rbs +123 -4
- data/sig/phronomy/runtime.rbs +1 -4
- data/sig/phronomy/workflow.rbs +2 -2
- data/sig/phronomy.rbs +10 -0
- metadata +84 -18
- 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
- data/lib/phronomy/multi_agent/handoff.rb +0 -64
|
@@ -45,75 +45,104 @@ module Phronomy
|
|
|
45
45
|
end
|
|
46
46
|
end
|
|
47
47
|
|
|
48
|
+
# Backend-side Agent record repository. DurableRecord is opaque here;
|
|
49
|
+
# identity/revision metadata is supplied explicitly by RepositoryFacades.
|
|
48
50
|
class Agents
|
|
49
51
|
def initialize(owner) = @owner = owner
|
|
50
52
|
|
|
51
|
-
def create(
|
|
53
|
+
def create(agent_id:, agent_revision:, record:)
|
|
54
|
+
record = @owner.require_durable_record!(record)
|
|
55
|
+
key = agent_id.to_s
|
|
56
|
+
revision = Integer(agent_revision)
|
|
57
|
+
raise ConflictError, "agent_id must not be empty" if key.empty?
|
|
58
|
+
raise ConflictError, "agent_revision must be non-negative" if revision.negative?
|
|
59
|
+
|
|
52
60
|
@owner.synchronize do
|
|
53
|
-
key = root.agent_id.to_s
|
|
54
|
-
raise ConflictError, "agent_id must not be empty" if key.empty?
|
|
55
61
|
raise ConflictError, "agent already exists: #{key}" if @owner.state[:agents].key?(key)
|
|
56
|
-
@owner.state[:agents][key] =
|
|
62
|
+
@owner.state[:agents][key] = record.copy
|
|
63
|
+
@owner.state[:agent_revisions][key] = revision
|
|
57
64
|
end
|
|
58
|
-
|
|
65
|
+
record.copy
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
def load(agent_id)
|
|
62
69
|
@owner.synchronize do
|
|
63
|
-
@owner.state[:agents].fetch(agent_id.to_s)
|
|
70
|
+
@owner.state[:agents].fetch(agent_id.to_s) do
|
|
71
|
+
raise NotFoundError, "agent not found: #{agent_id}"
|
|
72
|
+
end.copy
|
|
64
73
|
end
|
|
65
74
|
end
|
|
66
75
|
|
|
67
|
-
def save(agent_id, expected_revision:,
|
|
76
|
+
def save(agent_id, expected_revision:, next_revision:, record:)
|
|
77
|
+
record = @owner.require_durable_record!(record)
|
|
78
|
+
key = agent_id.to_s
|
|
79
|
+
expected = Integer(expected_revision)
|
|
80
|
+
next_value = Integer(next_revision)
|
|
68
81
|
@owner.synchronize do
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
raise ConflictError,
|
|
72
|
-
"agent revision conflict: expected #{expected_revision}, actual #{current.agent_revision}"
|
|
82
|
+
unless @owner.state[:agents].key?(key)
|
|
83
|
+
raise NotFoundError, "agent not found: #{agent_id}"
|
|
73
84
|
end
|
|
74
|
-
|
|
75
|
-
|
|
85
|
+
actual_revision = @owner.state[:agent_revisions].fetch(key)
|
|
86
|
+
unless actual_revision == expected
|
|
87
|
+
raise ConflictError,
|
|
88
|
+
"agent revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
76
89
|
end
|
|
77
|
-
unless
|
|
90
|
+
unless next_value == expected + 1
|
|
78
91
|
raise ConflictError,
|
|
79
92
|
"agent save must advance revision exactly once: " \
|
|
80
|
-
"expected #{
|
|
93
|
+
"expected #{expected + 1}, got #{next_value}"
|
|
81
94
|
end
|
|
82
|
-
@owner.state[:agents][
|
|
95
|
+
@owner.state[:agents][key] = record.copy
|
|
96
|
+
@owner.state[:agent_revisions][key] = next_value
|
|
83
97
|
end
|
|
84
|
-
|
|
98
|
+
record.copy
|
|
85
99
|
end
|
|
86
100
|
|
|
87
101
|
def delete(agent_id)
|
|
88
|
-
@owner.synchronize
|
|
102
|
+
@owner.synchronize do
|
|
103
|
+
key = agent_id.to_s
|
|
104
|
+
@owner.state[:agent_revisions].delete(key)
|
|
105
|
+
@owner.state[:agents].delete(key)
|
|
106
|
+
end
|
|
89
107
|
end
|
|
90
108
|
end
|
|
91
109
|
|
|
92
110
|
class Journals
|
|
93
111
|
def initialize(owner) = @owner = owner
|
|
94
112
|
|
|
95
|
-
def append(agent_id, expected_position:, records:)
|
|
113
|
+
def append(agent_id, expected_position:, records:, record_ids:)
|
|
114
|
+
encoded = Array(records).map { |record| @owner.require_durable_record!(record) }
|
|
115
|
+
ids = Array(record_ids).map(&:to_s)
|
|
116
|
+
unless encoded.length == ids.length
|
|
117
|
+
raise ConflictError,
|
|
118
|
+
"Journal records/record_ids length mismatch: #{encoded.length} != #{ids.length}"
|
|
119
|
+
end
|
|
120
|
+
if ids.any?(&:empty?)
|
|
121
|
+
raise ConflictError, "Journal record_id must not be empty"
|
|
122
|
+
end
|
|
123
|
+
|
|
96
124
|
@owner.synchronize do
|
|
97
|
-
|
|
98
|
-
|
|
125
|
+
key = agent_id.to_s
|
|
126
|
+
target = (@owner.state[:journals][key] ||= [])
|
|
127
|
+
known_ids = (@owner.state[:journal_record_ids][key] ||= {})
|
|
128
|
+
expected = Integer(expected_position)
|
|
129
|
+
unless target.length == expected
|
|
99
130
|
raise ConflictError,
|
|
100
|
-
"journal position conflict: expected #{
|
|
131
|
+
"journal position conflict: expected #{expected}, actual #{target.length}"
|
|
101
132
|
end
|
|
102
|
-
|
|
133
|
+
|
|
103
134
|
incoming_ids = {}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
raise ConflictError,
|
|
107
|
-
"Journal record Agent mismatch: #{record.agent_id} != #{agent_id}"
|
|
135
|
+
ids.each do |record_id|
|
|
136
|
+
if known_ids[record_id] || incoming_ids[record_id]
|
|
137
|
+
raise ConflictError, "duplicate Journal record_id: #{record_id}"
|
|
108
138
|
end
|
|
109
|
-
|
|
110
|
-
raise ConflictError, "duplicate Journal record_id: #{record.record_id}"
|
|
111
|
-
end
|
|
112
|
-
incoming_ids[record.record_id] = true
|
|
113
|
-
record.with_sequence(expected_position + index + 1)
|
|
139
|
+
incoming_ids[record_id] = true
|
|
114
140
|
end
|
|
115
|
-
|
|
116
|
-
|
|
141
|
+
|
|
142
|
+
stored = encoded.map(&:copy)
|
|
143
|
+
target.concat(stored)
|
|
144
|
+
incoming_ids.each_key { |record_id| known_ids[record_id] = true }
|
|
145
|
+
stored.map(&:copy).freeze
|
|
117
146
|
end
|
|
118
147
|
end
|
|
119
148
|
|
|
@@ -121,8 +150,8 @@ module Phronomy
|
|
|
121
150
|
@owner.synchronize do
|
|
122
151
|
result = Array(@owner.state[:journals][agent_id.to_s])
|
|
123
152
|
result = result.drop(Integer(after)) if after
|
|
124
|
-
result = result.first(limit) if limit
|
|
125
|
-
result.
|
|
153
|
+
result = result.first(Integer(limit)) if limit
|
|
154
|
+
result.map(&:copy).freeze
|
|
126
155
|
end
|
|
127
156
|
end
|
|
128
157
|
|
|
@@ -131,152 +160,478 @@ module Phronomy
|
|
|
131
160
|
end
|
|
132
161
|
|
|
133
162
|
def delete(agent_id)
|
|
134
|
-
@owner.synchronize
|
|
163
|
+
@owner.synchronize do
|
|
164
|
+
key = agent_id.to_s
|
|
165
|
+
@owner.state[:journal_record_ids].delete(key)
|
|
166
|
+
@owner.state[:journals].delete(key)
|
|
167
|
+
end
|
|
135
168
|
end
|
|
136
169
|
end
|
|
137
170
|
|
|
138
171
|
class Executions
|
|
139
172
|
def initialize(owner) = @owner = owner
|
|
140
173
|
|
|
141
|
-
def create_active(
|
|
174
|
+
def create_active(execution_id:, agent_id:, execution_revision:, record:)
|
|
175
|
+
record = @owner.require_durable_record!(record)
|
|
176
|
+
execution_key = execution_id.to_s
|
|
177
|
+
agent_key = agent_id.to_s
|
|
178
|
+
revision = Integer(execution_revision)
|
|
179
|
+
raise ConflictError, "execution_id must not be empty" if execution_key.empty?
|
|
180
|
+
raise ConflictError, "agent_id must not be empty" if agent_key.empty?
|
|
181
|
+
raise ConflictError, "execution_revision must be non-negative" if revision.negative?
|
|
182
|
+
|
|
142
183
|
@owner.synchronize do
|
|
143
|
-
if @owner.state[:executions].key?(
|
|
144
|
-
raise ConflictError, "execution already exists: #{
|
|
184
|
+
if @owner.state[:executions].key?(execution_key)
|
|
185
|
+
raise ConflictError, "execution already exists: #{execution_key}"
|
|
145
186
|
end
|
|
146
|
-
active = @owner.state[:
|
|
147
|
-
|
|
187
|
+
active = @owner.state[:execution_metadata].values.find do |metadata|
|
|
188
|
+
metadata.fetch(:agent_id) == agent_key && metadata.fetch(:active)
|
|
148
189
|
end
|
|
149
|
-
raise Phronomy::AgentBusyError, "agent is busy: #{
|
|
150
|
-
|
|
190
|
+
raise Phronomy::AgentBusyError, "agent is busy: #{agent_key}" if active
|
|
191
|
+
|
|
192
|
+
@owner.state[:executions][execution_key] = record.copy
|
|
193
|
+
@owner.state[:execution_metadata][execution_key] = {
|
|
194
|
+
agent_id: agent_key,
|
|
195
|
+
revision: revision,
|
|
196
|
+
active: true
|
|
197
|
+
}.freeze
|
|
151
198
|
end
|
|
152
|
-
|
|
199
|
+
record.copy
|
|
153
200
|
end
|
|
154
201
|
|
|
155
202
|
def load(execution_id)
|
|
156
203
|
@owner.synchronize do
|
|
157
204
|
@owner.state[:executions].fetch(execution_id.to_s) do
|
|
158
205
|
raise NotFoundError, "execution not found: #{execution_id}"
|
|
159
|
-
end
|
|
206
|
+
end.copy
|
|
160
207
|
end
|
|
161
208
|
end
|
|
162
209
|
|
|
163
|
-
def save(execution_id, expected_revision:,
|
|
210
|
+
def save(execution_id, expected_revision:, next_revision:, agent_id:, active:, record:)
|
|
211
|
+
record = @owner.require_durable_record!(record)
|
|
212
|
+
execution_key = execution_id.to_s
|
|
213
|
+
agent_key = agent_id.to_s
|
|
214
|
+
expected = Integer(expected_revision)
|
|
215
|
+
next_value = Integer(next_revision)
|
|
216
|
+
unless active.equal?(true) || active.equal?(false)
|
|
217
|
+
raise ConflictError, "execution active metadata must be true or false"
|
|
218
|
+
end
|
|
219
|
+
|
|
164
220
|
@owner.synchronize do
|
|
165
|
-
|
|
166
|
-
|
|
221
|
+
unless @owner.state[:executions].key?(execution_key)
|
|
222
|
+
raise NotFoundError, "execution not found: #{execution_id}"
|
|
223
|
+
end
|
|
224
|
+
current = @owner.state[:execution_metadata].fetch(execution_key)
|
|
225
|
+
actual_revision = current.fetch(:revision)
|
|
226
|
+
unless actual_revision == expected
|
|
167
227
|
raise ConflictError,
|
|
168
|
-
"execution revision conflict: expected #{
|
|
228
|
+
"execution revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
169
229
|
end
|
|
170
|
-
unless
|
|
230
|
+
unless current.fetch(:agent_id) == agent_key
|
|
171
231
|
raise ConflictError,
|
|
172
|
-
"Execution identity mismatch: #{
|
|
232
|
+
"Execution Agent identity mismatch: #{agent_key} != #{current.fetch(:agent_id)}"
|
|
173
233
|
end
|
|
174
|
-
unless
|
|
234
|
+
unless next_value == expected + 1
|
|
175
235
|
raise ConflictError,
|
|
176
236
|
"execution save must advance revision exactly once: " \
|
|
177
|
-
"expected #{
|
|
237
|
+
"expected #{expected + 1}, got #{next_value}"
|
|
178
238
|
end
|
|
179
|
-
|
|
239
|
+
|
|
240
|
+
@owner.state[:executions][execution_key] = record.copy
|
|
241
|
+
@owner.state[:execution_metadata][execution_key] = {
|
|
242
|
+
agent_id: agent_key,
|
|
243
|
+
revision: next_value,
|
|
244
|
+
active: active
|
|
245
|
+
}.freeze
|
|
180
246
|
end
|
|
181
|
-
|
|
247
|
+
record.copy
|
|
182
248
|
end
|
|
183
249
|
|
|
184
250
|
def list_active(agent_id)
|
|
185
251
|
@owner.synchronize do
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
252
|
+
agent_key = agent_id.to_s
|
|
253
|
+
ids = @owner.state[:execution_metadata].filter_map do |execution_id, metadata|
|
|
254
|
+
execution_id if metadata.fetch(:agent_id) == agent_key && metadata.fetch(:active)
|
|
255
|
+
end
|
|
256
|
+
ids.map { |execution_id| @owner.state[:executions].fetch(execution_id).copy }.freeze
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def list(agent_id, after: nil, limit: 100)
|
|
261
|
+
@owner.synchronize do
|
|
262
|
+
ids = @owner.state[:execution_metadata].filter_map do |id, metadata|
|
|
263
|
+
id if metadata.fetch(:agent_id) == agent_id.to_s && (!after || id > after.to_s)
|
|
264
|
+
end.sort.first(limit)
|
|
265
|
+
ids.map { |id| @owner.state[:executions].fetch(id).copy }.freeze
|
|
189
266
|
end
|
|
190
267
|
end
|
|
191
268
|
|
|
192
269
|
def delete(execution_id)
|
|
193
|
-
@owner.synchronize
|
|
270
|
+
@owner.synchronize do
|
|
271
|
+
key = execution_id.to_s
|
|
272
|
+
@owner.state[:execution_metadata].delete(key)
|
|
273
|
+
@owner.state[:executions].delete(key)
|
|
274
|
+
end
|
|
194
275
|
end
|
|
195
276
|
|
|
196
277
|
def delete_for_agent(agent_id)
|
|
197
278
|
@owner.synchronize do
|
|
198
|
-
|
|
279
|
+
agent_key = agent_id.to_s
|
|
280
|
+
ids = @owner.state[:execution_metadata].filter_map do |execution_id, metadata|
|
|
281
|
+
execution_id if metadata.fetch(:agent_id) == agent_key
|
|
282
|
+
end
|
|
283
|
+
ids.each do |execution_id|
|
|
284
|
+
@owner.state[:execution_metadata].delete(execution_id)
|
|
285
|
+
@owner.state[:executions].delete(execution_id)
|
|
286
|
+
end
|
|
199
287
|
end
|
|
200
288
|
end
|
|
201
289
|
|
|
202
|
-
# Raises AgentBusyError if there is an active execution for agent_id.
|
|
203
|
-
# Must be called from within a transaction (monitor already held).
|
|
204
290
|
def assert_idle!(agent_id)
|
|
205
|
-
|
|
206
|
-
|
|
291
|
+
@owner.synchronize do
|
|
292
|
+
active = @owner.state[:execution_metadata].values.find do |metadata|
|
|
293
|
+
metadata.fetch(:agent_id) == agent_id.to_s && metadata.fetch(:active)
|
|
294
|
+
end
|
|
295
|
+
if active
|
|
296
|
+
raise Phronomy::AgentBusyError,
|
|
297
|
+
"agent has an active or suspended execution: #{agent_id}"
|
|
298
|
+
end
|
|
207
299
|
end
|
|
208
|
-
|
|
300
|
+
true
|
|
209
301
|
end
|
|
210
302
|
end
|
|
211
303
|
|
|
212
|
-
class
|
|
304
|
+
class Teams
|
|
213
305
|
def initialize(owner) = @owner = owner
|
|
214
306
|
|
|
215
|
-
def
|
|
307
|
+
def create(team_id:, team_revision:, record:)
|
|
308
|
+
record = @owner.require_durable_record!(record)
|
|
309
|
+
key = team_id.to_s
|
|
310
|
+
revision = Integer(team_revision)
|
|
311
|
+
raise ConflictError, "team_id must not be empty" if key.empty?
|
|
312
|
+
raise ConflictError, "team_revision must be non-negative" if revision.negative?
|
|
313
|
+
|
|
216
314
|
@owner.synchronize do
|
|
217
|
-
|
|
218
|
-
|
|
315
|
+
raise ConflictError, "agent already exists: #{key}" if @owner.state[:teams].key?(key)
|
|
316
|
+
@owner.state[:teams][key] = record.copy
|
|
317
|
+
@owner.state[:team_revisions][key] = revision
|
|
318
|
+
end
|
|
319
|
+
record.copy
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
def load(team_id)
|
|
323
|
+
@owner.synchronize do
|
|
324
|
+
@owner.state[:teams].fetch(team_id.to_s) do
|
|
325
|
+
raise NotFoundError, "agent not found: #{team_id}"
|
|
326
|
+
end.copy
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def save(team_id, expected_revision:, next_revision:, record:)
|
|
331
|
+
record = @owner.require_durable_record!(record)
|
|
332
|
+
key = team_id.to_s
|
|
333
|
+
expected = Integer(expected_revision)
|
|
334
|
+
next_value = Integer(next_revision)
|
|
335
|
+
@owner.synchronize do
|
|
336
|
+
unless @owner.state[:teams].key?(key)
|
|
337
|
+
raise NotFoundError, "agent not found: #{team_id}"
|
|
338
|
+
end
|
|
339
|
+
actual_revision = @owner.state[:team_revisions].fetch(key)
|
|
340
|
+
unless actual_revision == expected
|
|
341
|
+
raise ConflictError,
|
|
342
|
+
"agent revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
343
|
+
end
|
|
344
|
+
unless next_value == expected + 1
|
|
345
|
+
raise ConflictError,
|
|
346
|
+
"agent save must advance revision exactly once: " \
|
|
347
|
+
"expected #{expected + 1}, got #{next_value}"
|
|
348
|
+
end
|
|
349
|
+
@owner.state[:teams][key] = record.copy
|
|
350
|
+
@owner.state[:team_revisions][key] = next_value
|
|
351
|
+
end
|
|
352
|
+
record.copy
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def delete(team_id)
|
|
356
|
+
@owner.synchronize do
|
|
357
|
+
key = team_id.to_s
|
|
358
|
+
@owner.state[:team_revisions].delete(key)
|
|
359
|
+
@owner.state[:teams].delete(key)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
class TeamExecutions
|
|
365
|
+
def initialize(owner) = @owner = owner
|
|
366
|
+
|
|
367
|
+
def create_active(team_execution_id:, team_id:, execution_revision:, record:)
|
|
368
|
+
record = @owner.require_durable_record!(record)
|
|
369
|
+
execution_key = team_execution_id.to_s
|
|
370
|
+
agent_key = team_id.to_s
|
|
371
|
+
revision = Integer(execution_revision)
|
|
372
|
+
raise ConflictError, "team_execution_id must not be empty" if execution_key.empty?
|
|
373
|
+
raise ConflictError, "team_id must not be empty" if agent_key.empty?
|
|
374
|
+
raise ConflictError, "execution_revision must be non-negative" if revision.negative?
|
|
219
375
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
376
|
+
@owner.synchronize do
|
|
377
|
+
if @owner.state[:team_executions].key?(execution_key)
|
|
378
|
+
raise ConflictError, "execution already exists: #{execution_key}"
|
|
379
|
+
end
|
|
380
|
+
active = @owner.state[:team_execution_metadata].values.find do |metadata|
|
|
381
|
+
metadata.fetch(:team_id) == agent_key && metadata.fetch(:active)
|
|
382
|
+
end
|
|
383
|
+
raise Phronomy::AgentBusyError, "agent is busy: #{agent_key}" if active
|
|
384
|
+
|
|
385
|
+
@owner.state[:team_executions][execution_key] = record.copy
|
|
386
|
+
@owner.state[:team_execution_metadata][execution_key] = {
|
|
387
|
+
team_id: agent_key,
|
|
388
|
+
revision: revision,
|
|
389
|
+
active: true
|
|
223
390
|
}.freeze
|
|
224
391
|
end
|
|
392
|
+
record.copy
|
|
225
393
|
end
|
|
226
394
|
|
|
227
|
-
def
|
|
395
|
+
def load(team_execution_id)
|
|
228
396
|
@owner.synchronize do
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
397
|
+
@owner.state[:team_executions].fetch(team_execution_id.to_s) do
|
|
398
|
+
raise NotFoundError, "execution not found: #{team_execution_id}"
|
|
399
|
+
end.copy
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def save(team_execution_id, expected_revision:, next_revision:, team_id:, active:, record:)
|
|
404
|
+
record = @owner.require_durable_record!(record)
|
|
405
|
+
execution_key = team_execution_id.to_s
|
|
406
|
+
agent_key = team_id.to_s
|
|
407
|
+
expected = Integer(expected_revision)
|
|
408
|
+
next_value = Integer(next_revision)
|
|
409
|
+
unless active.equal?(true) || active.equal?(false)
|
|
410
|
+
raise ConflictError, "execution active metadata must be true or false"
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
@owner.synchronize do
|
|
414
|
+
unless @owner.state[:team_executions].key?(execution_key)
|
|
415
|
+
raise NotFoundError, "execution not found: #{team_execution_id}"
|
|
416
|
+
end
|
|
417
|
+
current = @owner.state[:team_execution_metadata].fetch(execution_key)
|
|
418
|
+
actual_revision = current.fetch(:revision)
|
|
419
|
+
unless actual_revision == expected
|
|
420
|
+
raise ConflictError,
|
|
421
|
+
"execution revision conflict: expected #{expected}, actual #{actual_revision}"
|
|
422
|
+
end
|
|
423
|
+
unless current.fetch(:team_id) == agent_key
|
|
424
|
+
raise ConflictError,
|
|
425
|
+
"Execution Team identity mismatch: #{agent_key} != #{current.fetch(:team_id)}"
|
|
426
|
+
end
|
|
427
|
+
if active && !current.fetch(:active)
|
|
428
|
+
raise ConflictError, "A terminal Team execution cannot become active"
|
|
429
|
+
end
|
|
430
|
+
unless next_value == expected + 1
|
|
431
|
+
raise ConflictError,
|
|
432
|
+
"execution save must advance revision exactly once: " \
|
|
433
|
+
"expected #{expected + 1}, got #{next_value}"
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
@owner.state[:team_executions][execution_key] = record.copy
|
|
437
|
+
@owner.state[:team_execution_metadata][execution_key] = {
|
|
438
|
+
team_id: agent_key,
|
|
439
|
+
revision: next_value,
|
|
440
|
+
active: active
|
|
441
|
+
}.freeze
|
|
442
|
+
end
|
|
443
|
+
record.copy
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def list_active(team_id)
|
|
447
|
+
@owner.synchronize do
|
|
448
|
+
agent_key = team_id.to_s
|
|
449
|
+
ids = @owner.state[:team_execution_metadata].filter_map do |team_execution_id, metadata|
|
|
450
|
+
team_execution_id if metadata.fetch(:team_id) == agent_key && metadata.fetch(:active)
|
|
451
|
+
end
|
|
452
|
+
ids.map { |team_execution_id| @owner.state[:team_executions].fetch(team_execution_id).copy }.freeze
|
|
453
|
+
end
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
def list(team_id, after: nil, limit: 100)
|
|
457
|
+
@owner.synchronize do
|
|
458
|
+
ids = @owner.state[:team_execution_metadata].filter_map do |id, metadata|
|
|
459
|
+
id if metadata.fetch(:team_id) == team_id.to_s && (!after || id > after.to_s)
|
|
460
|
+
end.sort.first(limit)
|
|
461
|
+
ids.map { |id| @owner.state[:team_executions].fetch(id).copy }.freeze
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def delete(team_execution_id)
|
|
466
|
+
@owner.synchronize do
|
|
467
|
+
key = team_execution_id.to_s
|
|
468
|
+
@owner.state[:team_execution_metadata].delete(key)
|
|
469
|
+
@owner.state[:team_executions].delete(key)
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
|
|
473
|
+
def delete_for_team(team_id)
|
|
474
|
+
@owner.synchronize do
|
|
475
|
+
agent_key = team_id.to_s
|
|
476
|
+
ids = @owner.state[:team_execution_metadata].filter_map do |team_execution_id, metadata|
|
|
477
|
+
team_execution_id if metadata.fetch(:team_id) == agent_key
|
|
478
|
+
end
|
|
479
|
+
ids.each do |team_execution_id|
|
|
480
|
+
@owner.state[:team_execution_metadata].delete(team_execution_id)
|
|
481
|
+
@owner.state[:team_executions].delete(team_execution_id)
|
|
482
|
+
end
|
|
483
|
+
end
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
def assert_idle!(team_id)
|
|
487
|
+
@owner.synchronize do
|
|
488
|
+
active = @owner.state[:team_execution_metadata].values.find do |metadata|
|
|
489
|
+
metadata.fetch(:team_id) == team_id.to_s && metadata.fetch(:active)
|
|
490
|
+
end
|
|
491
|
+
if active
|
|
492
|
+
raise Phronomy::AgentBusyError,
|
|
493
|
+
"agent has an active or suspended execution: #{team_id}"
|
|
494
|
+
end
|
|
495
|
+
end
|
|
496
|
+
true
|
|
497
|
+
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
class HandoffStates
|
|
501
|
+
def initialize(owner) = @owner = owner
|
|
502
|
+
|
|
503
|
+
def load(main_agent_id)
|
|
504
|
+
@owner.synchronize do
|
|
505
|
+
record = @owner.state[:handoff_states][main_agent_id.to_s]
|
|
506
|
+
record&.copy
|
|
507
|
+
end
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def save(main_agent_id, expected_revision:, next_revision:, active_agent_id:, record:)
|
|
511
|
+
record = @owner.require_durable_record!(record)
|
|
512
|
+
raise ArgumentError, "active_agent_id is required" if active_agent_id.to_s.empty?
|
|
513
|
+
key = main_agent_id.to_s
|
|
514
|
+
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
515
|
+
next_value = Integer(next_revision)
|
|
516
|
+
@owner.synchronize do
|
|
517
|
+
actual_revision = @owner.state[:handoff_revisions][key]
|
|
518
|
+
unless actual_revision == expected
|
|
233
519
|
raise ConflictError,
|
|
234
520
|
"workflow state revision conflict for #{key}: " \
|
|
235
|
-
"expected #{
|
|
521
|
+
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
522
|
+
end
|
|
523
|
+
expected_next = expected.nil? ? 1 : expected + 1
|
|
524
|
+
unless next_value == expected_next
|
|
525
|
+
raise ConflictError,
|
|
526
|
+
"workflow state save must advance revision exactly once: " \
|
|
527
|
+
"expected #{expected_next}, got #{next_value}"
|
|
236
528
|
end
|
|
237
529
|
|
|
238
|
-
|
|
239
|
-
@owner.
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
530
|
+
@owner.state[:handoff_states][key] = record.copy
|
|
531
|
+
@owner.state[:handoff_revisions][key] = next_value
|
|
532
|
+
end
|
|
533
|
+
record.copy
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def delete(main_agent_id, expected_revision:)
|
|
537
|
+
@owner.synchronize do
|
|
538
|
+
key = main_agent_id.to_s
|
|
539
|
+
expected = Integer(expected_revision)
|
|
540
|
+
actual_revision = @owner.state[:handoff_revisions][key]
|
|
541
|
+
unless actual_revision == expected
|
|
542
|
+
raise ConflictError,
|
|
543
|
+
"workflow state revision conflict for #{key}: " \
|
|
544
|
+
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
545
|
+
end
|
|
546
|
+
@owner.state[:handoff_revisions].delete(key)
|
|
547
|
+
@owner.state[:handoff_states].delete(key)
|
|
548
|
+
end
|
|
549
|
+
nil
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
class WorkflowStates
|
|
554
|
+
def initialize(owner) = @owner = owner
|
|
555
|
+
|
|
556
|
+
def load(workflow_instance_id)
|
|
557
|
+
@owner.synchronize do
|
|
558
|
+
record = @owner.state[:workflow_states][workflow_instance_id.to_s]
|
|
559
|
+
record&.copy
|
|
244
560
|
end
|
|
245
561
|
end
|
|
246
562
|
|
|
247
|
-
def
|
|
563
|
+
def save(workflow_instance_id, expected_revision:, next_revision:, record:)
|
|
564
|
+
record = @owner.require_durable_record!(record)
|
|
565
|
+
key = workflow_instance_id.to_s
|
|
566
|
+
expected = expected_revision.nil? ? nil : Integer(expected_revision)
|
|
567
|
+
next_value = Integer(next_revision)
|
|
248
568
|
@owner.synchronize do
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
actual_revision = current&.fetch(:revision)
|
|
252
|
-
unless actual_revision == expected_revision
|
|
569
|
+
actual_revision = @owner.state[:workflow_revisions][key]
|
|
570
|
+
unless actual_revision == expected
|
|
253
571
|
raise ConflictError,
|
|
254
572
|
"workflow state revision conflict for #{key}: " \
|
|
255
|
-
"expected #{
|
|
573
|
+
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
574
|
+
end
|
|
575
|
+
expected_next = expected.nil? ? 1 : expected + 1
|
|
576
|
+
unless next_value == expected_next
|
|
577
|
+
raise ConflictError,
|
|
578
|
+
"workflow state save must advance revision exactly once: " \
|
|
579
|
+
"expected #{expected_next}, got #{next_value}"
|
|
256
580
|
end
|
|
257
|
-
|
|
581
|
+
|
|
582
|
+
@owner.state[:workflow_states][key] = record.copy
|
|
583
|
+
@owner.state[:workflow_revisions][key] = next_value
|
|
584
|
+
end
|
|
585
|
+
record.copy
|
|
586
|
+
end
|
|
587
|
+
|
|
588
|
+
def delete(workflow_instance_id, expected_revision:)
|
|
589
|
+
@owner.synchronize do
|
|
590
|
+
key = workflow_instance_id.to_s
|
|
591
|
+
expected = Integer(expected_revision)
|
|
592
|
+
actual_revision = @owner.state[:workflow_revisions][key]
|
|
593
|
+
unless actual_revision == expected
|
|
594
|
+
raise ConflictError,
|
|
595
|
+
"workflow state revision conflict for #{key}: " \
|
|
596
|
+
"expected #{expected.inspect}, actual #{actual_revision.inspect}"
|
|
597
|
+
end
|
|
598
|
+
@owner.state[:workflow_revisions].delete(key)
|
|
599
|
+
@owner.state[:workflow_states].delete(key)
|
|
258
600
|
end
|
|
259
601
|
nil
|
|
260
602
|
end
|
|
261
603
|
end
|
|
262
604
|
|
|
263
|
-
attr_reader :state
|
|
605
|
+
attr_reader :state
|
|
264
606
|
|
|
265
607
|
def initialize
|
|
266
608
|
@monitor = Monitor.new
|
|
267
|
-
@state = {
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
609
|
+
@state = {
|
|
610
|
+
contents: {},
|
|
611
|
+
agents: {},
|
|
612
|
+
agent_revisions: {},
|
|
613
|
+
journals: {},
|
|
614
|
+
journal_record_ids: {},
|
|
615
|
+
executions: {},
|
|
616
|
+
execution_metadata: {},
|
|
617
|
+
workflow_states: {},
|
|
618
|
+
workflow_revisions: {},
|
|
619
|
+
handoff_states: {}, handoff_revisions: {},
|
|
620
|
+
teams: {}, team_revisions: {}, team_executions: {}, team_execution_metadata: {}
|
|
621
|
+
}
|
|
622
|
+
@contents_backend = Contents.new(self)
|
|
623
|
+
@agents_backend = Agents.new(self)
|
|
624
|
+
@journals_backend = Journals.new(self)
|
|
625
|
+
@executions_backend = Executions.new(self)
|
|
626
|
+
@workflow_states_backend = WorkflowStates.new(self)
|
|
274
627
|
super(
|
|
275
|
-
contents: @
|
|
276
|
-
agents: @
|
|
277
|
-
journals: @
|
|
278
|
-
executions: @
|
|
279
|
-
workflow_states: @
|
|
628
|
+
contents: @contents_backend,
|
|
629
|
+
agents: @agents_backend,
|
|
630
|
+
journals: @journals_backend,
|
|
631
|
+
executions: @executions_backend,
|
|
632
|
+
workflow_states: @workflow_states_backend,
|
|
633
|
+
handoff_states: HandoffStates.new(self),
|
|
634
|
+
teams: Teams.new(self), team_executions: TeamExecutions.new(self)
|
|
280
635
|
)
|
|
281
636
|
end
|
|
282
637
|
|
|
@@ -286,17 +641,18 @@ module Phronomy
|
|
|
286
641
|
|
|
287
642
|
def assert_agent_watermark!(agent_id:, agent_revision:, journal_position:)
|
|
288
643
|
synchronize do
|
|
289
|
-
|
|
290
|
-
unless
|
|
644
|
+
key = agent_id.to_s
|
|
645
|
+
unless @state[:agents].key?(key)
|
|
291
646
|
raise NotFoundError, "Agent not found: #{agent_id}"
|
|
292
647
|
end
|
|
293
648
|
|
|
294
|
-
|
|
649
|
+
actual_revision = @state[:agent_revisions].fetch(key)
|
|
650
|
+
if actual_revision != agent_revision
|
|
295
651
|
raise ConflictError,
|
|
296
|
-
"agent revision conflict: expected #{agent_revision}, actual #{
|
|
652
|
+
"agent revision conflict: expected #{agent_revision}, actual #{actual_revision}"
|
|
297
653
|
end
|
|
298
654
|
|
|
299
|
-
actual_position = Array(@state[:journals][
|
|
655
|
+
actual_position = Array(@state[:journals][key]).length
|
|
300
656
|
if actual_position != journal_position
|
|
301
657
|
raise ConflictError,
|
|
302
658
|
"journal position conflict: expected #{journal_position}, actual #{actual_position}"
|
|
@@ -309,12 +665,10 @@ module Phronomy
|
|
|
309
665
|
def transaction
|
|
310
666
|
synchronize do
|
|
311
667
|
state_snapshot = Marshal.load(Marshal.dump(@state))
|
|
312
|
-
workflow_snapshot = deep_dup_workflow_value(@workflow_state_data)
|
|
313
668
|
begin
|
|
314
669
|
yield self
|
|
315
670
|
rescue
|
|
316
671
|
@state.replace(state_snapshot)
|
|
317
|
-
@workflow_state_data.replace(workflow_snapshot)
|
|
318
672
|
raise
|
|
319
673
|
end
|
|
320
674
|
end
|
|
@@ -324,28 +678,12 @@ module Phronomy
|
|
|
324
678
|
@monitor.synchronize(&block)
|
|
325
679
|
end
|
|
326
680
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
def deep_dup_workflow_value(value)
|
|
331
|
-
case value
|
|
332
|
-
when Hash
|
|
333
|
-
value.each_with_object({}) do |(key, child), result|
|
|
334
|
-
result[deep_dup_workflow_value(key)] = deep_dup_workflow_value(child)
|
|
335
|
-
end
|
|
336
|
-
when Array
|
|
337
|
-
value.map { |child| deep_dup_workflow_value(child) }
|
|
338
|
-
when NilClass, Symbol, Integer, Float, TrueClass, FalseClass
|
|
339
|
-
value
|
|
340
|
-
else
|
|
341
|
-
return value if value.frozen?
|
|
681
|
+
# @api private
|
|
682
|
+
def require_durable_record!(record)
|
|
683
|
+
return record if record.is_a?(Phronomy::Persistence::DurableRecord)
|
|
342
684
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
rescue TypeError
|
|
346
|
-
value
|
|
347
|
-
end
|
|
348
|
-
end
|
|
685
|
+
raise Phronomy::Persistence::SerializationError,
|
|
686
|
+
"backend repository expected Persistence::DurableRecord, got #{record.class}"
|
|
349
687
|
end
|
|
350
688
|
end
|
|
351
689
|
end
|