phronomy 0.32.0 → 0.33.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.
@@ -0,0 +1,223 @@
1
+ # Custom sharing in SharedState
2
+
3
+ SharedState can coordinate member Agents while an application owns the shared
4
+ data and the Tools that access it. The application provides one invocation-scoped
5
+ binding; SharedState owns member order, cycles, stopping decisions and final
6
+ aggregation. This Experimental extension is available since v0.33.0.
7
+
8
+ For example, an issue-review team can share records with `category` and `status`
9
+ fields. Its search Tool accepts those fields as separate arguments instead of
10
+ forcing the application to encode every record and condition as finding text.
11
+
12
+ ## Application provider and binding
13
+
14
+ A provider scopes shared resources around one synchronous team invocation. Its
15
+ binding contains an opaque store and a Tool-definition builder. SharedState
16
+ passes the store to application callbacks and does not require store methods or
17
+ a particular record schema.
18
+
19
+ For example, an application provider can open an issue board, yield its binding,
20
+ and close the board in `ensure`:
21
+
22
+ ```ruby
23
+ class IssueSharing
24
+ def open(input:, config:)
25
+ board = IssueBoard.new
26
+ yield Phronomy::MultiAgent::SharedState::Binding.new(
27
+ store: board,
28
+ tools: ->(member:, cycle:) { build_issue_tools(board, member, cycle) }
29
+ )
30
+ ensure
31
+ board&.close
32
+ end
33
+ end
34
+ ```
35
+
36
+ This excerpt shows the provider boundary. The
37
+ [complete executable application example](examples/shared_state_custom_sharing.rb)
38
+ defines its store, Tool builder, member Agent, team and offline check.
39
+
40
+ The provider must respond to `open(input:, config:)` and yield exactly one
41
+ `Phronomy::MultiAgent::SharedState::Binding` synchronously during that call.
42
+ Zero yields, multiple yields or an invalid binding violate the protocol and
43
+ raise `ArgumentError`. Do not retain the block for deferred execution. The
44
+ provider owns cleanup, including failures raised by member execution or
45
+ aggregation; SharedState does not call `close` on the opaque store.
46
+
47
+ `Binding.new(store:, tools:)` accepts a callable Tool builder. The public
48
+ `binding.store` returns the opaque store, and
49
+ `binding.tool_definitions(member:, cycle:)` calls the builder. `member` is the
50
+ registered Agent class; `cycle` is the current positive cycle number. The builder
51
+ must return a Hash whose keys are Tool classes and whose values are aliases or
52
+ `nil`, using the same Tool-definition shape as the existing Agent Tool API.
53
+ Arrays are not accepted. Invalid configuration or protocol values raise
54
+ `ArgumentError`.
55
+
56
+ ## Team composition and definition identity
57
+
58
+ A team selects its provider and supplies the application operations that consume
59
+ the shared store. The provider's stable ID and version identify the meaning of
60
+ its injected Tools independently from an individual member's definition.
61
+
62
+ For example, the complete issue-review example declares:
63
+
64
+ ```ruby
65
+ class ReviewTeam < Phronomy::MultiAgent::SharedState
66
+ sharing IssueSharing.new, id: "shared-issues", version: 1
67
+ member Researcher
68
+ max_cycles 2
69
+ coordination "Use search_issues to inspect relevant issues. " \
70
+ "Use record_issue to record each new issue with its category and status."
71
+ aggregate { |board| board.snapshot }
72
+ end
73
+ ```
74
+
75
+ Custom sharing requires an explicit `aggregate` block because SharedState does
76
+ not know how to read the application store. An optional `terminate_when` block
77
+ also receives that same store. Existing cycle order, stopping rules and result
78
+ shape remain in place. `config:` is provided to the sharing provider; this does
79
+ not introduce forwarding of `config:` to member invocations.
80
+
81
+ In custom mode, SharedState adds only the Tools returned by the application's
82
+ builder. It does not add `read_store` or `write_finding`, and it does not append
83
+ stored records to member prompts. The prompt consists of coordination text, the
84
+ task input and any member instruction. The member's existing Tools and aliases
85
+ remain available. Conflicting effective Tool names are rejected rather than
86
+ silently replacing an existing Tool.
87
+
88
+ Use a stable sharing ID and increment its version whenever the injected Tool
89
+ schemas or behavior change. Custom generated Agent identity incorporates that
90
+ ID/version alongside the member definition. A new store instance per invocation
91
+ does not by itself require a new sharing ID. The application must not substitute
92
+ different Tool semantics under an unchanged sharing revision.
93
+ The ID must be a nonempty String and the version must be a positive Integer.
94
+
95
+ The default mode remains available when `.sharing` is not configured. It retains
96
+ the existing invocation-local KnowledgeStore, built-in Tools, full-store prompt
97
+ behavior and instrumentation revision 1. This extension does not add a `query`
98
+ argument to the default `read_store` Tool or a search method to that store.
99
+
100
+ ## Deriving an Agent with a complete Tool set
101
+
102
+ The public `Agent::Base.with_tools(definitions, id:, version:)` factory creates
103
+ an Agent subclass with a replacement Tool map and an explicit definition
104
+ identity. Agent owns preservation of its authoring settings; SharedState uses
105
+ this public operation after composing the member and application Tools. It does
106
+ not need to read Agent private settings.
107
+
108
+ For example, inside the complete example's module, a provider can supply the
109
+ entire Tool map for a derived researcher class:
110
+
111
+ ```ruby
112
+ IssueSharing.new.open(input: "Review authentication", config: {}) do |binding|
113
+ tool_map = binding.tool_definitions(member: Researcher, cycle: 1)
114
+ derived = Researcher.with_tools(
115
+ tool_map, id: "issue-researcher-with-board", version: 1
116
+ )
117
+ # `derived` is a class; no Agent instance or Runtime resource was created.
118
+ end
119
+ ```
120
+
121
+ `definitions` is the complete Hash of Tool classes to aliases or `nil`; this
122
+ factory does not merge Tools or detect naming collisions. SharedState performs
123
+ those operations before calling it. A missing parent Tool is removed, and a
124
+ `nil` alias uses the Tool's own name even when the parent assigned an alias.
125
+ The supplied ID and version identify the derived definition directly.
126
+
127
+ The factory preserves Phronomy-authored model, provider, instructions,
128
+ ContextPolicy, generation settings, LLM-input hook and filters. Filter lists
129
+ are independent while configured filter and callback instances remain shared.
130
+ The original class is unchanged. This explicit factory does not change general
131
+ Agent subclass inheritance or define copying rules for application-specific
132
+ class state.
133
+
134
+ ## Application search and LLM input
135
+
136
+ Application Tools own record fields and search conditions. ContextPolicy and the
137
+ existing input-validation path continue to own LLM-input selection and budget
138
+ checks. A custom provider does not become another Context engine.
139
+
140
+ For example, `search_issues(category: "security", status: "open")` in the complete
141
+ example applies both field conditions and returns matching records as JSON. A
142
+ search can leave three records in the store while returning only one match. Its
143
+ Tool parameter types and enumerations use the existing Tool schema API.
144
+
145
+ The example has no result-size cap. A broad query may still produce a large Tool
146
+ result, and this extension does not make that result optional, rewrite recorded
147
+ Tool output or bypass the final input-budget check. Applications that need
148
+ paging, compaction or another acquisition policy must define that behavior
149
+ explicitly through the existing appropriate contracts.
150
+
151
+ SharedState continues to use the member's configured ContextPolicy. The binding
152
+ does not automatically populate `before_llm_input` candidates, inspect Context
153
+ internals or manage a provider message buffer. Example 22's adaptive reader is a
154
+ separate application implementation; this extension alone does not migrate it
155
+ or remove its existing private-API dependency.
156
+
157
+ ## Lifetime and verification
158
+
159
+ The provider owns resources for one synchronous invocation. SharedState does not
160
+ define a database schema, durable binding identity or process-transfer protocol
161
+ for the application's store. A live binding does not become a persisted
162
+ continuation. Custom sharing requires each member invocation to reach a terminal
163
+ state before coordination proceeds.
164
+
165
+ For example, the application file can check its store and Tools without calling
166
+ an LLM:
167
+
168
+ ```sh
169
+ bundle exec ruby docs/examples/shared_state_custom_sharing.rb --offline
170
+ ```
171
+
172
+ The check opens the provider, records three issues through `Tool#call`, searches
173
+ with both category and status, verifies one match and three retained records,
174
+ and verifies provider cleanup. It uses the real Tool argument-validation path.
175
+ It does not exercise the SharedState Agent loop or establish live-model,
176
+ database or multi-process acceptance.
177
+
178
+ In custom mode, SharedState releases terminal member instances through the
179
+ Agent's public `unload!` operation. A normal Agent approval wait keeps the public
180
+ `invoke` call pending. SharedState consequently stays inside the provider's
181
+ `open` block and retains its resources; it does not return a suspended result or
182
+ close the provider merely because an internal execution reaches an approval
183
+ wait. The application remains responsible for answering or cancelling approvals.
184
+
185
+ For example, when a member waits for approval of `record_issue`, the board stays
186
+ open while that invocation waits. An approval allows the Tool and member to
187
+ complete before aggregation and provider cleanup. A cancellation that ends the
188
+ member invocation also allows cleanup to run. The in-memory example's `ensure`
189
+ is scoped to the end of the invocation, not the start of an approval wait. This
190
+ is a live wait in the current process, not a way to release a Worker and restore
191
+ the board on another one.
192
+
193
+ The [SharedState integration suite](../spec/integration/shared_state_spec.rb)
194
+ separately exercises this approval lifecycle using a real Agent and fixed LLM
195
+ responses. It checks that the provider stays open and aggregation has not run
196
+ during the wait; after a public approval response, the Tool writes its record,
197
+ aggregation runs, the provider closes and the member is released. Fixed
198
+ responses verify the execution path, not a live model's Tool-selection behavior.
199
+
200
+ The lifecycle guard also detects a custom `invoke` override that returns a Hash
201
+ with a suspended status before reaching completion. That exceptional early
202
+ return raises `SharedState::MemberLifecycleError` with `reason: :suspended` and
203
+ stops coordination before later members or aggregation. It is not the behavior
204
+ of normal Agent HITL. SharedState does not approve, deny, cancel or unload the
205
+ pending execution on the application's behalf. Providers handling this case
206
+ must decide whether to retain their resources; merely retaining a binding adds
207
+ no SharedState resume operation.
208
+
209
+ `MemberLifecycleError#reason` also reports `:release_failed`. The error exposes
210
+ `agent`, the available `result`, `primary_error` and `release_error`. A failed
211
+ release preserves the Agent reference and both failure details when invocation
212
+ and release fail together, so the application can inspect the original failure
213
+ and decide whether to retry release. The provider's `open` scope can rescue this
214
+ error before its cleanup policy runs. Rescuing a coordination failure inside
215
+ `open` does not turn the invocation into success: SharedState still reports the
216
+ failure if the provider returns normally.
217
+
218
+ To run the team from an application, configure Phronomy's LLM backend normally,
219
+ set `PHRONOMY_EXAMPLE_MODEL` to that backend's model ID before requiring the file,
220
+ then invoke `SharedStateCustomSharingExample::ReviewTeam.new.invoke(task)`.
221
+ The example's fallback model name is a placeholder, not a remotely available
222
+ model. A DB-backed provider may replace the example's in-memory board, but its
223
+ transactions, isolation and recovery behavior remain application responsibilities.
@@ -77,6 +77,8 @@ module Phronomy
77
77
 
78
78
  def tool_aliases
79
79
  own = @tool_aliases || {}
80
+ return own if @tool_aliases_replace_inherited
81
+
80
82
  if superclass.respond_to?(:tool_aliases)
81
83
  superclass.tool_aliases.merge(own)
82
84
  else
@@ -84,6 +86,29 @@ module Phronomy
84
86
  end
85
87
  end
86
88
 
89
+ # Derive an Agent definition with a complete replacement Tool map.
90
+ # Phronomy authoring settings and filters are preserved; mutable filter
91
+ # lists are independent, while configured callable instances are shared.
92
+ # No Agent instance or Runtime resource is created by this factory.
93
+ # @param definitions [Hash<Class, String, nil>] Tool classes and aliases
94
+ # @param id [String] identity for the derived Agent definition
95
+ # @param version [Integer] revision for the derived Agent definition
96
+ # @return [Class<Phronomy::Agent::Base>]
97
+ # @api public
98
+ def with_tools(definitions, id:, version:)
99
+ unless definitions.is_a?(Hash)
100
+ raise ArgumentError, "with_tools expects a Hash of ToolClass => alias_or_nil"
101
+ end
102
+
103
+ derived = Class.new(self)
104
+ derived.agent_definition(id: id, version: version)
105
+ copy_authoring_settings_to(derived)
106
+ copy_authoring_filters_to(derived)
107
+ derived.tools(definitions)
108
+ derived.instance_variable_set(:@tool_aliases_replace_inherited, true)
109
+ derived
110
+ end
111
+
87
112
  def provider(name = nil)
88
113
  if name
89
114
  @provider = name
@@ -263,6 +288,34 @@ module Phronomy
263
288
 
264
289
  private
265
290
 
291
+ def copy_authoring_settings_to(derived)
292
+ settings = {
293
+ model: model,
294
+ provider: provider,
295
+ instructions: instructions,
296
+ context_policy: context_policy,
297
+ temperature: temperature,
298
+ max_iterations: max_iterations,
299
+ cache_instructions: cache_instructions,
300
+ max_output_tokens: max_output_tokens,
301
+ before_llm_input: before_llm_input
302
+ }
303
+ settings.each do |name, value|
304
+ derived.public_send(name, value) unless value.nil?
305
+ end
306
+ # These getters inherit by ivar presence. An explicit nil preserves
307
+ # absence even if the source class is configured again later.
308
+ %i[provider instructions].each do |name|
309
+ derived.instance_variable_set(:"@#{name}", nil) if settings[name].nil?
310
+ end
311
+ end
312
+
313
+ def copy_authoring_filters_to(derived)
314
+ _class_input_filters.each { |filter| derived.input_filter(filter) }
315
+ _class_output_filters.each { |filter| derived.output_filter(filter) }
316
+ _class_tool_result_filters.each { |filter| derived.tool_result_filter(filter) }
317
+ end
318
+
266
319
  def __construct_owned_agent(owner_runtime, reserved_agent_id, *args, **kwargs, &block)
267
320
  instance = allocate
268
321
  instance.send(:__prepare_runtime_owner!, owner_runtime, reserved_agent_id)
@@ -105,6 +105,7 @@ module Phronomy
105
105
  :coordinator, :operation, :delivery, :outcome, :error
106
106
  )
107
107
  DetachHostedWait = Data.define(:coordinator, :wait, :task, :parent, :completion)
108
+ private_constant :DetachHostedWait
108
109
  SuspensionCancelledCommand = Data.define(:coordinator, :execution_id)
109
110
 
110
111
  DeferredTerminalCommand = Data.define(
@@ -179,6 +180,16 @@ module Phronomy
179
180
  result_task
180
181
  end
181
182
 
183
+ # Submit the committed wait for final ownership checks on the EventLoop.
184
+ # @api private
185
+ def request_hosted_detach(wait, task:, parent:)
186
+ completion = Phronomy::TaskResult.deferred(name: "detach-hosted-approval")
187
+ command = DetachHostedWait.new(coordinator: self, wait: wait, task: task, parent: parent, completion: completion)
188
+ accepted = @agent.__execution_environment.registry.post(command, completion: completion)
189
+ raise Phronomy::RuntimeShutdownError, "EventLoop rejected hosted detach" unless accepted
190
+ completion
191
+ end
192
+
182
193
  # Called only from an Agent FSMSession external-operation dispatch state on
183
194
  # EventLoop. Each method captures one operation-specific immutable snapshot
184
195
  # and sends only that snapshot to OffloadPool.
@@ -37,15 +37,12 @@ module Phronomy
37
37
  unless wait && wait.approval_request_id == approval_request_id
38
38
  raise Phronomy::AgentBusyError, "No matching hosted approval wait"
39
39
  end
40
- completion = Phronomy::TaskResult.deferred(name: "detach-hosted-approval")
41
40
  owner = @environment.registry.agent_execution_owner(execution_id)
42
41
  unless owner && (child ? owner.agent.persistence.equal?(@agent.persistence) : owner.agent.equal?(@agent))
43
42
  raise Phronomy::AgentBusyError, "Hosted execution has no matching local owner"
44
43
  end
45
- command = ExecutionCoordinator::DetachHostedWait.new(coordinator: owner.coordinator, wait: wait,
46
- task: (child && !@resumed_from) ? nil : @task, parent: child ? @agent : nil, completion: completion)
47
- accepted = @environment.registry.post(command, completion: completion)
48
- raise Phronomy::RuntimeShutdownError, "EventLoop rejected hosted detach" unless accepted
44
+ completion = owner.coordinator.request_hosted_detach(wait,
45
+ task: (child && !@resumed_from) ? nil : @task, parent: child ? @agent : nil)
49
46
  completion.wait_result
50
47
  end
51
48
 
@@ -101,12 +101,13 @@ module Phronomy
101
101
  end
102
102
 
103
103
  def self.build_item(invocation)
104
+ display = invocation.approval_display
104
105
  Item.new(
105
106
  tool_invocation_id: invocation.id,
106
107
  tool_call_id: invocation.tool_call_id,
107
108
  tool_name: invocation.tool_name,
108
- arguments: invocation.display_arguments,
109
- facts: invocation.display_facts,
109
+ arguments: display.fetch(:arguments),
110
+ facts: display.fetch(:facts),
110
111
  reason: invocation.authorization_reason,
111
112
  origin: invocation.origin,
112
113
  metadata: invocation.metadata
@@ -333,14 +333,21 @@ module Phronomy
333
333
  @tool&.respond_to?(:parameters_schema) ? @tool.parameters_schema : {}
334
334
  end
335
335
 
336
- def display_arguments
337
- redact_for_display(@arguments || @raw_arguments)
338
- end
336
+ # The approval Item snapshots these fields at its own construction boundary.
337
+ # @api private
338
+ def approval_display
339
+ return {arguments: {}.freeze, facts: {}.freeze}.freeze unless @tool&.respond_to?(:approval_display)
339
340
 
340
- def display_facts
341
- redact_value(@facts, sensitive_argument_values)
341
+ display = @tool.approval_display(arguments: @arguments || @raw_arguments, facts: @facts)
342
+ unless Hash === display && Hash === display.fetch(:arguments, nil) && Hash === display.fetch(:facts, nil)
343
+ raise Phronomy::ConfigurationError, "Tool approval_display must return :arguments and :facts Hashes"
344
+ end
345
+ display
342
346
  end
343
347
 
348
+ def display_arguments = immutable_copy(approval_display.fetch(:arguments))
349
+ def display_facts = immutable_copy(approval_display.fetch(:facts))
350
+
344
351
  private
345
352
 
346
353
  def restore_saved_state!(status, result)
@@ -544,37 +551,6 @@ module Phronomy
544
551
  def immutable_copy(value)
545
552
  Phronomy::Values::Immutable.copy(value)
546
553
  end
547
-
548
- def redact_for_display(value)
549
- if @tool&.respond_to?(:redacted_args, true)
550
- immutable_copy(@tool.send(:redacted_args, value || {}))
551
- else
552
- immutable_copy(value || {})
553
- end
554
- end
555
-
556
- def sensitive_argument_values
557
- return [] unless @tool&.class&.respond_to?(:redact_params)
558
- normalized = (@arguments || @raw_arguments || {}).transform_keys(&:to_sym)
559
- @tool.class.redact_params.filter_map { |name| normalized[name] }
560
- end
561
-
562
- def redact_value(value, sensitive_values)
563
- return "[REDACTED]" if sensitive_values.any? { |sensitive| sensitive == value }
564
-
565
- case value
566
- when Hash
567
- value.each_with_object({}) do |(key, item), result|
568
- result[key] = redact_value(item, sensitive_values)
569
- end.freeze
570
- when Array
571
- value.map { |item| redact_value(item, sensitive_values) }.freeze
572
- when String
573
- "[REDACTED]"
574
- else
575
- value
576
- end
577
- end
578
554
  end
579
555
  end
580
556
  end
@@ -11,6 +11,16 @@ module Phronomy
11
11
 
12
12
  protected
13
13
 
14
+ # Uses the same canonical representation and estimator as generated Policy
15
+ # items. Estimates are planning values, not a model tokenizer guarantee.
16
+ # @param content [Object] text or JSON-serializable content
17
+ # @return [Integer] estimated token count
18
+ # @api public
19
+ def estimate_tokens(content)
20
+ bytes = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
21
+ Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
22
+ end
23
+
14
24
  def instruction_item(
15
25
  content:,
16
26
  role: :system,
@@ -113,8 +123,7 @@ module Phronomy
113
123
  end
114
124
 
115
125
  def estimate_content(content)
116
- bytes = content.is_a?(String) ? content : Phronomy::CanonicalJSON.dump(content)
117
- Phronomy::LlmContextWindow::TokenEstimator.estimate(bytes)
126
+ estimate_tokens(content)
118
127
  end
119
128
  end
120
129
  end
@@ -56,6 +56,32 @@ module Phronomy
56
56
  freeze
57
57
  end
58
58
 
59
+ # Reports the input limit supplied to this Policy without exposing the
60
+ # framework's budget representation. Unknown capability remains nil.
61
+ # @return [Integer, nil]
62
+ # @api public
63
+ def input_token_limit
64
+ token_budget&.effective_input_limit
65
+ end
66
+
67
+ # Returns a selection input with an application limit. A known limit may
68
+ # only be retained or reduced. An unknown limit accepts an explicit cap
69
+ # without asserting that the model can accept that many tokens.
70
+ # @param limit [Integer] positive selection limit
71
+ # @return [ContextPolicyInput]
72
+ # @raise [ArgumentError] for an invalid limit or a known-limit increase
73
+ # @api public
74
+ def with_input_token_limit(limit)
75
+ unless limit.is_a?(Integer) && limit.positive?
76
+ raise ArgumentError, "input token limit must be a positive Integer"
77
+ end
78
+ if input_token_limit && limit > input_token_limit
79
+ raise ArgumentError, "input token limit must not exceed the current limit"
80
+ end
81
+
82
+ with(token_budget: Phronomy::LlmContextWindow::TokenBudget.new(max_input_tokens: limit))
83
+ end
84
+
59
85
  private
60
86
 
61
87
  def validate_items!(items, expected_class, label)
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module MultiAgent
5
+ class SharedState
6
+ # Connects one application's shared data to the Tools offered to members.
7
+ # The application owns the store's shape, operations and resource lifetime.
8
+ # @api public
9
+ class Binding
10
+ # @param store [Object] opaque application data passed to coordination callbacks
11
+ # @param tools [#call] builder accepting member: (Agent class) and cycle:
12
+ # @api public
13
+ def initialize(store:, tools:)
14
+ raise ArgumentError, "sharing tools must respond to call" unless tools.respond_to?(:call)
15
+
16
+ @store = store
17
+ @tools = tools
18
+ freeze
19
+ end
20
+
21
+ # @api public
22
+ attr_reader :store
23
+
24
+ # Build invocation-bound Tools using the existing Agent.tools declaration shape.
25
+ # The builder receives the registered Agent class, not an Agent instance.
26
+ # @return [Hash<Class, String, nil>] Tool classes mapped to aliases or nil
27
+ # @api public
28
+ def tool_definitions(member:, cycle:)
29
+ definitions = @tools.call(member: member, cycle: cycle)
30
+ unless definitions.is_a?(Hash)
31
+ raise ArgumentError, "sharing tools must return a Hash of ToolClass => alias_or_nil"
32
+ end
33
+
34
+ definitions.to_h do |tool_class, alias_name|
35
+ unless tool_class.is_a?(Class) && tool_class <= Phronomy::Tool::Base
36
+ raise ArgumentError, "sharing tools must contain Phronomy::Tool::Base classes"
37
+ end
38
+ [tool_class, alias_name&.to_s&.dup&.freeze]
39
+ end.freeze
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Phronomy
4
+ module MultiAgent
5
+ class SharedState
6
+ # Keeps the member handle when invocation-scoped sharing cannot finish safely.
7
+ # Callers choose approval, cancellation or release through existing Agent APIs.
8
+ # @api public
9
+ class MemberLifecycleError < Phronomy::Error
10
+ # @api public
11
+ attr_reader :agent, :result, :reason, :primary_error, :release_error
12
+
13
+ # The live Agent and application resources are process-local, not a
14
+ # durable recipe for rebuilding the sharing provider or its Tools.
15
+ # @api public
16
+ def initialize(agent:, result:, reason:, primary_error: nil, release_error: nil)
17
+ @agent = agent
18
+ @result = result
19
+ @reason = reason
20
+ @primary_error = primary_error
21
+ @release_error = release_error
22
+ message = if reason == :suspended
23
+ "SharedState sharing cannot complete while a member is suspended"
24
+ else
25
+ "SharedState sharing could not release a member: #{release_error&.message}"
26
+ end
27
+ super(message)
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end