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.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.mutant.yml +3 -4
  3. data/CHANGELOG.md +200 -10
  4. data/CONTRIBUTING.md +81 -9
  5. data/README.md +15 -6
  6. data/VERIFY.sh +587 -0
  7. data/benchmark/bench_agent_invoke.rb +2 -2
  8. data/benchmark/bench_context_assembler.rb +39 -68
  9. data/benchmark/bench_regression.rb +2 -2
  10. data/docs/architecture/agent-context.md +174 -0
  11. data/docs/architecture/before-llm-input.md +78 -0
  12. data/docs/architecture/context-management.md +232 -0
  13. data/docs/architecture/knowledge-and-rag.md +130 -0
  14. data/docs/architecture/multi-agent-handoff.md +152 -0
  15. data/docs/architecture/persistence.md +175 -0
  16. data/docs/architecture/removed/agent-context.md +72 -0
  17. data/docs/architecture/security-boundaries.md +173 -0
  18. data/docs/architecture/tracing.md +194 -0
  19. data/docs/architecture.md +82 -0
  20. data/docs/archive/design/archived/04_api_design.md +507 -0
  21. data/docs/archive/design/archived/09_guardrails.md +186 -0
  22. data/docs/archive/design/archived/17_rails_integration.md +175 -0
  23. data/docs/archive/design/historical/00_design_philosophy.md +122 -0
  24. data/docs/archive/design/historical/01_rubyllm_evaluation.md +178 -0
  25. data/docs/archive/design/historical/06_design_decisions.md +143 -0
  26. data/docs/changelog/0.14-and-earlier.md +1 -1
  27. data/docs/decisions/001-rubyllm-as-provider-layer.md +6 -1
  28. data/docs/decisions/002-workflow-context-immutability.md +26 -1
  29. data/docs/decisions/006-no-built-in-guardrails.md +2 -1
  30. data/docs/decisions/012-canonical-execution-log-and-context-policy.md +120 -38
  31. data/docs/decisions/014-unified-persistence-durable-state.md +9 -2
  32. data/docs/decisions/016-semantic-multi-agent-handoff.md +112 -0
  33. data/docs/decisions/017-design-authority-and-adr-governance.md +200 -0
  34. data/docs/decisions/018-durability-guarantees-and-failure-model.md +488 -0
  35. data/docs/decisions/019-filter-contract-and-security-boundaries.md +229 -0
  36. data/docs/decisions/020-canonical-workflow-instance-identity.md +177 -0
  37. data/docs/decisions/021-generic-agent-invocation-identity-removal.md +119 -0
  38. data/docs/decisions/022-agent-execution-parent-identity-and-runtime-routing-boundary.md +193 -0
  39. data/docs/decisions/023-fsm-session-incarnation-identity-and-routing.md +139 -0
  40. data/docs/decisions/024-event-loop-single-writer-agent-runtime.md +188 -0
  41. data/docs/decisions/025-process-local-agent-ownership-and-runtime-admission.md +249 -0
  42. data/docs/decisions/026-workflow-runtime-admission-and-durable-terminal-barrier.md +257 -0
  43. data/docs/decisions/027-llm-adapter-provider-boundary.md +93 -0
  44. data/docs/decisions/README.md +172 -0
  45. data/docs/features.md +31 -11
  46. data/docs/getting-started.md +77 -45
  47. data/docs/migrations/0.19.md +14 -7
  48. data/docs/migrations/0.22.md +390 -0
  49. data/docs/persistence-backends.md +88 -38
  50. data/docs/runtime-and-concurrency.md +227 -33
  51. data/examples/README.md +13 -0
  52. data/lib/phronomy/agent/agent_execution.rb +19 -15
  53. data/lib/phronomy/agent/agent_invocation.rb +288 -93
  54. data/lib/phronomy/agent/agent_invocation_session_builder.rb +236 -202
  55. data/lib/phronomy/agent/agent_root.rb +3 -3
  56. data/lib/phronomy/agent/approval_evaluation_request.rb +37 -19
  57. data/lib/phronomy/agent/async_event_api.rb +145 -72
  58. data/lib/phronomy/agent/base.rb +388 -181
  59. data/lib/phronomy/agent/concerns/before_llm_input.rb +1 -1
  60. data/lib/phronomy/agent/context_assembler.rb +437 -178
  61. data/lib/phronomy/agent/context_candidate_resolver.rb +2 -2
  62. data/lib/phronomy/agent/context_plan.rb +18 -13
  63. data/lib/phronomy/agent/context_plan_validator.rb +246 -88
  64. data/lib/phronomy/agent/context_policies/default.rb +123 -34
  65. data/lib/phronomy/agent/context_policy.rb +109 -3
  66. data/lib/phronomy/agent/context_policy_input.rb +244 -0
  67. data/lib/phronomy/agent/context_policy_input_builder.rb +241 -0
  68. data/lib/phronomy/agent/execution_coordinator.rb +1975 -587
  69. data/lib/phronomy/agent/journal_record.rb +17 -4
  70. data/lib/phronomy/agent/llm_input_build_context.rb +1 -1
  71. data/lib/phronomy/agent/llm_input_manifest.rb +277 -2
  72. data/lib/phronomy/agent/llm_operation_result.rb +12 -7
  73. data/lib/phronomy/agent/phase_machine_builder.rb +19 -7
  74. data/lib/phronomy/agent/provider_call_outcome.rb +23 -7
  75. data/lib/phronomy/agent/recovery_coordinator/continuation.rb +271 -0
  76. data/lib/phronomy/agent/recovery_coordinator/installation.rb +427 -0
  77. data/lib/phronomy/agent/recovery_coordinator/resolution.rb +635 -0
  78. data/lib/phronomy/agent/recovery_coordinator.rb +211 -0
  79. data/lib/phronomy/agent/recovery_support.rb +512 -0
  80. data/lib/phronomy/agent/ruby_llm_materializer.rb +22 -13
  81. data/lib/phronomy/agent/selection/candidate.rb +53 -0
  82. data/lib/phronomy/agent/selection/constraint.rb +49 -0
  83. data/lib/phronomy/agent/shared_state.rb +38 -1
  84. data/lib/phronomy/agent/tool_approval_request.rb +33 -5
  85. data/lib/phronomy/agent/tool_definition_set.rb +49 -3
  86. data/lib/phronomy/agent/tool_invocation.rb +336 -102
  87. data/lib/phronomy/agent/tool_invocation_session_builder.rb +49 -45
  88. data/lib/phronomy/agent.rb +20 -2
  89. data/lib/phronomy/agent_already_exists_error.rb +5 -0
  90. data/lib/phronomy/agent_purged_error.rb +5 -0
  91. data/lib/phronomy/engine/concurrency/offload_pool.rb +17 -3
  92. data/lib/phronomy/engine/concurrency/physical_completion_task.rb +135 -0
  93. data/lib/phronomy/engine/event_loop.rb +622 -63
  94. data/lib/phronomy/engine/fsm_session.rb +194 -21
  95. data/lib/phronomy/engine/runtime/agent_ownership_registry.rb +352 -0
  96. data/lib/phronomy/engine/runtime.rb +77 -20
  97. data/lib/phronomy/generator_verifier.rb +12 -14
  98. data/lib/phronomy/invocation_context.rb +9 -29
  99. data/lib/phronomy/multi_agent/admission_registry.rb +51 -0
  100. data/lib/phronomy/multi_agent/coordination_state.rb +18 -0
  101. data/lib/phronomy/multi_agent/coordinator.rb +154 -0
  102. data/lib/phronomy/multi_agent/execution_coordinator.rb +116 -0
  103. data/lib/phronomy/multi_agent/fan_out_invocation.rb +24 -33
  104. data/lib/phronomy/multi_agent/fan_out_session_builder.rb +12 -19
  105. data/lib/phronomy/multi_agent/handoff.rb +24 -45
  106. data/lib/phronomy/multi_agent/handoff_capability_factory.rb +87 -0
  107. data/lib/phronomy/multi_agent/handoff_context.rb +95 -0
  108. data/lib/phronomy/multi_agent/handoff_policy.rb +137 -0
  109. data/lib/phronomy/multi_agent/handoff_projection.rb +191 -0
  110. data/lib/phronomy/multi_agent/handoff_request.rb +45 -0
  111. data/lib/phronomy/multi_agent/orchestrator.rb +12 -15
  112. data/lib/phronomy/multi_agent/runner.rb +98 -0
  113. data/lib/phronomy/persistence/durable_codec.rb +646 -0
  114. data/lib/phronomy/persistence/durable_record.rb +117 -0
  115. data/lib/phronomy/persistence/in_memory.rb +210 -134
  116. data/lib/phronomy/persistence/migration/initial_format_migration.rb +226 -0
  117. data/lib/phronomy/persistence/repository_facades.rb +316 -0
  118. data/lib/phronomy/persistence.rb +81 -41
  119. data/lib/phronomy/recovery.rb +186 -0
  120. data/lib/phronomy/testing/persistence_contract/a_journal_repository.rb +2 -2
  121. data/lib/phronomy/testing/persistence_contract/a_persistence_backend.rb +1 -1
  122. data/lib/phronomy/testing/persistence_contract/a_workflow_state_repository.rb +19 -19
  123. data/lib/phronomy/testing/persistence_contract/an_agent_repository.rb +3 -3
  124. data/lib/phronomy/testing/persistence_contract/an_execution_repository.rb +5 -5
  125. data/lib/phronomy/tracing/automatic.rb +176 -0
  126. data/lib/phronomy/tracing/base.rb +11 -2
  127. data/lib/phronomy/tracing/langfuse_tracer.rb +20 -12
  128. data/lib/phronomy/version.rb +1 -1
  129. data/lib/phronomy/workflow.rb +3 -6
  130. data/lib/phronomy/workflow_context.rb +14 -5
  131. data/lib/phronomy/workflow_recovery.rb +123 -0
  132. data/lib/phronomy/workflow_runner.rb +468 -256
  133. data/lib/phronomy.rb +6 -0
  134. data/scripts/api_snapshot.rb +12 -0
  135. data/sig/phronomy/agent.rbs +209 -7
  136. data/sig/phronomy/multi_agent.rbs +39 -0
  137. data/sig/phronomy/persistence.rbs +62 -4
  138. data/sig/phronomy/runtime.rbs +1 -4
  139. data/sig/phronomy/workflow.rbs +2 -2
  140. data/sig/phronomy.rbs +10 -0
  141. metadata +65 -17
  142. data/examples/workflows/agent_event_mapping.rb +0 -101
  143. data/examples/workflows/generic_task_event_mapping.rb +0 -66
  144. data/lib/phronomy/agent/activation_registry.rb +0 -28
  145. data/lib/phronomy/agent/agent_execution_activation.rb +0 -172
  146. data/lib/phronomy/agent/context_candidate.rb +0 -47
  147. data/lib/phronomy/agent/context_parts/budget/token_budget_packer.rb +0 -53
  148. data/lib/phronomy/agent/context_parts/requirements/required_context_resolver.rb +0 -56
  149. data/lib/phronomy/agent/context_parts/selectors/recent_first_selector.rb +0 -30
  150. data/lib/phronomy/agent/context_parts/unit_builders/dependency_aware_unit_builder.rb +0 -118
  151. data/lib/phronomy/agent/context_policy_descriptor.rb +0 -49
  152. data/lib/phronomy/agent/context_policy_registry.rb +0 -46
  153. data/lib/phronomy/agent/context_request.rb +0 -35
  154. data/lib/phronomy/agent/context_selection_unit.rb +0 -38
  155. data/lib/phronomy/agent/derived_content_spec.rb +0 -34
  156. data/lib/phronomy/agent/runner.rb +0 -97
@@ -13,51 +13,73 @@ module Phronomy
13
13
  optimistic_revision: true
14
14
  }.freeze
15
15
 
16
- # Durable repository accessors supplied by a Persistence backend.
16
+ # Content-addressed immutable content repository. ContentStore has its own
17
+ # codec/canonicalization boundary and is not wrapped in DurableRecord.
17
18
  #
18
- # The repository objects are part of the Backend SPI. They may be private
19
- # implementation classes owned by the backend; they do not need to inherit
20
- # from Phronomy repository base classes.
21
- #
22
- # @return [Object] content-addressed immutable content repository
19
+ # @return [Object]
23
20
  # @api public
24
21
  attr_reader :contents
25
22
 
26
- # @return [Object] AgentRoot repository
23
+ # Runtime/domain-facing AgentRoot repository. The backend repository supplied
24
+ # to #initialize is record-oriented; this facade owns encode/decode.
25
+ #
26
+ # @return [Object]
27
27
  # @api public
28
28
  attr_reader :agents
29
29
 
30
- # @return [Object] append-only Agent Journal repository
30
+ # Runtime/domain-facing append-only Agent Journal repository.
31
+ #
32
+ # @return [Object]
31
33
  # @api public
32
34
  attr_reader :journals
33
35
 
34
- # @return [Object] AgentExecution repository
36
+ # Runtime/domain-facing AgentExecution repository.
37
+ #
38
+ # @return [Object]
35
39
  # @api public
36
40
  attr_reader :executions
37
41
 
38
- # @return [Object] durable Workflow snapshot repository
42
+ # Runtime/domain-facing durable Workflow snapshot repository.
43
+ #
44
+ # @return [Object]
39
45
  # @api public
40
46
  attr_reader :workflow_states
41
47
 
42
- # Initializes a Persistence backend with its durable repositories.
43
- #
44
- # Subclasses normally construct backend-specific repository objects and then
45
- # call +super+. The backend must advertise every capability in
46
- # {REQUIRED_CAPABILITIES}; construction fails fast otherwise.
47
- #
48
- # @param contents [Object]
49
- # @param agents [Object]
50
- # @param journals [Object]
51
- # @param executions [Object]
52
- # @param workflow_states [Object]
53
- # @raise [UnsupportedBackendError] when a required capability is missing
48
+ # Initializes a Persistence backend from record-oriented storage
49
+ # repositories.
50
+ #
51
+ # Except for +contents+, backend repositories exchange
52
+ # {Phronomy::Persistence::DurableRecord} values. Phronomy's repository
53
+ # facades own current-format validation and domain-object encode/decode.
54
+ #
55
+ # Identity, revision, admission, and index metadata needed by a Backend is
56
+ # passed explicitly as repository arguments. A Backend must not inspect
57
+ # DurableRecord#payload to rediscover Phronomy domain semantics.
58
+ #
59
+ # Required raw repository shapes:
60
+ # - agents:
61
+ # create(agent_id:, agent_revision:, record:), load(id),
62
+ # save(id, expected_revision:, next_revision:, record:), delete(id)
63
+ # - journals:
64
+ # append(id, expected_position:, records:, record_ids:), read/head/delete
65
+ # - executions:
66
+ # create_active(execution_id:, agent_id:, execution_revision:, record:),
67
+ # load(id), save(id, expected_revision:, next_revision:, agent_id:,
68
+ # active:, record:), list_active/delete/delete_for_agent/assert_idle!
69
+ # - workflow_states:
70
+ # load(id), save(id, expected_revision:, next_revision:, record:), delete
71
+ #
72
+ # Subclasses normally construct backend-specific raw repository objects and
73
+ # call +super+. Construction fails fast when required capabilities are not
74
+ # advertised.
75
+ #
54
76
  # @api public
55
77
  def initialize(contents:, agents:, journals:, executions:, workflow_states:)
56
78
  @contents = contents
57
- @agents = agents
58
- @journals = journals
59
- @executions = executions
60
- @workflow_states = workflow_states
79
+ @agents = RepositoryFacades::Agents.new(agents)
80
+ @journals = RepositoryFacades::Journals.new(journals)
81
+ @executions = RepositoryFacades::Executions.new(executions)
82
+ @workflow_states = RepositoryFacades::WorkflowStates.new(workflow_states)
61
83
  validate_capabilities!
62
84
  end
63
85
 
@@ -84,11 +106,9 @@ module Phronomy
84
106
 
85
107
  # Executes one atomic durable transaction.
86
108
  #
87
- # The object yielded to the block is a transaction-scoped Persistence view.
88
- # It must respond to +contents+, +agents+, +journals+, +executions+,
89
- # +workflow_states+, and +assert_agent_watermark!+. It may be +self+, but
90
- # backends are free to yield a separate transaction view backed by a checked
91
- # out connection/session.
109
+ # The yielded object is a transaction-scoped Persistence view exposing the
110
+ # same domain-facing repository facades. Its underlying backend repositories
111
+ # remain DurableRecord-oriented.
92
112
  #
93
113
  # If the block raises, mutations made through the transaction view must not
94
114
  # be committed. Storage failures whose commit outcome is fundamentally
@@ -103,6 +123,36 @@ module Phronomy
103
123
  raise UnsupportedBackendError, "#{self.class} does not provide atomic_all"
104
124
  end
105
125
 
126
+ # Backend SPI helper for transaction implementations whose transaction-scoped
127
+ # raw repositories differ from the root repository objects.
128
+ #
129
+ # +watermark+ is a transaction-scoped object responding to
130
+ # +assert_agent_watermark!+. The returned view owns the same Phronomy codec
131
+ # facades as the root Persistence instance, so backend authors never need to
132
+ # instantiate RepositoryFacades directly.
133
+ #
134
+ # Backends whose repository objects are already transaction-scoped may yield
135
+ # +self+ and need not call this helper.
136
+ #
137
+ # @api public
138
+ def build_transaction_view(
139
+ contents:,
140
+ agents:,
141
+ journals:,
142
+ executions:,
143
+ workflow_states:,
144
+ watermark:
145
+ )
146
+ RepositoryFacades::View.new(
147
+ contents: contents,
148
+ agents: agents,
149
+ journals: journals,
150
+ executions: executions,
151
+ workflow_states: workflow_states,
152
+ watermark: watermark
153
+ )
154
+ end
155
+
106
156
  # Verifies that a live Agent still owns the durable base it hydrated.
107
157
  #
108
158
  # This is a Backend SPI operation invoked by Phronomy at durable barriers.
@@ -111,16 +161,6 @@ module Phronomy
111
161
  # supplied watermark in the same storage consistency view used by subsequent
112
162
  # writes in the surrounding transaction.
113
163
  #
114
- # The method is a precondition check only. It must not reload or return
115
- # replacement mutable Agent state; the live Agent remains the logical owner.
116
- #
117
- # @param agent_id [String]
118
- # @param agent_revision [Integer]
119
- # @param journal_position [Integer]
120
- # @return [true]
121
- # @raise [NotFoundError] when the Agent does not exist
122
- # @raise [ConflictError] when either durable watermark component differs
123
- # @raise [UnsupportedBackendError] when the backend does not implement the check
124
164
  # @api public
125
165
  def assert_agent_watermark!(agent_id:, agent_revision:, journal_position:)
126
166
  raise UnsupportedBackendError,
@@ -0,0 +1,186 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ # Small private Recovery semantic/primitives layer shared by Agent and
5
+ # Workflow recovery. Entity-specific orchestration remains outside this module.
6
+ # @api private
7
+ module Recovery
8
+ RESUMABLE = :resumable
9
+ RECONCILABLE = :reconcilable
10
+ RESOLUTION_REQUIRED = :resolution_required
11
+ DISPOSITIONS = [RESUMABLE, RECONCILABLE, RESOLUTION_REQUIRED].freeze
12
+
13
+ ALLOWED_OUTCOMES = %i[succeeded failed not_performed].freeze
14
+ # Internal shorthand used by the Agent Recovery implementation. This is not
15
+ # a public API constant; both names refer to the same frozen value.
16
+ OUTCOMES = ALLOWED_OUTCOMES
17
+
18
+ MISSING = Object.new.freeze
19
+
20
+ Classification = Data.define(
21
+ :disposition, :reason, :subject, :allowed_outcomes, :facts
22
+ ) do
23
+ def initialize(
24
+ disposition:,
25
+ reason:,
26
+ subject: nil,
27
+ allowed_outcomes: [],
28
+ facts: {}
29
+ )
30
+ normalized = disposition.to_sym
31
+ unless Phronomy::Recovery::DISPOSITIONS.include?(normalized)
32
+ raise ArgumentError, "unknown Recovery disposition: #{disposition.inspect}"
33
+ end
34
+ super(
35
+ disposition: normalized,
36
+ reason: reason.to_sym,
37
+ subject: subject && Phronomy::Recovery.normalize_subject(subject),
38
+ allowed_outcomes: Array(allowed_outcomes).map(&:to_sym).freeze,
39
+ facts: Phronomy::Agent::Immutable.copy(facts || {})
40
+ )
41
+ end
42
+ end
43
+
44
+ module_function
45
+
46
+ def normalize_outcome(outcome)
47
+ normalized = outcome.to_sym
48
+ unless OUTCOMES.include?(normalized)
49
+ raise ArgumentError, "unsupported Recovery outcome: #{outcome.inspect}"
50
+ end
51
+ normalized
52
+ end
53
+
54
+ def validate_resolution_material!(outcome:, result_present:, error_present:)
55
+ normalized = normalize_outcome(outcome)
56
+ case normalized
57
+ when :succeeded
58
+ raise ArgumentError, "Recovery :succeeded requires result:" unless result_present
59
+ raise ArgumentError, "Recovery :succeeded forbids error:" if error_present
60
+ when :failed
61
+ raise ArgumentError, "Recovery :failed requires error:" unless error_present
62
+ raise ArgumentError, "Recovery :failed forbids result:" if result_present
63
+ when :not_performed
64
+ if result_present || error_present
65
+ raise ArgumentError, "Recovery :not_performed accepts neither result: nor error:"
66
+ end
67
+ end
68
+ true
69
+ end
70
+
71
+ def normalize_subject(subject)
72
+ hash = subject.to_h.transform_keys(&:to_sym)
73
+ type = hash.fetch(:type).to_sym
74
+ normalized = {type: type}
75
+ case type
76
+ when :llm_call
77
+ normalized[:llm_call_id] = required_id(hash, :llm_call_id)
78
+ when :tool_invocation
79
+ normalized[:tool_invocation_id] = required_id(hash, :tool_invocation_id)
80
+ when :persistence_operation
81
+ # Persistence operation references are structural only. Do not introduce
82
+ # a generic durable operation/recovery identity here.
83
+ normalized[:entity] = hash.fetch(:entity).to_sym
84
+ else
85
+ raise ArgumentError, "unsupported Recovery subject type: #{type.inspect}"
86
+ end
87
+ normalized.freeze
88
+ end
89
+
90
+ def subject_key(subject)
91
+ normalized = normalize_subject(subject)
92
+ case normalized.fetch(:type)
93
+ when :llm_call
94
+ "llm_call:#{normalized.fetch(:llm_call_id)}"
95
+ when :tool_invocation
96
+ "tool_invocation:#{normalized.fetch(:tool_invocation_id)}"
97
+ when :persistence_operation
98
+ "persistence_operation:#{normalized.fetch(:entity)}"
99
+ end
100
+ end
101
+
102
+ def subject_equal?(left, right)
103
+ normalize_subject(left) == normalize_subject(right)
104
+ rescue ArgumentError, KeyError
105
+ false
106
+ end
107
+
108
+ # Compare authoritative current state with a known pre/post revision pair.
109
+ # This is deliberately small: callers still validate entity-specific content.
110
+ def compare_revisions(current_revision:, expected_pre_revision:, intended_post_revision:)
111
+ current = current_revision
112
+ return :post_state if current == intended_post_revision
113
+ return :pre_state if current == expected_pre_revision
114
+
115
+ :conflict
116
+ end
117
+
118
+ # Generic F1 helper for revisioned snapshot repositories such as Workflow.
119
+ # Some repositories return a newly allocated revision only after save, so
120
+ # callers may not know the intended post revision in advance. In that case
121
+ # the intended snapshot plus a revision different from the expected pre
122
+ # revision identifies the post-state.
123
+ def compare_revisioned_snapshot(
124
+ record:,
125
+ expected_pre_revision:,
126
+ intended_snapshot:,
127
+ intended_post_revision: nil
128
+ )
129
+ return :pre_state if record.nil? && expected_pre_revision.nil?
130
+ return :conflict if record.nil?
131
+
132
+ revision = fetch_value(record, :revision)
133
+ snapshot = fetch_value(record, :snapshot)
134
+ normalized_snapshot = normalize_value(snapshot)
135
+ normalized_intended = normalize_value(intended_snapshot)
136
+
137
+ if intended_post_revision
138
+ if revision == intended_post_revision &&
139
+ normalized_snapshot == normalized_intended
140
+ return :post_state
141
+ end
142
+ elsif normalized_snapshot == normalized_intended &&
143
+ revision != expected_pre_revision
144
+ return :post_state
145
+ end
146
+
147
+ return :pre_state if revision == expected_pre_revision
148
+
149
+ :conflict
150
+ end
151
+
152
+ def normalize_value(value)
153
+ case value
154
+ when Hash
155
+ value.each_with_object({}) do |(key, child), result|
156
+ result[key.to_s] = normalize_value(child)
157
+ end
158
+ when Array
159
+ value.map { |child| normalize_value(child) }
160
+ when Symbol
161
+ value.to_s
162
+ else
163
+ value
164
+ end
165
+ end
166
+
167
+ def fetch_value(record, key)
168
+ return nil unless record
169
+ return record.public_send(key) if record.respond_to?(key)
170
+ return record[key] if record.respond_to?(:key?) && record.key?(key)
171
+ string_key = key.to_s
172
+ return record[string_key] if record.respond_to?(:key?) && record.key?(string_key)
173
+
174
+ nil
175
+ end
176
+
177
+ def required_id(hash, key)
178
+ value = hash.fetch(key)
179
+ text = value.to_s
180
+ raise ArgumentError, "#{key} must not be empty" if text.empty?
181
+
182
+ text.freeze
183
+ end
184
+ private_class_method :required_id
185
+ end
186
+ end
@@ -8,7 +8,7 @@ RSpec.shared_examples "a Journal repository" do
8
8
  Phronomy::Agent::AgentRoot.create(
9
9
  agent_id: "journal-agent-#{SecureRandom.uuid}",
10
10
  agent_definition_id: "contract-agent",
11
- definition_version: 1
11
+ agent_definition_version: 1
12
12
  )
13
13
  end
14
14
 
@@ -106,7 +106,7 @@ RSpec.shared_examples "a Journal repository" do
106
106
  expected_position: 0,
107
107
  records: [record]
108
108
  )
109
- end.to raise_error(Phronomy::Persistence::ConflictError)
109
+ end.to raise_error(Phronomy::Persistence::SerializationError)
110
110
  end
111
111
 
112
112
  it "reads records in sequence order and supports after/limit" do
@@ -7,7 +7,7 @@ RSpec.shared_examples "a Persistence backend" do
7
7
  Phronomy::Agent::AgentRoot.create(
8
8
  agent_id: "backend-agent-#{SecureRandom.uuid}",
9
9
  agent_definition_id: "contract-agent",
10
- definition_version: 1
10
+ agent_definition_version: 1
11
11
  )
12
12
  end
13
13
 
@@ -17,15 +17,15 @@ RSpec.shared_examples "a workflow state repository" do
17
17
  workflow_contract_value(record, :revision)
18
18
  end
19
19
 
20
- it "returns nil for an unknown thread_id" do
20
+ it "returns nil for an unknown workflow_instance_id" do
21
21
  expect(repository.load("missing-#{SecureRandom.uuid}")).to be_nil
22
22
  end
23
23
 
24
24
  it "uses optimistic revisions for save" do
25
- thread_id = "t1-#{SecureRandom.uuid}"
25
+ workflow_instance_id = "t1-#{SecureRandom.uuid}"
26
26
  expect(
27
27
  repository.save(
28
- thread_id,
28
+ workflow_instance_id,
29
29
  expected_revision: nil,
30
30
  snapshot: {fields: {value: 1}, phase: "pause"}
31
31
  )
@@ -33,7 +33,7 @@ RSpec.shared_examples "a workflow state repository" do
33
33
 
34
34
  expect(
35
35
  repository.save(
36
- thread_id,
36
+ workflow_instance_id,
37
37
  expected_revision: 1,
38
38
  snapshot: {fields: {value: 2}, phase: "__end__"}
39
39
  )
@@ -41,16 +41,16 @@ RSpec.shared_examples "a workflow state repository" do
41
41
  end
42
42
 
43
43
  it "rejects a stale expected_revision" do
44
- thread_id = "t1-#{SecureRandom.uuid}"
44
+ workflow_instance_id = "t1-#{SecureRandom.uuid}"
45
45
  repository.save(
46
- thread_id,
46
+ workflow_instance_id,
47
47
  expected_revision: nil,
48
48
  snapshot: {fields: {value: 1}, phase: "pause"}
49
49
  )
50
50
 
51
51
  expect do
52
52
  repository.save(
53
- thread_id,
53
+ workflow_instance_id,
54
54
  expected_revision: nil,
55
55
  snapshot: {fields: {value: 2}, phase: "__end__"}
56
56
  )
@@ -58,14 +58,14 @@ RSpec.shared_examples "a workflow state repository" do
58
58
  end
59
59
 
60
60
  it "returns a snapshot representation accepted by WorkflowRunner" do
61
- thread_id = "t1-#{SecureRandom.uuid}"
61
+ workflow_instance_id = "t1-#{SecureRandom.uuid}"
62
62
  repository.save(
63
- thread_id,
63
+ workflow_instance_id,
64
64
  expected_revision: nil,
65
65
  snapshot: {fields: {value: 1}, phase: "pause"}
66
66
  )
67
67
 
68
- record = repository.load(thread_id)
68
+ record = repository.load(workflow_instance_id)
69
69
  snapshot = workflow_contract_snapshot(record)
70
70
  fields = workflow_contract_value(snapshot, :fields)
71
71
 
@@ -75,14 +75,14 @@ RSpec.shared_examples "a workflow state repository" do
75
75
  end
76
76
 
77
77
  it "isolates stored snapshots from caller mutation" do
78
- thread_id = "t1-#{SecureRandom.uuid}"
78
+ workflow_instance_id = "t1-#{SecureRandom.uuid}"
79
79
  repository.save(
80
- thread_id,
80
+ workflow_instance_id,
81
81
  expected_revision: nil,
82
82
  snapshot: {fields: {values: [1]}, phase: "pause"}
83
83
  )
84
84
 
85
- loaded = repository.load(thread_id)
85
+ loaded = repository.load(workflow_instance_id)
86
86
  snapshot = workflow_contract_snapshot(loaded)
87
87
  fields = workflow_contract_value(snapshot, :fields)
88
88
  values = workflow_contract_value(fields, :values)
@@ -93,7 +93,7 @@ RSpec.shared_examples "a workflow state repository" do
93
93
  nil
94
94
  end
95
95
 
96
- reloaded = repository.load(thread_id)
96
+ reloaded = repository.load(workflow_instance_id)
97
97
  reloaded_fields = workflow_contract_value(
98
98
  workflow_contract_snapshot(reloaded),
99
99
  :fields
@@ -102,18 +102,18 @@ RSpec.shared_examples "a workflow state repository" do
102
102
  end
103
103
 
104
104
  it "deletes only at the expected revision" do
105
- thread_id = "t1-#{SecureRandom.uuid}"
105
+ workflow_instance_id = "t1-#{SecureRandom.uuid}"
106
106
  repository.save(
107
- thread_id,
107
+ workflow_instance_id,
108
108
  expected_revision: nil,
109
109
  snapshot: {fields: {}, phase: "pause"}
110
110
  )
111
111
 
112
112
  expect do
113
- repository.delete(thread_id, expected_revision: 99)
113
+ repository.delete(workflow_instance_id, expected_revision: 99)
114
114
  end.to raise_error(Phronomy::Persistence::ConflictError)
115
115
 
116
- repository.delete(thread_id, expected_revision: 1)
117
- expect(repository.load(thread_id)).to be_nil
116
+ repository.delete(workflow_instance_id, expected_revision: 1)
117
+ expect(repository.load(workflow_instance_id)).to be_nil
118
118
  end
119
119
  end
@@ -8,7 +8,7 @@ RSpec.shared_examples "an Agent repository" do
8
8
  Phronomy::Agent::AgentRoot.create(
9
9
  agent_id: "contract-agent-#{SecureRandom.uuid}",
10
10
  agent_definition_id: "contract-agent",
11
- definition_version: 1
11
+ agent_definition_version: 1
12
12
  )
13
13
  end
14
14
 
@@ -62,7 +62,7 @@ RSpec.shared_examples "an Agent repository" do
62
62
  other = Phronomy::Agent::AgentRoot.create(
63
63
  agent_id: "other-#{SecureRandom.uuid}",
64
64
  agent_definition_id: "contract-agent",
65
- definition_version: 1
65
+ agent_definition_version: 1
66
66
  ).with(agent_revision: 1)
67
67
 
68
68
  expect do
@@ -71,7 +71,7 @@ RSpec.shared_examples "an Agent repository" do
71
71
  expected_revision: 0,
72
72
  root: other
73
73
  )
74
- end.to raise_error(Phronomy::Persistence::ConflictError)
74
+ end.to raise_error(Phronomy::Persistence::SerializationError)
75
75
  end
76
76
 
77
77
  it "requires revision to advance exactly once" do
@@ -8,7 +8,7 @@ RSpec.shared_examples "an Execution repository" do
8
8
  Phronomy::Agent::AgentRoot.create(
9
9
  agent_id: "execution-agent-#{SecureRandom.uuid}",
10
10
  agent_definition_id: "contract-agent",
11
- definition_version: 1
11
+ agent_definition_version: 1
12
12
  )
13
13
  end
14
14
 
@@ -66,7 +66,7 @@ RSpec.shared_examples "an Execution repository" do
66
66
  other_root = Phronomy::Agent::AgentRoot.create(
67
67
  agent_id: "execution-agent-#{SecureRandom.uuid}",
68
68
  agent_definition_id: "contract-agent",
69
- definition_version: 1
69
+ agent_definition_version: 1
70
70
  )
71
71
  persistence.agents.create(other_root)
72
72
 
@@ -116,7 +116,7 @@ RSpec.shared_examples "an Execution repository" do
116
116
  other_root = Phronomy::Agent::AgentRoot.create(
117
117
  agent_id: "execution-agent-#{SecureRandom.uuid}",
118
118
  agent_definition_id: "contract-agent",
119
- definition_version: 1
119
+ agent_definition_version: 1
120
120
  )
121
121
  persistence.agents.create(other_root)
122
122
  other = build_contract_execution(other_root).with(
@@ -131,7 +131,7 @@ RSpec.shared_examples "an Execution repository" do
131
131
  expected_revision: 0,
132
132
  execution: other
133
133
  )
134
- end.to raise_error(Phronomy::Persistence::ConflictError)
134
+ end.to raise_error(Phronomy::Persistence::SerializationError)
135
135
  end
136
136
 
137
137
  it "requires execution revision to advance exactly once" do
@@ -186,7 +186,7 @@ RSpec.shared_examples "an Execution repository" do
186
186
  other_root = Phronomy::Agent::AgentRoot.create(
187
187
  agent_id: "execution-agent-#{SecureRandom.uuid}",
188
188
  agent_definition_id: "contract-agent",
189
- definition_version: 1
189
+ agent_definition_version: 1
190
190
  )
191
191
  persistence.agents.create(other_root)
192
192
  own = build_contract_execution(execution_agent_root)